Go Dockerfile with Alpine Runtime: Multi-Stage, Non-Root

This Go Dockerfile builds a static binary on golang:1.25-alpine and runs it on a small alpine:3.22 image. The runtime has a shell, CA certificates and time zone data, and a dedicated non-root user. Choose it when you want a tiny image but still need to exec into the container to debug. The service listens on port 8080.

CI status
Build passing
Image size (uncompressed)
14.8 MB
Build time
30 s
Runtime image
alpine:3.22
Port
8080
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.25-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/server

FROM alpine:3.22
RUN apk add --no-cache ca-certificates tzdata \
 && adduser -D -u 10001 app
COPY --from=build /out/app /usr/local/bin/app
USER app
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/app"]
.dockerignore
.git
bin/
*.test
.env
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM golang:1.25-alpine AS build

    The full Go toolchain lives only in this stage. Nothing from it is shipped except the compiled binary.

  2. COPY go.mod go.sum ./ RUN go mod download

    Dependencies are downloaded in their own cached layer before the source code is copied, so code edits rebuild quickly.

  3. RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/server

    Produces a static, stripped binary with no local paths embedded. Being static, it runs on any Linux base image regardless of the C library.

  4. FROM alpine:3.22

    Alpine is one of the smallest general-purpose bases. Unlike scratch or distroless, it includes a shell and the apk package manager for debugging and extra tools.

  5. RUN apk add --no-cache ca-certificates tzdata \

    Adds the certificate bundle for outgoing HTTPS and the time zone database. --no-cache keeps apk's index out of the image.

  6. && adduser -D -u 10001 app

    Creates a passwordless user with a fixed UID in the same layer, so file ownership stays predictable across image rebuilds.

  7. COPY --from=build /out/app /usr/local/bin/app

    Places the binary on the PATH, so you can also run it by name when you exec into the container.

  8. USER app EXPOSE 8080 ENTRYPOINT ["/usr/local/bin/app"]

    Drops root privileges, documents the listening port, and starts the binary as the main process so it receives stop signals.

Build and run

terminal
docker build -t go-app .
docker run --rm -p 8080:8080 go-app

Run the build from the module root. Because Alpine has a shell, docker exec -it <container> sh lets you inspect files, environment variables and network settings while it runs.

Other Go Dockerfile variants

Alpine trades a few extra megabytes for a shell and package manager, which is handy while debugging. Distroless static and scratch are smaller and expose less, but you can't open a shell inside them. The table below compares the measured sizes of all three.

VariantRuntime imageImage sizeCI
Alpine runtime (shell available)alpine:3.2214.8 MBPassing
Distroless static (nonroot)gcr.io/distroless/static-debian12:nonroot7.7 MBPassing
Scratch (static, nonroot UID)scratch6.2 MBPassing

Troubleshooting

exec /usr/local/bin/app: no such file or directory
The binary was built with cgo against glibc, which Alpine doesn't use. Keep CGO_ENABLED=0, or install the C libraries your build needs in both stages.
x509: certificate signed by unknown authority
The ca-certificates package is missing from the runtime stage. Keep it in the apk add line so outgoing TLS connections can be verified.
permission denied when the app writes a file
The app user can't write to root-owned paths. Write to a directory you create and chown to app in the Dockerfile, or mount a volume.

FAQ

When should I choose Alpine over distroless for Go?
Pick Alpine when you or your platform need a shell inside the container, for example for debugging, init scripts or extra command-line tools. Pick distroless when minimal attack surface matters most.
Does Alpine's musl libc affect Go?
Not for static binaries built with CGO_ENABLED=0. They contain their own implementations and don't call into the C library. musl only matters when cgo is enabled.
How do I add curl for health checks?
Add curl to the apk add line in the runtime stage. Many orchestrators can probe the HTTP port directly, though, which avoids adding tools to the image.