Go Docker
Build small, secure, production-ready containers. Static binaries, distroless images, non-root users.
Contents
- Decision Framework: Base Image Selection
- Pattern 1: Multi-Stage Build (Production-Ready)
- Pattern 2: Layer Caching Optimization
- Pattern 3: .dockerignore (Reduce Build Context)
- Pattern 4: Build Arguments and Metadata
- Pattern 5: Non-Root User Security
- Pattern 6: Health Checks
- Anti-Patterns
- Additional Resources
Note: Examples use golang:1.24-alpine — always substitute the latest stable Go release.
Decision Framework: Base Image Selection
| Base Image | Size | Use Case | Security |
|---|---|---|---|
| scratch | ~2MB | Static binaries only (CGO_ENABLED=0) | Minimal attack surface |
| distroless/static | ~2MB | Static binaries with better debugging | Minimal, no shell |
| distroless/base | ~20MB | CGO binaries, need libc | Minimal, no shell |
| alpine | ~5MB | Need shell/debugging tools | Small but has shell |
| debian:slim | ~70MB | Complex dependencies, debugging | Full OS tools |
Decision Rule: Use distroless/static for production (security + small size). Use alpine for development/debugging.
Pattern 1: Multi-Stage Build (Production-Ready)
Build in one stage, run in minimal distroless image.
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM golang:1.24-alpine AS builder
WORKDIR /build
# Copy dependency files first (layer caching)
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build static binary
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags='-w -s -extldflags "-static"' \
-o app \
./cmd/api
# Stage 2: Runtime
FROM gcr.io/distroless/static:nonroot
# Copy binary from builder
COPY --from=builder /build/app /app
# Use non-root user (automatically provided by distroless:nonroot)
USER nonroot:nonroot
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/app", "healthcheck"]
# Run application
ENTRYPOINT ["/app"]
Build flags explained:
CGO_ENABLED=0- Build static binary (no C dependencies)-ldflags='-w -s'- Strip debug info and symbol table (smaller binary)-extldflags "-static"- Force static linking
Rules:
- Use multi-stage builds (builder + runtime)
- Build in golang image, run in distroless/static
- Copy only the binary to runtime image
- Use nonroot user for security
Size impact: Full golang image ~1.2GB → multi-stage distroless ~10-20MB (95% smaller).
Pattern 2: Layer Caching Optimization
Order Dockerfile instructions for maximum cache reuse.
FROM golang:1.24-alpine AS builder
WORKDIR /build
# 1. Copy dependency files FIRST (changes infrequently)
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# 2. Copy source code LAST (changes frequently)
COPY . .
# 3. Build
RUN CGO_ENABLED=0 go build -o app ./cmd/api
Build time impact: Code-only changes reuse cached dependency layers (~5s vs ~60s full build).
Pattern 3: .dockerignore (Reduce Build Context)
Exclude unnecessary files from build context.
# .dockerignore
# Git
.git
.gitignore
.github
# Development
.vscode
.idea
*.swp
*.swo
# Documentation
README.md
docs/
*.md
# Testing
*_test.go
testdata/
coverage.out
# Build artifacts
bin/
dist/
*.exe
# Environment
.env
.env.local
*.pem
*.key
# Dependencies (downloaded in container)
vendor/
# CI/CD
.gitlab-ci.yml
.circleci/
Jenkinsfile
# Docker
Dockerfile
docker-compose.yml
Pattern 4: Build Arguments and Metadata
Use build args for versioning and configuration.
FROM golang:1.24-alpine AS builder
# Build arguments
ARG VERSION=dev
ARG BUILD_DATE
ARG GIT_COMMIT
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Inject build metadata via ldflags
RUN CGO_ENABLED=0 go build \
-ldflags="-w -s \
-X main.Version=${VERSION} \
-X main.BuildDate=${BUILD_DATE} \
-X main.GitCommit=${GIT_COMMIT}" \
-o app ./cmd/api
FROM gcr.io/distroless/static:nonroot
# Labels for metadata (OCI standard)
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.created="${BUILD_DATE}"
LABEL org.opencontainers.image.revision="${GIT_COMMIT}"
LABEL org.opencontainers.image.title="My API"
LABEL org.opencontainers.image.description="Production API service"
COPY --from=builder /build/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
Build command:
docker build \
--build-arg VERSION=$(git describe --tags --always) \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg GIT_COMMIT=$(git rev-parse HEAD) \
-t myapi:latest \
.
Pattern 5: Non-Root User Security
Always run containers as non-root user.
# Using distroless:nonroot (user built-in)
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /build/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
# Using Alpine (create user manually)
FROM alpine:3.19
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /build/app /app
USER appuser:appgroup
ENTRYPOINT ["/app"]
# Using scratch (copy from builder with --chown)
FROM scratch
COPY --from=builder --chown=65532:65532 /build/app /app
USER 65532:65532
ENTRYPOINT ["/app"]
User ID 65532:
- Standard non-root user ID
- Used by distroless images
- Recognized by Kubernetes security contexts
Pattern 6: Health Checks
Define health checks in Dockerfile for container orchestration.
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /build/app /app
USER nonroot:nonroot
EXPOSE 8080
# Health check using built-in endpoint
HEALTHCHECK --interval=30s \
--timeout=3s \
--start-period=5s \
--retries=3 \
CMD ["/app", "healthcheck"]
ENTRYPOINT ["/app"]
Health check in Go code:
func main() {
if len(os.Args) > 1 && os.Args[1] == "healthcheck" {
healthCheck()
return
}
// Normal application startup
startServer()
}
func healthCheck() {
resp, err := http.Get("http://localhost:8080/health")
if err != nil || resp.StatusCode != http.StatusOK {
os.Exit(1)
}
os.Exit(0)
}
Anti-Patterns
Not pinning base image versions
# BAD: Unpredictable builds
FROM golang:latest
FROM alpine
# GOOD: Pinned versions
FROM golang:1.24.5-alpine3.19
FROM gcr.io/distroless/static:nonroot-amd64@sha256:abc123...
Exposing secrets in layers
# BAD: Secret persists in layer history!
ARG GITHUB_TOKEN
ENV GITHUB_TOKEN=${GITHUB_TOKEN}
RUN git clone https://${GITHUB_TOKEN}@github.com/private/repo.git
# GOOD: Use --mount=type=secret
RUN --mount=type=secret,id=github_token \
git clone https://$(cat /run/secrets/github_token)@github.com/private/repo.git
Installing unnecessary packages in runtime
# BAD: Bloated runtime image
FROM alpine
RUN apk add --no-cache bash curl wget vim git
COPY app /app
# GOOD: Use distroless (see Pattern 1)
Additional Resources
- For handling private Go modules in Docker builds, see private-modules.md
- For Makefile targets and CI/CD security scanning, see ci-cd.md
