Smaller Go Docker images with multi-stage builds

Shrink a Go service image with Docker multi-stage builds: compile in a golang image, then copy only the static binary into a minimal runtime such as Alpine so the final image stays small for deploy and bandwidth.

Published

Updated

Read time 2 min read

Reviewed byDeepak Prasad

Smaller Go Docker images with multi-stage builds

Packaging a Go HTTP server in Docker starts with a single-stage image that includes the compiler; that is convenient for development but often hundreds of megabytes on disk. A golang Docker multi-stage build compiles in one stage, then copies the server binary into a slim runtime such as Alpine, which typically shrinks the artifact to tens of megabytes depending on base tags and cgo. For background on images and containers, see Docker containers and keeping containers running.

Tested with Go 1.24 and Docker on Linux; exact image sizes depend on registry tags and cache.


Prerequisites


Minimal HTTP app

Create a directory, add main.go, and run go mod init (custom modules; if go.mod is missing you may see go.mod not found).

go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hello from Go Docker multistage")
}

func main() {
	http.HandleFunc("/", handler)
	fmt.Println("server running at 0.0.0.0:8080")
	log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))
}

Run with go run . and open port 8080 on the host to confirm the handler text.


Single-stage Dockerfile (large)

dockerfile
FROM golang:1.24-alpine
WORKDIR /src/app
COPY go.mod main.go ./
RUN go build -o /server
CMD ["/server"]

docker build -t go-single . then docker images shows a large image because the compiler and module layers remain in the final layer stack. Docker build without cache is useful when you need a clean rebuild.


Multi-stage Dockerfile (small runtime)

dockerfile
FROM golang:1.24-alpine AS builder
WORKDIR /src/app
COPY go.mod main.go ./
RUN CGO_ENABLED=0 go build -o server

FROM alpine:3.20
WORKDIR /root/
COPY --from=builder /src/app/server ./server
EXPOSE 8080
CMD ["./server"]

The first stage produces server; the second stage is a minimal OS that only receives the binary. CGO_ENABLED=0 avoids libc linkage when you target musl/glibc mix—adjust if you need cgo.

Build and run:

text
docker build -t go-multistage .
docker run --rm -p 8080:8080 go-multistage

Map host port to container port the same way as other published services (open ports on Linux).

Compare sizes with docker images | grep go-multistage; the final tag should be far smaller than a single FROM golang stage on the same machine.


Summary

A golang Docker multi-stage build separates compile from run: the builder stage has Go tooling; the runtime stage only needs the static-ish binary and a tiny base. That cuts download size and attack surface compared to shipping the full SDK in production.


References


Frequently Asked Questions

1. Why is the official golang image large for production?

It carries the full compiler, toolchain, and module cache layers you only need at build time, not at runtime.

2. What does COPY --from=builder do?

It copies artifacts from an earlier build stage into the current stage, so the final image can omit compilers and source.

3. Should I use scratch or Alpine for the runtime stage?

Alpine includes libc and shell for debugging; scratch is smaller but requires a fully static binary and more care for DNS and TLS roots.

4. How do I publish a smaller image without multi-stage?

You can still trim base tags (for example golang:alpine) and strip symbols, but multi-stage is the usual way to drop build tooling entirely.

5. Where can I read more about Docker builds?

See Docker build examples and container basics on the site for cache, tags, and lifecycle.
Antony Shikubu

Systems Integration Engineer

Highly skilled software developer with expertise in Python, Golang, and AWS cloud services.