Verwendung
- Installationsbefehl kopieren und das Quellpaket vor der Installation in der eigenen Agentenumgebung prüfen.
- Benötigte Werkzeuge, Zugangsdaten und Hosts im Original prüfen.
- Eingabe und gewünschtes Ergebnis beschreiben und die Ausgabe mit den Quellen abgleichen.
npx skills add https://github.com/victorzhuk/skills --skill z-go-dockerfileVor dem Start
- Die Installation umfasst weder Modellabo noch API-Kontingent oder Dateizugriffsrechte.
- Sterne und Installationen zeigen Aktivität, keine unabhängige Qualitäts- oder Sicherheitsprüfung.
Aus dem Quell-Repository gerendert; Überschriften, Beispiele, Code, Tabellen, Links und Bilder bleiben erhalten.
Go Dockerfile
Multi-stage build: a deps stage that caches modules, a build stage that compiles a static binary, and a minimal final stage that only carries the binary. Every layer earns its place — no shell, no package manager, no root user in the shipped image unless a specific need demands it.
Final image choice
| Base | Certs & tzdata | Shell / exec debugging | Reach for it when |
|---|---|---|---|
gcr.io/distroless/static-debian13:nonroot | bundled (ca-certificates + tzdata) | none in :nonroot; swap to the :debug tag (busybox shell) to triage | default for a CGO_ENABLED=0 Go binary |
scratch | none — COPY both from the builder yourself | none, ever — no :debug variant exists | absolute minimal footprint is a hard requirement and you're fine hand-rolling certs/tzdata |
alpine | apk add --no-cache ca-certificates tzdata (neither ships by default) | ash shell + apk built in | you need a shell for an entrypoint script, kubectl exec triage, or a musl-linked CGO build |
distroless/static is the sane default: it already carries what scratch forces you to reinvent, and it's smaller than alpine with a strictly smaller attack surface (no shell, no package manager). Reach for scratch only when a security policy mandates zero non-binary content, and for alpine only when something in the running container genuinely needs a shell.
Reference Dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.26-bookworm AS deps
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
FROM deps AS build
ARG VERSION=dev
ARG GIT_COMMIT=none
ARG BUILD_DATE=
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -trimpath \
-ldflags="-s -w -X main.Version=${VERSION} -X main.CommitHash=${GIT_COMMIT} -X main.BuildDate=${BUILD_DATE}" \
-o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian13:nonroot
COPY --from=build /out/app /app
USER 65532:65532
ENTRYPOINT ["/app"]Numeric UID, not the nonroot name: a Pod with securityContext.runAsNonRoot: true fails to start against a string USER — Kubernetes can't verify non-root status from a name, only a number, and 65532 is the exact UID the :nonroot tag already sets up.
go.mod/go.sum land in their own COPY before the rest of the source, so go mod download only reruns when dependencies actually change — editing a .go file never invalidates that layer.
BuildKit cache mounts
--mount=type=cache,target=... needs the # syntax=docker/dockerfile:1 directive on line one; without it the mount flag is silently unsupported. Two mounts cover the two Go caches:
/go/pkg/mod—GOMODCACHE, the default under the officialgolangimage'sGOPATH=/go./root/.cache/go-build—GOCACHE, the compiler's build-artifact cache (builder stage runs as root, hence/root).
These caches persist across builds on the same daemon/builder — a cold CI runner still pays the full cost, but a warm local Docker daemon or a CI cache-mount backend (e.g. --cache-from/--cache-to in docker buildx build) turns a go build layer into an incremental one. This is the default accelerator, not optional hardening: skip it and every source-touching rebuild redownloads and recompiles the entire module graph.
Version stamping
-X main.Version / main.CommitHash / main.BuildDate mirror the -ldflags convention in [[z-go-makefile]] — same three variable names in package main, so a local make build and the image produced by this Dockerfile report identical version strings. Pass all three at build time: docker build --build-arg VERSION=$(git describe --tags --always --dirty) --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) --build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) .; the pipeline step that computes and passes them belongs in [[z-go-ci]].
-s -w strips the symbol table and DWARF debug info — smaller binary, but dlv can't symbolicate it. If a container image genuinely needs live delve debugging, build a second, unstripped target for that image; don't drop -s -w from the production build to accommodate a debugging session that happens rarely.
ca-certificates and tzdata on scratch
scratch starts from nothing: no /etc/ssl/certs, no /usr/share/zoneinfo, no /etc/passwd. Any outbound TLS call or time.LoadLocation fails without an explicit copy from the builder:
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfodistroless/static already bundles both — this is the main reason it beats scratch for anything that makes an HTTPS call or handles time zones.
Healthcheck reality
Docker's HEALTHCHECK CMD instruction needs a shell or an exec-able binary in the image, and it's meaningless in Kubernetes — the kubelet drives livenessProbe/readinessProbe/startupProbe from the Pod spec and never reads the image's HEALTHCHECK metadata. Define probes there (httpGet, grpc, or exec against a tiny health binary), not in the Dockerfile. A Dockerfile HEALTHCHECK only matters for docker run/Compose/Swarm, and on scratch/distroless:nonroot there's no shell for the shell form anyway.
.dockerignore essentials
.git
bin/
dist/
*.md
.env
.env.*
.github/
testdata/
compose.override.ymlDon't ignore vendor/ if the module actually vendors dependencies — the build stage needs it. Ignoring .git matters most: without it, every commit invalidates Docker's build-context cache even when no tracked file changed.
Multi-arch
BuildKit auto-populates TARGETOS/TARGETARCH for cross-compiling the same Dockerfile to linux/arm64 and linux/amd64:
FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS build
ARG TARGETOS
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/app ./cmd/app--platform=$BUILDPLATFORM pins the builder stage to the host's own architecture — Go then cross-compiles natively via GOOS/GOARCH with no QEMU emulation of the build step. Drop that pin and buildx --platform linux/arm64 on an amd64 host emulates the entire builder under QEMU instead. The docker buildx build --platform linux/amd64,linux/arm64 invocation and the CI job that runs it belong in [[z-go-ci]]; this Dockerfile only needs the platform pin and the two ARGs to be cross-compile-ready.
Local Compose stack
Commit a compose.yml for local dev — one file, docker compose up gives the service a real database and cache:
services:
app:
build:
context: .
args:
VERSION: ${VERSION:-dev}
GIT_COMMIT: ${GIT_COMMIT:-none}
BUILD_DATE: ${BUILD_DATE:-}
env_file: [.env]
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5Expose the 9090 management port only on explicit opt-in, in a companion file named compose.mgmt.yml — do NOT call it compose.override.yml, that exact name auto-loads and would publish 9090 on every plain up:
services:
app:
ports: ["9090:9090"]A plain docker compose up keeps 9090 private — no host access; docker compose -f compose.yml -f compose.mgmt.yml up adds it. An optional otel profile adds an otel-collector service for local trace inspection. The build.args names (VERSION/GIT_COMMIT/BUILD_DATE) mirror the stamping vars in [[z-go-makefile]].
Production Compose is not this file: it uses the [[z-selfhost-deploy]] patterns — pinned image tag, no build:, restart: unless-stopped. This compose is local dev only.
Container healthcheck: prefer the app's own subcommand — test: ["CMD", "/app", "healthcheck"] — or fall back to wget -qO- http://localhost:8080/healthz (see [[z-go-observability]]). Minimal images (scratch, distroless:nonroot) ship no curl and usually no wget; a shell-form HEALTHCHECK fails outright there.
Do not
- Copy the full source tree before
go mod download— it collapses the deps-cache layer into the source layer, so every code change re-downloads every module. - Ship
alpineor a fullgolangimage to production because a shell "might be useful someday" — that's exactly the attack surfacedistrolessremoves. - Add a Dockerfile
HEALTHCHECKas a substitute for a Kubernetes probe — the kubelet never reads it. - Run the final stage as root —
distroless/static-debian13:nonrootand an explicitUSER nonroot:nonroot/numeric UID exist precisely so you don't have to. - Strip
-s -wfrom every build "just in case delve is needed" — keep it in prod, build a separate debug target for the rare session that needs symbols.
Verify
DOCKER_BUILDKIT=1 docker build -t app:local .
docker run --rm app:local # exits/serves without a missing-cert or missing-tzdata error
docker inspect app:local --format '{{.Config.User}}' # 65532:65532, not root/empty
docker history app:local --no-trunc | head # deps layer cached, no shell/package manager baked in
