← Back to DevBytes

Docker Scratch Images: Production Guide

Understanding Docker Scratch Images

A Docker scratch image is the most minimal base image possible — it is completely empty. Unlike alpine, ubuntu, or even busybox, the scratch “image” contains no files, no package manager, no shell, no libraries, and not even a minimal root filesystem. It is essentially a blank layer that tells Docker to start from nothing. When you use FROM scratch in a Dockerfile, you are committing to supply every single file your application needs at runtime.

Because of its emptiness, scratch is not an image you can pull from a registry — it is a reserved, virtual name that represents the lowest possible foundation. It works exclusively with statically compiled binaries that do not depend on dynamic libraries. In practice, languages like Go, Rust, and C (when compiled with musl and static flags) produce executables that can run directly on scratch.

Why Scratch Images Matter for Production

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Production environments demand security, small attack surfaces, and fast deployment. Scratch images deliver on all three:

However, this minimalism comes with real tradeoffs. Without a shell, you cannot docker exec into a running container for debugging. There are no CA certificates by default, so TLS connections will fail. Timezone data, DNS resolution helpers, and user/group databases are also absent. Production use requires you to consciously address each of these gaps.

Building a Production-Ready Scratch Image: A Step-by-Step Guide

Let's walk through creating a secure, production-grade Docker image from scratch for a typical Go microservice. The same principles apply to Rust, C, or any language that can produce a truly static binary.

Step 1: Create a Statically Compiled Binary

The binary must not rely on any dynamic linker or shared libraries. In Go, a static binary is produced by disabling CGO and setting the target operating system:

# Build a static, Linux-amd64 binary
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp .

For Rust, you might use the musl target: rustup target add x86_64-unknown-linux-musl and build with that target. For C, you can link against musl and use -static. The key is to verify the binary is completely static by running ldd myapp on a Linux system — you should see “not a dynamic executable” or a message indicating no shared libraries.

Step 2: Write a Multi-Stage Dockerfile

Use a build stage to compile your application and a final scratch stage to copy only the necessary artifacts. Here’s a minimal example:

# ---- Build Stage ----
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp .

# ---- Runtime Stage ----
FROM scratch

# Copy the static binary
COPY --from=builder /app/myapp /myapp

# Optionally copy additional files (CA certs, zoneinfo) – covered later

EXPOSE 8080
ENTRYPOINT ["/myapp"]
CMD ["serve"]

This Dockerfile produces an image that contains exactly one file: /myapp. The binary must handle all configuration via arguments, environment variables, or embedded defaults.

Step 3: Handle TLS and Certificates

One of the most common surprises when moving to scratch is that HTTPS requests fail with “x509: certificate signed by unknown authority”. The runtime has no root CA certificates. To fix this, copy the CA bundle from a trusted intermediate image:

# ---- Build Stage (same as before) ----
FROM golang:1.21-alpine AS builder
# ... build steps ...

# ---- Certificates Intermediate Stage ----
FROM alpine:latest AS certs
RUN apk add --no-cache ca-certificates

# ---- Final Scratch Stage ----
FROM scratch

COPY --from=builder /app/myapp /myapp
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

EXPOSE 8080
ENTRYPOINT ["/myapp"]
CMD ["serve"]

Most TLS libraries (including Go’s crypto/tls) look for the CA file at well-known paths like /etc/ssl/certs/ca-certificates.crt. Adjust the destination path according to your application’s TLS library. For Alpine’s CA bundle, the standard path is as shown above.

Step 4: Include Essential Files (Time Zones, DNS Config)

Beyond TLS, other missing OS data can cause subtle failures:

A common practice is to copy the whole /etc/ssl, /usr/share/zoneinfo, and a minimal /etc/nsswitch.conf from a lightweight intermediate image. Here’s how to combine them:

FROM alpine:latest AS os-files
RUN apk add --no-cache ca-certificates tzdata

# Create minimal nsswitch.conf
RUN echo "hosts: dns" > /etc/nsswitch.minimal

FROM scratch

COPY --from=builder /app/myapp /myapp
COPY --from=os-files /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=os-files /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=os-files /etc/nsswitch.minimal /etc/nsswitch.conf

USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/myapp"]
CMD ["serve"]

Step 5: Set User, Entrypoint, and Default CMD

Since scratch has no adduser or useradd, you must define the user numerically. Using a high-numbered user like 1000 avoids clashing with well-known system UIDs. The ENTRYPOINT and CMD are critical — they define what runs when the container starts. Prefer the “exec form” (JSON array) to avoid shell interpretation.

# Inside the final FROM scratch stage
USER 1000:1000
ENTRYPOINT ["/myapp"]
CMD ["serve"]

Step 6: Build and Run the Image

Build the image as usual and run it with port mapping:

docker build -t myapp:prod .
docker run -d -p 8080:8080 --name myapp-instance myapp:prod

Verify the container is running and check that TLS, timezone, and DNS-dependent features work correctly. Because there is no shell, you cannot docker exec into it. Instead, use your application’s health endpoints, logs, or debugging tools that attach from the host (e.g., strace, gdb, or docker inspect).

Best Practices for Scratch Images in Production

Common Pitfalls and How to Avoid Them

Even experienced developers hit stumbling blocks with scratch. Here are the most frequent issues and their remedies:

Conclusion

Docker scratch images are the pinnacle of minimalism in containerized deployments. They strip away everything except your application, resulting in tiny, fast, and exceptionally secure runtime artifacts. However, this power comes with the responsibility to explicitly supply every dependency your app needs — from CA certificates and timezone data to DNS configuration and user IDs. By following the multi-stage build pattern, copying only essential files from lightweight intermediates, and rigorously testing for common pitfalls, you can confidently run scratch-based containers in production. The result is a lean, focused runtime that aligns perfectly with modern microservice principles and significantly reduces your operational overhead.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles