relay: Dockerfile + base hardening — portable deploy artifact (#32)#45
Merged
Conversation
Multi-stage Dockerfile producing a host-agnostic, hardened image:
- Build stage: golang:1.26-bookworm pinned by digest. Builds with
CGO_ENABLED=0, -trimpath, -s -w, and -X main.Version=${VERSION} so the
container build mirrors `make build` semantics. VERSION defaults to
"dev" via ARG; release tooling (#38) can pass a real version through.
- Runtime stage: gcr.io/distroless/static-debian12:nonroot pinned by
digest. Belt-and-suspenders `USER nonroot:nonroot` so a base swap
cannot silently regress non-root execution. EXPOSE 80 443; VOLUME
/var/lib/relay/autocert as the documented cache mount point. The host
manifest (#38) wires actual port publishing, volume backing, and TLS
termination policy.
Both base-image digests carry `# Tracks: <tag>` comments so a reviewer
(or Renovate) can sanity-check what the digest is supposed to track.
.dockerignore trims the build context: `.git`, build outputs, and the
Dockerfile itself. README gains a short Docker subsection under Build.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
Author
Code Review: #32Decision: PASS FindingsNone. No MUST FIX, no SHOULD FIX, no NIT worth raising. SummaryImplementation matches the architecture spec verbatim. All five AC bullets satisfied:
Security review (security-sensitive label)
NotesThe developer disclosed that the worktree lacks a Docker daemon, so the spec's image-level verification ( 🤖 Generated with Claude Code |
- Feature doc (docs/knowledge/features/docker-image.md) — evergreen description of the portable OCI artifact: multi-stage build, digest-pinned bases with # Tracks: comments, static binary into distroless/static-debian12:nonroot, :80/:443 exposure, /var/lib/relay/autocert volume. - Per-ticket codebase notes (docs/knowledge/codebase/32.md) — what landed and the judgement calls (VERSION as build arg, .dockerignore minimal, USER nonroot belt-and-suspenders). - INDEX update — top entry under Features. - PROJECT-MEMORY patterns — digest-pin + # Tracks: sibling comment; belt-and-suspenders invariants across base layers. - Lessons — flag.Parse() exits 2 on --help (only --version is zero-exit no-network); distroless/static has no shell so pre-creation belongs in the host manifest.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a multi-stage
Dockerfileand minimal.dockerignoreat the repo root, plus a short Docker subsection in the README. No Go code changes.golang:1.26-bookwormpinned by digest.CGO_ENABLED=0,-trimpath,-s -w,-X main.Version=${VERSION}— mirrors theMakefileLDFLAGS.VERSIONarrives viaARGand defaults todev, leaving room for release tooling in relay: host-specific deploy manifest — blocked on hosting + TLS decisions #38 to pass a real version through without touching this file.gcr.io/distroless/static-debian12:nonrootpinned by digest. Belt-and-suspendersUSER nonroot:nonrootso a base swap cannot silently regress non-root execution.EXPOSE 80 443;VOLUME /var/lib/relay/autocertas the documented autocert-cache mount point.ENTRYPOINT ["/pyrycode-relay"].# Tracks: <tag>comments so a human reviewer (or Renovate) can sanity-check what the digest is supposed to track when proposing a bump..dockerignoreexcludes.git,bin/,dist/,coverage.txt,*.test, and the Dockerfile/.dockerignore themselves.docs/is intentionally not excluded —COPY . .pulls it in for the build stage and the multi-stage boundary drops it from the runtime image.Issue
Closes #32.
Testing
This worktree has no Docker daemon available, so the spec's image-level verification steps (
docker build,docker run --version,docker inspectforUser/ExposedPorts/Volumes) cannot be exercised here and should be run by reviewer/CI. To de-risk that, the build invocation the Dockerfile uses was exercised natively:go vet ./...— clean.go test -race ./...— pass.CGO_ENABLED=0 go build -trimpath -ldflags=\"-s -w -X main.Version=dev\" -o /tmp/relay ./cmd/pyrycode-relay && /tmp/relay --version— printsdev, exits0. This validates the exact flag combination used inside the build stage; the only difference vs. the Dockerfile isGOOS=linux(cross-compile target).Reviewer / CI to run on a Docker-capable host:
docker build -t pyrycode-relay:dev .docker run --rm pyrycode-relay:dev --version→ printsdev, exit0.docker inspect --format '{{.Config.User}}' pyrycode-relay:dev→nonroot:nonroot(or65532:65532).docker inspect --format '{{.Config.ExposedPorts}}' pyrycode-relay:dev→80/tcpand443/tcp.docker inspect --format '{{.Config.Volumes}}' pyrycode-relay:dev→/var/lib/relay/autocert.Architecture compliance
Implements
docs/specs/architecture/32-dockerfile-base-hardening.mdas written:# Tracks:comments — satisfies all five AC bullets.VERSIONbuild-arg strategy (defaultdev) per the spec's §VERSIONstrategy.USER nonroot:nonrootper the spec's § Privilege..dockerignorecontent matches the spec's § On.dockerignore.HEALTHCHECK, no host-specific wiring, no Go code changes — host-specific concerns deferred to relay: host-specific deploy manifest — blocked on hosting + TLS decisions #38 / relay: single-instance constraint — doc + startup self-check (registry is in-memory) #39 / relay: startup security posture self-check — refuse to boot in misconfigured environments #42 as the issue dictates.The base-image digests were fetched at implementation time via the Docker Hub and gcr.io registry APIs (no local Docker daemon needed):
golang:1.26-bookworm→sha256:252599aeb51ad60b83e4d8821802068127c528c707cb7dd7afd93be057c6011cgcr.io/distroless/static-debian12:nonroot→sha256:a9329520abc449e3b14d5bc3a6ffae065bdde0f02667fa10880c49b35c109fd1🤖 Generated with Claude Code