DEV Community

Peon Sh
Peon Sh

Posted on Originally published at peon.sh

Deploying Go Applications: The Smallest Docker Images You’ll Ever Ship

Go compiles to a single static binary, perfect for containers. Build 10 MB images and deploy them to your VPS with zero-downtime rollouts.

Go was made for this
Go compiles to a single statically linked binary with no runtime, no interpreter and no system dependencies. That makes it the best-case scenario for containerization: the final image is your binary plus a few kilobytes of metadata, it starts in milliseconds, and there is no dependency tree to patch. Where a Node image fights to get under 150 MB, a Go image lands under 15 MB without trying.

The production Dockerfile
CGO_ENABLED=0 forces pure-Go networking and DNS, making the binary truly static
-ldflags="-s -w" strips debug symbols, roughly 30% smaller binaries
Copying go.mod/go.sum before the source means dependency downloads cache across code changes, keeping rebuilds in the seconds

FROM golang:1.24-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download # cached layer: re-runs only when deps change
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
EXPOSE 8080
ENTRYPOINT ["/app"]
Why distroless beats scratch
Pure FROM scratch images work until they mysteriously do not: HTTPS calls fail because there are no CA certificates, and time handling misbehaves because there is no tzdata. Distroless static includes CA certs, tzdata and a nonroot user in about 2 MB, eliminating the whole class of "works locally, fails in prod" surprises while staying effectively as small.

Graceful shutdown and health
Zero-downtime deploys need the binary to cooperate on two points, both a few lines in Go:

Add a /healthz handler that verifies dependencies (DB ping) and wire it as the container health check
With both in place, rollouts overlap old and new containers with zero dropped requests

srv := &http.Server{Addr: ":8080", Handler: mux}
go srv.ListenAndServe()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGTERM, os.Interrupt)
<-stop
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx) // stop accepting, drain in-flight requests

Deploy and operate
Point the Git repo at your server as a Peon service: builds take seconds (module cache plus tiny images), deploys and rollbacks are near-instant because moving 10 MB is nothing, and a 2 GB VPS hosts a small fleet of Go services, each idling at 10 to 30 MB of RSS. Cross-compilation is also trivial if you ever target ARM servers: set GOARCH=arm64 and the same Dockerfile works on a Hetzner CAX or a Raspberry Pi.

Top comments (0)