Go produces statically-linked binaries by default if you disable CGO. That makes Go uniquely well-suited to the absolute-smallest Docker image: build the binary in a Go image, copy just the binary into FROM scratch, and ship a 10-15 MB image with literally nothing else in it. No shell, no glibc, no userland — just the binary and its environment.
A working Dockerfile
# Build stage
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app ./cmd/app
# Runtime stage
FROM scratch
COPY --from=build /out/app /app
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/app"]Build:
docker build -t my-go-app .
docker images my-go-app
# REPOSITORY TAG SIZE
# my-go-app latest 11.3MBThat's the whole thing. Run it:
docker run -d --name my-go-app -p 8080:8080 my-go-appWhy CGO_ENABLED=0 matters
Go's default build dynamically links against the C library (glibc on Debian/Alpine). That means the resulting binary needs a libc inside the container, which means you can't use FROM scratch.
CGO_ENABLED=0 disables CGO entirely. Go uses its own implementations of DNS resolution, file operations, etc., and the resulting binary has no C-library dependencies. That binary runs in a fully empty image.
The exception: if your app imports a package that uses CGO (like older versions of mattn/go-sqlite3), you can't disable CGO and you need a glibc-bearing base image like distroless/base or gcr.io/distroless/static-debian12.
The three things to copy in (besides the binary)
FROM scratch is completely empty. Even basic things you take for granted aren't there. The three you usually need:
ca-certificates.crt— for HTTPS calls. Without it, everyhttp.Clientrequest to an HTTPS URL fails with "x509: certificate signed by unknown authority." Copy from the build image./usr/share/zoneinfo— for time-zone-aware code. Without it,time.LoadLocation("America/New_York")returns an error.- A non-root user.
USER 65534:65534(the well-known nobody UID) works in scratch since there's no/etc/passwdto consult — Docker just runs the process as UID 65534. The kernel doesn't care that there's no user record.
The -ldflags="-s -w" flag strips symbol tables and debug info. Shaves a few MB off the binary.
Alternative: distroless
If FROM scratch is too sparse (you really want ca-certificates and timezone data without copying them manually, or you have CGO dependencies), Google's distroless images are the next tier up.
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
EXPOSE 8080
ENTRYPOINT ["/app"]distroless/static-debian12:nonroot ships:
- A non-root user already set up.
ca-certificates,tzdata, and a few other essentials.- No shell, no package manager, no useless binaries.
- About 2 MB on top of your Go binary.
distroless/static is for pure static binaries (CGO_ENABLED=0). distroless/base-debian12 includes glibc for CGO-using binaries.
I default to scratch + the manual file copies above for "absolutely smallest"; distroless for "I want some niceties without giving up much size."
Layer-cache pattern
Same idea as Node/Python: copy go.mod and go.sum first, run go mod download, then copy the source. Source edits don't bust the dependency-download layer.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build ...Without this, every source change re-downloads every module. With it, the download layer caches until go.mod or go.sum changes.
Building for multiple architectures
Go cross-compiles trivially. To build for both amd64 and arm64 in one go:
docker buildx build --platform linux/amd64,linux/arm64 -t my-go-app --push .The Dockerfile doesn't change — Go picks up the target architecture from the build environment. See docker buildx: Multi-Architecture Builds.
Practical usage: a small HTTP server
main.go:
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"ok":true}`)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello from Go in Docker")
})
port := os.Getenv("PORT")
if port == "" { port = "8080" }
http.ListenAndServe(":"+port, nil)
}Build and run with the Dockerfile above:
docker build -t my-go-app .
docker run -d --name my-go-app -p 8080:8080 my-go-app
curl http://localhost:8080/healthz
# {"ok":true}Image size: about 11 MB. The Go binary itself is about 8 MB after -ldflags="-s -w"; the rest is the ca-certificates file and the timezone data.
Compose
services:
app:
build: .
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://postgres:hello@db:5432/myapp
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: hello
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pg-data:Common pitfalls
- HTTPS calls fail with "certificate signed by unknown authority." You forgot to copy
ca-certificates.crtinto the scratch image. time.LoadLocation("America/New_York")returns an error. Missing zoneinfo. Copy/usr/share/zoneinfofrom the build stage, or import_ "time/tzdata"in your Go code (embeds the data into the binary at ~500 KB cost).- CGO surprise. A dependency uses CGO and your
CGO_ENABLED=0build fails. Either find a pure-Go alternative, or switch todistroless/baseand don't disable CGO. - Building on Apple Silicon without setting GOOS/GOARCH. The host's Docker build can produce arm64 images by default; if your target is amd64 servers, set the platform explicitly with
--platform linux/amd64. - No
USERin scratch image. Running as root inside scratch isn't a security gain — there's nothing to attack. But settingUSER 65534:65534is one line and good hygiene.
What to do next
- Docker Image Size Optimization — going further across other languages.
- docker buildx: Multi-Architecture Builds — building Go images for arm64 + amd64.
- How to Write a Dockerfile — fundamentals.
FAQ
Sources
Authoritative references this article was fact-checked against.
- Official Go image — Docker Hubhub.docker.com
- Distroless images — GoogleContainerToolsgithub.com

