Skip to content

Production Deployment

Complete guide to deploying Promenade Platform in production environments with Docker, cloud hosting, monitoring, and scaling strategies.


Quick Deployment Options

Option 1: Docker Compose (Single Server)

Best for: Small teams, MVP stage, up to 1000 users

bash
# 1. Clone repository
git clone https://github.com/basilex/promenade.git
cd promenade

# 2. Configure production environment
cp config/app.postgres-prod.yaml config/app.postgres-prod.local.yaml
vim config/app.postgres-prod.local.yaml  # Set DB_PASSWORD, JWT_SECRET, etc.

# 3. Start production stack
docker compose -f docker/docker-compose.postgres.prod.yml up -d

# 4. Run migrations
docker exec promenade_api /app/bin/migrate up

# 5. Verify health
curl http://localhost:8081/health

Option 2: Kubernetes (Scalable)

Best for: Growth stage, 1000+ users, high availability

bash
# 1. Apply Kubernetes manifests
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/postgres.yaml
kubectl apply -f k8s/redis.yaml
kubectl apply -f k8s/app.yaml

# 2. Check deployment
kubectl get pods -n promenade
kubectl logs -f deployment/promenade-api -n promenade

Option 3: Cloud Platforms

Best for: Quick MVP, managed infrastructure

  • AWS: ECS Fargate + RDS PostgreSQL + ElastiCache Redis
  • GCP: Cloud Run + Cloud SQL + Memorystore
  • Azure: Container Apps + PostgreSQL + Redis Cache

Docker Deployment

Production Docker Compose

docker/docker-compose.postgres.prod.yml:

yaml
version: "3.8"

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DB_USER:-promenade}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME:-promenade_prod}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U promenade"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    image: promenade/api:latest
    build:
      context: .
      dockerfile: docker/Dockerfile
    environment:
      ENVIRONMENT: production
      DB_HOST: postgres
      DB_PORT: 5432
      DB_USER: ${DB_USER:-promenade}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_NAME: ${DB_NAME:-promenade_prod}
      REDIS_ADDR: redis:6379
      REDIS_PASSWORD: ${REDIS_PASSWORD}
      JWT_SECRET: ${JWT_SECRET}
    ports:
      - "8081:8081"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  postgres_data:
  redis_data:

Build Production Image

docker/Dockerfile (Multi-stage build):

dockerfile
# Stage 1: Build
FROM golang:1.23-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 bin/promenade ./cmd/api

# Stage 2: Runtime
FROM alpine:latest

RUN apk --no-cache add ca-certificates curl
WORKDIR /app

COPY --from=builder /app/bin/promenade .
COPY --from=builder /app/config ./config
COPY --from=builder /app/migrations ./migrations

EXPOSE 8081

HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD curl -f http://localhost:8081/health || exit 1

CMD ["./promenade"]

Build and Push Image

bash
# Build image
docker build -f docker/Dockerfile -t promenade/api:0.1.0 .
docker tag promenade/api:0.1.0 promenade/api:latest

# Push to registry (Docker Hub, ECR, GCR, ACR)
docker push promenade/api:0.1.0
docker push promenade/api:latest

Environment Configuration

Production Config File

config/app.postgres-prod.yaml:

yaml
app:
  name: "Promenade Platform"
  environment: "production"
  version: "1.0.0"

server:
  host: "0.0.0.0"
  port: 8081
  read_timeout: 30s
  write_timeout: 30s

database:
  postgres:
    host: "${DB_HOST}"
    port: 5432
    user: "${DB_USER}"
    password: "${DB_PASSWORD}"
    database: "${DB_NAME}"
    ssl_mode: "require"
    max_connections: 50
    max_idle_connections: 10
    connection_max_lifetime: 1h
  redis:
    addr: "${REDIS_ADDR}"
    password: "${REDIS_PASSWORD}"
    pool_size: 100
    max_retries: 3
    databases:
      revocation: 0
      bus: 1
      cache: 2
      sessions: 3

jwt:
  secret: "${JWT_SECRET}"
  access_token_duration: 15m
  refresh_token_duration: 168h # 7 days
  issuer: "promenade-platform"

bus:
  adapter: "redis" # Distributed event bus
  worker_pool_size: 50
  buffer_size: 10000
  retry_attempts: 5
  retry_delay: 2s
  retry_max_delay: 60s
  retry_multiplier: 2.0

cache:
  enabled: true
  adapter: "redis"
  prefix: "promenade:"
  ttl:
    reference: 86400 # 24 hours
    user: 1800 # 30 minutes
    session: 3600 # 1 hour

logging:
  level: "info"
  format: "json"
  add_source: false

Environment Variables

Required secrets (store in .env.prod or secrets manager):

bash
# Database
export DB_HOST=your-db-host.rds.amazonaws.com
export DB_USER=promenade
export DB_PASSWORD=your-secure-password-here
export DB_NAME=promenade_prod

# Redis
export REDIS_ADDR=your-redis.cache.amazonaws.com:6379
export REDIS_PASSWORD=your-redis-password

# JWT
export JWT_SECRET=$(openssl rand -base64 32)

# Optional
export SENTRY_DSN=https://your-sentry-dsn
export SLACK_WEBHOOK=https://hooks.slack.com/services/YOUR/WEBHOOK

Load secrets:

bash
# From file (development/staging)
source .env.prod

# From AWS Secrets Manager (production)
aws secretsmanager get-secret-value --secret-id promenade-prod | jq -r '.SecretString'

# From Google Secret Manager
gcloud secrets versions access latest --secret="promenade-prod"

# From Azure Key Vault
az keyvault secret show --vault-name promenade-vault --name prod-secrets

Cloud Platform Deployment

AWS ECS Fargate

1. Infrastructure Setup:

bash
# Create VPC, subnets, security groups
terraform init
terraform plan -var-file=prod.tfvars
terraform apply

# Or use CloudFormation
aws cloudformation create-stack \
  --stack-name promenade-infrastructure \
  --template-body file://cloudformation/infrastructure.yaml

2. Database (RDS PostgreSQL):

bash
# Create database instance
aws rds create-db-instance \
  --db-instance-identifier promenade-prod \
  --db-instance-class db.t3.medium \
  --engine postgres \
  --engine-version 16.1 \
  --master-username promenade \
  --master-user-password $DB_PASSWORD \
  --allocated-storage 100 \
  --storage-type gp3 \
  --backup-retention-period 7 \
  --multi-az \
  --vpc-security-group-ids sg-xxxxxxxx

3. Cache (ElastiCache Redis):

bash
# Create Redis cluster
aws elasticache create-cache-cluster \
  --cache-cluster-id promenade-redis \
  --cache-node-type cache.t3.micro \
  --engine redis \
  --engine-version 7.0 \
  --num-cache-nodes 1 \
  --security-group-ids sg-xxxxxxxx

4. Container (ECS Fargate):

bash
# Create ECS cluster
aws ecs create-cluster --cluster-name promenade-prod

# Register task definition
aws ecs register-task-definition --cli-input-json file://ecs-task-definition.json

# Create service
aws ecs create-service \
  --cluster promenade-prod \
  --service-name promenade-api \
  --task-definition promenade-api:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-xxx],securityGroups=[sg-xxx],assignPublicIp=ENABLED}"

5. Load Balancer (ALB):

bash
# Create target group
aws elbv2 create-target-group \
  --name promenade-api-tg \
  --protocol HTTP \
  --port 8081 \
  --vpc-id vpc-xxxxxxxx \
  --target-type ip \
  --health-check-path /health

# Create load balancer
aws elbv2 create-load-balancer \
  --name promenade-alb \
  --subnets subnet-xxx subnet-yyy \
  --security-groups sg-xxxxxxxx \
  --scheme internet-facing

Google Cloud Platform (GCP)

1. Cloud SQL (PostgreSQL):

bash
# Create database instance
gcloud sql instances create promenade-prod \
  --database-version=POSTGRES_16 \
  --tier=db-custom-2-7680 \
  --region=us-central1 \
  --backup \
  --availability-type=REGIONAL

# Create database
gcloud sql databases create promenade_prod --instance=promenade-prod

2. Memorystore (Redis):

bash
# Create Redis instance
gcloud redis instances create promenade-redis \
  --size=1 \
  --region=us-central1 \
  --redis-version=redis_7_0 \
  --tier=standard

3. Cloud Run:

bash
# Deploy service
gcloud run deploy promenade-api \
  --image gcr.io/your-project/promenade:latest \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars ENVIRONMENT=production,DB_HOST=$DB_HOST \
  --set-secrets DB_PASSWORD=promenade-db-password:latest,JWT_SECRET=promenade-jwt:latest \
  --min-instances 1 \
  --max-instances 10 \
  --cpu 2 \
  --memory 4Gi

Azure

1. Azure Database for PostgreSQL:

bash
# Create resource group
az group create --name promenade-prod --location eastus

# Create database server
az postgres flexible-server create \
  --resource-group promenade-prod \
  --name promenade-db \
  --location eastus \
  --admin-user promenade \
  --admin-password $DB_PASSWORD \
  --sku-name Standard_D2s_v3 \
  --tier GeneralPurpose \
  --version 16

2. Azure Cache for Redis:

bash
# Create Redis cache
az redis create \
  --resource-group promenade-prod \
  --name promenade-redis \
  --location eastus \
  --sku Standard \
  --vm-size c1

3. Container Apps:

bash
# Create container app environment
az containerapp env create \
  --name promenade-env \
  --resource-group promenade-prod \
  --location eastus

# Deploy app
az containerapp create \
  --name promenade-api \
  --resource-group promenade-prod \
  --environment promenade-env \
  --image your-registry.azurecr.io/promenade:latest \
  --target-port 8081 \
  --ingress external \
  --min-replicas 1 \
  --max-replicas 5 \
  --cpu 1 \
  --memory 2Gi

Database Migrations in Production

Pre-Deployment Checklist

  1. Backup database before migrations
  2. Test migrations in staging environment
  3. Review migration SQL for performance impact
  4. Plan rollback strategy
  5. Schedule downtime if needed (or use zero-downtime strategy)

Running Migrations

Option 1: Manual (via container):

bash
# Connect to running API container
docker exec -it promenade_api bash

# Run migrations
./bin/migrate up

# Check status
./bin/migrate status

Option 2: Automated (CI/CD):

yaml
# .github/workflows/deploy.yml
- name: Run Migrations
  run: |
    docker run --rm \
      -e DB_HOST=${{ secrets.DB_HOST }} \
      -e DB_PASSWORD=${{ secrets.DB_PASSWORD }} \
      promenade/api:${{ github.sha }} \
      /app/bin/migrate up

Option 3: Kubernetes Job:

yaml
# k8s/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: promenade-migration
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: promenade/api:latest
          command: ["/app/bin/migrate", "up"]
          env:
            - name: DB_HOST
              valueFrom:
                secretKeyRef:
                  name: promenade-secrets
                  key: db-host
      restartPolicy: OnFailure

Zero-Downtime Migrations

Strategy:

  1. Backward-compatible changes: Add new columns (nullable), new tables
  2. Deploy new code: App works with old and new schema
  3. Run migration: Add columns, backfill data
  4. Remove old code: Clean up deprecated fields

Example:

sql
-- Migration 1: Add new column (nullable)
ALTER TABLE customer_customers ADD COLUMN email_verified BOOLEAN DEFAULT FALSE;

-- Deploy new code (reads/writes email_verified)

-- Migration 2: Backfill data
UPDATE customer_customers SET email_verified = TRUE WHERE email IS NOT NULL;

-- Migration 3: Make NOT NULL
ALTER TABLE customer_customers ALTER COLUMN email_verified SET NOT NULL;

Monitoring & Observability

Health Checks

Kubernetes Probes:

yaml
livenessProbe:
  httpGet:
    path: /health
    port: 8081
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /health/db
    port: 8081
  initialDelaySeconds: 10
  periodSeconds: 5

Load Balancer Health:

  • ALB: /health every 30s
  • NGINX: health_check directive
  • HAProxy: option httpchk GET /health

Logging

Centralized Logging (ELK Stack, CloudWatch, Stackdriver):

yaml
# Fluent Bit config (ship logs to Elasticsearch)
[OUTPUT]
    Name  es
    Match *
    Host  elasticsearch.prod.internal
    Port  9200
    Index promenade-logs
    Type  _doc

Structured Logs (JSON format in production):

json
{
  "timestamp": "2025-12-29T10:15:30Z",
  "level": "error",
  "message": "Failed to create customer",
  "context": {
    "user_id": "01JGABC123",
    "request_id": "req-xyz",
    "error": "database connection timeout"
  }
}

Metrics

Prometheus Metrics (future):

go
// Instrument code with metrics
httpRequestsTotal.WithLabelValues("POST", "/customers").Inc()
httpRequestDuration.WithLabelValues("POST", "/customers").Observe(duration.Seconds())

Grafana Dashboard:

  • Request rate (req/s)
  • Response time (p50, p95, p99)
  • Error rate (%)
  • Database connections (active/idle)
  • Redis operations (get/set rate)

Error Tracking

Sentry Integration (future):

go
import "github.com/getsentry/sentry-go"

sentry.Init(sentry.ClientOptions{
    Dsn: os.Getenv("SENTRY_DSN"),
    Environment: "production",
})

// Capture errors
sentry.CaptureException(err)

Scaling Strategies

Horizontal Scaling (Multiple Instances)

Benefits:

  • Handle more traffic
  • High availability (redundancy)
  • Rolling deployments (zero downtime)

Implementation:

bash
# Docker Swarm
docker service scale promenade_api=5

# Kubernetes
kubectl scale deployment promenade-api --replicas=5

# ECS
aws ecs update-service --service promenade-api --desired-count 5

Auto-scaling:

yaml
# Kubernetes HPA (Horizontal Pod Autoscaler)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: promenade-api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: promenade-api
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Database Scaling

Read Replicas:

  • Route read queries to replicas
  • Write queries to primary
  • Reduce load on primary database

Connection Pooling:

  • Use PgBouncer for PostgreSQL
  • Max connections: 100 (application pool)
  • Database max connections: 200

Query Optimization:

  • Add indexes for common queries
  • Use EXPLAIN ANALYZE for slow queries
  • Enable query caching (Redis)

Redis Scaling

Cluster Mode:

  • Sharding for large datasets
  • High availability with replicas
  • Automatic failover

Configuration:

yaml
redis:
  mode: cluster
  master_replicas: 3
  shards: 3
  read_from_replicas: true

Security Checklist

Pre-Production Security

  • [ ] HTTPS Enforced: TLS 1.3, valid SSL certificate
  • [ ] Secrets Rotation: Regular rotation of JWT secret, DB password
  • [ ] Database Encryption: Encryption at rest (RDS, Cloud SQL)
  • [ ] Network Isolation: Private subnets for DB/Redis
  • [ ] Firewall Rules: Whitelist only necessary IPs
  • [ ] Rate Limiting: Protect authentication endpoints (5 req/min)
  • [ ] CORS Configuration: Allow only trusted domains
  • [ ] SQL Injection Prevention: Parameterized queries (sqlx)
  • [ ] XSS Protection: Input validation, output escaping
  • [ ] CSRF Tokens: For state-changing requests
  • [ ] Audit Logging: Log sensitive operations (user creation, role changes)
  • [ ] Dependency Scanning: go list -m all | nancy for vulnerabilities

TLS/SSL Setup

Let's Encrypt (Free Certificate):

bash
# Install Certbot
sudo apt-get install certbot

# Generate certificate
sudo certbot certonly --standalone -d api.promenade.com

# Configure NGINX
server {
    listen 443 ssl;
    server_name api.promenade.com;

    ssl_certificate /etc/letsencrypt/live/api.promenade.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.promenade.com/privkey.pem;

    location / {
        proxy_pass http://localhost:8081;
    }
}

CI/CD Pipeline

GitHub Actions Workflow

.github/workflows/deploy.yml:

yaml
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@v4
        with:
          go-version: "1.23"

      - name: Run tests
        run: make test

      - name: Run linter
        run: make lint

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

      - name: Build Docker image
        run: docker build -f docker/Dockerfile -t promenade/api:${{ github.sha }} .

      - name: Push to registry
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          docker push promenade/api:${{ github.sha }}
          docker tag promenade/api:${{ github.sha }} promenade/api:latest
          docker push promenade/api:latest

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster promenade-prod \
            --service promenade-api \
            --force-new-deployment

Backup & Disaster Recovery

Database Backups

Automated Backups:

bash
# RDS PostgreSQL (AWS)
aws rds modify-db-instance \
  --db-instance-identifier promenade-prod \
  --backup-retention-period 30 \
  --preferred-backup-window "03:00-04:00"

# Cloud SQL (GCP)
gcloud sql instances patch promenade-prod \
  --backup-start-time=03:00

Manual Backup:

bash
# pg_dump backup
pg_dump -h your-db-host.com -U promenade promenade_prod > backup_$(date +%Y%m%d).sql

# Upload to S3
aws s3 cp backup_$(date +%Y%m%d).sql s3://promenade-backups/

Restore:

bash
# Download backup
aws s3 cp s3://promenade-backups/backup_20250129.sql .

# Restore database
psql -h your-db-host.com -U promenade promenade_prod < backup_20250129.sql

Point-in-Time Recovery

Enable PITR (RDS, Cloud SQL):

  • Restore to any second within retention period
  • Useful for accidental data deletion
  • Retention: 7-35 days

Cost Optimization

Resource Sizing

Start Small:

  • API: 2 vCPU, 4GB RAM (1-2 instances)
  • DB: db.t3.medium (2 vCPU, 4GB RAM)
  • Redis: cache.t3.micro (1GB RAM)
  • Est. Cost: $200-300/month (AWS)

Scale as Needed:

  • Monitor CPU/Memory usage
  • Upgrade when consistently > 70%
  • Use auto-scaling to save costs

Cost-Saving Tips

  1. Reserved Instances: 30-60% savings (1-year commitment)
  2. Spot Instances: 70% savings for non-critical workloads
  3. S3 Intelligent Tiering: Auto-move cold data to cheaper storage
  4. CloudFront CDN: Cache static assets, reduce bandwidth
  5. Lambda Functions: Use for infrequent tasks (migrations, cron jobs)

Troubleshooting

Common Issues

1. Database Connection Failures

bash
# Check database reachability
psql -h $DB_HOST -U $DB_USER -d $DB_NAME

# Check security group rules (AWS)
aws ec2 describe-security-groups --group-ids sg-xxxxxxxx

# Check logs
docker logs promenade_api | grep "database"

2. Redis Connection Errors

bash
# Test Redis connection
redis-cli -h $REDIS_ADDR -a $REDIS_PASSWORD ping

# Check Redis memory usage
redis-cli -h $REDIS_ADDR -a $REDIS_PASSWORD INFO memory

3. High CPU/Memory Usage

bash
# Check container stats
docker stats promenade_api

# Kubernetes
kubectl top pods -n promenade

4. Slow API Responses

bash
# Enable query logging (PostgreSQL)
ALTER DATABASE promenade_prod SET log_min_duration_statement = 1000;  # Log queries > 1s

# Check slow queries
SELECT query, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;


Questions? Open a discussion

Need help deploying? Email: alexander.vasilenko@gmail.com

Built with Domain-Driven Design and Go