Skip to content

Build a Small Docker Image with Multi-Stage Builds

A multi-stage Docker build keeps compilers and build tools out of the final runtime image. This guide builds a small Go program in a builder image, copies the compiled binary into an empty scratch image, and sets a non-root runtime user. USER controls the process identity inside the image; it does not make the Docker daemon rootless.

The commands below use a Linux shell and Docker BuildKit. They build a local demonstration image and do not publish it. Docker image tags can move over time, so use an approved, maintained base-image digest for production builds.


01

Prepare the Source Files

Workspace

Create a new directory, then add a Go program and a .dockerignore file. The ignore file keeps repository metadata and local output out of the build context.

Terminal window
project_dir="$PWD/docker-image-lab"
if [[ -e "$project_dir" || -L "$project_dir" ]]; then
printf 'Project path already exists; choose a fresh path.\n' >&2
exit 1
fi
mkdir -- "$project_dir" || exit 1
cd -- "$project_dir" || exit 1
cat > main.go <<'EOF'
package main
import "fmt"
func main() {
fmt.Println("Hello from a small container")
}
EOF
cat > .dockerignore <<'EOF'
.git
*.tar
*.tar.gz
.env
EOF
❯ Illustrative Output (Results Vary)
docker-image-lab/
β”œβ”€β”€ .dockerignore
└── main.go

02

Write the Multi-Stage Dockerfile

Dockerfile

The first stage compiles the program with Go 1.27. The final scratch stage contains only the binary. The numeric user and group ID work without an /etc/passwd file in the empty runtime image.

FROM golang:1.27-alpine AS build
WORKDIR /src
COPY main.go .
RUN mkdir -p /out && CGO_ENABLED=0 GOOS=linux go build \
-trimpath -ldflags="-s -w" -o /out/hello ./main.go
FROM scratch
COPY --from=build /out/hello /hello
USER 65532:65532
ENTRYPOINT ["/hello"]
❯ Illustrative Output (Results Vary)
build stage: Go compiler and source
runtime stage: /hello only
runtime user: 65532:65532

03

Build the Runtime Image

Build

Save the previous content as Dockerfile. The final stage is built by default because it is the last stage in the file. β€”pull asks Docker to check for an updated builder image matching the tag.

Terminal window
docker build --pull --tag hello-small:local .
❯ Illustrative Output (Results Vary)
[+] Building ...
=> [build 1/3] FROM docker.io/library/golang:1.27-alpine
=> [build 3/3] RUN CGO_ENABLED=0 GOOS=linux go build ...
=> [stage-1 2/2] COPY --from=build /out/hello /hello
=> naming to docker.io/library/hello-small:local

04

Check the Configured User and Run the Program

Verify

Inspect the image’s configured user, then run the program with a read-only root filesystem, no Linux capabilities, and no-new-privileges. These flags suit this simple program because it only writes to standard output.

Terminal window
docker image inspect --format '{{.Config.User}}' hello-small:local
docker run --rm --read-only --cap-drop=ALL \
--security-opt=no-new-privileges hello-small:local
❯ Illustrative Output (Results Vary)
65532:65532
Hello from a small container

05

Keep the Build Reproducible and Reviewable

Release Readiness

Record the source revision, builder and runtime base-image digests, build command, and image digest with the release. Scan the image with the tool approved by your organization, then rebuild from reviewed base images and dependencies on a patch schedule.

Terminal window
docker image inspect --format '{{.Id}}' hello-small:local
docker image ls hello-small:local
❯ Illustrative Output (Results Vary)
sha256:<local image ID varies by build>

Comments