build(docker): run container as non-root, drop dead module download
What
Two container-build hygiene changes. No Go code changes; the binary is unchanged.
Run as non-root (uid 65534)
The runtime image ran as root (no USER). It now runs as USER 65534:65534, the nobody/nobody account that the ubi9/ubi-minimal base already defines in /etc/passwd and /etc/group, so it resolves to a named user with no extra useradd. The manifold process never needs root to collect, analyze, render, or serve.
WORKDIR is set to /workspace, owned by 65534. That covers two runtime modes:
- Direct
podman run/docker run(container-commands.md):WORKDIRis kept, so the relative default output dirs (data/,analysis/,public/) are created by 65534 under/workspace. For bind-mounted output dirs, the host dir must be writable by uid 65534; documented in docs/container-commands.md. - GitLab CI job image (deploy.yml): the runner overrides
WORKDIRto$CI_PROJECT_DIRand makes that build dir writable by the job user. manifold's writes are relativeos.MkdirAll("data"|"analysis"|"public")calls under that dir, so the collect -> analyze -> render -> pages chain still works. No deploy.yml change needed.
Drop dead go mod download
go.mod has no require block (pure standard library, no go.sum), so RUN go mod download fetched nothing. Removed it and the now-pointless COPY go.mod go.sum* that fed it.
Proof
Built the image with podman and ran it unprivileged:
idinside the container:uid=65534(nobody) gid=65534(nobody).manifold versionruns as nobody and prints the build-arg version.- Full collect -> analyze -> render chain (using
generatefor synthetic input) writing to bind-mounted dirs as nobody: 200 users generated, scored, and rendered toindex.html+_headers+assets/+data/. - Relative-path
MkdirAll/write inside/workspaceas nobody (the deploy.yml mechanism) succeeds. - Build log confirms
go mod downloadis gone.
Go validation (binary unchanged): go vet clean, go test -race pass, coverage 69.9% (floor 65%), golangci-lint run 0 issues.