The builder pattern separates build-time tools (compilers, test frameworks, package managers) from runtime dependencies, creating a natural dev-to-prod airlock that reduces production image size by 80-90%, eliminates build tools from running containers, enforces hermetic builds, and maintains FIPS compliance. A CleanStart builder image contains everything needed to compile; the -prod variant contains only runtime requirements.
Architecture: Dev Boundary to Prod Boundary
The multi-stage build process creates a clear separation between development and production boundaries. The workflow begins with source code from a git repository, which is then pulled into the builder stage. The builder stage uses the cleanstart-gcc:14-builder image containing gcc, make, git, and build tools, resulting in an uncompressed size of approximately 2.5GB.
In this stage, you copy the source code, run ./configure, compile with make -j8, execute tests with make test, and finally install the compiled binaries with make install PREFIX=/usr. The crucial step is that only the compiled binaries are copied to the next stage, not the entire builder image.
The production stage then starts with a fresh cleanstart-gcc:14-prod image containing only runtime libraries—compressed to about 80MB. This production image explicitly excludes gcc, make, git, and libtool. You copy only the compiled artifacts from the builder stage, set the non-root user (USER 65532), and define the application command. The final result is an image of 80-120MB, compared to 2.5GB for a single-stage approach—an approximate 30x size reduction that also dramatically improves security by eliminating all build tools from the runtime environment.
Why Builder Pattern Matters
Size Reduction
Single-stage approach includes gcc, make, autotools, pkg-config, git, man pages totaling 2.5GB. Multi-stage with -builder → -prod discards the builder layer and keeps only the 80MB prod layer, resulting in 96.8% size reduction.
Security Benefits
Running containers without build tools prevents attackers from compiling exploits at runtime, since the compiler is not available. The absence of a compiler in the container eliminates one major attack surface. Removing git history prevents attackers from accessing source code or version history if they compromise a running container. Fewer packages in production images means fewer vulnerabilities to patch and maintain.
Verification Properties
Builds are hermetic, meaning the same input always produces the same output. This reproducibility allows you to verify that the binary matches the source code. SLSA provenance documents the complete build history, proving the origin of artifacts.
Multi-Language Examples
Go Application
The builder stage uses cleanstart-golang:1.21-builder to download dependencies, verify them, and build the binary with CGO_ENABLED=0. The production stage uses cleanstart-golang:1.21-prod with non-root user and copies only the compiled binary.
Result: Builder is 500MB (Go SDK, build tools), Prod is 12MB (musl libc only, static binary), Final image is 12MB.
Python Application
The builder stage uses cleanstart-python:3.11-builder with a virtual environment to install Python packages. The production stage uses cleanstart-python:3.11-prod and copies the venv from the builder with source code.
Result: Builder is 800MB (Python dev tools, pip cache), Prod is 180MB (Python runtime + dependencies only), Final image is 180MB.
Node.js Application
The builder stage uses cleanstart-node:18-builder to install dependencies and build TypeScript or bundle. The production stage uses cleanstart-node:18-prod and copies only node_modules and dist from the builder.
Result: Builder is 1.2GB (Node, npm, dev deps, TypeScript), Prod is 220MB (Node runtime + prod dependencies only), Final image is 220MB.
C/C++ Application (Apache/NGINX)
The builder stage uses cleanstart-gcc:14-builder to download source, configure for production with static modules, and compile. The production stage uses cleanstart-gcc:14-prod and copies only the final binary and runtime libraries.
Result: Builder is 2.5GB (GCC, build tools, source), Prod is 110MB (runtime libs + Apache binary), Final image is 110MB.
Dockerfile Best Practices
Good: Minimal Builder Configuration
Copy only what's needed from the builder. Copy only binary and essential libs.
Bad: Copying Too Much
Copying entire builder filesystem defeats the purpose of multi-stage build.
Best: Explicit Artifact Selection
List exactly what's needed. Copy compiled binary, runtime configuration, and SBOM for attestation.
Size Comparison: Real Examples
NGINX
Build Approach | Size | Build Time |
|---|---|---|
Single-stage with -builder | 1.8GB | 8 min |
Multi-stage -builder → -prod | 85MB | 8 min + 2 sec pack |
Reduction | 95.3% | Same |
PostgreSQL
Build Approach | Size | Build Time |
|---|---|---|
Single-stage Debian | 450MB | 12 min |
Multi-stage -builder → -prod | 95MB | 12 min + 2 sec pack |
Reduction | 78.9% | Same |
Go Service
Build Approach | Size | Build Time |
|---|---|---|
Bitnami golang-builder | 750MB | 10 min |
Multi-stage -builder → -prod | 8MB | 10 min + 1 sec pack |
Reduction | 98.9% | Same |
Dev vs Prod Variant Selection
CleanStart provides both -dev and -prod variants. For development builds (has debug tools, source) use cleanstart-gcc:14-dev. For production builds (minimal, FIPS-verified, signed) use cleanstart-gcc:14-prod. For building other images use cleanstart-gcc:14-builder.
Usage: -builder always use as first stage. -prod always use as final stage. -dev only for development/debugging containers.
Handling Secrets in Builder Pattern
BAD: Secrets baked into image with COPY and available in final layer.
GOOD: Use build secrets (Docker BuildKit) where secrets mount at build time only (not in final layer).
Build with secret by creating the secret file, then building with docker build --secret buildkey=private.key -t myapp:latest ..
Hermetic Build Verification
Verify builds are hermetic (reproducible) by building the same Dockerfile twice, comparing digests (should be identical), and confirming if digests match that build is hermetic.
CI/CD Integration
GitHub Actions example builds with builder pattern using docker/build-push-action, explicitly using prod stage, and also building debug variant for testing.
Troubleshooting Builder Pattern Issues
Layer Caching Not Working
BAD: Dependencies don't cache well because all files copied together. GOOD: Separate dependency layer by copying only lock files first, then running resolution, then copying remaining source.
Permission Issues in Multi-Stage
BAD: app might not be readable by UID 65532. GOOD: Ensure permissions are compatible by chmod a+rx.
Missing Runtime Dependencies
BAD: Binary compiled with shared libs won't run in prod without libc. GOOD: Build with static linking so app runs without dependencies.
