Go: Deploying Go with Docker
Last updated: 2026-08-26
Go's static compilation makes it the ideal language for containerized deployments—one binary file plus a scratch image equals a 12MB production image.
When you need to deploy a Go API to a production environment, a well-designed Dockerfile can help you optimize the size from a 1.2GB base image down to 12MB.
1. You will learn
- Multi-stage Build
- Selecting a Scratch, Alpine, or Distroless image
CGO_ENABLED=0Static compilation- Cross-platform cross-compilation
- Docker health check
- Docker Compose multi-service orchestration
- Example K8s Deployment YAML
2. A True Story of a Backend Engineer
(1) Pain Points: A 1.2 GB Docker image takes 5 minutes to deploy each time
Bob's e-commerce API is ready to go live:
"I used the most convenient approach:
FROM golang:1.22as the base image,COPYed the source code into it, and compiled it inside the container. The image is 1.2 GB, and it takes 5 minutes to سحب it for each deployment. The CI/CD pipeline takes 15 minutes from إيداع to deployment. My boss said, 'Deployment is too slow—it takes 10 minutes to roll back.'"
# Bad approach: compile inside container, keep all build tools
FROM golang:1.22 # 800MB + compiler tools
WORKDIR /app
COPY . .
RUN go build -o server .
EXPOSE 8080
CMD ["./server"] # Image 1.2GB! Includes compiler, dependencies, toolchain
(2) Go Solution: Multi-stage Build
# Good approach: multi-stage build
# Stage 1: compile (use full Go image)
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o خادم .
# Stage 2: run (use minimal image)
FROM scratch
COPY --from=builder /app/خادم /خادم
EXPOSE 8080
CMD ["/خادم"] # Image 12MB! Only the binary
(3) Results: Before and After Optimization
| Metric | Single-stage (Go: 1.22) | Multi-stage (Scratch) | Improvement |
|---|---|---|---|
| Image Size | 1.2 GB | 12 MB | 100x |
| Deployment Pull | 5 minutes | 5 seconds | 60x |
| Security Risks | Includes compilers and toolchains | Binary-only | Minimal attack surface |
| Build Cache | ❌ Full Compilation Every Time | ✅ Tiered Dependency Caching | — |
3. Dockerfile Best Practices
▶ Example: Go Multi-Stage Dockerfile
# ===== Stage 1: Build =====
FROM golang:1.22-alpine AS builder
# Set working directory
WORKDIR /app
# Copy dependency files first (leverage Docker cache)
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Static compilation
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .
# ===== Stage 2: Run =====
FROM scratch
# Copy binary from builder stage
COPY --from=builder /app/server /server
# If timezone files are needed
# COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# If SSL certificates are needed
# COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/server", "-health"]
CMD ["/server"]
▶ Example: Alpine version
# ===== Stage 1: Build =====
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .
# ===== Stage 2: Run (Alpine) =====
FROM alpine:3.19
# Install runtime dependencies (if needed)
# RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
(3) Selecting a Base Image
| Image | Size | Security | Use Cases |
|---|---|---|---|
scratch |
0 MB | ✅ Minimal attack surface | Compiled statically in pure Go, with no external dependencies |
alpine |
5 MB | ⚠️ musl libc | Requires a shell, curl, certificates, etc. |
distroless |
20 MB | ✅ Minimal + Tools | Requires SSL certificate, time zone data |
golang:alpine |
350 MB | ❌ For development | For the build phase only |
golang:1.22 |
800 MB | ❌ | Never use in production |
-ldflags="-s -w" can reduce the binary size: -s removes the symbol table, and -w removes DWARF debug information. This can reduce the size of your binary by another 30–40% without affecting its operation.
4. Cross-compilation
▶ Example: Cross-compilation script
# Makefile
APP=server
.PHONY: build-all
# Build for current platform
build:
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP) .
# Cross-compile for multiple platforms
build-all:
# Linux amd64 (most common)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/$(APP)-linux-amd64 .
# Linux arm64 (AWS Graviton / Apple M1)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/$(APP)-linux-arm64 .
# macOS
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o bin/$(APP)-darwin-amd64 .
# Windows
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o bin/$(APP)-windows-amd64.exe
# Docker build
docker-build:
docker build -t myapp:latest .
(2) Impact of CGO_ENABLED
| CGO_ENABLED | Advantages | Disadvantages |
|---|---|---|
=0 |
Statically compiled, runs on any platform, minimal image | Cannot use C libraries (such as the C driver for SQLite) |
=1 (default) |
C library available | Requires C runtime; increases image size |
mattn/go-sqlite3 (C driver), setting CGO_ENABLED=0 will cause the compilation to fail. Solution: Use a pure Go SQLite driver (such as modernc.org/sqlite), or set CGO_ENABLED=1 with the alpine image (requires installing gcc and musl-dev).
5. Docker Compose for Multiple Services
▶ Example: E-commerce API + Database + Redis
# docker-compose.yml
version: '3.8'
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- DB_HOST=db
- DB_PORT=3306
- DB_USER=app
- DB_PASSWORD=secret
- DB_NAME=shop
- REDIS_ADDR=redis:6379
- GIN_MODE=release
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
healthcheck:
test: ["CMD", "/server", "-health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: shop
MYSQL_USER: app
MYSQL_PASSWORD: secret
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
volumes:
db_data:
6. K8s Deployment
▶ Example: Minimal K8s Deployment
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-api
labels:
app: go-api
spec:
replicas: 3
selector:
matchLabels:
app: go-api
template:
metadata:
labels:
app: go-api
spec:
containers:
- name: api
image: myregistry/go-api:latest
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: "mysql-service"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: go-api-service
spec:
selector:
app: go-api
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
flowchart TD
subgraph Build ["Build Phase"]
SRC[Source code] --> DEP[go mod download]
DEP --> BUILD[go build]
BUILD --> BIN[Binary 15MB]
end
subgraph Container ["Container Phase"]
BIN --> CP[COPY to scratch]
CP --> IMG[Image 12MB]
end
subgraph Deploy ["Deploy Phase"]
IMG --> PUSH[Push to Registry]
PUSH --> K8S[K8s Deployment]
K8S --> POD[Pod 3 replicas]
end
Build --> Container --> Deploy
7. Complete Example: The Entire Process of Containerizing an E-commerce API
▶ Example: Full API with Health Check
// cmd/server/main.go (complete API with health check)
package main
import (
"context"
"encoding/json"
"flag"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
healthFlag := flag.Bool("health", false, "run health check")
flag.Parse()
if *healthFlag {
// Health check mode: check if service is reachable
resp, err := http.Get("http://localhost:8080/health")
if err != nil {
os.Exit(1)
}
resp.Body.Close()
os.Exit(0)
}
mux := http.NewServeMux()
// Health check endpoint
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
mux.HandleFunc("GET /ready", func(w http.ResponseWriter, r *http.Request) {
// Check if dependencies like database are ready
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"ready": "true"})
})
// Business endpoint
mux.HandleFunc("GET /api/products", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"products": []string{"laptop", "mouse", "keyboard"},
})
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
log.Println("Service listening on :8080")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
(2) Accompanying Dockerfile
# Dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Static compilation + strip debug info
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/خادم ./cmd/خادم
# === Run stage ===
FROM scratch
# Copy binary
COPY --from=builder /app/خادم /خادم
# If SSL certificates are needed (accessing external HTTPS APIs)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/خادم", "-health"]
CMD ["/خادم"]
(3) .dockerignore file
# .dockerignore
.git
.gitignore
*.md
bin/
tmp/
Dockerfile
.dockerignore
.dockerignore has a huge impact on build speed—it excludes unnecessary files, reducing the size of the context sent to the Docker daemon. Adding the .git directory (which can be hundreds of MB) can significantly speed up builds.
❓ FAQ
alpine/distroless.CGO_ENABLED=0 causes Go to generate a fully static binary—one that does not depend on any external libraries. This allows the binary to run on any Linux system, including scratch.GOOS=linux GOARCH=amd64 go build compiles a Linux binary on macOS. Go supports almost all platform combinations. Cross-compilation is simplest when CGO_ENABLED=0.node:22) ≈ 400 MB. A Java application (based on amazoncorretto:21) ≈ 500 MB. Go images have a significant size advantage.📖 Summary
- Multi-stage Build: Compilation during the "builder" stage; minimal image during the "run" stage
- Base images: scratch (minimal) > distroless > alpine > golang:alpine
CGO_ENABLED=0: Purely static compilation; runs on any platform-ldflags="-s -w": Remove symbol tables and debug information- Cross-compilation:
GOOS=linux GOARCH=arm64 go build HEALTHCHECK: Docker's native health checkdocker-compose: Multi-service orchestration (API + DB + Redis)- K8s Deployment: Minimal YAML Example (liveness + readiness probes)
.dockerignore: Exclude unnecessary files to speed up the build
📝 Exercises
-
Basic (Difficulty ⭐): Write a Dockerfile (multi-stage build) for a simple Go HTTP service. Build the image and verify that it can be accessed using
docker run -p 8080:8080. Usedocker imagesto check the image size. -
Advanced (Difficulty ⭐⭐): Implement full-stack containerization of a Go application with a قاعدة بيانات. Requirements: (1) Three services: Go API, MySQL, and Redis; (2) docker-compose.yml orchestration; (3) Implement a health check to verify that the قاعدة بيانات and Redis are ready before starting the API; (4) Use
os.Getenvin the Go code to read قاعدة بيانات connection information; (5) Use.dockerignoreto exclude unnecessary files. -
Challenge (Difficulty ⭐⭐⭐): Implement a complete CI/CD pipeline (conceptual implementation; no actual CI required). Requirements: (1) The Makefile must support
make build(cross-compilation for linux/amd64 + linux/arm64),make docker-build, andmake docker-دفع; (2) Multi-stage Dockerfile builds; (3) Build ذاكرة مخبأة optimization for each stage; (4) K8sdeployment.yaml+service.yaml; (5) Includes liveness and readiness probes; (6) Resource limits (requestsandlimits).