Back to Curriculum

Go Deployment and Production Best Practices

📚 Lesson 16 of 16 ⏱️ 45 min

Go Deployment and Production Best Practices

45 min

Go applications can be compiled to single binaries for easy deployment, requiring no runtime dependencies. Go's static compilation produces self-contained executables that can run on target systems without Go installed. This simplifies deployment and reduces dependency issues. Understanding Go's deployment model helps you deploy applications efficiently.

Environment variables configure applications for different environments (development, staging, production), allowing the same binary to work across environments. Environment variables are accessed via `os.Getenv()` or configuration packages. This enables flexible, environment-specific configuration. Understanding environment configuration helps you deploy applications across environments.

Health checks and monitoring ensure application reliability by providing endpoints that report application status. Health checks verify the application is running; readiness checks verify it can serve traffic. Monitoring tracks metrics, logs, and performance. Understanding health checks and monitoring helps you maintain reliable production systems.

Containerization with Docker simplifies deployment and scaling by packaging applications and dependencies into containers. Dockerfiles define container images; containers run consistently across environments. Containerization enables easy scaling, deployment, and management. Understanding containerization helps you deploy applications reliably.

Production best practices include graceful shutdown (handling SIGTERM/SIGINT), proper logging, error handling, security (HTTPS, authentication), rate limiting, and observability (metrics, tracing). Production applications must be reliable, secure, and maintainable. Understanding production practices helps you build robust systems.

Deployment strategies include blue-green deployments, rolling updates, and canary releases. CI/CD pipelines automate building, testing, and deployment. Understanding deployment strategies helps you release updates safely and efficiently.

Key Concepts

  • Go compiles to single binaries for easy deployment.
  • Environment variables configure applications for different environments.
  • Health checks and monitoring ensure application reliability.
  • Containerization with Docker simplifies deployment and scaling.
  • Production applications require proper logging, security, and monitoring.

Learning Objectives

Master

  • Deploying Go applications as single binaries
  • Configuring applications with environment variables
  • Implementing health checks and monitoring
  • Containerizing applications with Docker

Develop

  • Production deployment thinking
  • Understanding reliability and observability
  • Building production-ready applications

Tips

  • Use environment variables for configuration.
  • Implement health and readiness checks.
  • Use Docker for consistent deployments.
  • Implement graceful shutdown for clean restarts.

Common Pitfalls

  • Hardcoding configuration, making deployment inflexible.
  • Not implementing health checks, making monitoring difficult.
  • Not handling graceful shutdown, causing data loss.
  • Not securing applications, causing security vulnerabilities.

Summary

  • Go compiles to single binaries for easy deployment.
  • Environment variables enable flexible configuration.
  • Health checks and monitoring ensure reliability.
  • Containerization simplifies deployment and scaling.
  • Understanding production practices enables robust systems.

Exercise

Create a production-ready Go application with Docker, environment configuration, and health checks.

package main

import (
    "context"
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "testing"
    "time"
    
    "github.com/gorilla/mux"
)

// Config holds application configuration
type Config struct {
    Port         string "json:"port""
    Environment  string "json:"environment""
    LogLevel     string "json:"log_level""
    DatabaseURL  string "json:"database_url""
    RedisURL     string "json:"redis_url""
}

// App represents the main application
type App struct {
    config *Config
    router *mux.Router
    server *http.Server
}

// NewApp creates a new application instance
func NewApp(config *Config) *App {
    router := mux.NewRouter()
    
    app := &App{
        config: config,
        router: router,
    }
    
    // Setup routes
    app.setupRoutes()
    
    // Setup server
    app.server = &http.Server{
        Addr:         ":" + config.Port,
        Handler:      router,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }
    
    return app
}

func (app *App) setupRoutes() {
    // Health check endpoint
    app.router.HandleFunc("/health", app.healthHandler).Methods("GET")
    
    // Readiness check endpoint
    app.router.HandleFunc("/ready", app.readyHandler).Methods("GET")
    
    // Metrics endpoint
    app.router.HandleFunc("/metrics", app.metricsHandler).Methods("GET")
    
    // API routes
    api := app.router.PathPrefix("/api/v1").Subrouter()
    api.HandleFunc("/status", app.statusHandler).Methods("GET")
    
    // Apply middleware
    app.router.Use(app.loggingMiddleware)
    app.router.Use(app.recoveryMiddleware)
}

func (app *App) healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    
    response := map[string]interface{}{
        "status":    "healthy",
        "timestamp": time.Now().UTC().Format(time.RFC3339),
        "version":   "1.0.0",
        "uptime":    time.Since(startTime).String(),
    }
    
    json.NewEncoder(w).Encode(response)
}

func (app *App) readyHandler(w http.ResponseWriter, r *http.Request) {
    // Check if application is ready to serve traffic
    ready := app.checkReadiness()
    
    w.Header().Set("Content-Type", "application/json")
    
    if ready {
        w.WriteHeader(http.StatusOK)
        response := map[string]interface{}{
            "status":  "ready",
            "message": "Application is ready to serve traffic",
        }
        json.NewEncoder(w).Encode(response)
    } else {
        w.WriteHeader(http.StatusServiceUnavailable)
        response := map[string]interface{}{
            "status":  "not_ready",
            "message": "Application is not ready",
        }
        json.NewEncoder(w).Encode(response)
    }
}

func (app *App) metricsHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    
    metrics := fmt.Sprintf("# HELP go_app_requests_total Total number of requests
" +
        "# TYPE go_app_requests_total counter
" +
        "go_app_requests_total{method="GET",endpoint="/health"} %d
" +
        "go_app_requests_total{method="GET",endpoint="/ready"} %d
" +
        "go_app_requests_total{method="GET",endpoint="/metrics"} %d
" +
        "# HELP go_app_uptime_seconds Application uptime in seconds
" +
        "# TYPE go_app_uptime_seconds gauge
" +
        "go_app_uptime_seconds %f
",
        healthRequests, readyRequests, metricsRequests,
        time.Since(startTime).Seconds())
    
    w.Write([]byte(metrics))
}

func (app *App) statusHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    
    response := map[string]interface{}{
        "status":      "running",
        "environment": app.config.Environment,
        "port":        app.config.Port,
        "timestamp":   time.Now().UTC().Format(time.RFC3339),
    }
    
    json.NewEncoder(w).Encode(response)
}

func (app *App) loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Create response writer wrapper to capture status code
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        
        next.ServeHTTP(wrapped, r)
        
        // Log request details
        log.Printf("%s %s %d %v %s",
            r.Method,
            r.URL.Path,
            wrapped.statusCode,
            time.Since(start),
            r.RemoteAddr)
    })
}

func (app *App) recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("Panic: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        
        next.ServeHTTP(w, r)
    })
}

func (app *App) checkReadiness() bool {
    // In a real application, check:
    // - Database connectivity
    // - Redis connectivity
    // - External service dependencies
    // - Resource availability
    
    // For demo purposes, always return true
    return true
}

func (app *App) Start() error {
    log.Printf("Starting application on port %s", app.config.Port)
    log.Printf("Environment: %s", app.config.Environment)
    
    return app.server.ListenAndServe()
}

func (app *App) Shutdown(ctx context.Context) error {
    log.Println("Shutting down application...")
    return app.server.Shutdown(ctx)
}

// Response writer wrapper to capture status code
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// Global variables for metrics
var (
    startTime        = time.Now()
    healthRequests   = 0
    readyRequests    = 0
    metricsRequests  = 0
)

func main() {
    // Parse command line flags
    port := flag.String("port", "8080", "Server port")
    environment := flag.String("env", "development", "Environment (development/staging/production)")
    flag.Parse()
    
    // Load configuration
    config := &Config{
        Port:        getEnv("PORT", *port),
        Environment: getEnv("ENVIRONMENT", *environment),
        LogLevel:    getEnv("LOG_LEVEL", "info"),
        DatabaseURL: getEnv("DATABASE_URL", ""),
        RedisURL:    getEnv("REDIS_URL", ""),
    }
    
    // Create application
    app := NewApp(config)
    
    // Setup graceful shutdown
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
    
    // Start server in goroutine
    go func() {
        if err := app.Start(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()
    
    // Wait for shutdown signal
    <-stop
    
    // Graceful shutdown
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := app.Shutdown(ctx); err != nil {
        log.Printf("Error during shutdown: %v", err)
    }
    
    log.Println("Application stopped gracefully")
}

// Helper function to get environment variables
func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

// Dockerfile example:
/*
FROM golang:1.21-alpine AS builder

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

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/

COPY --from=builder /app/main .
EXPOSE 8080

CMD ["./main"]
*/

// docker-compose.yml example:
/*
version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - ENVIRONMENT=production
      - PORT=8080
      - LOG_LEVEL=info
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped
*/

Code Editor

Output