Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Production Best Practices

Building production-ready Go applications requires more than just functional code. This chapter covers the “last mile” that separates a working prototype from a service you can deploy with confidence: structured logging, configuration management, graceful shutdown, health checks, metrics, containerization, and security hardening. These are the concerns that distinguish senior engineers from junior ones — and they are where Go truly shines as an operations-friendly language.

Structured Logging

Using slog (Go 1.21+)

Go 1.21 introduced log/slog for structured logging.
import "log/slog"

func main() {
    // JSON logger for production
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
        AddSource: true,
    }))
    
    slog.SetDefault(logger)
    
    // Basic logging
    slog.Info("Server starting", "port", 8080)
    slog.Error("Failed to connect", "error", err, "host", "localhost")
    
    // With context
    logger := slog.With("service", "user-service", "version", "1.0.0")
    logger.Info("Request received", "method", "GET", "path", "/users")
}

Custom Logger with Context

type contextKey string

const loggerKey contextKey = "logger"

func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
    return context.WithValue(ctx, loggerKey, logger)
}

func LoggerFromContext(ctx context.Context) *slog.Logger {
    if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
        return logger
    }
    return slog.Default()
}

// Middleware to add request-scoped logger
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := uuid.New().String()
        
        logger := slog.With(
            "request_id", requestID,
            "method", r.Method,
            "path", r.URL.Path,
            "remote_addr", r.RemoteAddr,
        )
        
        ctx := WithLogger(r.Context(), logger)
        r = r.WithContext(ctx)
        
        start := time.Now()
        wrapped := &responseWriter{ResponseWriter: w, statusCode: 200}
        
        next.ServeHTTP(wrapped, r)
        
        logger.Info("Request completed",
            "status", wrapped.statusCode,
            "duration_ms", time.Since(start).Milliseconds(),
        )
    })
}

Using Zap (High Performance)

import "go.uber.org/zap"

func NewLogger(env string) (*zap.Logger, error) {
    var config zap.Config
    
    if env == "production" {
        config = zap.NewProductionConfig()
        config.EncoderConfig.TimeKey = "timestamp"
        config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    } else {
        config = zap.NewDevelopmentConfig()
        config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
    }
    
    return config.Build()
}

func main() {
    logger, _ := NewLogger("production")
    defer logger.Sync()
    
    // Use sugar for printf-style
    sugar := logger.Sugar()
    sugar.Infow("Server starting",
        "port", 8080,
        "env", "production",
    )
    
    // Or structured logger
    logger.Info("Request received",
        zap.String("method", "GET"),
        zap.String("path", "/users"),
        zap.Duration("latency", time.Millisecond*50),
    )
}

Configuration Management

Using Environment Variables

type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
    Redis    RedisConfig
    Logger   LoggerConfig
}

type ServerConfig struct {
    Host         string        `env:"SERVER_HOST" envDefault:"0.0.0.0"`
    Port         int           `env:"SERVER_PORT" envDefault:"8080"`
    ReadTimeout  time.Duration `env:"SERVER_READ_TIMEOUT" envDefault:"5s"`
    WriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT" envDefault:"10s"`
}

type DatabaseConfig struct {
    Host     string `env:"DB_HOST" envDefault:"localhost"`
    Port     int    `env:"DB_PORT" envDefault:"5432"`
    User     string `env:"DB_USER,required"`
    Password string `env:"DB_PASSWORD,required"`
    Name     string `env:"DB_NAME,required"`
    SSLMode  string `env:"DB_SSLMODE" envDefault:"disable"`
}

// Using envconfig
import "github.com/caarlos0/env/v9"

func LoadConfig() (*Config, error) {
    cfg := &Config{}
    if err := env.Parse(cfg); err != nil {
        return nil, err
    }
    return cfg, nil
}
import "github.com/spf13/viper"

func LoadConfig(path string) (*Config, error) {
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(path)
    viper.AddConfigPath(".")
    
    // Environment variable override
    viper.AutomaticEnv()
    viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    
    // Set defaults
    viper.SetDefault("server.port", 8080)
    viper.SetDefault("server.read_timeout", "5s")
    
    if err := viper.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
            return nil, err
        }
        // Config file not found, use defaults and env vars
    }
    
    var cfg Config
    if err := viper.Unmarshal(&cfg); err != nil {
        return nil, err
    }
    
    return &cfg, nil
}

Config File Example

# config.yaml
server:
  host: "0.0.0.0"
  port: 8080
  read_timeout: "5s"
  write_timeout: "10s"

database:
  host: "localhost"
  port: 5432
  user: "postgres"
  name: "myapp"
  ssl_mode: "disable"

redis:
  host: "localhost"
  port: 6379
  db: 0

logger:
  level: "info"
  format: "json"

Graceful Shutdown

Graceful shutdown is the difference between “we deployed with zero downtime” and “some requests got 502 errors during the deploy.” When your service receives SIGTERM (which Kubernetes sends before killing a pod), it needs to stop accepting new requests, wait for in-flight requests to complete, close database connections cleanly, and then exit. Here is the standard pattern:
func main() {
    cfg := LoadConfig()
    
    // Initialize dependencies
    db := initDatabase(cfg.Database)
    defer db.Close()
    
    cache := initRedis(cfg.Redis)
    defer cache.Close()
    
    // Create server
    srv := &http.Server{
        Addr:         fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
        Handler:      setupRoutes(db, cache),
        ReadTimeout:  cfg.Server.ReadTimeout,
        WriteTimeout: cfg.Server.WriteTimeout,
    }
    
    // Start server in goroutine
    go func() {
        slog.Info("Server starting", "addr", srv.Addr)
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            slog.Error("Server error", "error", err)
            os.Exit(1)
        }
    }()
    
    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    slog.Info("Shutting down server...")
    
    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    // Shutdown order matters!
    // 1. Stop accepting new requests
    if err := srv.Shutdown(ctx); err != nil {
        slog.Error("Server shutdown error", "error", err)
    }
    
    // 2. Close database connections
    if err := db.Close(); err != nil {
        slog.Error("Database close error", "error", err)
    }
    
    // 3. Close cache connections
    if err := cache.Close(); err != nil {
        slog.Error("Cache close error", "error", err)
    }
    
    slog.Info("Server stopped")
}

Health Checks

type HealthChecker struct {
    db    *sql.DB
    redis *redis.Client
}

type HealthStatus struct {
    Status    string            `json:"status"`
    Timestamp time.Time         `json:"timestamp"`
    Services  map[string]string `json:"services"`
}

func (h *HealthChecker) Check(ctx context.Context) *HealthStatus {
    status := &HealthStatus{
        Status:    "healthy",
        Timestamp: time.Now(),
        Services:  make(map[string]string),
    }
    
    // Check database
    if err := h.db.PingContext(ctx); err != nil {
        status.Status = "unhealthy"
        status.Services["database"] = err.Error()
    } else {
        status.Services["database"] = "ok"
    }
    
    // Check Redis
    if err := h.redis.Ping(ctx).Err(); err != nil {
        status.Status = "unhealthy"
        status.Services["redis"] = err.Error()
    } else {
        status.Services["redis"] = "ok"
    }
    
    return status
}

// Endpoints
func (h *HealthChecker) LivenessHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func (h *HealthChecker) ReadinessHandler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()
    
    status := h.Check(ctx)
    
    w.Header().Set("Content-Type", "application/json")
    if status.Status != "healthy" {
        w.WriteHeader(http.StatusServiceUnavailable)
    }
    json.NewEncoder(w).Encode(status)
}

Metrics with Prometheus

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},
    )
    
    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "path"},
    )
    
    dbQueryDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "db_query_duration_seconds",
            Help:    "Database query duration in seconds",
            Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1},
        },
        []string{"query"},
    )
    
    activeConnections = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Name: "active_connections",
            Help: "Number of active connections",
        },
    )
)

func init() {
    prometheus.MustRegister(httpRequestsTotal)
    prometheus.MustRegister(httpRequestDuration)
    prometheus.MustRegister(dbQueryDuration)
    prometheus.MustRegister(activeConnections)
}

// Middleware
func MetricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        wrapped := &responseWriter{ResponseWriter: w, statusCode: 200}
        
        next.ServeHTTP(wrapped, r)
        
        duration := time.Since(start).Seconds()
        path := r.URL.Path
        
        httpRequestsTotal.WithLabelValues(r.Method, path, fmt.Sprint(wrapped.statusCode)).Inc()
        httpRequestDuration.WithLabelValues(r.Method, path).Observe(duration)
    })
}

func main() {
    mux := http.NewServeMux()
    
    // Your routes
    mux.Handle("/api/", apiHandler)
    
    // Metrics endpoint
    mux.Handle("/metrics", promhttp.Handler())
    
    http.ListenAndServe(":8080", MetricsMiddleware(mux))
}

Docker Deployment

Dockerfile

# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download

# Build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/server

# Final stage
FROM alpine:3.18

# Add CA certificates for HTTPS
RUN apk --no-cache add ca-certificates tzdata

WORKDIR /app

# Copy binary
COPY --from=builder /app/server .
COPY --from=builder /app/config ./config

# Create non-root user
RUN adduser -D -g '' appuser
USER appuser

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health/live || exit 1

ENTRYPOINT ["./server"]

Docker Compose

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SERVER_PORT=8080
      - DB_HOST=postgres
      - DB_USER=postgres
      - DB_PASSWORD=secret
      - DB_NAME=myapp
      - REDIS_HOST=redis
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "--spider", "http://localhost:8080/health/live"]
      interval: 10s
      timeout: 5s
      retries: 3

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:
  redis_data:

Kubernetes Deployment

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp:latest
          ports:
            - containerPort: 8080
          env:
            - name: DB_HOST
              valueFrom:
                configMapKeyRef:
                  name: myapp-config
                  key: db_host
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: myapp-secrets
                  key: db_password
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

Build and Version Info

// Injected at build time
var (
    Version   = "dev"
    GitCommit = "unknown"
    BuildTime = "unknown"
)

// Build with:
// go build -ldflags "-X main.Version=1.0.0 -X main.GitCommit=$(git rev-parse HEAD) -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)"

func main() {
    slog.Info("Starting application",
        "version", Version,
        "commit", GitCommit,
        "build_time", BuildTime,
    )
}

// Version endpoint
func versionHandler(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode(map[string]string{
        "version":    Version,
        "git_commit": GitCommit,
        "build_time": BuildTime,
    })
}

Makefile

VERSION ?= $(shell git describe --tags --always --dirty)
COMMIT := $(shell git rev-parse HEAD)
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -ldflags "-X main.Version=$(VERSION) -X main.GitCommit=$(COMMIT) -X main.BuildTime=$(BUILD_TIME)"

.PHONY: build
build:
	go build $(LDFLAGS) -o bin/server ./cmd/server

.PHONY: test
test:
	go test -v -race -cover ./...

.PHONY: lint
lint:
	golangci-lint run

.PHONY: docker
docker:
	docker build -t myapp:$(VERSION) .

.PHONY: run
run: build
	./bin/server

Security Best Practices

// Secure HTTP server
srv := &http.Server{
    Addr:              ":8080",
    Handler:           mux,
    ReadTimeout:       5 * time.Second,
    WriteTimeout:      10 * time.Second,
    IdleTimeout:       120 * time.Second,
    ReadHeaderTimeout: 2 * time.Second,
    MaxHeaderBytes:    1 << 20, // 1 MB
}

// Security headers middleware
func SecurityHeadersMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        w.Header().Set("Content-Security-Policy", "default-src 'self'")
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        next.ServeHTTP(w, r)
    })
}

// Rate limiting per IP
func PerIPRateLimiter() func(http.Handler) http.Handler {
    type client struct {
        limiter  *rate.Limiter
        lastSeen time.Time
    }
    
    var (
        mu      sync.Mutex
        clients = make(map[string]*client)
    )
    
    // Cleanup old entries
    go func() {
        for {
            time.Sleep(time.Minute)
            mu.Lock()
            for ip, c := range clients {
                if time.Since(c.lastSeen) > 3*time.Minute {
                    delete(clients, ip)
                }
            }
            mu.Unlock()
        }
    }()
    
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ip := getIP(r)
            
            mu.Lock()
            c, exists := clients[ip]
            if !exists {
                c = &client{limiter: rate.NewLimiter(10, 20)} // 10 req/s, burst 20
                clients[ip] = c
            }
            c.lastSeen = time.Now()
            mu.Unlock()
            
            if !c.limiter.Allow() {
                http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

Interview Questions

  • Use environment variables for secrets and deployment-specific values
  • Use config files (YAML/JSON) for application defaults
  • Libraries like Viper for unified configuration
  • Never commit secrets to version control
  • Support configuration hot-reloading for non-sensitive values
  1. Stop accepting new connections
  2. Wait for in-flight requests to complete (with timeout)
  3. Close database connections
  4. Close cache connections
  5. Flush logs and metrics
  6. Exit cleanly
Two types:
  • Liveness: Is the process running? Simple 200 OK response
  • Readiness: Can the service handle traffic? Check dependencies (DB, cache, etc.)
Kubernetes uses these to manage pod lifecycle.
  • HTTP request count/rate by status code
  • Request latency (histogram)
  • Active connections
  • Database query latency
  • Cache hit/miss rate
  • Goroutine count
  • Memory usage
  • Error rates

Summary

AreaKey Points
LoggingStructured (slog/zap), request IDs, context propagation
ConfigurationEnv vars + files, secrets management, validation
ShutdownGraceful, proper ordering, timeouts
Health ChecksLiveness + readiness probes
MetricsPrometheus, business + technical metrics
DeploymentMulti-stage Docker, K8s manifests
SecurityHeaders, rate limiting, input validation

Interview Deep-Dive

Strong Answer:
  • The shutdown order must be the reverse of the dependency order. You shut down consumers before producers, and application-level resources before infrastructure-level resources.
  • Step 1: Stop accepting new HTTP connections by calling srv.Shutdown(ctx). This stops the listener, returns 503 to new connections, and waits for in-flight requests to complete (up to the context timeout). This is the first step because you want to stop new work from arriving.
  • Step 2: Signal background workers to stop. Cancel their context or close their input channel. Wait for them to finish (with a timeout). This is second because workers might have in-progress database writes that need to complete.
  • Step 3: Close the database connection pool with db.Close(). This waits for in-use connections to be returned, then closes all connections. This must happen after workers finish because workers might be using database connections.
  • Step 4: Close the Redis client. Same reasoning — it must outlive anything that uses it.
  • Step 5: Flush logs and metrics. This is last because you want to capture logs from all the shutdown steps above.
  • The timeout is critical. Each step should have a deadline. If a step hangs (a worker is stuck on a blocking call), the overall shutdown context should expire and force exit. A typical total shutdown timeout is 30 seconds.
  • In Go, the shutdown signal comes from signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM). Kubernetes sends SIGTERM, waits the terminationGracePeriodSeconds (default 30s), then sends SIGKILL. Your shutdown must complete within that window.
Follow-up: What is the difference between liveness and readiness probes in Kubernetes, and how do you implement them in Go?Liveness answers “is the process alive and not deadlocked?” A simple 200 OK response is sufficient. If liveness fails, Kubernetes restarts the pod. Readiness answers “can this instance handle traffic?” It should check all dependencies: database reachable, Redis connected, background workers running. If readiness fails, Kubernetes removes the pod from the service’s endpoints but does NOT restart it. This is the correct behavior when, for example, the database is temporarily down — restarting the pod would not help. In Go, liveness is a handler that always returns 200. Readiness is a handler that pings the database and Redis with a short timeout (2-3 seconds). During graceful shutdown, set readiness to fail first (so the load balancer stops sending traffic), then proceed with shutdown.
Strong Answer:
  • slog (Go 1.21+) is the standard library’s structured logging package. Advantages: zero dependencies, part of the standard library so it will be maintained forever, good enough performance for most services, and a standard slog.Handler interface that allows pluggable backends. Disadvantages: slower than zap for very high-throughput logging, fewer features out of the box.
  • zap (uber) is a high-performance structured logger. Advantages: 3-5x faster than slog for JSON encoding (uses a zero-allocation encoder), has both a typed logger (zap.Logger) and a sugared logger (zap.SugaredLogger) for convenience, and has extensive middleware integrations. Disadvantages: external dependency, more complex API, and if your service is not CPU-bound on logging, the performance difference is irrelevant.
  • My recommendation: use slog for new projects unless profiling shows logging is a bottleneck (rare). Use zap if you are in a team that already uses it or if you genuinely need the maximum throughput (services logging 100K+ lines per second).
  • For request IDs in every log line: create a logging middleware that generates or extracts a request ID, creates a child logger with the request ID as a field (slog.With("request_id", id)), and stores it in the context using context.WithValue. Every function that logs retrieves the logger from the context. This way, every log line for a request automatically includes the request ID without any manual effort at each log call site.
Follow-up: How do you handle log levels in production versus development, and what should you log at each level?In development, use DEBUG level with a human-readable text format (colorized, with source file/line). In production, use INFO level with JSON format (for log aggregation systems like ELK, Datadog, Splunk). DEBUG: detailed internal state, variable values, loop iterations — only during active investigation. INFO: request received/completed, service started/stopped, configuration loaded, significant state changes. WARN: recoverable errors, degraded performance, deprecated usage, retry attempts. ERROR: failures that affect the user — failed requests, database errors, unrecoverable states. Never use ERROR for expected conditions (like 404 Not Found). The key principle: if someone pages you at 3 AM, ERROR logs should tell them what went wrong. If they need to investigate deeper, INFO and DEBUG logs provide the trail. In production, dynamically changing log levels without restart (via an admin endpoint or environment variable reload) is invaluable for debugging live issues.
Strong Answer:
  • Stage 1 (builder): Use golang:1.21-alpine as the build image. Copy go.mod and go.sum first, then RUN go mod download — this caches dependencies as a Docker layer so they are not re-downloaded on every code change. Then copy the source code and build with CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/server. The CGO_ENABLED=0 ensures a statically linked binary with no C dependencies. The -ldflags="-w -s" strips debug info for smaller binaries.
  • Stage 2 (runtime): Use alpine:3.18 (about 5MB) or scratch (0 bytes) as the runtime image. Copy only the compiled binary from the builder stage. Add ca-certificates if making HTTPS calls and tzdata for timezone support. Create a non-root user and run the binary as that user.
  • Image size optimization: the final image is typically 10-20MB (alpine + binary) versus 800MB+ if you used the golang image as the runtime. With scratch, it can be under 10MB, but you lose a shell for debugging, DNS resolution from libc (though Go’s pure-Go resolver works), and the ability to exec into the container.
  • Additional optimizations: use .dockerignore to exclude tests, docs, and development files from the build context (speeds up docker build). Inject version info via build args and ldflags. Add a HEALTHCHECK instruction so Docker (and Docker Compose) can monitor the container’s health natively.
  • Security: never run as root in production. Use USER appuser in the Dockerfile. Do not include secrets in the image — pass them via environment variables or secret management at runtime.
Follow-up: In Kubernetes, what resource requests and limits would you set for a Go service, and what happens if you set them wrong?CPU requests should match your typical usage (say 100m for a light service, 500m for compute-heavy). Memory requests should match the steady-state heap plus the Go runtime overhead (128Mi to 512Mi for most services). Limits should be 2-3x the requests to handle bursts. If memory limits are too low, the OOM killer terminates the pod with no warning. If CPU limits are too low, the pod gets CPU throttled, causing latency spikes. A critical Go-specific detail: Go’s garbage collector is CPU-intensive, and CPU throttling can cause GC pauses to stretch from milliseconds to seconds. Some teams set no CPU limit at all (only requests) and rely on cluster autoscaling, because CPU throttling harms Go services disproportionately. For memory, always set a limit — but set it high enough to account for GC overhead (the Go heap can temporarily be 2x the live data during a GC cycle).