“Obese” Containers and the Deployment Speed Nightmare
When I first started Dockerizing Go applications, I thought it was as simple as packaging and running. I wrote a “textbook” Dockerfile: using FROM golang:latest, copying the code, and running go build. The result left me stunned. A simple Hello World app produced an image weighing nearly 800MB.
Imagine deploying to a cheap VPS or over a flaky 4G connection. Waiting to pull that massive image is pure torture. It doesn’t just waste storage; it makes the CI/CD pipeline irrationally slow. Go is famous for creating ultra-compact binaries, so where did that 800MB come from?
Analyzing the Root Cause: Why Are Images So Heavy?
The problem isn’t your code. It’s the “base image” you choose. When you use golang:latest based on Debian or Ubuntu, you’re lugging an entire operating system into your container.
- Bulky build tools: The image contains the compiler, debugger, and dozens of heavy C libraries. In reality, your app doesn’t need them at runtime.
- Redundant packages: System utilities, shells, and package managers take up hundreds of MBs while sitting idle.
- Security vulnerabilities: The more pre-installed software, the larger the attack surface. Hackers love containers with
curloraptfor privilege escalation.
“Weight Loss” Tactics: From Amateur to Pro
After many trials and errors on production projects, I’ve identified three distinct levels of optimization.
Level 1: Alpine Linux – The “Quick Fix”
Instead of using the default image, switch to Alpine. It’s an ultra-lightweight distribution, weighing only about 5MB.
FROM golang:1.22-alpine
WORKDIR /app
COPY . .
RUN go build -o main .
CMD ["./main"]
With just this change, the image drops to around 300MB. Much better, but still not optimal because it still contains the entire Go SDK.
Level 2: Multi-stage Build – A Mindset Revolution
This is my favorite technique and the standard for all modern projects. The idea is simple: split the packaging process into two independent stages.
- Stage 1 (Builder): Use a full image to compile the code into a binary file.
- Stage 2 (Runner): Copy only that single binary file into an ultra-light image for execution.
All source code, cache, and the compiler are completely removed from the final image.
Scratch Image – The “Final Boss” of Size Optimization
If Alpine still doesn’t satisfy you, use scratch. This is a completely empty image (0 bytes). Since Go can build self-contained static binaries, it doesn’t need an operating system environment to run.
Production-Ready Dockerfile Template
Here is the configuration I currently use, which is both secure and incredibly lightweight.
# Stage 1: Build binary
FROM golang:1.22-alpine AS builder
# Install necessary helper libraries
RUN apk update && apk add --no-cache git ca-certificates tzdata
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build with size-optimized flags
# CGO_ENABLED=0: Create a fully static binary
# -ldflags="-w -s": Remove debug info, further reducing size by ~20%
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./main.go
# Stage 2: Ultra-compact runner
FROM scratch
# Copy security certificates and timezone data
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
The result? The final image weighs only 12MB. Image pull times are now measured in seconds instead of minutes.
Common Pitfalls to Avoid with Scratch
Using scratch is great but can be a trap if you’re not experienced in handling runtime errors.
1. HTTPS Connection Errors
Because scratch is completely empty, it doesn’t come with CA Certificates. If your app needs to call third-party APIs (like Stripe or AWS), it will immediately throw an SSL error. Don’t forget to copy the ca-certificates.crt file from the builder stage as shown above.
2. Debugging in the Dark
In scratch, there’s no ls, no bash, and no curl. You cannot docker exec into it to inspect files.
My tip is to log everything to stdout in JSON format. When I need to investigate errors from system logs, I often use the JSON Formatter at Toolcraft to format them for readability. This is much faster and cleaner than trying to install debugging tools in a production container.
Results After 6 Months of Real-World Practice
Optimizing images isn’t just about pretty numbers. After applying this to my company’s microservices system, I’ve noticed three key benefits:
- Lightning-fast deployment: Time from code commit to the app running on the server dropped from 5 minutes to under 1 minute.
- Budget savings: Storage usage on AWS ECR decreased significantly, saving monthly storage costs.
- Total peace of mind: No shell in the image means hackers lose the most basic tool for destruction if they happen to break into the container.
If you’re working with Go, try shrinking your image size today. It might feel a bit strange handling certificates at first, but believe me, the results are well worth it.

