Artifact e0f11ceaf059393bc51f37f02d4f80da7ec81b98b9cb16b607a1d9cf9c9ca462:
- File
Dockerfile
— part of check-in
[a8cbc48878]
at
2026-08-17 16:53:51
on branch trunk
— deploy: the image builds and serves — three faults only running it found
The Dockerfile and compose were committed unverified because no container runtime was installed. With one available they were wrong in three ways, none visible by reading:
- The Zig download 404s. Naming is zig-<arch>-linux-<version>; I had the older zig-linux-<arch>-<version>. - The zit clone fails: the GitHub mirror is private, so an anonymous clone asks for a username. zit's source now arrives as a named build context, which needs no credentials and pins the exact checkout being shipped. A public fossil mirror exists at fossil.craftdesign.group/zit and would make a clone step viable later. - /data was root-owned. VOLUME creates a missing directory as root while the process runs as uid 10001, so quarry could not open its own database — 'permission denied' on the first line of the log.
Now verified end to end: a 90.1MB image, /etc/quarry-build recording both versions, and a container serving libfossil's 3,835 check-ins from a read-only mounted store with zit reading it from inside. (user: dmestas, size: 4607) [annotate] [blame] [check-ins using]
# syntax=docker/dockerfile:1
#
# quarry ships WITH zit, deliberately.
#
# 0001 §2 makes the zit CLI the entire store interface: every read and
# every write is a subprocess call. So zit's behaviour IS quarry's
# behaviour, and the two cannot be allowed to drift. On 2026-08-16 alone,
# three zit builds changed what quarry does — `frames` got 31% faster,
# a card-parsing fix changed which artifacts parse AT ALL (before it,
# sqlite's 794 tickets were invisible), and a new `stat -R` let quarry
# serve a bare store. An image that resolved zit at run time from
# whatever happened to be on PATH would reintroduce exactly the class of
# silent divergence that cost a day to find.
#
# Hence: ZIT_REF is a build argument, the built binary is copied in, and
# `quarry version` is recorded next to `zit version` at build time so a
# running container can answer "which zit is this?".
#
# docker build --build-arg ZIT_REF=<commit> -t quarry .
# ---- zit builder ---------------------------------------------------------
# Zig rather than Go: zit is a Zig program, which is why this stage cannot
# simply be another golang builder.
FROM alpine:3.21 AS zit-builder
ARG ZIG_VERSION=0.16.0
# Naming is zig-<arch>-linux-<version>, NOT zig-linux-<arch>-<version>:
# the order changed in a recent Zig release, and the old form 404s.
RUN apk add --no-cache curl xz
RUN case "$(uname -m)" in \
aarch64) ZIG_ARCH=aarch64 ;; \
x86_64) ZIG_ARCH=x86_64 ;; \
*) echo "unsupported architecture $(uname -m)" >&2; exit 1 ;; \
esac && \
curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}.tar.xz" \
-o /tmp/zig.tar.xz && \
mkdir -p /opt/zig && tar -xJf /tmp/zig.tar.xz -C /opt/zig --strip-components=1 && \
ln -s /opt/zig/zig /usr/local/bin/zig
WORKDIR /src
# zit's source arrives as a NAMED BUILD CONTEXT rather than a clone:
#
# docker build --build-context zit=/path/to/zit .
#
# A clone would need credentials today — the GitHub mirror is private,
# and the two ways to supply them are both worse than this: a token
# baked into a layer, or an SSH agent forwarded into a builder. zit is
# a fossil repository whose public mirror lives at
# https://fossil.craftdesign.group/zit, so once that is the source of
# record a `fossil clone` step is a fine alternative.
#
# A named context is worth keeping regardless: it pins exactly the
# checkout being shipped rather than whatever a branch name means at
# build time, which is the same reason quarry pins the pair at all. The
# recorded `zit version` in /etc/quarry-build is what says which one an
# image actually carries.
COPY --from=zit . /src/zit
# --release=fast, not -Doptimize: zit's build.zig uses Zig 0.16's spelling.
RUN cd zit && zig build --release=fast && ./zig-out/bin/zit version
# ---- quarry builder ------------------------------------------------------
# CGO stays off: quarry's SQLite is ncruces/go-sqlite3 (WASM, no C) and
# Pikchr runs on wazero, so the whole binary is pure Go and static.
FROM golang:1.26 AS quarry-builder
ARG VERSION=0.0.0-dev
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ENV CGO_ENABLED=0
RUN go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" -o /out/quarry ./cmd/quarry
# ---- runtime -------------------------------------------------------------
# busybox wget is present so compose and k8s health checks work, matching
# the notify image's posture.
FROM alpine:3.21
RUN apk add --no-cache ca-certificates && adduser -D -u 10001 quarry
COPY --from=zit-builder /src/zit/zig-out/bin/zit /usr/local/bin/zit
COPY --from=quarry-builder /out/quarry /usr/local/bin/quarry
# The pair, recorded together. `docker run <image> cat /etc/quarry-build`
# answers which zit a given quarry was shipped against without starting it.
RUN printf 'quarry: %s\nzit: %s\n' "$(quarry version)" "$(zit version)" > /etc/quarry-build && \
cat /etc/quarry-build
# The mount points must exist AND be owned by the runtime user before
# VOLUME declares them: docker creates a missing volume directory as
# root, and the server then cannot open its own database. Found by
# running the image, not by reading it.
RUN mkdir -p /repos /data && chown quarry:quarry /repos /data
USER quarry
# /repos holds the zit stores quarry serves; /data holds derived state
# (indexes, embedded NATS, the OLTP database) and is disposable by 0001
# §11 — wiping it and re-ingesting must converge to identical state.
VOLUME ["/repos", "/data"]
EXPOSE 8080
ENTRYPOINT ["quarry"]
CMD ["serve", "--addr", "0.0.0.0:8080", "--data", "/data"]