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.
Prepare the Source Files
WorkspaceCreate 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.
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 1fimkdir -- "$project_dir" || exit 1cd -- "$project_dir" || exit 1cat > main.go <<'EOF'package main
import "fmt"
func main() { fmt.Println("Hello from a small container")}EOFcat > .dockerignore <<'EOF'.git*.tar*.tar.gz.envEOFβ― Illustrative Output (Results Vary)
docker-image-lab/βββ .dockerignoreβββ main.goWrite the Multi-Stage Dockerfile
DockerfileThe 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 buildWORKDIR /srcCOPY main.go .RUN mkdir -p /out && CGO_ENABLED=0 GOOS=linux go build \ -trimpath -ldflags="-s -w" -o /out/hello ./main.go
FROM scratchCOPY --from=build /out/hello /helloUSER 65532:65532ENTRYPOINT ["/hello"]β― Illustrative Output (Results Vary)
build stage: Go compiler and sourceruntime stage: /hello onlyruntime user: 65532:65532Build the Runtime Image
BuildSave 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.
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:localCheck the Configured User and Run the Program
VerifyInspect 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.
docker image inspect --format '{{.Config.User}}' hello-small:localdocker run --rm --read-only --cap-drop=ALL \ --security-opt=no-new-privileges hello-small:localβ― Illustrative Output (Results Vary)
65532:65532Hello from a small containerKeep the Build Reproducible and Reviewable
Release ReadinessRecord 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.
docker image inspect --format '{{.Id}}' hello-small:localdocker image ls hello-small:localβ― Illustrative Output (Results Vary)
sha256:<local image ID varies by build>