Docker / Build evidence and production operations / Lesson 13
Multi-stage builds and reproducible release inputs
Separate toolchains from runtime artifacts and distinguish a small image from a reproducible build.
On this page
Lesson goals
- Trace named stages and artifact copies through a complete Go fixture.
- Identify the inputs needed for repeatable content and reliable rebuilds.
A build environment is not a runtime requirement
A compiler, package manager, and source tree may be necessary to produce an application but unnecessary to serve requests. Multi-stage builds give those responsibilities separate filesystem roots. COPY --from transfers selected artifacts rather than the entire toolchain. This reduces runtime content and accidental exposure, yet it does not automatically prove the copied binary is trustworthy, free of vulnerabilities, or reproducible from reviewed source. Stage boundaries need the same dependency and provenance review as ordinary builds.
Make the compiled fixture complete
This lesson introduces a separate standard-library Go fixture, not a hidden rewrite of the Python application. Save the two following files together in a new disposable source directory. It returns parcel-go over HTTP on port 8080 and imports no third-party modules. The module path is synthetic. These files define the source input explicitly so the later stage copy can be reasoned about without inventing a prebuilt executable.
module example.test/parcel
go 1.26
The language directive requests the Go 1.26 language baseline; the selected toolchain image must satisfy it. There are no external module requirements in this fixture, so no invented dependency hashes or network-resolved packages are needed.
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "parcel-go")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
The server stays in the foreground and prints parcel-go in a successful response body. This source is presented for static explanation only. No compilation, image build, request, or runtime success is claimed. A real service also needs bounded server timeouts and shutdown behavior beyond this minimal fixture.
Copy only the intended runtime artifact
The build stage compiles with CGO disabled and places the executable at a known path. The runtime stage starts from scratch and receives only that executable. Numeric non-root identity avoids depending on a user database in the empty base. The example needs no shell to start. A scratch image has no normal shell, package manager, CA bundle, or timezone database, so workloads requiring those facilities must supply them deliberately or select another reviewed runtime base.
# syntax=docker/dockerfile:1
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY go.mod main.go ./
RUN CGO_ENABLED=0 go build -trimpath -o /out/parcel ./
FROM scratch AS runtime
COPY --from=build /out/parcel /parcel
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/parcel"]
COPY --from=build reads /out/parcel from the named build stage and places it at /parcel. The final entrypoint references that exact path. The readable toolchain tag is mutable teaching input, so this recipe is not a claim of byte-identical release reproduction until base digest, toolchain, source, and other build inputs are fixed.
Pin identity without freezing security updates forever
Reproducibility requires controlled source, resolved base digests, dependency locks, toolchain versions, build arguments, target platform, and relevant environment inputs. A deterministic-looking Dockerfile can still fetch changing network content. Pinning enables review and comparison but also requires a policy for timely updates. Resolve real digests from trusted metadata; never paste a fabricated hash merely to make an example appear immutable. Rebuild and compare through a controlled pipeline before asserting reproducibility.
Verify artifacts at the stage boundary
Check the final image's executable path, architecture, runtime dependencies, user, and expected network behavior. Confirm build-only files and credentials are absent from the final content. A smaller image may make debugging harder, so plan external observability and approved diagnostics rather than inserting a shell after release. Keep source-to-artifact provenance and test results with the resolved digest so rollback selects known content instead of reconstructing yesterday's mutable inputs.
Common errors and fixes
COPY --from reads the wrong stage or missing artifact.
- Why it happens
- The stage name or output path differs from the compiler's actual destination.
- How to fix it
- Align the named stage and exact output location before changing the runtime base.
- Prevent it next time
- Treat artifact paths as explicit contracts between stages.
A scratch executable fails because a runtime dependency is missing.
- Why it happens
- The binary was not self-contained or needed data such as trust certificates.
- How to fix it
- Review linking and required runtime files, then supply only approved dependencies.
- Prevent it next time
- Test the final image rather than only the build stage.
Rebuilding the same recipe produces different content.
- Why it happens
- Mutable base tags, network inputs, platform, or build environment changed.
- How to fix it
- Record and control every relevant input before diagnosing nondeterminism.
- Prevent it next time
- Keep repeatability evidence separate from an image-size claim.
Check your understanding
1. Does COPY --from=build /out/parcel /parcel copy the compiler too?
Show worked solution
No. It selects only that source artifact. The final stage starts from scratch, so its filesystem does not automatically inherit the build stage's tools. The application can still need runtime data or dynamic libraries, which must be assessed independently of the copy instruction.
2. Why does -trimpath alone not prove reproducibility?
Show worked solution
It removes certain local path information from Go build outputs, but does not pin the compiler, base image, source, dependencies, target platform, or external inputs. A reproducibility claim needs a controlled input set and actual comparison, not one promising compiler flag.
Key takeaways
- Multi-stage boundaries reduce runtime content and clarify responsibility.
- Reproducibility is an input-and-evidence property, not an image-size label.