DEV Community

Cover image for πŸš€ Stop Chasing Small Docker Images: What Actually Matters for Go in Production
Chiman Jain
Chiman Jain

Posted on

πŸš€ Stop Chasing Small Docker Images: What Actually Matters for Go in Production

A practical guide to reproducible builds, faster CI pipelines, and debuggable containers for Go engineers


Most Docker + Go tutorials end the same way:

β€œUse multi-stage builds, switch to Alpine, done.”

That advice works until it doesn’t.

At scale, different problems show up:

  • CI pipelines slow down unpredictably
  • Builds stop being reproducible
  • Debugging minimal containers becomes painful
  • Monorepos destroy Docker cache efficiency

This article focuses on what actually matters in production:
πŸ‘‰ reproducibility, caching, and operability


πŸ” How Docker + Go Builds Actually Work

Before optimizing, it helps to visualize what’s happening.

πŸ“Š Build Flow Diagram

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚   Source Code β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
               β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚  go.mod/sum   β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
               β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ go mod download      β”‚
        β”‚ (dependency layer)   β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
               β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ go build             β”‚
        β”‚ (compile layer)      β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
               β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ Final Image          β”‚
        β”‚ (distroless/scratch) β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

⚠️ Key Insight:

If go.mod changes β†’ everything below it rebuilds


🧠 Reproducible Builds: The Overlooked Problem

❌ What Goes Wrong

Same code, different builds:

  • Different architectures (amd64 vs arm64)
  • Embedded file paths
  • Environment-dependent outputs

βœ… Fixing It

1. Remove Local Paths

go build -trimpath -o app
Enter fullscreen mode Exit fullscreen mode

2. Lock Dependencies

go mod download
Enter fullscreen mode Exit fullscreen mode

Optional stricter control:

GOPROXY=https://proxy.golang.org
GONOSUMDB=*
Enter fullscreen mode Exit fullscreen mode

3. Standardize Build Environment

FROM golang:1.26 AS builder

ENV CGO_ENABLED=0

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN go build -trimpath -ldflags="-s -w" -o app
Enter fullscreen mode Exit fullscreen mode

πŸ“Š Multi-Arch Build Flow

           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
           β”‚   Source     β”‚
           β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β–Ό                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ linux/amd64  β”‚        β”‚ linux/arm64  β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
       β–Ό                       β–Ό
   Binary A               Binary B
 (different hash)      (different hash)
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Focus on behavior consistency, not identical binaries.


⚑ Docker Caching: Why Monorepos Break It

πŸ“Š Layer Caching Model

Layer 1: OS Base Image
Layer 2: go.mod / go.sum
Layer 3: Dependencies (go mod download)
Layer 4: Source Code
Layer 5: Build Output
Enter fullscreen mode Exit fullscreen mode

❌ Problem

Change in go.mod
        ↓
Layer 2 invalidated
        ↓
Layer 3 re-runs (slow)
        ↓
Everything rebuilds
Enter fullscreen mode Exit fullscreen mode

βœ… Optimized Caching Strategy

πŸ“Š Improved Flow

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ go.mod/sum   β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
               β–Ό
   (cached via BuildKit mount)
        β–Ό
   Dependencies reused βœ…

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ Source Code  β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
               β–Ό
        Build runs faster ⚑
Enter fullscreen mode Exit fullscreen mode

πŸ”§ Practical Fixes

Use BuildKit Cache

RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download
Enter fullscreen mode Exit fullscreen mode

Cache Build Artifacts

RUN --mount=type=cache,target=/root/.cache/go-build \
    go build -trimpath -ldflags="-s -w" -o app
Enter fullscreen mode Exit fullscreen mode

Scope Dependencies

COPY services/service-a/go.mod services/service-a/go.sum ./
RUN go mod download
Enter fullscreen mode Exit fullscreen mode

🐳 Minimal Images: The Trade-Off Nobody Talks About

πŸ“Š Image Comparison

scratch      β†’ smallest β†’ hardest to debug ❌
distroless   β†’ balanced β†’ production-ready βœ…
alpine       β†’ larger   β†’ easiest debugging πŸ”§
Enter fullscreen mode Exit fullscreen mode

🚨 Real-World Issues

scratch

  • No TLS certs β†’ HTTPS fails
  • No shell β†’ cannot debug
  • No timezone/DNS tools

distroless

  • Secure and minimal
  • But still no shell

alpine

  • Debuggable
  • But uses musl libc (can cause subtle issues)

βœ… Practical Strategy

Production   β†’ distroless
Debug build  β†’ alpine
Special case β†’ scratch
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ CI/CD Optimized Dockerfile

πŸš€ Production-Ready Template

FROM golang:1.26 AS builder

WORKDIR /app

ENV CGO_ENABLED=0

COPY go.mod go.sum ./

RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download

COPY . .

RUN --mount=type=cache,target=/root/.cache/go-build \
    go build \
    -trimpath \
    -ldflags="-s -w" \
    -o app

FROM gcr.io/distroless/base-debian12

WORKDIR /

COPY --from=builder /app/app /app

USER nonroot:nonroot

ENTRYPOINT ["/app"]
Enter fullscreen mode Exit fullscreen mode

πŸ”§ Debug Variant

FROM alpine:3.19

RUN apk add --no-cache ca-certificates

COPY --from=builder /app/app /app

ENTRYPOINT ["/app"]
Enter fullscreen mode Exit fullscreen mode

🧩 The Bigger Picture

Most engineers optimize for:

  • Image size
  • Build completion

But production systems care about:

Reproducibility  β†’ Can I trust this build?
Debuggability    β†’ Can I fix issues fast?
Performance      β†’ Can CI scale?
Enter fullscreen mode Exit fullscreen mode

🎯 Final Takeaway

If your Docker setup feels:

  • slow
  • fragile
  • hard to debug

…it’s not Docker.

It’s how:

  • caching
  • dependencies
  • and runtime assumptions

interact.


Top comments (0)