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



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.22 as 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.'"

DOCKERFILE
# 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

DOCKERFILE
# 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

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

DOCKERFILE
# ===== 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
💡 Tip: -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
# 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 .
▶ Try it Yourself

(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
🔥 Common Mistake: If your Go code uses 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

YAML
# 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

YAML
# 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
100%
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

GO 📖 Display only
// 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)
    }
}
56 logic lines (exceeds 40-line limit, display only)

(2) Accompanying Dockerfile

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

TEXT 📖 Display only
# .dockerignore
.git
.gitignore
*.md
bin/
tmp/
Dockerfile
.dockerignore
💡 Tip: .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

Q Why does Multi-stage Build reduce the size of the image?
A The first stage (builder) uses the full Go image to compile the binaries, while the second stage (run) only copies the compiled binaries to a minimal base image. Build tools (compilers, dependency managers, source code) do not appear in the final image—only the binaries themselves.
Q Is the scratch image secure?
A scratch is the most secure—it is completely empty, with no shell, libraries, or tools. An attacker cannot execute any commands inside the container. However, if your program requires an SSL certificate, time zone data, or a shell, you'll need to copy these files from the builder stage or switch to alpine/distroless.
Q What does CGO_ENABLED=0 mean?
A By default, Go uses dynamic linking (CGO_ENABLED=1, using the C runtime). Setting 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.
Q How do I use cross-compilation?
A Set the environment variables GOOS (target OS) and GOARCH (target architecture). 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.
Q What is the difference between a health check and a readiness probe?
A A health check verifies whether a process is healthy—if it is not, K8s restarts the Pod. A readiness probe checks whether a service is ready to accept traffic—if it is not ready, K8s removes the Pod from the Service. Use readiness probes during the startup phase and liveness probes during the runtime phase.
Q What is the relationship between Docker and K8s? Do I need to learn K8s?
A Docker is a container runtime—it packages applications into standardized units. K8s is a container orchestration platform—it manages the deployment, scaling, and health checks of multiple containers. Small projects only need Docker (or Docker Compose), while large projects require K8s. This course only covers K8s orchestration examples and does not delve into K8s concepts.
Q How large are Go Docker images typically?
A Go static binaries + scratch ≈ 10–20 MB. Python applications (based on python:3.12) ≈ 300 MB. A Node.js application (based on node:22) ≈ 400 MB. A Java application (based on amazoncorretto:21) ≈ 500 MB. Go images have a significant size advantage.

📖 Summary


📝 Exercises

  1. 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. Use docker images to check the image size.

  2. 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.Getenv in the Go code to read قاعدة بيانات connection information; (5) Use .dockerignore to exclude unnecessary files.

  3. 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, and make docker-دفع; (2) Multi-stage Dockerfile builds; (3) Build ذاكرة مخبأة optimization for each stage; (4) K8s deployment.yaml + service.yaml; (5) Includes liveness and readiness probes; (6) Resource limits (requests and limits).

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏