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:
- Container Architecture - Understanding how containers work and why they're perfect for Go
- Multi-Stage Builds - Creating optimized production Docker images
- Performance Optimization - Minimizing image size and startup time
- 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:
- Static Binaries: No runtime dependencies, no complex installation
- Single Binary: Simplified container structure and minimal attack surface
- Cross-Compilation: Build for any platform from any machine
- 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:
- Bridge (Default): Isolated network with port mapping
- Host: Shares host's network stack
- Overlay: Multi-host networking for swarm mode
- Macvlan: Assigns MAC address to container
- 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:
- Readiness vs Liveness Probes
- Resource Requests and Limits
- Pod Disruption Budgets
- Horizontal Pod Autoscaling
- 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:
- Build-time Security: Scanning and hardening during build
- Image Security: Minimal base images and vulnerability scanning
- Runtime Security: Non-root users, read-only filesystems
- Network Security: Proper firewall rules and segmentation
- 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:
- Build stage with Go 1.21, dependency caching, and optimization flags
- Runtime stage using Alpine Linux with non-root user
- Health checks, proper signal handling, and environment configuration
- 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:
- Separate build and runtime stages
- Cached dependency layer
- Stripped binary
- Non-root user with minimal permissions
- Health checks for monitoring
- 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:
- Go API service with health checks and metrics
- PostgreSQL database with persistence and backups
- Redis cache with persistence
- Nginx reverse proxy with SSL termination
- Monitoring stack
- 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:
- High Availability: Multiple API replicas with load balancing
- Data Persistence: PostgreSQL and Redis with volume mounts
- Monitoring: Prometheus + Grafana + Jaeger for full observability
- Security: SSL termination, rate limiting, non-root containers
- Health Checks: All services have comprehensive health monitoring
- Graceful Deployment: Zero-downtime deployment with backup/restore
Key Optimizations:
- Resource Limits: Proper CPU and memory constraints
- Network Isolation: Separate networks for app and monitoring
- Service Discovery: Internal DNS resolution
- Log Management: Structured logging with rotation
- 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:
- Three API services that discover each other
- A service registry for tracking healthy services
- Custom bridge networks with proper isolation
- Health checking and automatic failover
- 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:
- Service Discovery: Automatic registration with central registry
- Health Checking: Continuous health monitoring of all services
- Load Balancing: Nginx distributes traffic across healthy instances
- Network Isolation: Custom bridge network for service communication
- 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:
- Separate readiness and liveness probes
- Dependency health checking (database, cache, external APIs)
- Graceful shutdown with connection draining
- Startup probe for slow-initializing applications
- 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:
- Three Probe Types: Startup, readiness, and liveness probes
- Dependency Checking: Database, cache, and external API verification
- Graceful Shutdown: 30-second grace period with connection draining
- Zero-Downtime Deployments: Proper readiness handling prevents traffic to unready pods
- 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:
- Multi-stage Docker build with security scanning at each stage
- Vulnerability scanning with Trivy and Snyk
- Secret detection and prevention
- Security policy enforcement
- 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:
- Multi-stage Scanning: Vulnerabilities caught at dependency, build, and runtime stages
- Minimal Attack Surface: FROM scratch base with only necessary files
- Non-root User: UID 10001 with no shell access
- Security Labels: Metadata for security tooling
- CI/CD Integration: Automated security gates prevent vulnerable images from deploying
- Secret Management: No hardcoded credentials, secure secret injection
- SBOM Generation: Software Bill of Materials for supply chain security
- Image Signing: Cryptographic verification with Cosign
Summary
Key Takeaways
- Multi-stage builds are essential for production Go images
- Security requires non-root users and minimal attack surfaces
- Performance comes from optimized layer caching and small images
- 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
- Orchestration: Learn Kubernetes for container orchestration
- CI/CD: Implement automated testing and deployment pipelines
- Monitoring: Add Prometheus/Grafana for observability
- Security: Implement image scanning and runtime protection