Documentation

Deployment

Overview

Deploying your application involves building, configuring, and running your application in production environments. This guide covers various deployment strategies from simple server deployment to containerized solutions.

Building for Production

Production Build Script

#!/bin/bash
# bin/build/app_prod.sh

set -e

echo "Building application for production..."

# Clean previous builds
rm -f app

# Set production environment
export CGO_ENABLED=1
export GOOS=linux
export GOARCH=amd64

# Tidy dependencies
echo "Tidying Go modules..."
go mod tidy

# Vendor dependencies for reproducible builds
echo "Vendoring dependencies..."
go mod vendor

# Build optimized binary
echo "Building optimized binary..."
go build -ldflags="-w -s" -o app ./main.go

# Build CSS
echo "Building CSS assets..."
npx tailwindcss -i input.css -o ./assets/css/output.css --minify

echo "Production build complete!"
echo "Binary size: $(du -h app | cut -f1)"

Dockerfile for Production

# Multi-stage build
FROM node:18-alpine AS css-builder

WORKDIR /app
COPY package*.json ./
COPY input.css ./
COPY tailwind.config.js ./
COPY templates/ ./templates/

RUN npm ci --only=production
RUN npx tailwindcss -i input.css -o assets/css/output.css --minify

FROM golang:1.21-alpine AS go-builder

RUN apk add --no-cache gcc musl-dev sqlite-dev

WORKDIR /app

# Copy go mod files first for better caching
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build the application
RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-w -s" -o app ./main.go

# Final stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates sqlite

WORKDIR /root/

# Copy binary from builder stage
COPY --from=go-builder /app/app .

# Copy CSS assets from css-builder stage
COPY --from=css-builder /app/assets ./assets

# Copy other necessary files
COPY --from=go-builder /app/templates ./templates
COPY --from=go-builder /app/data ./data

# Create directories for runtime
RUN mkdir -p data logs

# Expose port
EXPOSE 8080

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

CMD ["./app"]

Docker Compose for Production

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - ENV=prod
      - PORT=8080
      - DB_DRIVER=postgres
      - DB_HOST=db
      - DB_PORT=5432
      - DB_NAME=gardening_prod
      - DB_USER=garden_user
      - DB_PASS=${DB_PASSWORD}
      - JWT_SECRET=${JWT_SECRET}
    volumes:
      - ./data:/root/data
      - ./logs:/root/logs
    depends_on:
      - db
      - redis
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=gardening_prod
      - POSTGRES_USER=garden_user
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./data/schema.pg.sql:/docker-entrypoint-initdb.d/schema.sql
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U garden_user -d gardening_prod"]
      interval: 30s
      timeout: 10s
      retries: 3

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

Server Deployment

Systemd Service

# /etc/systemd/system/gardening-app.service
[Unit]
Description=Gardening Application
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=garden-app
Group=garden-app
WorkingDirectory=/opt/gardening-app
ExecStart=/opt/gardening-app/app
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=gardening-app

# Environment
Environment=ENV=prod
Environment=PORT=8080
Environment=DB_DRIVER=postgres
Environment=DB_HOST=localhost
Environment=DB_PORT=5432
Environment=DB_NAME=gardening_prod

# Security
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/opt/gardening-app/data /opt/gardening-app/logs

[Install]
WantedBy=multi-user.target

Nginx Configuration

# /etc/nginx/sites-available/gardening-app
upstream gardening_app {
    server 127.0.0.1:8080;
    # Add more servers for load balancing
    # server 127.0.0.1:8081;
}

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    # SSL configuration
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # Security headers
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;

    # Static assets
    location /assets/ {
        root /opt/gardening-app;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Health check
    location /health {
        proxy_pass http://gardening_app;
        access_log off;
    }

    # Main application
    location / {
        proxy_pass http://gardening_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 86400;
    }
}

Environment Configuration

Production Environment Variables

# .env.prod
ENV=prod
PORT=8080

# Database
DB_DRIVER=postgres
DB_HOST=localhost
DB_PORT=5432
DB_NAME=gardening_prod
DB_USER=garden_user
DB_PASS=your_secure_password
DB_SSLMODE=require

# Security
JWT_SECRET=your_super_secret_jwt_key
CSRF_SECRET=your_csrf_secret_key

# Logging
LOG_LEVEL=info
LOG_FORMAT=json

# Performance
MAX_OPEN_CONNS=25
MAX_IDLE_CONNS=5
CONN_MAX_LIFETIME=5m

# External services
REDIS_URL=redis://localhost:6379
EMAIL_SMTP_HOST=smtp.gmail.com
EMAIL_SMTP_PORT=587

# Application
APP_URL=https://yourdomain.com
RATE_LIMIT=1000

Configuration Management

type Config struct {
    Environment string `env:"ENV" envDefault:"dev"`
    Port        string `env:"PORT" envDefault:"8080"`
    Debug       bool   `env:"DEBUG" envDefault:"false"`

    Database DatabaseConfig
    JWT      JWTConfig
    Redis    RedisConfig
    Email    EmailConfig
    Security SecurityConfig
}

type DatabaseConfig struct {
    Driver          string `env:"DB_DRIVER" envDefault:"sqlite3"`
    Host            string `env:"DB_HOST" envDefault:""`
    Port            string `env:"DB_PORT" envDefault:""`
    Name            string `env:"DB_NAME" envDefault:""`
    User            string `env:"DB_USER" envDefault:""`
    Password        string `env:"DB_PASS" envDefault:""`
    SSLMode         string `env:"DB_SSLMODE" envDefault:"disable"`
    MaxOpenConns    int    `env:"MAX_OPEN_CONNS" envDefault:"25"`
    MaxIdleConns    int    `env:"MAX_IDLE_CONNS" envDefault:"5"`
    ConnMaxLifetime string `env:"CONN_MAX_LIFETIME" envDefault:"5m"`
}

func LoadConfig() (*Config, error) {
    config := &Config{}

    if err := env.Parse(config); err != nil {
        return nil, fmt.Errorf("failed to parse config: %w", err)
    }

    // Validate required fields in production
    if config.Environment == "prod" {
        if config.JWT.Secret == "" {
            return nil, errors.New("JWT_SECRET is required in production")
        }
        if config.Database.Driver == "postgres" && config.Database.Password == "" {
            return nil, errors.New("DB_PASS is required for PostgreSQL")
        }
    }

    return config, nil
}

Database Migration

Migration System

type Migration struct {
    Version int
    Name    string
    SQL     string
}

var migrations = []Migration{
    {
        Version: 1,
        Name:    "Initial schema",
        SQL:     readMigrationFile("001_initial.sql"),
    },
    {
        Version: 2,
        Name:    "Add plant height column",
        SQL:     readMigrationFile("002_add_plant_height.sql"),
    },
}

func RunMigrations(db *sql.DB) error {
    // Create migrations table
    _, err := db.Exec(`
        CREATE TABLE IF NOT EXISTS schema_migrations (
            version INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    `)
    if err != nil {
        return fmt.Errorf("failed to create migrations table: %w", err)
    }

    // Get current version
    currentVersion := 0
    row := db.QueryRow("SELECT COALESCE(MAX(version), 0) FROM schema_migrations")
    row.Scan(&currentVersion)

    // Apply pending migrations
    for _, migration := range migrations {
        if migration.Version > currentVersion {
            log.Printf("Applying migration %d: %s", migration.Version, migration.Name)

            tx, err := db.Begin()
            if err != nil {
                return fmt.Errorf("failed to begin transaction: %w", err)
            }

            // Apply migration
            _, err = tx.Exec(migration.SQL)
            if err != nil {
                tx.Rollback()
                return fmt.Errorf("failed to apply migration %d: %w", migration.Version, err)
            }

            // Record migration
            _, err = tx.Exec("INSERT INTO schema_migrations (version, name) VALUES (?, ?)",
                migration.Version, migration.Name)
            if err != nil {
                tx.Rollback()
                return fmt.Errorf("failed to record migration %d: %w", migration.Version, err)
            }

            if err = tx.Commit(); err != nil {
                return fmt.Errorf("failed to commit migration %d: %w", migration.Version, err)
            }
        }
    }

    return nil
}

Monitoring and Observability

Health Checks

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

func (hc *HealthChecker) CheckHealth() map[string]interface{} {
    health := map[string]interface{}{
        "status":    "ok",
        "timestamp": time.Now().UTC(),
        "version":   version,
        "uptime":    time.Since(startTime).String(),
    }

    // Database health
    if err := hc.db.Ping(); err != nil {
        health["database"] = map[string]interface{}{
            "status": "error",
            "error":  err.Error(),
        }
        health["status"] = "degraded"
    } else {
        health["database"] = map[string]interface{}{
            "status": "ok",
        }
    }

    // Redis health (if configured)
    if hc.redis != nil {
        if err := hc.redis.Ping(context.Background()).Err(); err != nil {
            health["redis"] = map[string]interface{}{
                "status": "error",
                "error":  err.Error(),
            }
            health["status"] = "degraded"
        } else {
            health["redis"] = map[string]interface{}{
                "status": "ok",
            }
        }
    }

    return health
}

Metrics Collection

import "github.com/prometheus/client_golang/prometheus"

type Metrics struct {
    HTTPRequestsTotal   *prometheus.CounterVec
    HTTPRequestDuration *prometheus.HistogramVec
    DatabaseQueries     *prometheus.CounterVec
    ActiveUsers         prometheus.Gauge
}

func NewMetrics() *Metrics {
    return &Metrics{
        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",
            },
            []string{"method", "path"},
        ),
        DatabaseQueries: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "database_queries_total",
                Help: "Total number of database queries",
            },
            []string{"operation", "table"},
        ),
        ActiveUsers: prometheus.NewGauge(
            prometheus.GaugeOpts{
                Name: "active_users",
                Help: "Number of active users",
            },
        ),
    }
}

func (m *Metrics) Register() {
    prometheus.MustRegister(m.HTTPRequestsTotal)
    prometheus.MustRegister(m.HTTPRequestDuration)
    prometheus.MustRegister(m.DatabaseQueries)
    prometheus.MustRegister(m.ActiveUsers)
}

Deployment Automation

CI/CD Pipeline (GitHub Actions)

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: 1.21

    - name: Install dependencies
      run: go mod download

    - name: Run tests
      run: go test -v ./...

    - name: Run linting
      uses: golangci/golangci-lint-action@v3

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    - name: Build and push
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: yourusername/gardening-app:latest

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
    - name: Deploy to server
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.HOST }}
        username: ${{ secrets.USERNAME }}
        key: ${{ secrets.SSH_KEY }}
        script: |
          cd /opt/gardening-app
          docker pull yourusername/gardening-app:latest
          docker-compose up -d
          docker system prune -f

Deployment Best Practices

Security Checklist

  • [ ] Use HTTPS with proper SSL certificates
  • [ ] Set secure environment variables (no hardcoded secrets)
  • [ ] Enable security headers
  • [ ] Use non-root user for application
  • [ ] Enable firewall and restrict ports
  • [ ] Regular security updates
  • [ ] Database connection encryption
  • [ ] Implement rate limiting
  • [ ] Log security events
  • [ ] Regular backup strategy

Performance Optimization

  • [ ] Enable gzip compression
  • [ ] Optimize static asset delivery
  • [ ] Implement proper caching headers
  • [ ] Use CDN for static assets
  • [ ] Database connection pooling
  • [ ] Query optimization and indexing
  • [ ] Monitor and alert on performance metrics
  • [ ] Implement graceful shutdown
  • [ ] Load balancing for high traffic

Monitoring Setup

  • [ ] Application health checks
  • [ ] Database monitoring
  • [ ] Log aggregation and analysis
  • [ ] Error tracking and alerting
  • [ ] Performance metrics collection
  • [ ] Uptime monitoring
  • [ ] Capacity planning
  • [ ] Backup monitoring

Proper deployment practices ensure your application runs reliably in production with good performance, security, and observability.