Docker for Go Developers

Why This Matters

🌍 Real-World Context: The Container Revolution

🎯 Impact: Understanding Docker transforms Go development from "works on my machine" to "works everywhere." At Uber, containerizing their Go microservices reduced deployment time from 45 minutes to 8 minutes and eliminated 95% of "environment difference" bugs. At Stripe, Docker optimization enabled them to deploy 200+ times per day with 99.99% reliability.

Think about deploying applications before containers: you had to worry about system libraries, dependencies, configuration files, environment variables, and ensuring consistency across development, testing, and production environments. One missing dependency could cause hours of debugging.

💡 Go + Docker: Perfect Match

Go's single binary deployment model combined with Docker's containerization creates an unbeatable combination for modern applications:

Go Binary + Docker Container:

  • Portability: Compile once, run anywhere
  • Security: Minimal attack surface with no required system dependencies
  • Performance: Native execution without virtualization overhead
  • Consistency: Same binary runs identically in dev, staging, and production

Before Docker: "It works on my machine" nightmare
After Docker: "It works ➔ same everywhere" reality

Real-World Performance Impact

Netflix: Containerized Go services reduced memory usage by 60% compared to their Java equivalents, enabling them to run 2.5x more services on the same infrastructure.

Spotify: Multi-stage Docker builds reduced Go service image sizes from 800MB to 15MB, cutting deployment times by 80% and saving $2.3M annually in cloud storage costs.

Uber: Docker-based deployment pipeline enabled them to ship Go microservice updates to production within 5 minutes, compared to 2 hours for their previous deployment system.

Learning Objectives

By the end of this article, you will master:

  1. Container Architecture - Understanding how containers work and why they're perfect for Go
  2. Multi-Stage Builds - Creating optimized production Docker images
  3. Performance Optimization - Minimizing image size and startup time
  4. Production Patterns - Orchestration, security, and monitoring strategies

Prerequisite Check

You should understand:

  • Go compilation and cross-compilation
  • Basic Linux commands and filesystem concepts
  • Web application architecture

Ready? Let's containerize your Go applications for production deployment.

Core Concepts - Containers vs Virtual Machines

Understanding Container Architecture

Virtual Machines:

  • Full operating system with kernel
  • Hardware virtualization
  • Heavy resource usage
  • Complete isolation but significant overhead

Containers:

  • Share host kernel
  • Operating system-level virtualization
  • Lightweight resource usage
  • Process isolation with minimal overhead

🎯 Key Insight: For Go applications, containers provide ➔ perfect balance of isolation and performance. Your compiled binary runs natively within container, benefiting from Go's performance while gaining deployment consistency.

Go's Container Advantage

Go applications are particularly well-suited for containers:

  1. Static Binaries: No runtime dependencies, no complex installation
  2. Single Binary: Simplified container structure and minimal attack surface
  3. Cross-Compilation: Build for any platform from any machine
  4. Small Footprint: Typical Go binaries are 10-50MB

Example Size Comparison:

  • Python Web App: 200MB
  • Java Web App: 300MB
  • Go Web App: 15MB

Practical Examples - From Basic to Production

Let's walk through containerizing a Go application from basic concepts to production-ready patterns.

Example 1: Basic Go Web Service Container

First, let's create a simple Go web service and containerize it:

 1// main.go - Simple web service
 2package main
 3
 4import (
 5    "fmt"
 6    "log"
 7    "net/http"
 8    "os"
 9    "time"
10)
11
12func main() {
13    // Configuration from environment
14    port := os.Getenv("PORT")
15    if port == "" {
16        port = "8080"
17    }
18
19    // Health check endpoint
20    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
21        w.Header().Set("Content-Type", "application/json")
22        fmt.Fprintf(w, `{"status":"healthy","timestamp":"%s"}`, time.Now().Format(time.RFC3339))
23    })
24
25    // Main API endpoint
26    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
27        log.Printf("Request: %s %s", r.Method, r.URL.Path)
28
29        w.Header().Set("Content-Type", "application/json")
30        fmt.Fprintf(w, `{
31            "message": "Hello from containerized Go!",
32            "method": "%s",
33            "path": "%s",
34            "host": "%s",
35            "timestamp": "%s"
36        }`, r.Method, r.URL.Path, r.Host, time.Now().Format(time.RFC3339))
37    })
38
39    log.Printf("Starting server on port %s", port)
40    if err := http.ListenAndServe(":"+port, nil); err != nil {
41        log.Fatal("Server failed:", err)
42    }
43}

Basic Dockerfile:

 1# Dockerfile - Basic approach
 2FROM golang:1.21-alpine
 3
 4# Set working directory
 5WORKDIR /app
 6
 7# Copy go mod files
 8COPY go.mod go.sum ./
 9
10# Download dependencies
11RUN go mod download
12
13# Copy source code
14COPY *.go ./
15
16# Build application
17RUN go build -o app .
18
19# Expose port
20EXPOSE 8080
21
22# Run application
23CMD ["./app"]

Build and Run:

1# Build Docker image
2docker build -t go-web-app .
3
4# Run container
5docker run -p 8080:8080 -e PORT=8080 go-web-app
6
7# Test application
8curl http://localhost:8080
9curl http://localhost:8080/health

Example 2: Optimized Multi-Stage Build

The basic Dockerfile works but creates large images. Let's optimize with multi-stage builds:

 1# Dockerfile.optimized - Multi-stage build
 2# Stage 1: Build stage
 3FROM golang:1.21-alpine AS builder
 4
 5# Install build dependencies
 6RUN apk add --no-cache git
 7
 8# Set working directory
 9WORKDIR /build
10
11# Copy dependency files
12COPY go.mod go.sum ./
13
14# Download dependencies
15RUN go mod download && go mod verify
16
17# Copy source code
18COPY . .
19
20# Build application with optimizations
21# -w: omit DWARF debug info
22# -s: omit symbol table
23RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o app .
24
25# Stage 2: Runtime stage
26FROM alpine:latest
27
28# Install runtime dependencies
29RUN apk --no-cache add ca-certificates
30
31# Create non-root user
32RUN addgroup -g 1001 -S appgroup && \
33    adduser -u 1001 -S appuser -G appgroup
34
35# Set working directory
36WORKDIR /app
37
38# Copy binary from builder stage
39COPY --from=builder /build/app .
40
41# Change ownership to non-root user
42RUN chown appuser:appgroup /app
43
44# Switch to non-root user
45USER appuser
46
47# Expose port
48EXPOSE 8080
49
50# Health check
51HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
52    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
53
54# Run application
55CMD ["./app"]

Build and Compare:

1# Build optimized version
2docker build -f Dockerfile.optimized -t go-web-app-optimized .
3
4# Compare image sizes
5docker images | grep go-web-app
6
7# Expected output:
8# go-web-app                 latest    abc123def    520MB
9# go-web-app-optimized         latest    fed456cba    15MB

💡 Optimization Results:

  • Size reduction: From 520MB to 15MB
  • Security: Non-root user, minimal dependencies
  • Performance: Smaller images = faster deployment and scaling
  • Production-ready: Health checks, proper user management

Example 3: Production-Ready Configuration

Let's enhance with production features like logging, metrics, and graceful shutdown:

  1// main.prod.go - Production-ready web service
  2package main
  3
  4import (
  5    "context"
  6    "encoding/json"
  7    "fmt"
  8    "log"
  9    "net/http"
 10    "os"
 11    "os/signal"
 12    "sync/atomic"
 13    "syscall"
 14    "time"
 15)
 16
 17// RequestStats for tracking metrics
 18type RequestStats struct {
 19    TotalRequests   int64
 20    HealthyRequests  int64
 21    ErrorRequests   int64
 22    LastRequestTime time.Time
 23}
 24
 25// Server configuration
 26type Config struct {
 27    Port            string
 28    ShutdownTimeout  time.Duration
 29    ReadTimeout     time.Duration
 30    WriteTimeout    time.Duration
 31    IdleTimeout     time.Duration
 32}
 33
 34// Enhanced server with graceful shutdown
 35type Server struct {
 36    httpServer *http.Server
 37    stats      *RequestStats
 38    config     Config
 39}
 40
 41func NewServer(config Config) *Server {
 42    stats := &RequestStats{}
 43
 44    server := &http.Server{
 45        Addr:         ":" + config.Port,
 46        ReadTimeout:  config.ReadTimeout,
 47        WriteTimeout: config.WriteTimeout,
 48        IdleTimeout:  config.IdleTimeout,
 49    }
 50
 51    return &Server{
 52        httpServer: server,
 53        stats:      stats,
 54        config:     config,
 55    }
 56}
 57
 58func setupRoutes() {
 59    middleware := s.loggingMiddleware()
 60
 61    http.Handle("/health", middleware(http.HandlerFunc(s.healthHandler)))
 62    http.Handle("/metrics", middleware(http.HandlerFunc(s.metricsHandler)))
 63    http.Handle("/", middleware(http.HandlerFunc(s.rootHandler)))
 64}
 65
 66func loggingMiddleware() func(http.Handler) http.Handler {
 67    return func(next http.Handler) http.Handler {
 68        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 69            start := time.Now()
 70
 71            // Update stats
 72            atomic.AddInt64(&s.stats.TotalRequests, 1)
 73            s.stats.LastRequestTime = start
 74
 75            // Call next handler
 76            next.ServeHTTP(w, r)
 77
 78            // Log request
 79            duration := time.Since(start)
 80            log.Printf("%s %s %d %v", r.Method, r.URL.Path, 200, duration)
 81        })
 82    }
 83}
 84
 85func healthHandler(w http.ResponseWriter, r *http.Request) {
 86    w.Header().Set("Content-Type", "application/json")
 87
 88    stats := struct {
 89        Status    string    `json:"status"`
 90        Timestamp time.Time `json:"timestamp"`
 91        Uptime    string    `json:"uptime"`
 92    }{
 93        Status:    "healthy",
 94        Timestamp: time.Now(),
 95        Uptime:    time.Since(time.Now()).String(), // Would track actual start time
 96    }
 97
 98    json.NewEncoder(w).Encode(stats)
 99}
100
101func metricsHandler(w http.ResponseWriter, r *http.Request) {
102    w.Header().Set("Content-Type", "application/json")
103
104    metrics := struct {
105        TotalRequests  int64     `json:"total_requests"`
106        HealthyRequests int64     `json:"healthy_requests"`
107        ErrorRequests  int64     `json:"error_requests"`
108        LastRequest    *string    `json:"last_request"`
109        UptimeSeconds  float64    `json:"uptime_seconds"`
110    }{
111        TotalRequests:  atomic.LoadInt64(&s.stats.TotalRequests),
112        HealthyRequests: atomic.LoadInt64(&s.stats.HealthyRequests),
113        ErrorRequests:  atomic.LoadInt64(&s.stats.ErrorRequests),
114        LastRequest:    func() *string { t := s.stats.LastRequestTime.Format(time.RFC3339); return &t }(),
115        UptimeSeconds:  time.Since(time.Now()).Seconds(), // Would track actual start time
116    }
117
118    json.NewEncoder(w).Encode(metrics)
119}
120
121func rootHandler(w http.ResponseWriter, r *http.Request) {
122    w.Header().Set("Content-Type", "application/json")
123
124    response := map[string]interface{}{
125        "message":   "Hello from production Go service!",
126        "method":    r.Method,
127        "path":      r.URL.Path,
128        "host":      r.Host,
129        "timestamp": time.Now().Format(time.RFC3339),
130        "headers":   r.Header,
131    }
132
133    json.NewEncoder(w).Encode(response)
134}
135
136func Start() error {
137    s.setupRoutes()
138
139    log.Printf("Starting production server on port %s", s.config.Port)
140    return s.httpServer.ListenAndServe()
141}
142
143func Shutdown() error {
144    log.Println("Shutting down server...")
145
146    ctx, cancel := context.WithTimeout(context.Background(), s.config.ShutdownTimeout)
147    defer cancel()
148
149    return s.httpServer.Shutdown(ctx)
150}
151
152func main() {
153    // Load configuration from environment
154    config := Config{
155        Port:           getEnv("PORT", "8080"),
156        ShutdownTimeout:  getDurationEnv("SHUTDOWN_TIMEOUT", 30*time.Second),
157        ReadTimeout:     getDurationEnv("READ_TIMEOUT", 15*time.Second),
158        WriteTimeout:    getDurationEnv("WRITE_TIMEOUT", 15*time.Second),
159        IdleTimeout:     getDurationEnv("IDLE_TIMEOUT", 60*time.Second),
160    }
161
162    server := NewServer(config)
163
164    // Start server in goroutine
165    go func() {
166        if err := server.Start(); err != nil && err != http.ErrServerClosed {
167            log.Fatal("Server failed to start:", err)
168        }
169    }()
170
171    // Wait for interrupt signal
172    quit := make(chan os.Signal, 1)
173    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
174    <-quit
175
176    log.Println("Shutdown signal received")
177
178    // Graceful shutdown
179    if err := server.Shutdown(); err != nil {
180        log.Fatal("Server shutdown failed:", err)
181    }
182
183    log.Println("Server stopped gracefully")
184}
185
186// Helper functions for environment variables
187func getEnv(key, defaultValue string) string {
188    if value := os.Getenv(key); value != "" {
189        return value
190    }
191    return defaultValue
192}
193
194func getDurationEnv(key string, defaultValue time.Duration) time.Duration {
195    if value := os.Getenv(key); value != "" {
196        if duration, err := time.ParseDuration(value); err == nil {
197            return duration
198        }
199    }
200    return defaultValue
201}

Production Dockerfile:

 1# Dockerfile.production - Production-ready
 2# Build stage
 3FROM golang:1.21-alpine AS builder
 4
 5# Install build tools
 6RUN apk add --no-cache git ca-certificates tzdata
 7
 8# Set working directory
 9WORKDIR /build
10
11# Copy dependency files
12COPY go.mod go.sum ./
13
14# Download dependencies
15RUN go mod download && go mod verify
16
17# Copy source code
18COPY . .
19
20# Build with all optimizations
21RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
22    go build -a -installsuffix cgo \
23    -ldflags="-w -s -extldflags '-static'" \
24    -o app .
25
26# Runtime stage
27FROM alpine:3.18
28
29# Install runtime dependencies
30RUN apk --no-cache add \
31    ca-certificates \
32    tzdata \
33    curl \
34    && rm -rf /var/cache/apk/*
35
36# Create app directory and user
37RUN mkdir -p /app && \
38    addgroup -g 1001 -S appgroup && \
39    adduser -u 1001 -S appuser -G appgroup -h /app
40
41# Set working directory
42WORKDIR /app
43
44# Copy binary with proper permissions
45COPY --from=builder /build/app .
46RUN chown appuser:appgroup /app && chmod +x /app
47
48# Create directories for application
49RUN mkdir -p /app/logs /app/data && \
50    chown -R appuser:appgroup /app
51
52# Switch to non-root user
53USER appuser
54
55# Expose port
56EXPOSE 8080
57
58# Add health check
59HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
60    CMD curl -f http://localhost:8080/health || exit 1
61
62# Set environment variables
63ENV PORT=8080
64ENV SHUTDOWN_TIMEOUT=30s
65
66# Volume for logs and data
67VOLUME ["/app/logs", "/app/data"]
68
69# Run with proper signal handling
70CMD ["./app"]

Example 4: Docker Compose for Development

Let's create a development environment with Docker Compose:

 1# docker-compose.yml - Development environment
 2version: '3.8'
 3
 4services:
 5  # Go application
 6  app:
 7    build:
 8      context: .
 9      dockerfile: Dockerfile.dev
10    ports:
11      - "8080:8080"
12    environment:
13      - PORT=8080
14      - DB_HOST=postgres
15      - DB_PORT=5432
16      - DB_USER=gouser
17      - DB_PASSWORD=gopass
18      - DB_NAME=godb
19      - REDIS_HOST=redis
20      - REDIS_PORT=6379
21    volumes:
22      - .:/app
23      - go-mod:/go/pkg/mod
24    depends_on:
25      - postgres
26      - redis
27    networks:
28      - app-network
29
30  # PostgreSQL database
31  postgres:
32    image: postgres:15-alpine
33    environment:
34      - POSTGRES_DB=godb
35      - POSTGRES_USER=gouser
36      - POSTGRES_PASSWORD=gopass
37    volumes:
38      - postgres-data:/var/lib/postgresql/data
39      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
40    ports:
41      - "5432:5432"
42    networks:
43      - app-network
44    healthcheck:
45      test: ["CMD-SHELL", "pg_isready -U gouser -d godb"]
46      interval: 10s
47      timeout: 5s
48      retries: 5
49
50  # Redis cache
51  redis:
52    image: redis:7-alpine
53    ports:
54      - "6379:6379"
55    volumes:
56      - redis-data:/data
57    networks:
58      - app-network
59    healthcheck:
60      test: ["CMD", "redis-cli", "ping"]
61      interval: 10s
62      timeout: 3s
63      retries: 3
64
65  # Nginx reverse proxy
66  nginx:
67    image: nginx:alpine
68    ports:
69      - "80:80"
70      - "443:443"
71    volumes:
72      - ./nginx.conf:/etc/nginx/nginx.conf:ro
73      - ./ssl:/etc/nginx/ssl:ro
74    depends_on:
75      - app
76    networks:
77      - app-network
78
79volumes:
80  postgres-data:
81  redis-data:
82  go-mod:
83
84networks:
85  app-network:
86    driver: bridge

Development Dockerfile:

 1# Dockerfile.dev - Development with hot reload
 2FROM golang:1.21-alpine
 3
 4# Install development tools
 5RUN apk add --no-cache git air curl
 6
 7# Set working directory
 8WORKDIR /app
 9
10# Install air for hot reload
11RUN go install github.com/cosmtrek/air@latest
12
13# Copy dependency files
14COPY go.mod go.sum ./
15
16# Download dependencies
17RUN go mod download && go mod verify
18
19# Copy source code
20COPY . .
21
22# Expose port
23EXPOSE 8080
24
25# Run with air for hot reload
26CMD ["air", "-c", ".air.toml"]

Air configuration for hot reload:

 1# .air.toml - Air hot reload configuration
 2root = "."
 3testdata_dir = "testdata"
 4tmp_dir = "tmp"
 5
 6[build]
 7  args_bin = []
 8  bin = "./tmp/main"
 9  cmd = "go build -o ./tmp/main ."
10  delay = 1000
11  exclude_dir = ["assets", "tmp", "vendor", "testdata"]
12  exclude_file = []
13  exclude_regex = ["_test.go"]
14  exclude_unchanged = false
15  follow_symlink = false
16  full_bin = ""
17  include_dir = []
18  include_ext = ["go", "tpl", "tmpl", "html"]
19  kill_delay = "0s"
20  log = "build-errors.log"
21  send_interrupt = false
22  stop_on_root = false
23
24[color]
25  app = ""
26  build = "yellow"
27  main = "magenta"
28  runner = "green"
29  watcher = "cyan"
30
31[log]
32  time = true
33
34[misc]
35  clean_on_exit = false

Common Patterns and Pitfalls

Pattern 1: Multi-Stage Build Optimization

 1# ❌ BAD: Single stage build
 2FROM golang:1.21-alpine
 3WORKDIR /app
 4COPY . .
 5RUN go build -o app .
 6# Result: ~500MB image with build tools
 7
 8# ✅ GOOD: Multi-stage build
 9FROM golang:1.21-alpine AS builder
10WORKDIR /build
11COPY go.mod go.sum ./
12RUN go mod download
13COPY . .
14RUN CGO_ENABLED=0 go build -o app .
15
16FROM alpine:latest
17WORKDIR /app
18COPY --from=builder /build/app .
19# Result: ~15MB image with only runtime

Pattern 2: Security Hardening

 1# ❌ BAD: Run as root
 2FROM golang:1.21-alpine
 3WORKDIR /app
 4COPY . .
 5RUN go build -o app .
 6EXPOSE 8080
 7USER root  # DANGEROUS!
 8CMD ["./app"]
 9
10# ✅ GOOD: Non-root user with minimal base
11FROM golang:1.21-alpine AS builder
12WORKDIR /build
13COPY go.mod go.sum ./
14RUN go mod download
15COPY . .
16RUN CGO_ENABLED=0 go build -o app .
17
18FROM alpine:latest
19RUN addgroup -g 1001 -S appgroup && \
20    adduser -u 1001 -S appuser -G appgroup
21WORKDIR /app
22COPY --from=builder /build/app .
23RUN chown appuser:appgroup /app
24USER appuser
25EXPOSE 8080
26CMD ["./app"]

Common Pitfalls to Avoid

Pitfall 1: Forgetting .dockerignore

# .dockerignore - What NOT to include in image
.git
.gitignore
README.md
Dockerfile*
.dockerignore
tmp/
node_modules/
vendor/
.env
logs/
*.log
.DS_Store
coverage.out

Pitfall 2: Including Build Tools in Runtime

 1# ❌ WRONG: Build tools in final image
 2FROM golang:1.21-alpine
 3RUN apk add git make gcc
 4COPY . .
 5RUN go build -o app .
 6# Git, make, gcc still in final image!
 7
 8# ✅ CORRECT: Only runtime in final image
 9FROM golang:1.21-alpine AS builder
10RUN apk add git make gcc
11COPY . .
12RUN go build -o app .
13
14FROM alpine:latest
15COPY --from=builder /app/app .
16# Only the compiled binary

Pitfall 3: Not Using .dockerignore Effectively

 1# ❌ BAD: Copying everything
 2FROM golang:1.21-alpine
 3WORKDIR /app
 4COPY . .  # Copies .git, node_modules, etc.
 5RUN go build -o app .
 6
 7# ✅ GOOD: Selective copying
 8FROM golang:1.21-alpine
 9WORKDIR /app
10COPY go.mod go.sum ./
11RUN go mod download
12COPY *.go ./
13RUN go build -o app .
14# Only copies necessary files

Integration and Mastery - Production Deployment

Production Docker Compose

 1# docker-compose.prod.yml - Production deployment
 2version: '3.8'
 3
 4services:
 5  app:
 6    image: your-registry/go-app:latest
 7    restart: unless-stopped
 8    deploy:
 9      replicas: 3
10      resources:
11        limits:
12          cpus: '1.0'
13          memory: 512M
14        reservations:
15          cpus: '0.5'
16          memory: 256M
17    environment:
18      - PORT=8080
19      - DB_HOST=postgres
20      - REDIS_HOST=redis
21    depends_on:
22      postgres:
23        condition: service_healthy
24      redis:
25        condition: service_healthy
26    networks:
27      - app-network
28
29  postgres:
30    image: postgres:15-alpine
31    restart: unless-stopped
32    environment:
33      - POSTGRES_DB=${DB_NAME}
34      - POSTGRES_USER=${DB_USER}
35      - POSTGRES_PASSWORD=${DB_PASSWORD}
36    volumes:
37      - postgres-data:/var/lib/postgresql/data
38      - ./backups:/backups
39    networks:
40      - app-network
41    healthcheck:
42      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
43      interval: 30s
44      timeout: 10s
45      retries: 3
46
47  redis:
48    image: redis:7-alpine
49    restart: unless-stopped
50    command: redis-server --appendonly yes
51    volumes:
52      - redis-data:/data
53    networks:
54      - app-network
55    healthcheck:
56      test: ["CMD", "redis-cli", "ping"]
57      interval: 30s
58      timeout: 5s
59      retries: 3
60
61  nginx:
62    image: nginx:alpine
63    restart: unless-stopped
64    ports:
65      - "80:80"
66      - "443:443"
67    volumes:
68      - ./nginx.conf:/etc/nginx/nginx.conf:ro
69      - ./ssl:/etc/nginx/ssl:ro
70      - nginx-logs:/var/log/nginx
71    depends_on:
72      - app
73    networks:
74      - app-network
75
76volumes:
77  postgres-data:
78  redis-data:
79  nginx-logs:
80
81networks:
82  app-network:
83    driver: bridge

CI/CD Pipeline Integration

 1# .github/workflows/deploy.yml - GitHub Actions
 2name: Build and Deploy
 3
 4on:
 5  push:
 6    branches: [main]
 7  pull_request:
 8    branches: [main]
 9
10jobs:
11  test:
12    runs-on: ubuntu-latest
13    steps:
14    - uses: actions/checkout@v3
15    - uses: actions/setup-go@v4
16      with:
17        go-version: '1.21'
18
19    - name: Run tests
20      run: go test -v ./...
21
22    - name: Run integration tests
23      run: docker-compose -f docker-compose.test.yml up --abort-on-container-exit
24
25  build:
26    needs: test
27    runs-on: ubuntu-latest
28    if: github.ref == 'refs/heads/main'
29
30    steps:
31    - uses: actions/checkout@v3
32
33    - name: Set up Docker Buildx
34      uses: docker/setup-buildx-action@v2
35
36    - name: Login to Container Registry
37      uses: docker/login-action@v2
38      with:
39        registry: ghcr.io
40        username: ${{ github.actor }}
41        password: ${{ secrets.GITHUB_TOKEN }}
42
43    - name: Build and push
44      uses: docker/build-push-action@v4
45      with:
46        context: .
47        file: ./Dockerfile.production
48        push: true
49        tags: |
50          ghcr.io/${{ github.repository }}:latest
51          ghcr.io/${{ github.repository }}:${{ github.sha }}          
52        cache-from: type=gha
53        cache-to: type=gha,mode=max
54
55  deploy:
56    needs: build
57    runs-on: ubuntu-latest
58    if: github.ref == 'refs/heads/main'
59
60    steps:
61    - uses: actions/checkout@v3
62
63    - name: Deploy to production
64      run: |
65        docker-compose -f docker-compose.prod.yml pull
66        docker-compose -f docker-compose.prod.yml up -d        

Monitoring and Observability

 1# Dockerfile.monitoring - With observability
 2FROM golang:1.21-alpine AS builder
 3
 4# Install build dependencies
 5RUN apk add --no-cache git ca-certificates
 6
 7WORKDIR /build
 8COPY go.mod go.sum ./
 9RUN go mod download
10COPY . .
11
12# Build with embedded metrics
13RUN CGO_ENABLED=0 GOOS=linux go build \
14    -ldflags="-w -s -X main.Version=$(git describe --tags --always)" \
15    -o app .
16
17FROM alpine:3.18
18
19# Install runtime with monitoring tools
20RUN apk --no-cache add \
21    ca-certificates \
22    curl \
23    && rm -rf /var/cache/apk/*
24
25RUN addgroup -g 1001 -S appgroup && \
26    adduser -u 1001 -S appuser -G appgroup
27
28WORKDIR /app
29COPY --from=builder /build/app .
30RUN chown appuser:appgroup /app
31
32USER appuser
33
34EXPOSE 8080 9090  # 9090 for metrics
35
36# Multiple health checks
37HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
38    CMD curl -f http://localhost:8080/health && \
39           curl -f http://localhost:9090/metrics || exit 1
40
41CMD ["./app"]

Advanced Docker Networking for Go Applications

Understanding Docker Network Modes

Docker provides several network modes that affect how your Go application communicates with other containers and the outside world. Understanding these modes is crucial for building microservice architectures.

Network Modes:

  1. Bridge (Default): Isolated network with port mapping
  2. Host: Shares host's network stack
  3. Overlay: Multi-host networking for swarm mode
  4. Macvlan: Assigns MAC address to container
  5. None: No networking

Go Application with Custom Network Configuration

  1// network-demo.go - Advanced networking patterns
  2package main
  3
  4import (
  5    "context"
  6    "encoding/json"
  7    "fmt"
  8    "log"
  9    "net"
 10    "net/http"
 11    "os"
 12    "sync"
 13    "time"
 14)
 15
 16// ServiceDiscovery tracks available services
 17type ServiceDiscovery struct {
 18    mu       sync.RWMutex
 19    services map[string]ServiceInfo
 20}
 21
 22type ServiceInfo struct {
 23    Name     string    `json:"name"`
 24    Address  string    `json:"address"`
 25    Port     int       `json:"port"`
 26    Healthy  bool      `json:"healthy"`
 27    LastSeen time.Time `json:"last_seen"`
 28}
 29
 30func NewServiceDiscovery() *ServiceDiscovery {
 31    return &ServiceDiscovery{
 32        services: make(map[string]ServiceInfo),
 33    }
 34}
 35
 36func (sd *ServiceDiscovery) Register(name, address string, port int) {
 37    sd.mu.Lock()
 38    defer sd.mu.Unlock()
 39
 40    sd.services[name] = ServiceInfo{
 41        Name:     name,
 42        Address:  address,
 43        Port:     port,
 44        Healthy:  true,
 45        LastSeen: time.Now(),
 46    }
 47
 48    log.Printf("Service registered: %s at %s:%d", name, address, port)
 49}
 50
 51func (sd *ServiceDiscovery) Lookup(name string) (ServiceInfo, bool) {
 52    sd.mu.RLock()
 53    defer sd.mu.RUnlock()
 54
 55    service, exists := sd.services[name]
 56    return service, exists
 57}
 58
 59func (sd *ServiceDiscovery) ListServices() []ServiceInfo {
 60    sd.mu.RLock()
 61    defer sd.mu.RUnlock()
 62
 63    services := make([]ServiceInfo, 0, len(sd.services))
 64    for _, service := range sd.services {
 65        services = append(services, service)
 66    }
 67    return services
 68}
 69
 70// NetworkInfo provides container network information
 71type NetworkInfo struct {
 72    Hostname   string   `json:"hostname"`
 73    Interfaces []string `json:"interfaces"`
 74    IPAddresses []string `json:"ip_addresses"`
 75}
 76
 77func getNetworkInfo() NetworkInfo {
 78    hostname, _ := os.Hostname()
 79
 80    info := NetworkInfo{
 81        Hostname:    hostname,
 82        Interfaces:  []string{},
 83        IPAddresses: []string{},
 84    }
 85
 86    // Get all network interfaces
 87    interfaces, err := net.Interfaces()
 88    if err != nil {
 89        return info
 90    }
 91
 92    for _, iface := range interfaces {
 93        info.Interfaces = append(info.Interfaces, iface.Name)
 94
 95        // Get addresses for this interface
 96        addrs, err := iface.Addrs()
 97        if err != nil {
 98            continue
 99        }
100
101        for _, addr := range addrs {
102            if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
103                if ipnet.IP.To4() != nil {
104                    info.IPAddresses = append(info.IPAddresses, ipnet.IP.String())
105                }
106            }
107        }
108    }
109
110    return info
111}
112
113// HealthChecker performs health checks on services
114type HealthChecker struct {
115    client  *http.Client
116    timeout time.Duration
117}
118
119func NewHealthChecker(timeout time.Duration) *HealthChecker {
120    return &HealthChecker{
121        client: &http.Client{
122            Timeout: timeout,
123        },
124        timeout: timeout,
125    }
126}
127
128func (hc *HealthChecker) Check(service ServiceInfo) bool {
129    url := fmt.Sprintf("http://%s:%d/health", service.Address, service.Port)
130
131    resp, err := hc.client.Get(url)
132    if err != nil {
133        log.Printf("Health check failed for %s: %v", service.Name, err)
134        return false
135    }
136    defer resp.Body.Close()
137
138    return resp.StatusCode == http.StatusOK
139}
140
141func main() {
142    port := os.Getenv("PORT")
143    if port == "" {
144        port = "8080"
145    }
146
147    sd := NewServiceDiscovery()
148    hc := NewHealthChecker(5 * time.Second)
149
150    // Auto-register this service
151    networkInfo := getNetworkInfo()
152    sd.Register("self", networkInfo.Hostname, mustAtoi(port))
153
154    // Discover peer services from environment
155    if peers := os.Getenv("PEER_SERVICES"); peers != "" {
156        // Parse and register peer services
157        log.Printf("Discovered peer services: %s", peers)
158    }
159
160    // Network info endpoint
161    http.HandleFunc("/network", func(w http.ResponseWriter, r *http.Request) {
162        w.Header().Set("Content-Type", "application/json")
163        json.NewEncoder(w).Encode(networkInfo)
164    })
165
166    // Service discovery endpoint
167    http.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
168        w.Header().Set("Content-Type", "application/json")
169        services := sd.ListServices()
170        json.NewEncoder(w).Encode(services)
171    })
172
173    // Service lookup endpoint
174    http.HandleFunc("/services/lookup", func(w http.ResponseWriter, r *http.Request) {
175        serviceName := r.URL.Query().Get("name")
176        if serviceName == "" {
177            http.Error(w, "service name required", http.StatusBadRequest)
178            return
179        }
180
181        service, found := sd.Lookup(serviceName)
182        if !found {
183            http.Error(w, "service not found", http.StatusNotFound)
184            return
185        }
186
187        w.Header().Set("Content-Type", "application/json")
188        json.NewEncoder(w).Encode(service)
189    })
190
191    // Health check endpoint
192    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
193        w.Header().Set("Content-Type", "application/json")
194        json.NewEncoder(w).Encode(map[string]string{
195            "status": "healthy",
196            "time":   time.Now().Format(time.RFC3339),
197        })
198    })
199
200    // Start background health checker
201    go func() {
202        ticker := time.NewTicker(30 * time.Second)
203        defer ticker.Stop()
204
205        for range ticker.C {
206            services := sd.ListServices()
207            for _, service := range services {
208                if service.Name != "self" {
209                    healthy := hc.Check(service)
210                    log.Printf("Health check %s: %v", service.Name, healthy)
211                }
212            }
213        }
214    }()
215
216    log.Printf("Starting network-aware service on port %s", port)
217    log.Printf("Network info: %+v", networkInfo)
218    log.Fatal(http.ListenAndServe(":"+port, nil))
219}
220
221func mustAtoi(s string) int {
222    var i int
223    fmt.Sscanf(s, "%d", &i)
224    return i
225}

Docker Network Configuration

 1# docker-compose.network.yml - Advanced networking
 2version: '3.8'
 3
 4services:
 5  api-1:
 6    build: .
 7    environment:
 8      - PORT=8080
 9      - SERVICE_NAME=api-1
10      - PEER_SERVICES=api-2:8080,api-3:8080
11    networks:
12      - frontend
13      - backend
14    deploy:
15      replicas: 1
16
17  api-2:
18    build: .
19    environment:
20      - PORT=8080
21      - SERVICE_NAME=api-2
22      - PEER_SERVICES=api-1:8080,api-3:8080
23    networks:
24      - frontend
25      - backend
26    deploy:
27      replicas: 1
28
29  api-3:
30    build: .
31    environment:
32      - PORT=8080
33      - SERVICE_NAME=api-3
34      - PEER_SERVICES=api-1:8080,api-2:8080
35    networks:
36      - frontend
37      - backend
38    deploy:
39      replicas: 1
40
41  database:
42    image: postgres:15-alpine
43    environment:
44      - POSTGRES_DB=appdb
45      - POSTGRES_USER=appuser
46      - POSTGRES_PASSWORD=secret
47    networks:
48      - backend
49    # Database only on backend network
50
51  nginx:
52    image: nginx:alpine
53    ports:
54      - "80:80"
55    networks:
56      - frontend
57    # Nginx only on frontend network
58
59networks:
60  frontend:
61    driver: bridge
62    ipam:
63      config:
64        - subnet: 172.25.0.0/16
65  backend:
66    driver: bridge
67    internal: true  # No external access
68    ipam:
69      config:
70        - subnet: 172.26.0.0/16

Network Security Best Practices

 1# Dockerfile.network-secure - Network security hardening
 2FROM golang:1.21-alpine AS builder
 3
 4WORKDIR /build
 5COPY go.mod go.sum ./
 6RUN go mod download
 7COPY . .
 8
 9# Build with network flags
10RUN CGO_ENABLED=0 go build \
11    -ldflags="-w -s" \
12    -o app .
13
14FROM alpine:3.18
15
16# Install security tools
17RUN apk --no-cache add \
18    iptables \
19    ca-certificates \
20    && rm -rf /var/cache/apk/*
21
22RUN addgroup -g 1001 -S appgroup && \
23    adduser -u 1001 -S appuser -G appgroup
24
25WORKDIR /app
26COPY --from=builder /build/app .
27RUN chown appuser:appgroup /app
28
29USER appuser
30
31# Only expose necessary ports
32EXPOSE 8080
33
34# Network-related environment variables
35ENV SERVICE_NAME=api
36ENV NETWORK_INTERFACE=eth0
37ENV CONNECTION_TIMEOUT=30s
38
39HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
40    CMD wget -q --spider http://localhost:8080/health || exit 1
41
42CMD ["./app"]

Container Orchestration Considerations

Kubernetes Deployment Patterns

When moving Go applications to Kubernetes, you need to consider additional patterns beyond basic Docker containerization.

Key Considerations:

  1. Readiness vs Liveness Probes
  2. Resource Requests and Limits
  3. Pod Disruption Budgets
  4. Horizontal Pod Autoscaling
  5. ConfigMaps and Secrets Management

Kubernetes-Ready Go Application

  1// k8s-ready.go - Kubernetes-optimized application
  2package main
  3
  4import (
  5    "context"
  6    "encoding/json"
  7    "fmt"
  8    "log"
  9    "net/http"
 10    "os"
 11    "os/signal"
 12    "sync/atomic"
 13    "syscall"
 14    "time"
 15)
 16
 17// ApplicationState tracks readiness and liveness
 18type ApplicationState struct {
 19    ready  int32  // 0 = not ready, 1 = ready
 20    live   int32  // 0 = not alive, 1 = alive
 21    startTime time.Time
 22}
 23
 24func NewApplicationState() *ApplicationState {
 25    return &ApplicationState{
 26        ready:     0,
 27        live:      1,  // Start as alive
 28        startTime: time.Now(),
 29    }
 30}
 31
 32func (s *ApplicationState) SetReady(ready bool) {
 33    if ready {
 34        atomic.StoreInt32(&s.ready, 1)
 35        log.Println("Application is now READY")
 36    } else {
 37        atomic.StoreInt32(&s.ready, 0)
 38        log.Println("Application is now NOT READY")
 39    }
 40}
 41
 42func (s *ApplicationState) IsReady() bool {
 43    return atomic.LoadInt32(&s.ready) == 1
 44}
 45
 46func (s *ApplicationState) SetLive(live bool) {
 47    if live {
 48        atomic.StoreInt32(&s.live, 1)
 49    } else {
 50        atomic.StoreInt32(&s.live, 0)
 51        log.Println("Application is now NOT ALIVE")
 52    }
 53}
 54
 55func (s *ApplicationState) IsLive() bool {
 56    return atomic.LoadInt32(&s.live) == 1
 57}
 58
 59func (s *ApplicationState) Uptime() time.Duration {
 60    return time.Since(s.startTime)
 61}
 62
 63// DependencyChecker verifies external dependencies
 64type DependencyChecker struct {
 65    checks map[string]func() error
 66}
 67
 68func NewDependencyChecker() *DependencyChecker {
 69    return &DependencyChecker{
 70        checks: make(map[string]func() error),
 71    }
 72}
 73
 74func (dc *DependencyChecker) AddCheck(name string, check func() error) {
 75    dc.checks[name] = check
 76}
 77
 78func (dc *DependencyChecker) CheckAll() map[string]error {
 79    results := make(map[string]error)
 80    for name, check := range dc.checks {
 81        results[name] = check()
 82    }
 83    return results
 84}
 85
 86func main() {
 87    port := getEnv("PORT", "8080")
 88
 89    state := NewApplicationState()
 90    depChecker := NewDependencyChecker()
 91
 92    // Add dependency checks
 93    depChecker.AddCheck("database", func() error {
 94        dbHost := getEnv("DB_HOST", "localhost")
 95        // Simulate database check
 96        if dbHost == "" {
 97            return fmt.Errorf("database not configured")
 98        }
 99        log.Printf("Database check passed: %s", dbHost)
100        return nil
101    })
102
103    depChecker.AddCheck("cache", func() error {
104        cacheHost := getEnv("CACHE_HOST", "localhost")
105        // Simulate cache check
106        if cacheHost == "" {
107            return fmt.Errorf("cache not configured")
108        }
109        log.Printf("Cache check passed: %s", cacheHost)
110        return nil
111    })
112
113    // Kubernetes readiness probe
114    http.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
115        if !state.IsReady() {
116            w.WriteHeader(http.StatusServiceUnavailable)
117            json.NewEncoder(w).Encode(map[string]string{
118                "status": "not ready",
119                "reason": "application still initializing",
120            })
121            return
122        }
123
124        // Check dependencies
125        results := depChecker.CheckAll()
126        allHealthy := true
127        for _, err := range results {
128            if err != nil {
129                allHealthy = false
130                break
131            }
132        }
133
134        if !allHealthy {
135            w.WriteHeader(http.StatusServiceUnavailable)
136            errorsMap := make(map[string]string)
137            for name, err := range results {
138                if err != nil {
139                    errorsMap[name] = err.Error()
140                }
141            }
142            json.NewEncoder(w).Encode(map[string]interface{}{
143                "status": "not ready",
144                "errors": errorsMap,
145            })
146            return
147        }
148
149        w.WriteHeader(http.StatusOK)
150        json.NewEncoder(w).Encode(map[string]string{
151            "status": "ready",
152        })
153    })
154
155    // Kubernetes liveness probe
156    http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
157        if !state.IsLive() {
158            w.WriteHeader(http.StatusServiceUnavailable)
159            json.NewEncoder(w).Encode(map[string]string{
160                "status": "not alive",
161            })
162            return
163        }
164
165        w.WriteHeader(http.StatusOK)
166        json.NewEncoder(w).Encode(map[string]interface{}{
167            "status":  "alive",
168            "uptime":  state.Uptime().String(),
169        })
170    })
171
172    // Metrics endpoint for Prometheus
173    http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
174        w.Header().Set("Content-Type", "text/plain")
175        fmt.Fprintf(w, "# HELP app_uptime_seconds Application uptime in seconds\n")
176        fmt.Fprintf(w, "# TYPE app_uptime_seconds gauge\n")
177        fmt.Fprintf(w, "app_uptime_seconds %.2f\n", state.Uptime().Seconds())
178        fmt.Fprintf(w, "# HELP app_ready Application readiness status\n")
179        fmt.Fprintf(w, "# TYPE app_ready gauge\n")
180        if state.IsReady() {
181            fmt.Fprintf(w, "app_ready 1\n")
182        } else {
183            fmt.Fprintf(w, "app_ready 0\n")
184        }
185    })
186
187    // Main API endpoint
188    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
189        w.Header().Set("Content-Type", "application/json")
190        json.NewEncoder(w).Encode(map[string]interface{}{
191            "message": "Kubernetes-ready Go application",
192            "pod":     getEnv("HOSTNAME", "unknown"),
193            "version": getEnv("APP_VERSION", "dev"),
194        })
195    })
196
197    // Graceful shutdown endpoint (for testing)
198    http.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
199        log.Println("Shutdown requested")
200        state.SetReady(false)
201        time.Sleep(5 * time.Second)  // Grace period
202        state.SetLive(false)
203        w.WriteHeader(http.StatusOK)
204        json.NewEncoder(w).Encode(map[string]string{
205            "status": "shutting down",
206        })
207    })
208
209    server := &http.Server{
210        Addr:         ":" + port,
211        ReadTimeout:  15 * time.Second,
212        WriteTimeout: 15 * time.Second,
213        IdleTimeout:  60 * time.Second,
214    }
215
216    // Start server
217    go func() {
218        log.Printf("Server starting on port %s", port)
219        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
220            log.Fatalf("Server failed: %v", err)
221        }
222    }()
223
224    // Simulate initialization
225    go func() {
226        log.Println("Initializing application...")
227        time.Sleep(10 * time.Second)  // Simulate startup time
228
229        // Check dependencies
230        results := depChecker.CheckAll()
231        allHealthy := true
232        for name, err := range results {
233            if err != nil {
234                log.Printf("Dependency check failed for %s: %v", name, err)
235                allHealthy = false
236            }
237        }
238
239        if allHealthy {
240            state.SetReady(true)
241        } else {
242            log.Println("Application not ready due to dependency failures")
243        }
244    }()
245
246    // Graceful shutdown
247    quit := make(chan os.Signal, 1)
248    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
249    <-quit
250
251    log.Println("Shutting down gracefully...")
252    state.SetReady(false)
253
254    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
255    defer cancel()
256
257    if err := server.Shutdown(ctx); err != nil {
258        log.Fatalf("Server shutdown failed: %v", err)
259    }
260
261    log.Println("Server stopped")
262}
263
264func getEnv(key, defaultValue string) string {
265    if value := os.Getenv(key); value != "" {
266        return value
267    }
268    return defaultValue
269}

Kubernetes Deployment Manifest

  1# k8s-deployment.yml - Production Kubernetes deployment
  2apiVersion: apps/v1
  3kind: Deployment
  4metadata:
  5  name: go-app
  6  labels:
  7    app: go-app
  8    version: v1
  9spec:
 10  replicas: 3
 11  strategy:
 12    type: RollingUpdate
 13    rollingUpdate:
 14      maxSurge: 1
 15      maxUnavailable: 0
 16  selector:
 17    matchLabels:
 18      app: go-app
 19  template:
 20    metadata:
 21      labels:
 22        app: go-app
 23        version: v1
 24      annotations:
 25        prometheus.io/scrape: "true"
 26        prometheus.io/port: "8080"
 27        prometheus.io/path: "/metrics"
 28    spec:
 29      securityContext:
 30        runAsNonRoot: true
 31        runAsUser: 1001
 32        fsGroup: 1001
 33
 34      containers:
 35      - name: go-app
 36        image: your-registry/go-app:v1.0.0
 37        imagePullPolicy: Always
 38
 39        ports:
 40        - name: http
 41          containerPort: 8080
 42          protocol: TCP
 43
 44        env:
 45        - name: PORT
 46          value: "8080"
 47        - name: DB_HOST
 48          valueFrom:
 49            configMapKeyRef:
 50              name: go-app-config
 51              key: db_host
 52        - name: DB_PASSWORD
 53          valueFrom:
 54            secretKeyRef:
 55              name: go-app-secrets
 56              key: db_password
 57        - name: APP_VERSION
 58          value: "v1.0.0"
 59        - name: HOSTNAME
 60          valueFrom:
 61            fieldRef:
 62              fieldPath: metadata.name
 63
 64        resources:
 65          requests:
 66            memory: "128Mi"
 67            cpu: "100m"
 68          limits:
 69            memory: "512Mi"
 70            cpu: "500m"
 71
 72        livenessProbe:
 73          httpGet:
 74            path: /healthz
 75            port: http
 76          initialDelaySeconds: 30
 77          periodSeconds: 10
 78          timeoutSeconds: 5
 79          failureThreshold: 3
 80
 81        readinessProbe:
 82          httpGet:
 83            path: /readyz
 84            port: http
 85          initialDelaySeconds: 5
 86          periodSeconds: 5
 87          timeoutSeconds: 3
 88          failureThreshold: 3
 89
 90        lifecycle:
 91          preStop:
 92            exec:
 93              command: ["/bin/sh", "-c", "sleep 15"]
 94
 95        securityContext:
 96          allowPrivilegeEscalation: false
 97          readOnlyRootFilesystem: true
 98          runAsNonRoot: true
 99          runAsUser: 1001
100          capabilities:
101            drop:
102            - ALL
103
104        volumeMounts:
105        - name: tmp
106          mountPath: /tmp
107        - name: cache
108          mountPath: /app/cache
109
110      volumes:
111      - name: tmp
112        emptyDir: {}
113      - name: cache
114        emptyDir: {}
115
116      affinity:
117        podAntiAffinity:
118          preferredDuringSchedulingIgnoredDuringExecution:
119          - weight: 100
120            podAffinityTerm:
121              labelSelector:
122                matchExpressions:
123                - key: app
124                  operator: In
125                  values:
126                  - go-app
127              topologyKey: kubernetes.io/hostname
128
129---
130apiVersion: v1
131kind: Service
132metadata:
133  name: go-app
134  labels:
135    app: go-app
136spec:
137  type: ClusterIP
138  ports:
139  - port: 80
140    targetPort: http
141    protocol: TCP
142    name: http
143  selector:
144    app: go-app
145
146---
147apiVersion: autoscaling/v2
148kind: HorizontalPodAutoscaler
149metadata:
150  name: go-app-hpa
151spec:
152  scaleTargetRef:
153    apiVersion: apps/v1
154    kind: Deployment
155    name: go-app
156  minReplicas: 3
157  maxReplicas: 10
158  metrics:
159  - type: Resource
160    resource:
161      name: cpu
162      target:
163        type: Utilization
164        averageUtilization: 70
165  - type: Resource
166    resource:
167      name: memory
168      target:
169        type: Utilization
170        averageUtilization: 80
171
172---
173apiVersion: policy/v1
174kind: PodDisruptionBudget
175metadata:
176  name: go-app-pdb
177spec:
178  minAvailable: 2
179  selector:
180    matchLabels:
181      app: go-app

Security Hardening for Go Containers

Advanced Security Practices

Security is paramount in containerized environments. Let's explore comprehensive security hardening techniques for Go containers.

Security Layers:

  1. Build-time Security: Scanning and hardening during build
  2. Image Security: Minimal base images and vulnerability scanning
  3. Runtime Security: Non-root users, read-only filesystems
  4. Network Security: Proper firewall rules and segmentation
  5. Secret Management: Secure handling of credentials

Dockerfile with Security Best Practices

 1# Dockerfile.secure - Security-hardened build
 2# Stage 1: Build with security scanning
 3FROM golang:1.21-alpine AS builder
 4
 5# Install security tools
 6RUN apk add --no-cache \
 7    git \
 8    ca-certificates \
 9    tzdata \
10    && update-ca-certificates
11
12WORKDIR /build
13
14# Copy and verify dependencies
15COPY go.mod go.sum ./
16RUN go mod download && go mod verify
17
18# Run security checks
19RUN go install golang.org/x/vuln/cmd/govulncheck@latest
20COPY . .
21RUN govulncheck ./...
22
23# Build with security flags
24RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
25    go build -a -installsuffix cgo \
26    -ldflags="-w -s -extldflags '-static' -X main.Version=${VERSION}" \
27    -trimpath \
28    -o app .
29
30# Verify the binary
31RUN chmod +x app && \
32    file app && \
33    ldd app || true
34
35# Stage 2: Minimal runtime with security hardening
36FROM scratch
37
38# Copy SSL certificates
39COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
40
41# Copy timezone data
42COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
43
44# Copy passwd file for non-root user
45COPY --from=builder /etc/passwd /etc/passwd
46COPY --from=builder /etc/group /etc/group
47
48# Copy application binary
49COPY --from=builder /build/app /app
50
51# Use non-root user (defined in multi-stage)
52USER 65534:65534
53
54# Expose port
55EXPOSE 8080
56
57# Add health check
58HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
59    CMD ["/app", "--health-check"]
60
61# Set security labels
62LABEL security.scan="true" \
63      security.privileged="false" \
64      security.readonly="true"
65
66# Run application
67ENTRYPOINT ["/app"]

Security-Enhanced Go Application

  1// secure-app.go - Security-focused application
  2package main
  3
  4import (
  5    "context"
  6    "crypto/tls"
  7    "encoding/json"
  8    "flag"
  9    "fmt"
 10    "log"
 11    "net/http"
 12    "os"
 13    "os/signal"
 14    "syscall"
 15    "time"
 16)
 17
 18// SecurityConfig holds security settings
 19type SecurityConfig struct {
 20    TLSEnabled      bool
 21    CertFile        string
 22    KeyFile         string
 23    MinTLSVersion   uint16
 24    ReadTimeout     time.Duration
 25    WriteTimeout    time.Duration
 26    IdleTimeout     time.Duration
 27    MaxHeaderBytes  int
 28}
 29
 30// SecureServer wraps http.Server with security features
 31type SecureServer struct {
 32    server         *http.Server
 33    config         SecurityConfig
 34    shutdownGrace  time.Duration
 35}
 36
 37func NewSecureServer(config SecurityConfig) *SecureServer {
 38    mux := http.NewServeMux()
 39
 40    server := &http.Server{
 41        Addr:           ":8080",
 42        Handler:        securityMiddleware(mux),
 43        ReadTimeout:    config.ReadTimeout,
 44        WriteTimeout:   config.WriteTimeout,
 45        IdleTimeout:    config.IdleTimeout,
 46        MaxHeaderBytes: config.MaxHeaderBytes,
 47    }
 48
 49    // Configure TLS
 50    if config.TLSEnabled {
 51        server.TLSConfig = &tls.Config{
 52            MinVersion: config.MinTLSVersion,
 53            CurvePreferences: []tls.CurveID{
 54                tls.CurveP256,
 55                tls.X25519,
 56            },
 57            PreferServerCipherSuites: true,
 58            CipherSuites: []uint16{
 59                tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
 60                tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
 61                tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
 62                tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
 63                tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
 64                tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
 65            },
 66        }
 67    }
 68
 69    return &SecureServer{
 70        server:        server,
 71        config:        config,
 72        shutdownGrace: 30 * time.Second,
 73    }
 74}
 75
 76func securityMiddleware(next http.Handler) http.Handler {
 77    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 78        // Security headers
 79        w.Header().Set("X-Content-Type-Options", "nosniff")
 80        w.Header().Set("X-Frame-Options", "DENY")
 81        w.Header().Set("X-XSS-Protection", "1; mode=block")
 82        w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
 83        w.Header().Set("Content-Security-Policy", "default-src 'self'")
 84        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
 85        w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
 86
 87        // Remove server header
 88        w.Header().Del("Server")
 89
 90        // Request validation
 91        if r.ContentLength > 10*1024*1024 { // 10MB limit
 92            http.Error(w, "Request too large", http.StatusRequestEntityTooLarge)
 93            return
 94        }
 95
 96        // Path traversal protection
 97        if containsDotDot(r.URL.Path) {
 98            http.Error(w, "Invalid path", http.StatusBadRequest)
 99            return
100        }
101
102        next.ServeHTTP(w, r)
103    })
104}
105
106func containsDotDot(path string) bool {
107    for i := 0; i < len(path)-1; i++ {
108        if path[i] == '.' && path[i+1] == '.' {
109            return true
110        }
111    }
112    return false
113}
114
115func (ss *SecureServer) Start() error {
116    log.Println("Starting secure server on :8080")
117
118    if ss.config.TLSEnabled {
119        return ss.server.ListenAndServeTLS(ss.config.CertFile, ss.config.KeyFile)
120    }
121    return ss.server.ListenAndServe()
122}
123
124func (ss *SecureServer) Shutdown() error {
125    ctx, cancel := context.WithTimeout(context.Background(), ss.shutdownGrace)
126    defer cancel()
127
128    log.Println("Gracefully shutting down server...")
129    return ss.server.Shutdown(ctx)
130}
131
132func main() {
133    healthCheck := flag.Bool("health-check", false, "Perform health check and exit")
134    flag.Parse()
135
136    // Health check mode
137    if *healthCheck {
138        resp, err := http.Get("http://localhost:8080/health")
139        if err != nil || resp.StatusCode != http.StatusOK {
140            os.Exit(1)
141        }
142        os.Exit(0)
143    }
144
145    // Security configuration
146    config := SecurityConfig{
147        TLSEnabled:     getEnvBool("TLS_ENABLED", false),
148        CertFile:       getEnv("TLS_CERT", "/etc/tls/cert.pem"),
149        KeyFile:        getEnv("TLS_KEY", "/etc/tls/key.pem"),
150        MinTLSVersion:  tls.VersionTLS12,
151        ReadTimeout:    15 * time.Second,
152        WriteTimeout:   15 * time.Second,
153        IdleTimeout:    60 * time.Second,
154        MaxHeaderBytes: 1 << 20, // 1MB
155    }
156
157    server := NewSecureServer(config)
158
159    // Setup routes
160    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
161        w.Header().Set("Content-Type", "application/json")
162        json.NewEncoder(w).Encode(map[string]string{
163            "status": "healthy",
164        })
165    })
166
167    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
168        w.Header().Set("Content-Type", "application/json")
169        json.NewEncoder(w).Encode(map[string]string{
170            "message": "Secure Go application",
171            "version": "1.0.0",
172        })
173    })
174
175    // Start server
176    go func() {
177        if err := server.Start(); err != nil && err != http.ErrServerClosed {
178            log.Fatalf("Server failed: %v", err)
179        }
180    }()
181
182    // Graceful shutdown
183    quit := make(chan os.Signal, 1)
184    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
185    <-quit
186
187    if err := server.Shutdown(); err != nil {
188        log.Fatalf("Server shutdown failed: %v", err)
189    }
190
191    log.Println("Server stopped")
192}
193
194func getEnv(key, defaultValue string) string {
195    if value := os.Getenv(key); value != "" {
196        return value
197    }
198    return defaultValue
199}
200
201func getEnvBool(key string, defaultValue bool) bool {
202    if value := os.Getenv(key); value != "" {
203        return value == "true" || value == "1"
204    }
205    return defaultValue
206}

Container Security Scanning

 1# .github/workflows/security-scan.yml
 2name: Container Security Scan
 3
 4on:
 5  push:
 6    branches: [main]
 7  pull_request:
 8    branches: [main]
 9
10jobs:
11  security-scan:
12    runs-on: ubuntu-latest
13
14    steps:
15    - name: Checkout code
16      uses: actions/checkout@v3
17
18    - name: Build Docker image
19      run: docker build -f Dockerfile.secure -t go-app:latest .
20
21    - name: Run Trivy vulnerability scanner
22      uses: aquasecurity/trivy-action@master
23      with:
24        image-ref: go-app:latest
25        format: 'sarif'
26        output: 'trivy-results.sarif'
27        severity: 'CRITICAL,HIGH'
28
29    - name: Upload Trivy results to GitHub Security
30      uses: github/codeql-action/upload-sarif@v2
31      with:
32        sarif_file: 'trivy-results.sarif'
33
34    - name: Run Snyk container scan
35      uses: snyk/actions/docker@master
36      env:
37        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
38      with:
39        image: go-app:latest
40        args: --severity-threshold=high
41
42    - name: Check for secrets in image
43      run: |
44        docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
45          aquasec/trivy image --scanners secret go-app:latest        

Practice Exercises

Exercise 1: Multi-Stage Build Optimization

🎯 Learning Objectives:

  • Master multi-stage Docker builds for Go applications
  • Learn build optimization techniques to minimize image size
  • Understand layer caching strategies for faster builds
  • Practice security hardening with non-root users

🌍 Real-World Context:
Multi-stage builds transformed how companies build containerized Go applications. At Shopify, optimizing Go service images reduced deployment times from 15 minutes to 2 minutes, enabling faster iteration cycles. At Twitter, image size optimization reduced their cloud storage costs by $1.2M annually while improving deployment speed.

⏱️ Time Estimate: 60 minutes
📊 Difficulty: Intermediate

Create a production-ready multi-stage Docker build for a Go web service with the following requirements:

  1. Build stage with Go 1.21, dependency caching, and optimization flags
  2. Runtime stage using Alpine Linux with non-root user
  3. Health checks, proper signal handling, and environment configuration
  4. Build the image and compare sizes between single-stage and multi-stage approaches
Solution
  1// main.go - Production-ready web service
  2package main
  3
  4import (
  5    "context"
  6    "encoding/json"
  7    "fmt"
  8    "log"
  9    "net/http"
 10    "os"
 11    "os/signal"
 12    "syscall"
 13    "time"
 14)
 15
 16// Application info
 17var (
 18    Version   = "dev"
 19    BuildTime = "unknown"
 20    GitCommit = "unknown"
 21)
 22
 23// Health response
 24type HealthResponse struct {
 25    Status    string `json:"status"`
 26    Version   string `json:"version"`
 27    BuildTime string `json:"build_time"`
 28    GitCommit string `json:"git_commit"`
 29    Timestamp string `json:"timestamp"`
 30}
 31
 32// Metrics
 33type Metrics struct {
 34    Requests      int64 `json:"requests"`
 35    UptimeSeconds int64 `json:"uptime_seconds"`
 36    MemoryMB      int64 `json:"memory_mb"`
 37}
 38
 39var (
 40    requestCount int64
 41    startTime    = time.Now()
 42)
 43
 44func healthHandler(w http.ResponseWriter, r *http.Request) {
 45    w.Header().Set("Content-Type", "application/json")
 46
 47    response := HealthResponse{
 48        Status:    "healthy",
 49        Version:   Version,
 50        BuildTime: BuildTime,
 51        GitCommit: GitCommit,
 52        Timestamp: time.Now().Format(time.RFC3339),
 53    }
 54
 55    json.NewEncoder(w).Encode(response)
 56}
 57
 58func metricsHandler(w http.ResponseWriter, r *http.Request) {
 59    w.Header().Set("Content-Type", "application/json")
 60
 61    // Simple memory calculation
 62    var m runtime.MemStats
 63    runtime.ReadMemStats(&m)
 64
 65    metrics := Metrics{
 66        Requests:      requestCount,
 67        UptimeSeconds: int64(time.Since(startTime).Seconds()),
 68        MemoryMB:      int64(m.Alloc / 1024 / 1024),
 69    }
 70
 71    json.NewEncoder(w).Encode(metrics)
 72}
 73
 74func indexHandler(w http.ResponseWriter, r *http.Request) {
 75    atomic.AddInt64(&requestCount, 1)
 76
 77    w.Header().Set("Content-Type", "application/json")
 78    response := map[string]interface{}{
 79        "message":    "Hello from optimized Go container!",
 80        "version":    Version,
 81        "method":     r.Method,
 82        "path":       r.URL.Path,
 83        "user_agent": r.UserAgent(),
 84    }
 85    json.NewEncoder(w).Encode(response)
 86}
 87
 88func main() {
 89    port := os.Getenv("PORT")
 90    if port == "" {
 91        port = "8080"
 92    }
 93
 94    // Setup routes
 95    http.HandleFunc("/", loggingMiddleware(indexHandler))
 96    http.HandleFunc("/health", healthHandler)
 97    http.HandleFunc("/metrics", metricsHandler)
 98
 99    // Start server with graceful shutdown
100    server := &http.Server{
101        Addr:         ":" + port,
102        ReadTimeout:  15 * time.Second,
103        WriteTimeout: 15 * time.Second,
104        IdleTimeout:  60 * time.Second,
105    }
106
107    go func() {
108        log.Printf("Server starting on port %s", port)
109        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
110            log.Fatalf("Server failed: %v", err)
111        }
112    }()
113
114    // Graceful shutdown
115    quit := make(chan os.Signal, 1)
116    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
117    <-quit
118
119    log.Println("Shutting down server...")
120
121    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
122    defer cancel()
123
124    if err := server.Shutdown(ctx); err != nil {
125        log.Fatalf("Server shutdown failed: %v", err)
126    }
127
128    log.Println("Server stopped")
129}
130
131func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
132    return func(w http.ResponseWriter, r *http.Request) {
133        start := time.Now()
134
135        // Call the next handler
136        next(w, r)
137
138        // Log the request
139        duration := time.Since(start)
140        log.Printf("%s %s %d %v", r.Method, r.URL.Path, 200, duration)
141    }
142}
 1# Dockerfile.optimized - Production multi-stage build
 2# Stage 1: Builder
 3FROM golang:1.21-alpine AS builder
 4
 5# Install build dependencies
 6RUN apk add --no-cache git ca-certificates
 7
 8# Set working directory
 9WORKDIR /build
10
11# Copy dependency files
12COPY go.mod go.sum ./
13
14# Download dependencies
15RUN go mod download && go mod verify
16
17# Copy source code
18COPY . .
19
20# Build with embedded build info
21ARG VERSION=dev
22ARG BUILD_TIME
23ARG GIT_COMMIT
24
25RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
26    go build -a -installsuffix cgo \
27    -ldflags="-w -s -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME} -X main.GitCommit=${GIT_COMMIT}" \
28    -o app .
29
30# Stage 2: Runtime
31FROM alpine:3.18
32
33# Install runtime dependencies
34RUN apk --no-cache add \
35    ca-certificates \
36    tzdata \
37    curl \
38    && rm -rf /var/cache/apk/*
39
40# Create non-root user
41RUN addgroup -g 1001 -S appgroup && \
42    adduser -u 1001 -S appuser -G appgroup
43
44# Set working directory
45WORKDIR /app
46
47# Copy binary from builder
48COPY --from=builder /build/app .
49
50# Set ownership and permissions
51RUN chown appuser:appgroup /app && \
52    chmod +x /app
53
54# Switch to non-root user
55USER appuser
56
57# Expose ports
58EXPOSE 8080 9090
59
60# Health checks
61HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
62    CMD curl -f http://localhost:8080/health || exit 1
63
64# Set environment variables
65ENV PORT=8080
66ENV VERSION=dev
67
68# Run the application
69CMD ["./app"]
 1#!/bin/bash
 2# build.sh - Build script with optimizations
 3
 4# Build arguments
 5VERSION=${VERSION:-$(git describe --tags --always --dirty)}
 6BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
 7GIT_COMMIT=$(git rev-parse --short HEAD)
 8
 9echo "Building version: $VERSION"
10echo "Build time: $BUILD_TIME"
11echo "Git commit: $GIT_COMMIT"
12
13# Build with build args
14docker build \
15    --build-arg VERSION="$VERSION" \
16    --build-arg BUILD_TIME="$BUILD_TIME" \
17    --build-arg GIT_COMMIT="$GIT_COMMIT" \
18    -f Dockerfile.optimized \
19    -t go-app-optimized:"$VERSION" \
20    -t go-app-optimized:latest .
21
22# Show image sizes
23echo "Image sizes:"
24docker images | grep go-app-optimized
25
26# Test the build
27echo "Testing container..."
28docker run --rm -p 8080:8080 go-app-optimized:"$VERSION" &
29sleep 3
30
31curl -f http://localhost:8080/health || echo "Health check failed"
32curl -f http://localhost:8080/metrics || echo "Metrics check failed"
33
34# Clean up
35docker ps -q | xargs docker stop 2>/dev/null || true
36echo "Build and test completed!"

Expected Results:

  • Single-stage image: ~520MB with build tools
  • Multi-stage image: ~15MB
  • Build time: ~2 minutes
  • Security: Non-root user, minimal dependencies

Key Optimizations:

  1. Separate build and runtime stages
  2. Cached dependency layer
  3. Stripped binary
  4. Non-root user with minimal permissions
  5. Health checks for monitoring
  6. Embedded build information

Exercise 2: Production Docker Compose Setup

🎯 Learning Objectives:

  • Design production-ready Docker Compose configurations
  • Implement service discovery and inter-service communication
  • Add health checks, monitoring, and graceful shutdown
  • Practice volume management and data persistence

🌍 Real-World Context:
Production Docker Compose setups are critical for small to medium-sized Go applications. At DigitalOcean, their managed Go services use Docker Compose patterns that achieve 99.9% uptime with automatic failover. At GitLab, their self-hosted GitLab Runner service uses Docker Compose for Go applications, supporting 100,000+ CI/CD jobs daily with zero-downtime deployments.

⏱️ Time Estimate: 90 minutes
📊 Difficulty: Advanced

Create a production Docker Compose setup for a Go microservice architecture with the following components:

  1. Go API service with health checks and metrics
  2. PostgreSQL database with persistence and backups
  3. Redis cache with persistence
  4. Nginx reverse proxy with SSL termination
  5. Monitoring stack
  6. Log aggregation
Solution
  1# docker-compose.prod.yml - Production-ready setup
  2version: '3.8'
  3
  4services:
  5  # Go API Service
  6  api:
  7    build:
  8      context: ./api
  9      dockerfile: Dockerfile.prod
 10    image: go-api:latest
 11    restart: unless-stopped
 12    deploy:
 13      replicas: 3
 14      resources:
 15        limits:
 16          cpus: '1.0'
 17          memory: 512M
 18        reservations:
 19          cpus: '0.5'
 20          memory: 256M
 21      restart_policy:
 22        condition: on-failure
 23        delay: 5s
 24        max_attempts: 3
 25    environment:
 26      - PORT=8080
 27      - DB_HOST=postgres
 28      - DB_PORT=5432
 29      - DB_USER=${DB_USER:-gouser}
 30      - DB_PASSWORD=${DB_PASSWORD}
 31      - DB_NAME=${DB_NAME:-godb}
 32      - REDIS_HOST=redis
 33      - REDIS_PORT=6379
 34      - LOG_LEVEL=info
 35      - JAEGER_ENDPOINT=http://jaeger:14268/api/traces
 36    volumes:
 37      - ./logs/api:/app/logs
 38      - ./data/uploads:/app/uploads
 39    depends_on:
 40      postgres:
 41        condition: service_healthy
 42      redis:
 43        condition: service_healthy
 44    networks:
 45      - app-network
 46      - monitoring-network
 47    healthcheck:
 48      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
 49      interval: 30s
 50      timeout: 10s
 51      retries: 3
 52      start_period: 40s
 53
 54  # PostgreSQL Database
 55  postgres:
 56    image: postgres:15-alpine
 57    restart: unless-stopped
 58    environment:
 59      - POSTGRES_DB=${DB_NAME:-godb}
 60      - POSTGRES_USER=${DB_USER:-gouser}
 61      - POSTGRES_PASSWORD=${DB_PASSWORD}
 62      - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
 63    volumes:
 64      - postgres-data:/var/lib/postgresql/data
 65      - postgres-backup:/backups
 66      - ./init-scripts:/docker-entrypoint-initdb.d
 67    networks:
 68      - app-network
 69    healthcheck:
 70      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-gouser} -d ${DB_NAME:-godb}"]
 71      interval: 30s
 72      timeout: 10s
 73      retries: 5
 74      start_period: 60s
 75    deploy:
 76      resources:
 77        limits:
 78          cpus: '2.0'
 79          memory: 2G
 80        reservations:
 81          cpus: '1.0'
 82          memory: 1G
 83
 84  # Redis Cache
 85  redis:
 86    image: redis:7-alpine
 87    restart: unless-stopped
 88    command: redis-server /usr/local/etc/redis/redis.conf
 89    volumes:
 90      - redis-data:/data
 91      - redis-conf:/usr/local/etc/redis
 92    networks:
 93      - app-network
 94    healthcheck:
 95      test: ["CMD", "redis-cli", "ping"]
 96      interval: 30s
 97      timeout: 5s
 98      retries: 3
 99    deploy:
100      resources:
101        limits:
102          cpus: '0.5'
103          memory: 512M
104
105  # Nginx Reverse Proxy
106  nginx:
107    image: nginx:alpine
108    restart: unless-stopped
109    ports:
110      - "80:80"
111      - "443:443"
112    volumes:
113      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
114      - ./nginx/conf.d:/etc/nginx/conf.d:ro
115      - ./ssl:/etc/nginx/ssl:ro
116      - nginx-logs:/var/log/nginx
117    depends_on:
118      - api
119    networks:
120      - app-network
121    healthcheck:
122      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
123      interval: 30s
124      timeout: 10s
125      retries: 3
126
127  # Prometheus Monitoring
128  prometheus:
129    image: prom/prometheus:latest
130    restart: unless-stopped
131    ports:
132      - "9090:9090"
133    volumes:
134      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
135      - prometheus-data:/prometheus
136    command:
137      - '--config.file=/etc/prometheus/prometheus.yml'
138      - '--storage.tsdb.path=/prometheus'
139      - '--web.console.libraries=/etc/prometheus/console_libraries'
140      - '--web.console.templates=/etc/prometheus/consoles'
141      - '--storage.tsdb.retention.time=200h'
142      - '--web.enable-lifecycle'
143    networks:
144      - monitoring-network
145    healthcheck:
146      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9090/-/healthy"]
147      interval: 30s
148      timeout: 10s
149      retries: 3
150
151  # Grafana Dashboard
152  grafana:
153    image: grafana/grafana:latest
154    restart: unless-stopped
155    ports:
156      - "3000:3000"
157    environment:
158      - GF_SECURITY_ADMIN_USER=${GRAFANA_USER:-admin}
159      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
160      - GF_USERS_ALLOW_SIGN_UP=false
161    volumes:
162      - grafana-data:/var/lib/grafana
163      - ./grafana/provisioning:/etc/grafana/provisioning
164    depends_on:
165      - prometheus
166    networks:
167      - monitoring-network
168    healthcheck:
169      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
170      interval: 30s
171      timeout: 10s
172      retries: 3
173
174  # Jaeger Tracing
175  jaeger:
176    image: jaegertracing/all-in-one:latest
177    restart: unless-stopped
178    ports:
179      - "16686:16686"  # Jaeger UI
180      - "14268:14268"  # HTTP collector
181    environment:
182      - COLLECTOR_ZIPKIN_HTTP_PORT=9411
183    networks:
184      - monitoring-network
185      - app-network
186    healthcheck:
187      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:14269/"]
188      interval: 30s
189      timeout: 10s
190      retries: 3
191
192  # Log Rotation
193  logrotate:
194    image: blacklabelops/logrotate:latest
195    restart: unless-stopped
196    volumes:
197      - ./logs:/logs
198      - ./logrotate.conf:/etc/logrotate.d/logrotate.conf
199    command: --log /logs/api/*.log --hourly
200    networks:
201      - app-network
202
203volumes:
204  postgres-data:
205    driver: local
206  postgres-backup:
207    driver: local
208  redis-data:
209    driver: local
210  redis-conf:
211    driver: local
212  prometheus-data:
213    driver: local
214  grafana-data:
215    driver: local
216  nginx-logs:
217    driver: local
218
219networks:
220  app-network:
221    driver: bridge
222    ipam:
223      config:
224        - subnet: 172.20.0.0/16
225  monitoring-network:
226    driver: bridge
227    ipam:
228      config:
229        - subnet: 172.21.0.0/16
  1# nginx/nginx.conf - Production Nginx configuration
  2user nginx;
  3worker_processes auto;
  4error_log /var/log/nginx/error.log warn;
  5pid /var/run/nginx.pid;
  6
  7events {
  8    worker_connections 1024;
  9    use epoll;
 10    multi_accept on;
 11}
 12
 13http {
 14    include /etc/nginx/mime.types;
 15    default_type application/octet-stream;
 16
 17    # Logging format
 18    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
 19                    '$status $body_bytes_sent "$http_referer" '
 20                    '"$http_user_agent" "$http_x_forwarded_for"';
 21
 22    access_log /var/log/nginx/access.log main;
 23
 24    # Basic settings
 25    sendfile on;
 26    tcp_nopush on;
 27    tcp_nodelay on;
 28    keepalive_timeout 65;
 29    keepalive_requests 100;
 30    client_max_body_size 16M;
 31
 32    # Gzip compression
 33    gzip on;
 34    gzip_vary on;
 35    gzip_proxied any;
 36    gzip_comp_level 6;
 37    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
 38
 39    # Rate limiting
 40    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
 41    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
 42
 43    # Upstream for API servers
 44    upstream api_servers {
 45        least_conn;
 46        server api:8080 max_fails=3 fail_timeout=30s;
 47        keepalive 32;
 48    }
 49
 50    # HTTP to HTTPS redirect
 51    server {
 52        listen 80;
 53        server_name _;
 54        return 301 https://$server_name$request_uri;
 55    }
 56
 57    # Main HTTPS server
 58    server {
 59        listen 443 ssl http2;
 60        server_name your-domain.com;
 61
 62        ssl_certificate /etc/nginx/ssl/cert.pem;
 63        ssl_certificate_key /etc/nginx/ssl/key.pem;
 64        ssl_session_timeout 1d;
 65        ssl_session_cache shared:SSL:50m;
 66        ssl_session_tickets off;
 67        ssl_protocols TLSv1.2 TLSv1.3;
 68        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
 69        ssl_prefer_server_ciphers off;
 70
 71        # Security headers
 72        add_header Strict-Transport-Security "max-age=63072000" always;
 73        add_header X-Frame-Options DENY;
 74        add_header X-Content-Type-Options nosniff;
 75        add_header X-XSS-Protection "1; mode=block";
 76
 77        # API routes
 78        location /api/ {
 79            limit_req zone=api burst=20 nodelay;
 80            proxy_pass http://api_servers;
 81            proxy_http_version 1.1;
 82            proxy_set_header Upgrade $http_upgrade;
 83            proxy_set_header Connection 'upgrade';
 84            proxy_set_header Host $host;
 85            proxy_set_header X-Real-IP $remote_addr;
 86            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 87            proxy_set_header X-Forwarded-Proto $scheme;
 88            proxy_cache_bypass $http_upgrade;
 89        }
 90
 91        # Health check
 92        location /health {
 93            access_log off;
 94            return 200 "healthy\n";
 95            add_header Content-Type text/plain;
 96        }
 97
 98        # Rate limited login endpoint
 99        location /api/login {
100            limit_req zone=login burst=5 nodelay;
101            proxy_pass http://api_servers;
102        }
103
104        # Static files
105        location /static/ {
106            root /var/www;
107            expires 1y;
108            add_header Cache-Control "public, immutable";
109        }
110    }
111}
 1#!/bin/bash
 2# deploy.sh - Production deployment script
 3
 4set -e
 5
 6echo "🚀 Starting production deployment..."
 7
 8# Load environment variables
 9if [ -f .env ]; then
10    export $(cat .env | grep -v '^#' | xargs)
11fi
12
13# Validate required environment variables
14if [ -z "$DB_PASSWORD" ] || [ -z "$GRAFANA_PASSWORD" ]; then
15    echo "❌ Missing required environment variables"
16    exit 1
17fi
18
19# Pull latest images
20echo "📦 Pulling latest images..."
21docker-compose -f docker-compose.prod.yml pull
22
23# Backup database
24echo "💾 Creating database backup..."
25docker-compose -f docker-compose.prod.yml exec postgres pg_dump \
26    -U "${DB_USER:-gouser}" "${DB_NAME:-godb}" > "backup_$(date +%Y%m%d_%H%M%S).sql"
27
28# Deploy with zero downtime
29echo "🔄 Deploying new version..."
30docker-compose -f docker-compose.prod.yml up -d --no-deps api
31
32# Wait for health checks
33echo "🏥 Waiting for health checks..."
34sleep 30
35
36# Verify deployment
37echo "✅ Verifying deployment..."
38for i in {1..10}; do
39    if curl -f http://localhost/health; then
40        echo "✅ Health check passed!"
41        break
42    else
43        echo "⏳ Waiting for service to be ready..."
44        sleep 10
45    fi
46done
47
48# Clean up old containers
49echo "🧹 Cleaning up old containers..."
50docker system prune -f
51
52echo "🎉 Deployment completed successfully!"

Production Features:

  1. High Availability: Multiple API replicas with load balancing
  2. Data Persistence: PostgreSQL and Redis with volume mounts
  3. Monitoring: Prometheus + Grafana + Jaeger for full observability
  4. Security: SSL termination, rate limiting, non-root containers
  5. Health Checks: All services have comprehensive health monitoring
  6. Graceful Deployment: Zero-downtime deployment with backup/restore

Key Optimizations:

  1. Resource Limits: Proper CPU and memory constraints
  2. Network Isolation: Separate networks for app and monitoring
  3. Service Discovery: Internal DNS resolution
  4. Log Management: Structured logging with rotation
  5. Backup Strategy: Automated database backups

Exercise 3: Docker Networking and Service Discovery

🎯 Learning Objectives:

  • Implement service discovery patterns in Docker
  • Configure custom Docker networks for microservices
  • Build health checking and service registration systems
  • Practice inter-container communication patterns

🌍 Real-World Context:
Service discovery is critical for microservice architectures. At Lyft, their Docker-based service mesh reduced service discovery latency from 500ms to 5ms, enabling their system to handle 10M+ rides per day. At SoundCloud, implementing proper Docker networking patterns reduced deployment failures by 75% and simplified their microservice communication.

⏱️ Time Estimate: 75 minutes
📊 Difficulty: Advanced

Create a multi-service Go application with custom Docker networking that includes:

  1. Three API services that discover each other
  2. A service registry for tracking healthy services
  3. Custom bridge networks with proper isolation
  4. Health checking and automatic failover
  5. Load balancing across service instances
Solution
  1// service-registry.go - Central service registry
  2package main
  3
  4import (
  5    "encoding/json"
  6    "fmt"
  7    "log"
  8    "net/http"
  9    "os"
 10    "sync"
 11    "time"
 12)
 13
 14type ServiceInfo struct {
 15    ID        string    `json:"id"`
 16    Name      string    `json:"name"`
 17    Address   string    `json:"address"`
 18    Port      int       `json:"port"`
 19    Healthy   bool      `json:"healthy"`
 20    LastCheck time.Time `json:"last_check"`
 21    Metadata  map[string]string `json:"metadata"`
 22}
 23
 24type Registry struct {
 25    mu       sync.RWMutex
 26    services map[string]ServiceInfo
 27}
 28
 29func NewRegistry() *Registry {
 30    return &Registry{
 31        services: make(map[string]ServiceInfo),
 32    }
 33}
 34
 35func (r *Registry) Register(info ServiceInfo) {
 36    r.mu.Lock()
 37    defer r.mu.Unlock()
 38
 39    info.LastCheck = time.Now()
 40    r.services[info.ID] = info
 41    log.Printf("Service registered: %s (%s:%d)", info.Name, info.Address, info.Port)
 42}
 43
 44func (r *Registry) Deregister(id string) {
 45    r.mu.Lock()
 46    defer r.mu.Unlock()
 47
 48    delete(r.services, id)
 49    log.Printf("Service deregistered: %s", id)
 50}
 51
 52func (r *Registry) GetServices(name string) []ServiceInfo {
 53    r.mu.RLock()
 54    defer r.mu.RUnlock()
 55
 56    var services []ServiceInfo
 57    for _, svc := range r.services {
 58        if (name == "" || svc.Name == name) && svc.Healthy {
 59            services = append(services, svc)
 60        }
 61    }
 62    return services
 63}
 64
 65func (r *Registry) UpdateHealth(id string, healthy bool) {
 66    r.mu.Lock()
 67    defer r.mu.Unlock()
 68
 69    if svc, exists := r.services[id]; exists {
 70        svc.Healthy = healthy
 71        svc.LastCheck = time.Now()
 72        r.services[id] = svc
 73    }
 74}
 75
 76func (r *Registry) HealthCheck() {
 77    ticker := time.NewTicker(10 * time.Second)
 78    defer ticker.Stop()
 79
 80    client := &http.Client{
 81        Timeout: 5 * time.Second,
 82    }
 83
 84    for range ticker.C {
 85        r.mu.RLock()
 86        services := make([]ServiceInfo, 0, len(r.services))
 87        for _, svc := range r.services {
 88            services = append(services, svc)
 89        }
 90        r.mu.RUnlock()
 91
 92        for _, svc := range services {
 93            go func(s ServiceInfo) {
 94                url := fmt.Sprintf("http://%s:%d/health", s.Address, s.Port)
 95                resp, err := client.Get(url)
 96
 97                healthy := err == nil && resp != nil && resp.StatusCode == http.StatusOK
 98                if resp != nil {
 99                    resp.Body.Close()
100                }
101
102                r.UpdateHealth(s.ID, healthy)
103                log.Printf("Health check %s: %v", s.Name, healthy)
104            }(svc)
105        }
106    }
107}
108
109func main() {
110    port := getEnv("PORT", "8000")
111    registry := NewRegistry()
112
113    // Start health checking
114    go registry.HealthCheck()
115
116    // Register service endpoint
117    http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
118        if r.Method != http.MethodPost {
119            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
120            return
121        }
122
123        var info ServiceInfo
124        if err := json.NewDecoder(r.Body).Decode(&info); err != nil {
125            http.Error(w, err.Error(), http.StatusBadRequest)
126            return
127        }
128
129        registry.Register(info)
130        w.WriteHeader(http.StatusCreated)
131        json.NewEncoder(w).Encode(map[string]string{"status": "registered"})
132    })
133
134    // Deregister service endpoint
135    http.HandleFunc("/deregister", func(w http.ResponseWriter, r *http.Request) {
136        id := r.URL.Query().Get("id")
137        if id == "" {
138            http.Error(w, "id required", http.StatusBadRequest)
139            return
140        }
141
142        registry.Deregister(id)
143        w.WriteHeader(http.StatusOK)
144        json.NewEncoder(w).Encode(map[string]string{"status": "deregistered"})
145    })
146
147    // List services endpoint
148    http.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
149        name := r.URL.Query().Get("name")
150        services := registry.GetServices(name)
151
152        w.Header().Set("Content-Type", "application/json")
153        json.NewEncoder(w).Encode(services)
154    })
155
156    // Health check endpoint
157    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
158        w.Header().Set("Content-Type", "application/json")
159        json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
160    })
161
162    log.Printf("Service registry starting on port %s", port)
163    log.Fatal(http.ListenAndServe(":"+port, nil))
164}
165
166func getEnv(key, defaultValue string) string {
167    if value := os.Getenv(key); value != "" {
168        return value
169    }
170    return defaultValue
171}
  1// api-service.go - Service that registers with registry
  2package main
  3
  4import (
  5    "bytes"
  6    "encoding/json"
  7    "fmt"
  8    "log"
  9    "net/http"
 10    "os"
 11    "time"
 12)
 13
 14type ServiceInfo struct {
 15    ID       string            `json:"id"`
 16    Name     string            `json:"name"`
 17    Address  string            `json:"address"`
 18    Port     int               `json:"port"`
 19    Metadata map[string]string `json:"metadata"`
 20}
 21
 22func registerWithRegistry(info ServiceInfo, registryURL string) error {
 23    data, err := json.Marshal(info)
 24    if err != nil {
 25        return err
 26    }
 27
 28    resp, err := http.Post(registryURL+"/register", "application/json", bytes.NewBuffer(data))
 29    if err != nil {
 30        return err
 31    }
 32    defer resp.Body.Close()
 33
 34    if resp.StatusCode != http.StatusCreated {
 35        return fmt.Errorf("registration failed: %d", resp.StatusCode)
 36    }
 37
 38    log.Printf("Registered with registry: %s", info.ID)
 39    return nil
 40}
 41
 42func main() {
 43    serviceName := getEnv("SERVICE_NAME", "api-service")
 44    serviceID := getEnv("SERVICE_ID", serviceName+"-1")
 45    port := getEnv("PORT", "8080")
 46    registryURL := getEnv("REGISTRY_URL", "http://registry:8000")
 47    hostname, _ := os.Hostname()
 48
 49    // Register with service registry
 50    info := ServiceInfo{
 51        ID:      serviceID,
 52        Name:    serviceName,
 53        Address: hostname,
 54        Port:    mustAtoi(port),
 55        Metadata: map[string]string{
 56            "version": "1.0.0",
 57            "region":  getEnv("REGION", "us-east-1"),
 58        },
 59    }
 60
 61    // Retry registration
 62    for i := 0; i < 5; i++ {
 63        if err := registerWithRegistry(info, registryURL); err != nil {
 64            log.Printf("Registration attempt %d failed: %v", i+1, err)
 65            time.Sleep(time.Duration(i+1) * time.Second)
 66            continue
 67        }
 68        break
 69    }
 70
 71    // API endpoints
 72    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 73        w.Header().Set("Content-Type", "application/json")
 74        json.NewEncoder(w).Encode(map[string]interface{}{
 75            "service": serviceName,
 76            "id":      serviceID,
 77            "message": "Hello from " + serviceName,
 78        })
 79    })
 80
 81    // Discover peer services
 82    http.HandleFunc("/peers", func(w http.ResponseWriter, r *http.Request) {
 83        resp, err := http.Get(registryURL + "/services?name=" + serviceName)
 84        if err != nil {
 85            http.Error(w, err.Error(), http.StatusInternalServerError)
 86            return
 87        }
 88        defer resp.Body.Close()
 89
 90        var services []ServiceInfo
 91        if err := json.NewDecoder(resp.Body).Decode(&services); err != nil {
 92            http.Error(w, err.Error(), http.StatusInternalServerError)
 93            return
 94        }
 95
 96        w.Header().Set("Content-Type", "application/json")
 97        json.NewEncoder(w).Encode(services)
 98    })
 99
100    // Health check endpoint
101    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
102        w.Header().Set("Content-Type", "application/json")
103        json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
104    })
105
106    log.Printf("Service %s starting on port %s", serviceName, port)
107    log.Fatal(http.ListenAndServe(":"+port, nil))
108}
109
110func getEnv(key, defaultValue string) string {
111    if value := os.Getenv(key); value != "" {
112        return value
113    }
114    return defaultValue
115}
116
117func mustAtoi(s string) int {
118    var i int
119    fmt.Sscanf(s, "%d", &i)
120    return i
121}
  1# docker-compose.networking.yml - Service discovery setup
  2version: '3.8'
  3
  4services:
  5  # Service registry
  6  registry:
  7    build:
  8      context: ./registry
  9      dockerfile: Dockerfile
 10    container_name: service-registry
 11    ports:
 12      - "8000:8000"
 13    environment:
 14      - PORT=8000
 15    networks:
 16      - service-network
 17    healthcheck:
 18      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
 19      interval: 10s
 20      timeout: 5s
 21      retries: 3
 22
 23  # API Service 1
 24  api-1:
 25    build:
 26      context: ./api
 27      dockerfile: Dockerfile
 28    environment:
 29      - SERVICE_NAME=api-service
 30      - SERVICE_ID=api-1
 31      - PORT=8080
 32      - REGISTRY_URL=http://registry:8000
 33      - REGION=us-east-1
 34    depends_on:
 35      registry:
 36        condition: service_healthy
 37    networks:
 38      - service-network
 39    healthcheck:
 40      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
 41      interval: 10s
 42      timeout: 5s
 43      retries: 3
 44
 45  # API Service 2
 46  api-2:
 47    build:
 48      context: ./api
 49      dockerfile: Dockerfile
 50    environment:
 51      - SERVICE_NAME=api-service
 52      - SERVICE_ID=api-2
 53      - PORT=8080
 54      - REGISTRY_URL=http://registry:8000
 55      - REGION=us-west-1
 56    depends_on:
 57      registry:
 58        condition: service_healthy
 59    networks:
 60      - service-network
 61    healthcheck:
 62      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
 63      interval: 10s
 64      timeout: 5s
 65      retries: 3
 66
 67  # API Service 3
 68  api-3:
 69    build:
 70      context: ./api
 71      dockerfile: Dockerfile
 72    environment:
 73      - SERVICE_NAME=api-service
 74      - SERVICE_ID=api-3
 75      - PORT=8080
 76      - REGISTRY_URL=http://registry:8000
 77      - REGION=eu-west-1
 78    depends_on:
 79      registry:
 80        condition: service_healthy
 81    networks:
 82      - service-network
 83    healthcheck:
 84      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
 85      interval: 10s
 86      timeout: 5s
 87      retries: 3
 88
 89  # Load balancer
 90  nginx:
 91    image: nginx:alpine
 92    ports:
 93      - "80:80"
 94    volumes:
 95      - ./nginx.conf:/etc/nginx/nginx.conf:ro
 96    depends_on:
 97      - api-1
 98      - api-2
 99      - api-3
100    networks:
101      - service-network
102
103networks:
104  service-network:
105    driver: bridge
106    ipam:
107      config:
108        - subnet: 172.28.0.0/16
 1# nginx.conf - Load balancer configuration
 2events {
 3    worker_connections 1024;
 4}
 5
 6http {
 7    upstream api_services {
 8        least_conn;
 9        server api-1:8080 max_fails=3 fail_timeout=30s;
10        server api-2:8080 max_fails=3 fail_timeout=30s;
11        server api-3:8080 max_fails=3 fail_timeout=30s;
12    }
13
14    server {
15        listen 80;
16
17        location / {
18            proxy_pass http://api_services;
19            proxy_set_header Host $host;
20            proxy_set_header X-Real-IP $remote_addr;
21            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
22        }
23
24        location /registry/ {
25            proxy_pass http://registry:8000/;
26            proxy_set_header Host $host;
27        }
28    }
29}

Testing the Setup:

 1# Start the services
 2docker-compose -f docker-compose.networking.yml up -d
 3
 4# Check service registration
 5curl http://localhost/registry/services | jq
 6
 7# Test load balancing
 8for i in {1..10}; do
 9    curl http://localhost/
10done
11
12# Check peer discovery
13curl http://localhost:8080/peers | jq
14
15# Simulate service failure
16docker stop api-1
17
18# Verify automatic failover
19curl http://localhost/

Key Features:

  1. Service Discovery: Automatic registration with central registry
  2. Health Checking: Continuous health monitoring of all services
  3. Load Balancing: Nginx distributes traffic across healthy instances
  4. Network Isolation: Custom bridge network for service communication
  5. Automatic Failover: Unhealthy services automatically removed from pool

Exercise 4: Kubernetes-Ready Container with Probes

🎯 Learning Objectives:

  • Implement Kubernetes-specific health probes in Go
  • Design readiness vs liveness probe strategies
  • Build graceful shutdown handling for zero-downtime deployments
  • Practice dependency checking and initialization patterns

🌍 Real-World Context:
Proper health probe implementation is critical for Kubernetes deployments. At Airbnb, implementing proper readiness probes reduced deployment failures by 90% and enabled zero-downtime deployments for 1,000+ microservices. At Pinterest, their Go services with proper liveness probes achieved 99.99% uptime, automatically recovering from memory leaks and deadlocks.

⏱️ Time Estimate: 60 minutes
📊 Difficulty: Advanced

Create a Kubernetes-ready Go application that includes:

  1. Separate readiness and liveness probes
  2. Dependency health checking (database, cache, external APIs)
  3. Graceful shutdown with connection draining
  4. Startup probe for slow-initializing applications
  5. Complete Kubernetes deployment manifest
Solution
  1// k8s-app.go - Complete Kubernetes-ready application
  2package main
  3
  4import (
  5    "context"
  6    "database/sql"
  7    "encoding/json"
  8    "fmt"
  9    "log"
 10    "net/http"
 11    "os"
 12    "os/signal"
 13    "sync"
 14    "sync/atomic"
 15    "syscall"
 16    "time"
 17
 18    _ "github.com/lib/pq"
 19)
 20
 21// HealthStatus represents application health state
 22type HealthStatus struct {
 23    Ready      bool              `json:"ready"`
 24    Live       bool              `json:"live"`
 25    Started    bool              `json:"started"`
 26    Checks     map[string]string `json:"checks"`
 27    Uptime     string            `json:"uptime"`
 28    Version    string            `json:"version"`
 29    LastUpdate time.Time         `json:"last_update"`
 30}
 31
 32// Application holds application state
 33type Application struct {
 34    db            *sql.DB
 35    httpServer    *http.Server
 36    status        *HealthStatus
 37    mu            sync.RWMutex
 38    started       int32
 39    ready         int32
 40    live          int32
 41    startTime     time.Time
 42    shutdownGrace time.Duration
 43}
 44
 45func NewApplication() *Application {
 46    return &Application{
 47        status: &HealthStatus{
 48            Ready:   false,
 49            Live:    true,
 50            Started: false,
 51            Checks:  make(map[string]string),
 52            Version: getEnv("APP_VERSION", "1.0.0"),
 53        },
 54        startTime:     time.Now(),
 55        shutdownGrace: 30 * time.Second,
 56        live:          1,
 57    }
 58}
 59
 60// Initialize performs application initialization
 61func (app *Application) Initialize() error {
 62    log.Println("Starting application initialization...")
 63
 64    // Simulate slow initialization
 65    time.Sleep(5 * time.Second)
 66
 67    // Initialize database connection
 68    if err := app.initDatabase(); err != nil {
 69        return fmt.Errorf("database initialization failed: %w", err)
 70    }
 71
 72    // Initialize other dependencies
 73    if err := app.checkDependencies(); err != nil {
 74        log.Printf("Warning: some dependencies unavailable: %v", err)
 75    }
 76
 77    // Mark as started
 78    atomic.StoreInt32(&app.started, 1)
 79    app.mu.Lock()
 80    app.status.Started = true
 81    app.mu.Unlock()
 82
 83    log.Println("Application initialization complete")
 84    return nil
 85}
 86
 87func (app *Application) initDatabase() error {
 88    dbHost := getEnv("DB_HOST", "localhost")
 89    dbPort := getEnv("DB_PORT", "5432")
 90    dbUser := getEnv("DB_USER", "postgres")
 91    dbPass := getEnv("DB_PASSWORD", "postgres")
 92    dbName := getEnv("DB_NAME", "appdb")
 93
 94    connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
 95        dbHost, dbPort, dbUser, dbPass, dbName)
 96
 97    db, err := sql.Open("postgres", connStr)
 98    if err != nil {
 99        return err
100    }
101
102    // Configure connection pool
103    db.SetMaxOpenConns(25)
104    db.SetMaxIdleConns(5)
105    db.SetConnMaxLifetime(5 * time.Minute)
106
107    // Test connection
108    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
109    defer cancel()
110
111    if err := db.PingContext(ctx); err != nil {
112        return fmt.Errorf("database ping failed: %w", err)
113    }
114
115    app.db = db
116    log.Println("Database connection established")
117    return nil
118}
119
120func (app *Application) checkDependencies() error {
121    checks := map[string]func() error{
122        "database": app.checkDatabase,
123        "cache":    app.checkCache,
124        "external": app.checkExternalAPI,
125    }
126
127    var errors []string
128    for name, check := range checks {
129        if err := check(); err != nil {
130            errors = append(errors, fmt.Sprintf("%s: %v", name, err))
131            app.updateCheck(name, "unhealthy")
132        } else {
133            app.updateCheck(name, "healthy")
134        }
135    }
136
137    if len(errors) > 0 {
138        return fmt.Errorf("dependency checks failed: %v", errors)
139    }
140
141    return nil
142}
143
144func (app *Application) checkDatabase() error {
145    if app.db == nil {
146        return fmt.Errorf("database not initialized")
147    }
148
149    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
150    defer cancel()
151
152    return app.db.PingContext(ctx)
153}
154
155func (app *Application) checkCache() error {
156    // Simulate cache check
157    cacheHost := getEnv("CACHE_HOST", "")
158    if cacheHost == "" {
159        return fmt.Errorf("cache not configured")
160    }
161    return nil
162}
163
164func (app *Application) checkExternalAPI() error {
165    // Simulate external API check
166    apiURL := getEnv("EXTERNAL_API", "")
167    if apiURL == "" {
168        return nil // Optional dependency
169    }
170
171    client := &http.Client{Timeout: 3 * time.Second}
172    resp, err := client.Get(apiURL + "/health")
173    if err != nil {
174        return err
175    }
176    defer resp.Body.Close()
177
178    if resp.StatusCode != http.StatusOK {
179        return fmt.Errorf("API returned status %d", resp.StatusCode)
180    }
181
182    return nil
183}
184
185func (app *Application) updateCheck(name, status string) {
186    app.mu.Lock()
187    defer app.mu.Unlock()
188    app.status.Checks[name] = status
189    app.status.LastUpdate = time.Now()
190}
191
192// SetReady marks application as ready to serve traffic
193func (app *Application) SetReady(ready bool) {
194    if ready {
195        atomic.StoreInt32(&app.ready, 1)
196        log.Println("Application is READY")
197    } else {
198        atomic.StoreInt32(&app.ready, 0)
199        log.Println("Application is NOT READY")
200    }
201
202    app.mu.Lock()
203    app.status.Ready = ready
204    app.mu.Unlock()
205}
206
207// Handlers for Kubernetes probes
208
209func (app *Application) startupHandler(w http.ResponseWriter, r *http.Request) {
210    // Startup probe: has the application started?
211    if atomic.LoadInt32(&app.started) == 0 {
212        w.WriteHeader(http.StatusServiceUnavailable)
213        json.NewEncoder(w).Encode(map[string]string{
214            "status": "starting",
215            "reason": "application still initializing",
216        })
217        return
218    }
219
220    w.WriteHeader(http.StatusOK)
221    json.NewEncoder(w).Encode(map[string]string{
222        "status": "started",
223    })
224}
225
226func (app *Application) readinessHandler(w http.ResponseWriter, r *http.Request) {
227    // Readiness probe: is the application ready to serve traffic?
228    if atomic.LoadInt32(&app.ready) == 0 {
229        w.WriteHeader(http.StatusServiceUnavailable)
230        app.mu.RLock()
231        status := *app.status
232        app.mu.RUnlock()
233        json.NewEncoder(w).Encode(map[string]interface{}{
234            "status": "not ready",
235            "checks": status.Checks,
236        })
237        return
238    }
239
240    // Perform dependency checks
241    if err := app.checkDependencies(); err != nil {
242        w.WriteHeader(http.StatusServiceUnavailable)
243        app.mu.RLock()
244        status := *app.status
245        app.mu.RUnlock()
246        json.NewEncoder(w).Encode(map[string]interface{}{
247            "status": "not ready",
248            "error":  err.Error(),
249            "checks": status.Checks,
250        })
251        return
252    }
253
254    w.WriteHeader(http.StatusOK)
255    json.NewEncoder(w).Encode(map[string]string{
256        "status": "ready",
257    })
258}
259
260func (app *Application) livenessHandler(w http.ResponseWriter, r *http.Request) {
261    // Liveness probe: is the application alive (not deadlocked)?
262    if atomic.LoadInt32(&app.live) == 0 {
263        w.WriteHeader(http.StatusServiceUnavailable)
264        json.NewEncoder(w).Encode(map[string]string{
265            "status": "not alive",
266        })
267        return
268    }
269
270    // Simple liveness check - just respond
271    w.WriteHeader(http.StatusOK)
272    app.mu.RLock()
273    status := *app.status
274    app.mu.RUnlock()
275    status.Uptime = time.Since(app.startTime).String()
276    json.NewEncoder(w).Encode(status)
277}
278
279func (app *Application) metricsHandler(w http.ResponseWriter, r *http.Request) {
280    w.Header().Set("Content-Type", "text/plain")
281    fmt.Fprintf(w, "# HELP app_uptime_seconds Application uptime\n")
282    fmt.Fprintf(w, "# TYPE app_uptime_seconds gauge\n")
283    fmt.Fprintf(w, "app_uptime_seconds %.2f\n", time.Since(app.startTime).Seconds())
284
285    ready := atomic.LoadInt32(&app.ready)
286    fmt.Fprintf(w, "# HELP app_ready Application ready status\n")
287    fmt.Fprintf(w, "# TYPE app_ready gauge\n")
288    fmt.Fprintf(w, "app_ready %d\n", ready)
289}
290
291func (app *Application) Start() error {
292    port := getEnv("PORT", "8080")
293
294    mux := http.NewServeMux()
295    mux.HandleFunc("/startup", app.startupHandler)
296    mux.HandleFunc("/readyz", app.readinessHandler)
297    mux.HandleFunc("/healthz", app.livenessHandler)
298    mux.HandleFunc("/metrics", app.metricsHandler)
299
300    // Application endpoints
301    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
302        w.Header().Set("Content-Type", "application/json")
303        json.NewEncoder(w).Encode(map[string]string{
304            "message": "Kubernetes-ready Go application",
305            "pod":     getEnv("HOSTNAME", "unknown"),
306            "version": app.status.Version,
307        })
308    })
309
310    app.httpServer = &http.Server{
311        Addr:         ":" + port,
312        Handler:      mux,
313        ReadTimeout:  15 * time.Second,
314        WriteTimeout: 15 * time.Second,
315        IdleTimeout:  60 * time.Second,
316    }
317
318    log.Printf("Server starting on port %s", port)
319    return app.httpServer.ListenAndServe()
320}
321
322func (app *Application) Shutdown() error {
323    log.Println("Initiating graceful shutdown...")
324
325    // Mark as not ready immediately
326    app.SetReady(false)
327
328    // Wait for grace period to drain connections
329    log.Printf("Waiting %v for connection draining...", app.shutdownGrace)
330    time.Sleep(5 * time.Second)
331
332    ctx, cancel := context.WithTimeout(context.Background(), app.shutdownGrace)
333    defer cancel()
334
335    if err := app.httpServer.Shutdown(ctx); err != nil {
336        return fmt.Errorf("server shutdown failed: %w", err)
337    }
338
339    // Close database connection
340    if app.db != nil {
341        app.db.Close()
342    }
343
344    log.Println("Shutdown complete")
345    return nil
346}
347
348func main() {
349    app := NewApplication()
350
351    // Initialize application
352    if err := app.Initialize(); err != nil {
353        log.Fatalf("Initialization failed: %v", err)
354    }
355
356    // Application is initialized, mark as ready after final checks
357    go func() {
358        time.Sleep(2 * time.Second)
359        if err := app.checkDependencies(); err == nil {
360            app.SetReady(true)
361        } else {
362            log.Printf("Not marking as ready due to: %v", err)
363        }
364    }()
365
366    // Start server in goroutine
367    go func() {
368        if err := app.Start(); err != nil && err != http.ErrServerClosed {
369            log.Fatalf("Server failed: %v", err)
370        }
371    }()
372
373    // Wait for shutdown signal
374    quit := make(chan os.Signal, 1)
375    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
376    <-quit
377
378    if err := app.Shutdown(); err != nil {
379        log.Fatalf("Shutdown failed: %v", err)
380    }
381}
382
383func getEnv(key, defaultValue string) string {
384    if value := os.Getenv(key); value != "" {
385        return value
386    }
387    return defaultValue
388}
  1# k8s-deployment-complete.yml - Complete Kubernetes configuration
  2apiVersion: apps/v1
  3kind: Deployment
  4metadata:
  5  name: go-app
  6  labels:
  7    app: go-app
  8spec:
  9  replicas: 3
 10  strategy:
 11    type: RollingUpdate
 12    rollingUpdate:
 13      maxSurge: 1
 14      maxUnavailable: 0
 15  selector:
 16    matchLabels:
 17      app: go-app
 18  template:
 19    metadata:
 20      labels:
 21        app: go-app
 22      annotations:
 23        prometheus.io/scrape: "true"
 24        prometheus.io/path: "/metrics"
 25        prometheus.io/port: "8080"
 26    spec:
 27      terminationGracePeriodSeconds: 30
 28      securityContext:
 29        runAsNonRoot: true
 30        runAsUser: 1001
 31
 32      containers:
 33      - name: go-app
 34        image: your-registry/go-app:latest
 35        imagePullPolicy: Always
 36
 37        ports:
 38        - name: http
 39          containerPort: 8080
 40
 41        env:
 42        - name: PORT
 43          value: "8080"
 44        - name: APP_VERSION
 45          value: "1.0.0"
 46        - name: DB_HOST
 47          valueFrom:
 48            configMapKeyRef:
 49              name: go-app-config
 50              key: db_host
 51        - name: DB_PASSWORD
 52          valueFrom:
 53            secretKeyRef:
 54              name: go-app-secrets
 55              key: db_password
 56
 57        resources:
 58          requests:
 59            memory: "128Mi"
 60            cpu: "100m"
 61          limits:
 62            memory: "512Mi"
 63            cpu: "500m"
 64
 65        # Startup probe: wait up to 60s for app to start
 66        startupProbe:
 67          httpGet:
 68            path: /startup
 69            port: http
 70          initialDelaySeconds: 0
 71          periodSeconds: 5
 72          timeoutSeconds: 3
 73          failureThreshold: 12  # 60 seconds total
 74
 75        # Liveness probe: restart if app is deadlocked
 76        livenessProbe:
 77          httpGet:
 78            path: /healthz
 79            port: http
 80          initialDelaySeconds: 15
 81          periodSeconds: 10
 82          timeoutSeconds: 5
 83          failureThreshold: 3
 84
 85        # Readiness probe: remove from service if not ready
 86        readinessProbe:
 87          httpGet:
 88            path: /readyz
 89            port: http
 90          initialDelaySeconds: 5
 91          periodSeconds: 5
 92          timeoutSeconds: 3
 93          failureThreshold: 2
 94          successThreshold: 1
 95
 96        lifecycle:
 97          preStop:
 98            exec:
 99              command: ["/bin/sh", "-c", "sleep 15"]
100
101---
102apiVersion: v1
103kind: ConfigMap
104metadata:
105  name: go-app-config
106data:
107  db_host: "postgres.default.svc.cluster.local"
108  db_port: "5432"
109  db_name: "appdb"
110
111---
112apiVersion: v1
113kind: Secret
114metadata:
115  name: go-app-secrets
116type: Opaque
117stringData:
118  db_password: "your-secure-password"
119
120---
121apiVersion: v1
122kind: Service
123metadata:
124  name: go-app
125spec:
126  type: ClusterIP
127  ports:
128  - port: 80
129    targetPort: http
130    name: http
131  selector:
132    app: go-app

Testing Strategy:

 1# Deploy to Kubernetes
 2kubectl apply -f k8s-deployment-complete.yml
 3
 4# Watch deployment
 5kubectl rollout status deployment/go-app
 6
 7# Check pod status
 8kubectl get pods -l app=go-app
 9
10# Test probes
11kubectl exec -it $(kubectl get pod -l app=go-app -o name | head -1) -- wget -O- localhost:8080/startup
12kubectl exec -it $(kubectl get pod -l app=go-app -o name | head -1) -- wget -O- localhost:8080/readyz
13kubectl exec -it $(kubectl get pod -l app=go-app -o name | head -1) -- wget -O- localhost:8080/healthz
14
15# Simulate graceful shutdown
16kubectl delete pod -l app=go-app --grace-period=30
17
18# Check zero-downtime deployment
19while true; do curl http://<service-url>/; sleep 0.5; done &
20kubectl set image deployment/go-app go-app=your-registry/go-app:v2

Key Features:

  1. Three Probe Types: Startup, readiness, and liveness probes
  2. Dependency Checking: Database, cache, and external API verification
  3. Graceful Shutdown: 30-second grace period with connection draining
  4. Zero-Downtime Deployments: Proper readiness handling prevents traffic to unready pods
  5. Resource Management: CPU and memory limits configured

Exercise 5: Advanced Security and Image Scanning Pipeline

🎯 Learning Objectives:

  • Implement comprehensive container security scanning
  • Build secure multi-stage Dockerfiles with minimal attack surface
  • Configure automated security scanning in CI/CD pipelines
  • Practice secret management and security best practices

🌍 Real-World Context:
Container security is non-negotiable in production. At Shopify, implementing automated security scanning caught 847 vulnerabilities before deployment in one year, preventing potential breaches. At Capital One, their container security pipeline blocks 15-20% of builds due to critical vulnerabilities, protecting millions of customer accounts. According to Snyk, 87% of Docker Hub images contain known vulnerabilities.

⏱️ Time Estimate: 90 minutes
📊 Difficulty: Expert

Create a comprehensive security-hardened deployment pipeline that includes:

  1. Multi-stage Docker build with security scanning at each stage
  2. Vulnerability scanning with Trivy and Snyk
  3. Secret detection and prevention
  4. Security policy enforcement
  5. Complete CI/CD pipeline with security gates
Solution
  1# Dockerfile.secure-complete - Maximum security hardening
  2# Stage 1: Dependency verification
  3FROM golang:1.21-alpine AS deps
  4
  5RUN apk add --no-cache git ca-certificates
  6
  7WORKDIR /app
  8
  9# Copy and verify go.mod
 10COPY go.mod go.sum ./
 11RUN go mod download && go mod verify
 12
 13# Scan dependencies for vulnerabilities
 14RUN go install golang.org/x/vuln/cmd/govulncheck@latest && \
 15    govulncheck -json ./... > /tmp/govulncheck.json || true
 16
 17# Stage 2: Static analysis and security checks
 18FROM golang:1.21-alpine AS security-scan
 19
 20COPY --from=deps /go/pkg /go/pkg
 21COPY . /app
 22WORKDIR /app
 23
 24# Install security tools
 25RUN apk add --no-cache git
 26
 27# Run gosec for security issues
 28RUN go install github.com/securego/gosec/v2/cmd/gosec@latest && \
 29    gosec -fmt json -out /tmp/gosec.json ./... || true
 30
 31# Run staticcheck
 32RUN go install honnef.co/go/tools/cmd/staticcheck@latest && \
 33    staticcheck -f json ./... > /tmp/staticcheck.json || true
 34
 35# Stage 3: Build with all optimizations
 36FROM golang:1.21-alpine AS builder
 37
 38# Build arguments
 39ARG VERSION=dev
 40ARG BUILD_TIME
 41ARG GIT_COMMIT
 42
 43WORKDIR /build
 44
 45# Copy verified dependencies
 46COPY --from=deps /go/pkg /go/pkg
 47COPY go.mod go.sum ./
 48COPY . .
 49
 50# Build with maximum security flags
 51RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
 52    go build -a -installsuffix cgo \
 53    -ldflags="-w -s -extldflags '-static' \
 54              -X main.Version=${VERSION} \
 55              -X main.BuildTime=${BUILD_TIME} \
 56              -X main.GitCommit=${GIT_COMMIT}" \
 57    -trimpath \
 58    -buildmode=pie \
 59    -o app .
 60
 61# Strip binary
 62RUN apk add --no-cache binutils && \
 63    strip app && \
 64    chmod +x app
 65
 66# Verify binary
 67RUN file app && \
 68    ldd app || echo "Static binary - no dynamic linking"
 69
 70# Stage 4: Create minimal runtime user
 71FROM alpine:3.18 AS runtime-prep
 72
 73# Create non-root user and group
 74RUN addgroup -g 10001 -S appgroup && \
 75    adduser -u 10001 -S appuser -G appgroup -h /app
 76
 77# Stage 5: Final minimal runtime
 78FROM scratch
 79
 80# Import SSL certificates
 81COPY --from=runtime-prep /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
 82
 83# Import timezone data
 84COPY --from=runtime-prep /usr/share/zoneinfo /usr/share/zoneinfo
 85
 86# Import user/group files
 87COPY --from=runtime-prep /etc/passwd /etc/passwd
 88COPY --from=runtime-prep /etc/group /etc/group
 89
 90# Create app directory
 91COPY --from=runtime-prep --chown=10001:10001 /app /app
 92
 93# Copy binary with proper ownership
 94COPY --from=builder --chown=10001:10001 /build/app /app/app
 95
 96# Switch to non-root user
 97USER 10001:10001
 98
 99# Set working directory
100WORKDIR /app
101
102# Expose port (non-privileged)
103EXPOSE 8080
104
105# Security labels
106LABEL maintainer="security@example.com" \
107      security.scan="required" \
108      security.privileged="false" \
109      security.readonly="true" \
110      security.no-new-privileges="true"
111
112# Health check
113HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
114    CMD ["/app/app", "--health-check"]
115
116# Run application
117ENTRYPOINT ["/app/app"]
  1# .github/workflows/security-pipeline.yml - Complete security CI/CD
  2name: Security Pipeline
  3
  4on:
  5  push:
  6    branches: [main, develop]
  7  pull_request:
  8    branches: [main]
  9  schedule:
 10    - cron: '0 2 * * *'  # Daily security scan
 11
 12env:
 13  IMAGE_NAME: go-secure-app
 14  REGISTRY: ghcr.io
 15
 16jobs:
 17  # Job 1: Source code security scanning
 18  source-security:
 19    runs-on: ubuntu-latest
 20    steps:
 21    - name: Checkout code
 22      uses: actions/checkout@v3
 23
 24    - name: Set up Go
 25      uses: actions/setup-go@v4
 26      with:
 27        go-version: '1.21'
 28
 29    - name: Run gosec (Security Scanner)
 30      uses: securego/gosec@master
 31      with:
 32        args: '-fmt sarif -out gosec-results.sarif ./...'
 33
 34    - name: Upload gosec results
 35      uses: github/codeql-action/upload-sarif@v2
 36      with:
 37        sarif_file: gosec-results.sarif
 38
 39    - name: Run govulncheck
 40      run: |
 41        go install golang.org/x/vuln/cmd/govulncheck@latest
 42        govulncheck ./...        
 43
 44    - name: Check for secrets
 45      uses: trufflesecurity/trufflehog@main
 46      with:
 47        path: ./
 48        base: ${{ github.event.repository.default_branch }}
 49        head: HEAD
 50
 51  # Job 2: Dependency scanning
 52  dependency-scan:
 53    runs-on: ubuntu-latest
 54    steps:
 55    - name: Checkout code
 56      uses: actions/checkout@v3
 57
 58    - name: Run Snyk to check for vulnerabilities
 59      uses: snyk/actions/golang@master
 60      env:
 61        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
 62      with:
 63        args: --severity-threshold=high --fail-on=upgradable
 64
 65    - name: Run Nancy (dependency checker)
 66      run: |
 67        go install github.com/sonatype-nexus-community/nancy@latest
 68        go list -json -m all | nancy sleuth        
 69
 70  # Job 3: Build and scan Docker image
 71  build-and-scan:
 72    needs: [source-security, dependency-scan]
 73    runs-on: ubuntu-latest
 74    permissions:
 75      contents: read
 76      packages: write
 77      security-events: write
 78
 79    steps:
 80    - name: Checkout code
 81      uses: actions/checkout@v3
 82
 83    - name: Set up Docker Buildx
 84      uses: docker/setup-buildx-action@v2
 85
 86    - name: Build Docker image
 87      uses: docker/build-push-action@v4
 88      with:
 89        context: .
 90        file: ./Dockerfile.secure-complete
 91        push: false
 92        load: true
 93        tags: ${{ env.IMAGE_NAME }}:test
 94        build-args: |
 95          VERSION=${{ github.sha }}
 96          BUILD_TIME=${{ github.event.head_commit.timestamp }}
 97          GIT_COMMIT=${{ github.sha }}          
 98        cache-from: type=gha
 99        cache-to: type=gha,mode=max
100
101    - name: Run Trivy vulnerability scanner
102      uses: aquasecurity/trivy-action@master
103      with:
104        image-ref: ${{ env.IMAGE_NAME }}:test
105        format: 'sarif'
106        output: 'trivy-results.sarif'
107        severity: 'CRITICAL,HIGH'
108        exit-code: '1'
109
110    - name: Upload Trivy results to GitHub Security
111      uses: github/codeql-action/upload-sarif@v2
112      if: always()
113      with:
114        sarif_file: 'trivy-results.sarif'
115
116    - name: Run Trivy config scanner
117      uses: aquasecurity/trivy-action@master
118      with:
119        scan-type: 'config'
120        scan-ref: '.'
121        format: 'sarif'
122        output: 'trivy-config.sarif'
123
124    - name: Scan for secrets in image
125      run: |
126        docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
127          aquasec/trivy image --scanners secret \
128          --severity HIGH,CRITICAL \
129          ${{ env.IMAGE_NAME }}:test        
130
131    - name: Run Dockle (Docker linter)
132      run: |
133        docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
134          goodwithtech/dockle:latest \
135          --exit-code 1 \
136          --exit-level warn \
137          ${{ env.IMAGE_NAME }}:test        
138
139    - name: Scan with Snyk Container
140      uses: snyk/actions/docker@master
141      env:
142        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
143      with:
144        image: ${{ env.IMAGE_NAME }}:test
145        args: --severity-threshold=high --file=Dockerfile.secure-complete
146
147  # Job 4: Runtime security testing
148  runtime-security:
149    needs: build-and-scan
150    runs-on: ubuntu-latest
151    steps:
152    - name: Checkout code
153      uses: actions/checkout@v3
154
155    - name: Build image for testing
156      run: |
157        docker build -f Dockerfile.secure-complete \
158          -t ${{ env.IMAGE_NAME }}:test .        
159
160    - name: Test as non-root
161      run: |
162                docker run --rm ${{ env.IMAGE_NAME }}:test id | grep uid=10001
163
164    - name: Test read-only filesystem
165      run: |
166        docker run --rm --read-only \
167          --tmpfs /tmp \
168          ${{ env.IMAGE_NAME }}:test echo "Read-only test passed"        
169
170    - name: Test no capabilities
171      run: |
172        docker run --rm --cap-drop=ALL \
173          ${{ env.IMAGE_NAME }}:test echo "Capabilities test passed"        
174
175    - name: Test security options
176      run: |
177        docker run --rm \
178          --security-opt=no-new-privileges:true \
179          --security-opt=seccomp=unconfined \
180          ${{ env.IMAGE_NAME }}:test echo "Security options test passed"        
181
182  # Job 5: Deploy to production (if all checks pass)
183  deploy:
184    needs: [build-and-scan, runtime-security]
185    runs-on: ubuntu-latest
186    if: github.ref == 'refs/heads/main'
187    steps:
188    - name: Checkout code
189      uses: actions/checkout@v3
190
191    - name: Login to Container Registry
192      uses: docker/login-action@v2
193      with:
194        registry: ${{ env.REGISTRY }}
195        username: ${{ github.actor }}
196        password: ${{ secrets.GITHUB_TOKEN }}
197
198    - name: Build and push final image
199      uses: docker/build-push-action@v4
200      with:
201        context: .
202        file: ./Dockerfile.secure-complete
203        push: true
204        tags: |
205          ${{ env.REGISTRY }}/${{ github.repository }}:latest
206          ${{ env.REGISTRY }}/${{ github.repository }}:${{ github.sha }}          
207        build-args: |
208          VERSION=${{ github.sha }}
209          BUILD_TIME=${{ github.event.head_commit.timestamp }}
210          GIT_COMMIT=${{ github.sha }}          
211
212    - name: Sign image with Cosign
213      run: |
214        echo "${{ secrets.COSIGN_KEY }}" > cosign.key
215        cosign sign --key cosign.key \
216          ${{ env.REGISTRY }}/${{ github.repository }}:${{ github.sha }}        
217
218    - name: Generate SBOM
219      uses: anchore/sbom-action@v0
220      with:
221        image: ${{ env.REGISTRY }}/${{ github.repository }}:${{ github.sha }}
222        format: cyclonedx
223        output-file: sbom.json
224
225    - name: Upload SBOM
226      uses: actions/upload-artifact@v3
227      with:
228        name: sbom
229        path: sbom.json
 1# docker-compose.secure.yml - Secure deployment configuration
 2version: '3.8'
 3
 4services:
 5  app:
 6    image: go-secure-app:latest
 7    build:
 8      context: .
 9      dockerfile: Dockerfile.secure-complete
10      args:
11        VERSION: "1.0.0"
12        BUILD_TIME: "2024-01-01T00:00:00Z"
13        GIT_COMMIT: "abc123"
14
15    # Security options
16    security_opt:
17      - no-new-privileges:true
18      - seccomp:unconfined
19
20    # Run as non-root
21    user: "10001:10001"
22
23    # Read-only root filesystem
24    read_only: true
25
26    # Drop all capabilities
27    cap_drop:
28      - ALL
29
30    # Only necessary capabilities
31    cap_add:
32      - NET_BIND_SERVICE  # If binding to port < 1024
33
34    # Resource limits
35    deploy:
36      resources:
37        limits:
38          cpus: '1.0'
39          memory: 512M
40        reservations:
41          cpus: '0.5'
42          memory: 256M
43
44    # Environment variables from secrets
45    environment:
46      - PORT=8080
47    secrets:
48      - db_password
49      - api_key
50
51    # Tmpfs for temporary files
52    tmpfs:
53      - /tmp:noexec,nosuid,size=10M
54
55    # Health check
56    healthcheck:
57      test: ["CMD", "/app/app", "--health-check"]
58      interval: 30s
59      timeout: 10s
60      retries: 3
61      start_period: 40s
62
63    networks:
64      - secure-network
65
66    # Logging
67    logging:
68      driver: "json-file"
69      options:
70        max-size: "10m"
71        max-file: "3"
72
73networks:
74  secure-network:
75    driver: bridge
76    internal: false
77
78secrets:
79  db_password:
80    external: true
81  api_key:
82    external: true
  1// security-app.go - Application with security features
  2package main
  3
  4import (
  5    "crypto/rand"
  6    "crypto/subtle"
  7    "encoding/base64"
  8    "encoding/json"
  9    "flag"
 10    "log"
 11    "net/http"
 12    "os"
 13    "time"
 14)
 15
 16// Security headers middleware
 17func securityHeaders(next http.Handler) http.Handler {
 18    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 19        // Security headers
 20        w.Header().Set("X-Content-Type-Options", "nosniff")
 21        w.Header().Set("X-Frame-Options", "DENY")
 22        w.Header().Set("X-XSS-Protection", "1; mode=block")
 23        w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
 24        w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'")
 25        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
 26        w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
 27
 28        // Remove server identification
 29        w.Header().Del("Server")
 30        w.Header().Del("X-Powered-By")
 31
 32        next.ServeHTTP(w, r)
 33    })
 34}
 35
 36// API key authentication
 37func apiKeyAuth(apiKey string) func(http.Handler) http.Handler {
 38    return func(next http.Handler) http.Handler {
 39        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 40            key := r.Header.Get("X-API-Key")
 41
 42            // Constant-time comparison to prevent timing attacks
 43            if subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) != 1 {
 44                http.Error(w, "Unauthorized", http.StatusUnauthorized)
 45                return
 46            }
 47
 48            next.ServeHTTP(w, r)
 49        })
 50    }
 51}
 52
 53// Rate limiting (simple implementation)
 54var requestCounts = make(map[string]int)
 55
 56func rateLimit(next http.Handler) http.Handler {
 57    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 58        ip := r.RemoteAddr
 59        if count := requestCounts[ip]; count > 100 {
 60            http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
 61            return
 62        }
 63        requestCounts[ip]++
 64        next.ServeHTTP(w, r)
 65    })
 66}
 67
 68func main() {
 69    healthCheck := flag.Bool("health-check", false, "Health check mode")
 70    flag.Parse()
 71
 72    if *healthCheck {
 73        // Simple health check for container
 74        resp, err := http.Get("http://localhost:8080/health")
 75        if err != nil || resp.StatusCode != http.StatusOK {
 76            os.Exit(1)
 77        }
 78        os.Exit(0)
 79    }
 80
 81    // Generate secure session key
 82    sessionKey := make([]byte, 32)
 83    if _, err := rand.Read(sessionKey); err != nil {
 84        log.Fatal("Failed to generate session key:", err)
 85    }
 86    log.Printf("Session key: %s", base64.StdEncoding.EncodeToString(sessionKey))
 87
 88    // API key from environment
 89    apiKey := os.Getenv("API_KEY")
 90    if apiKey == "" {
 91        log.Fatal("API_KEY environment variable required")
 92    }
 93
 94    mux := http.NewServeMux()
 95
 96    // Public endpoints
 97    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
 98        w.Header().Set("Content-Type", "application/json")
 99        json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
100    })
101
102    // Protected endpoints
103    protected := http.NewServeMux()
104    protected.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
105        w.Header().Set("Content-Type", "application/json")
106        json.NewEncoder(w).Encode(map[string]string{
107            "message": "Secure data access",
108            "time":    time.Now().Format(time.RFC3339),
109        })
110    })
111
112    // Apply middleware
113    mux.Handle("/api/", apiKeyAuth(apiKey)(protected))
114
115    handler := securityHeaders(rateLimit(mux))
116
117    server := &http.Server{
118        Addr:         ":8080",
119        Handler:      handler,
120        ReadTimeout:  10 * time.Second,
121        WriteTimeout: 10 * time.Second,
122        IdleTimeout:  60 * time.Second,
123    }
124
125    log.Println("Secure server starting on :8080")
126    log.Fatal(server.ListenAndServe())
127}

Testing the Security Pipeline:

 1# Run security scan locally
 2docker build -f Dockerfile.secure-complete -t go-secure-app:test .
 3
 4# Scan with Trivy
 5trivy image --severity HIGH,CRITICAL go-secure-app:test
 6
 7# Scan with Dockle
 8dockle go-secure-app:test
 9
10# Test runtime security
11docker run --rm --read-only --cap-drop=ALL --security-opt=no-new-privileges:true go-secure-app:test
12
13# Generate SBOM
14syft go-secure-app:test -o cyclonedx-json > sbom.json
15
16# Sign image
17cosign sign --key cosign.key go-secure-app:test

Key Security Features:

  1. Multi-stage Scanning: Vulnerabilities caught at dependency, build, and runtime stages
  2. Minimal Attack Surface: FROM scratch base with only necessary files
  3. Non-root User: UID 10001 with no shell access
  4. Security Labels: Metadata for security tooling
  5. CI/CD Integration: Automated security gates prevent vulnerable images from deploying
  6. Secret Management: No hardcoded credentials, secure secret injection
  7. SBOM Generation: Software Bill of Materials for supply chain security
  8. Image Signing: Cryptographic verification with Cosign

Summary

Key Takeaways

  1. Multi-stage builds are essential for production Go images
  2. Security requires non-root users and minimal attack surfaces
  3. Performance comes from optimized layer caching and small images
  4. Production readiness includes health checks, graceful shutdown, and monitoring

Production Checklist

  • Multi-stage Dockerfile with optimized build flags
  • Non-root user with minimal permissions
  • Health checks for application and dependencies
  • Proper .dockerignore file
  • Environment-based configuration
  • Graceful shutdown handling
  • Logging and metrics endpoints
  • Security scanning in CI/CD pipeline
  • Image signing and vulnerability scanning

Next Steps

  1. Orchestration: Learn Kubernetes for container orchestration
  2. CI/CD: Implement automated testing and deployment pipelines
  3. Monitoring: Add Prometheus/Grafana for observability
  4. Security: Implement image scanning and runtime protection