Deployment and Production Readiness
Introduction
Deploying applications to production requires careful planning and execution. This guide covers containerization, monitoring, logging, and best practices for production-ready Go applications.
Core Concepts
Production Readiness Checklist
- Proper error handling and logging
- Health checks and monitoring
- Graceful shutdown
- Configuration management
- Database migrations
- Security hardening
- Performance optimization
Good: Containerization
Docker Setup
# โ
GOOD: Multi-stage Dockerfile
FROM golang:1.21 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 app .
# Final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/app .
EXPOSE 8080
CMD ["./app"]
Docker Compose
# โ
GOOD: Docker Compose setup
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://user:password@db:5432/mydb
- LOG_LEVEL=info
depends_on:
- db
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydb
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Good: Health Checks
Implementing Health Checks
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
// โ
GOOD: Health check endpoint
func handleHealth(c *gin.Context) {
health := map[string]interface{}{
"status": "ok",
"timestamp": time.Now(),
}
c.JSON(http.StatusOK, health)
}
// โ
GOOD: Readiness check
func handleReady(c *gin.Context) {
// Check database connection
if err := db.Ping(); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "not ready",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ready"})
}
// โ
GOOD: Liveness check
func handleLive(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "alive"})
}
// โ
GOOD: Setup health endpoints
func setupHealthChecks(router *gin.Engine) {
router.GET("/health", handleHealth)
router.GET("/ready", handleReady)
router.GET("/live", handleLive)
}
Good: Logging
Structured Logging
package main
import (
"github.com/sirupsen/logrus"
"os"
)
// โ
GOOD: Structured logging
func setupLogging() *logrus.Logger {
logger := logrus.New()
// Set output
logger.SetOutput(os.Stdout)
// Set format
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
// Set level
logLevel := os.Getenv("LOG_LEVEL")
if logLevel == "" {
logLevel = "info"
}
level, _ := logrus.ParseLevel(logLevel)
logger.SetLevel(level)
return logger
}
// โ
GOOD: Logging middleware
func loggingMiddleware(logger *logrus.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
logger.WithFields(logrus.Fields{
"method": c.Request.Method,
"path": c.Request.URL.Path,
"ip": c.ClientIP(),
}).Info("Request started")
c.Next()
logger.WithFields(logrus.Fields{
"method": c.Request.Method,
"path": c.Request.URL.Path,
"status": c.Writer.Status(),
"latency_ms": c.GetFloat64("latency"),
}).Info("Request completed")
}
}
// โ
GOOD: Structured logging in handlers
func handleRequest(c *gin.Context, logger *logrus.Logger) {
logger.WithFields(logrus.Fields{
"user_id": c.GetString("user_id"),
"action": "create_user",
}).Info("Creating user")
}
Good: Graceful Shutdown
Implementing Graceful Shutdown
package main
import (
"context"
"github.com/gin-gonic/gin"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
// โ
GOOD: Graceful shutdown
func main() {
router := gin.Default()
// Setup routes
router.GET("/", handleHome)
// Create server
server := &http.Server{
Addr: ":8080",
Handler: router,
}
// Start server in goroutine
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
panic(err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
panic(err)
}
}
func handleHome(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
Good: Configuration Management
Environment-Based Configuration
package main
import (
"os"
"strconv"
)
// โ
GOOD: Configuration from environment
type Config struct {
Port int
DatabaseURL string
LogLevel string
Environment string
JWTSecret string
}
func loadConfig() *Config {
return &Config{
Port: getEnvInt("PORT", 8080),
DatabaseURL: getEnv("DATABASE_URL", ""),
LogLevel: getEnv("LOG_LEVEL", "info"),
Environment: getEnv("ENVIRONMENT", "development"),
JWTSecret: getEnv("JWT_SECRET", ""),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intVal, err := strconv.Atoi(value); err == nil {
return intVal
}
}
return defaultValue
}
Advanced Patterns
Database Migrations
package main
import (
"database/sql"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
// โ
GOOD: Run migrations
func runMigrations(db *sql.DB) error {
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return err
}
m, err := migrate.NewWithDatabaseInstance(
"file://migrations",
"postgres",
driver,
)
if err != nil {
return err
}
return m.Up()
}
Monitoring and Metrics
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/gin-gonic/gin"
)
// โ
GOOD: Prometheus metrics
var (
requestCount = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
},
[]string{"method", "path", "status"},
)
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration",
},
[]string{"method", "path"},
)
)
func init() {
prometheus.MustRegister(requestCount)
prometheus.MustRegister(requestDuration)
}
// โ
GOOD: Metrics middleware
func metricsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
duration := time.Since(start).Seconds()
requestCount.WithLabelValues(
c.Request.Method,
c.Request.URL.Path,
strconv.Itoa(c.Writer.Status()),
).Inc()
requestDuration.WithLabelValues(
c.Request.Method,
c.Request.URL.Path,
).Observe(duration)
}
}
// โ
GOOD: Setup metrics endpoint
func setupMetrics(router *gin.Engine) {
router.GET("/metrics", gin.WrapH(promhttp.Handler()))
}
Best Practices
1. Use Environment Variables
// โ
GOOD: Configuration from environment
databaseURL := os.Getenv("DATABASE_URL")
// โ BAD: Hardcoded configuration
databaseURL := "postgres://user:password@localhost/db"
2. Implement Health Checks
// โ
GOOD: Health check endpoint
router.GET("/health", handleHealth)
// โ BAD: No health checks
3. Use Structured Logging
// โ
GOOD: Structured logging
logger.WithFields(logrus.Fields{
"user_id": userID,
"action": "login",
}).Info("User logged in")
// โ BAD: Unstructured logging
log.Printf("User %s logged in", userID)
4. Graceful Shutdown
// โ
GOOD: Graceful shutdown
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
server.Shutdown(ctx)
// โ BAD: Immediate shutdown
os.Exit(0)
Resources
- Docker: https://www.docker.com/
- Kubernetes: https://kubernetes.io/
- Prometheus: https://prometheus.io/
- 12 Factor App: https://12factor.net/
Summary
Production-ready applications require proper logging, monitoring, health checks, and graceful shutdown. Use containerization for consistency, environment variables for configuration, and structured logging for observability. Following these practices ensures reliable, maintainable production systems.
Comments