Documentation
Technical Documentation/Deployment Guide

Deployment Guide

The ReadyGolf platform is designed for scalable, reliable deployment across various environments. This guide covers production deployment, infrastructure setup, CI/CD pipelines, and DevOps best practices.

🎯 Deployment Overview

ReadyGolf supports multiple deployment strategies including cloud-native deployments, containerised environments, and traditional server deployments. The platform is optimised for high availability, performance, and security.

Deployment Options

  • Cloud-Native: AWS, Azure, Google Cloud Platform
  • Containerised: Docker, Kubernetes, Docker Compose
  • Traditional: VPS, dedicated servers
  • Serverless: AWS Lambda, Vercel, Netlify

🚀 Production Deployment

Environment Setup

Production Environment Variables

# Application Configuration
NODE_ENV=production
PORT=3000
HOST=0.0.0.0

# Database Configuration
DATABASE_URL=postgresql://user:password@host:5432/readygolf_prod
DATABASE_POOL_SIZE=50
DATABASE_TIMEOUT=30000

# Authentication Configuration
JWT_SECRET=your_very_secure_jwt_secret_here
JWT_EXPIRES_IN=24h
REFRESH_TOKEN_EXPIRES_IN=7d

# Email Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email@gmail.com
SMTP_PASSWORD=your_app_password
EMAIL_FROM=noreply@readygolf.com

# Payment Configuration
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

# File Storage Configuration
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_REGION=eu-west-2
AWS_S3_BUCKET=readygolf-prod-uploads

# Redis Configuration
REDIS_URL=redis://host:6379
REDIS_PASSWORD=your_redis_password

# Monitoring Configuration
SENTRY_DSN=https://your_sentry_dsn
LOG_LEVEL=warn

Environment-Specific Configuration

// config/environments/production.js
module.exports = {
  database: {
    url: process.env.DATABASE_URL,
    dialect: 'postgres',
    pool: {
      max: 50,
      min: 10,
      acquire: 30000,
      idle: 10000
    },
    logging: false,
    ssl: {
      require: true,
      rejectUnauthorized: false
    }
  },
  server: {
    port: process.env.PORT || 3000,
    host: process.env.HOST || '0.0.0.0',
    cors: {
      origin: ['https://readygolf.com', 'https://www.readygolf.com'],
      credentials: true
    },
    rateLimit: {
      windowMs: 15 * 60 * 1000,
      max: 100
    }
  },
  security: {
    bcrypt: {
      rounds: 12
    },
    jwt: {
      secret: process.env.JWT_SECRET,
      expiresIn: process.env.JWT_EXPIRES_IN || '24h'
    },
    helmet: {
      contentSecurityPolicy: {
        directives: {
          defaultSrc: ["'self'"],
          styleSrc: ["'self'", "'unsafe-inline'"],
          scriptSrc: ["'self'"],
          imgSrc: ["'self'", "data:", "https:"],
          connectSrc: ["'self'", "https://api.stripe.com"]
        }
      }
    }
  }
};

Database Deployment

PostgreSQL Setup

# Install PostgreSQL
sudo apt update
sudo apt install postgresql postgresql-contrib

# Create database and user
sudo -u postgres psql

CREATE DATABASE readygolf_prod;
CREATE USER readygolf_user WITH PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE readygolf_prod TO readygolf_user;
ALTER USER readygolf_user CREATEDB;

# Configure PostgreSQL for production
sudo nano /etc/postgresql/14/main/postgresql.conf

# Add these settings:
max_connections = 200
shared_buffers = 256MB
effective_cache_size = 1GB
maintenance_work_mem = 64MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 4MB
min_wal_size = 1GB
max_wal_size = 4GB

# Restart PostgreSQL
sudo systemctl restart postgresql

Database Migration

# Run database migrations
npm run db:migrate

# Seed production data (if needed)
npm run db:seed:prod

# Verify database connection
npm run db:health-check

Application Deployment

Docker Deployment

# Dockerfile
FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json pnpm-lock.yaml* ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Build the application
RUN npm run build

# Production image, copy all the files and run the app
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

Docker Compose

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://readygolf_user:password@db:5432/readygolf_prod
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    restart: unless-stopped

  db:
    image: postgres:14
    environment:
      - POSTGRES_DB=readygolf_prod
      - POSTGRES_USER=readygolf_user
      - POSTGRES_PASSWORD=secure_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass your_redis_password
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    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:

Nginx Configuration

# nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream readygolf_app {
        server app:3000;
    }

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

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

        ssl_certificate /etc/nginx/ssl/readygolf.crt;
        ssl_certificate_key /etc/nginx/ssl/readygolf.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-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=31536000; includeSubDomains" always;

        # Gzip compression
        gzip on;
        gzip_vary on;
        gzip_min_length 1024;
        gzip_proxied expired no-cache no-store private must-revalidate auth;
        gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss;

        location / {
            proxy_pass http://readygolf_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;
        }

        # Static file caching
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }
}

☁️ Cloud Deployment

AWS Deployment

AWS ECS Setup

# task-definition.json
{
  "family": "readygolf",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024",
  "memory": "2048",
  "executionRoleArn": "arn:aws:iam::account:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::account:role/readygolf-task-role",
  "containerDefinitions": [
    {
      "name": "readygolf-app",
      "image": "account.dkr.ecr.region.amazonaws.com/readygolf:latest",
      "portMappings": [
        {
          "containerPort": 3000,
          "protocol": "tcp"
        }
      ],
      "environment": [
        {
          "name": "NODE_ENV",
          "value": "production"
        },
        {
          "name": "DATABASE_URL",
          "value": "postgresql://user:password@host:5432/readygolf_prod"
        }
      ],
      "secrets": [
        {
          "name": "JWT_SECRET",
          "valueFrom": "arn:aws:secretsmanager:region:account:secret:readygolf/jwt-secret"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/readygolf",
          "awslogs-region": "eu-west-2",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
}

AWS RDS Setup

# Create RDS instance
aws rds create-db-instance \
  --db-instance-identifier readygolf-prod \
  --db-instance-class db.t3.micro \
  --engine postgres \
  --master-username readygolf_user \
  --master-user-password secure_password \
  --allocated-storage 20 \
  --storage-type gp2 \
  --vpc-security-group-ids sg-12345678 \
  --db-subnet-group-name readygolf-subnet-group \
  --backup-retention-period 7 \
  --multi-az \
  --storage-encrypted

# Configure RDS parameter group
aws rds create-db-parameter-group \
  --db-parameter-group-name readygolf-params \
  --db-parameter-group-family postgres14 \
  --description "ReadyGolf production parameters"

aws rds modify-db-parameter-group \
  --db-parameter-group-name readygolf-params \
  --parameters "ParameterName=max_connections,ParameterValue=200,ApplyMethod=immediate"

Kubernetes Deployment

Kubernetes Manifests

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: readygolf-app
  namespace: readygolf
spec:
  replicas: 3
  selector:
    matchLabels:
      app: readygolf
  template:
    metadata:
      labels:
        app: readygolf
    spec:
      containers:
      - name: readygolf-app
        image: readygolf:latest
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: "production"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: readygolf-secrets
              key: database-url
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: readygolf-secrets
              key: jwt-secret
        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "1Gi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /api/health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /api/health
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: readygolf-service
  namespace: readygolf
spec:
  selector:
    app: readygolf
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
  type: LoadBalancer
---
apiVersion: v1
kind: Secret
metadata:
  name: readygolf-secrets
  namespace: readygolf
type: Opaque
data:
  database-url: <base64-encoded-database-url>
  jwt-secret: <base64-encoded-jwt-secret>

🔄 CI/CD Pipeline

GitHub Actions Workflow

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

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run tests
      run: npm test
    
    - name: Run linting
      run: npm run lint
    
    - name: Build application
      run: npm run build

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Docker Buildx
      uses: docker/setup-buildx-action@v3
    
    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
    
    - name: Deploy to production
      run: |
        # Deploy to your infrastructure
        kubectl set image deployment/readygolf-app readygolf-app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

Database Migration Pipeline

# .github/workflows/migrate.yml
name: Database Migration

on:
  push:
    branches: [main]
    paths:
      - 'packages/database/migrations/**'

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run database migrations
      run: npm run db:migrate
      env:
        DATABASE_URL: ${{ secrets.DATABASE_URL }}
    
    - name: Verify migration
      run: npm run db:verify
      env:
        DATABASE_URL: ${{ secrets.DATABASE_URL }}

📊 Monitoring & Observability

Application Monitoring

// monitoring/sentry.js
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0,
  integrations: [
    new Sentry.BrowserTracing({
      tracePropagationTargets: ['localhost', 'readygolf.com'],
    }),
  ],
});

// Custom error tracking
export const trackError = (error, context = {}) => {
  Sentry.captureException(error, {
    extra: context,
  });
};

// Performance monitoring
export const trackPerformance = (name, duration) => {
  Sentry.addBreadcrumb({
    category: 'performance',
    message: `${name}: ${duration}ms`,
    level: 'info',
  });
};

Health Checks

// api/health/route.ts
import { NextResponse } from 'next/server';
import { db } from '@repo/database';

export async function GET() {
  try {
    // Database health check
    await db.$queryRaw`SELECT 1`;
    
    // Redis health check
    const redis = await import('redis');
    const client = redis.createClient({
      url: process.env.REDIS_URL
    });
    await client.ping();
    await client.quit();
    
    // External API health checks
    const stripeHealth = await fetch('https://api.stripe.com/v1/health');
    const stripeStatus = stripeHealth.ok ? 'healthy' : 'unhealthy';
    
    return NextResponse.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      services: {
        database: 'healthy',
        redis: 'healthy',
        stripe: stripeStatus
      }
    });
  } catch (error) {
    return NextResponse.json({
      status: 'unhealthy',
      timestamp: new Date().toISOString(),
      error: error.message
    }, { status: 503 });
  }
}

Logging Configuration

// lib/logger.js
import winston from 'winston';

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'readygolf' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

export default logger;

🔒 Security Configuration

SSL/TLS Setup

# Generate SSL certificate with Let's Encrypt
sudo apt install certbot python3-certbot-nginx

sudo certbot --nginx -d readygolf.com -d www.readygolf.com

# Auto-renewal
sudo crontab -e
# Add this line:
0 12 * * * /usr/bin/certbot renew --quiet

Security Headers

// middleware/security.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const response = NextResponse.next();
  
  // Security headers
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('X-XSS-Protection', '1; mode=block');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
  
  // HSTS header
  if (request.nextUrl.protocol === 'https:') {
    response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
  }
  
  return response;
}

export const config = {
  matcher: '/((?!api/health|_next/static|_next/image|favicon.ico).*)',
};

🚀 Performance Optimisation

Caching Strategy

// lib/cache.js
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

export const cache = {
  async get(key) {
    try {
      const value = await redis.get(key);
      return value ? JSON.parse(value) : null;
    } catch (error) {
      console.error('Cache get error:', error);
      return null;
    }
  },
  
  async set(key, value, ttl = 3600) {
    try {
      await redis.setex(key, ttl, JSON.stringify(value));
    } catch (error) {
      console.error('Cache set error:', error);
    }
  },
  
  async del(key) {
    try {
      await redis.del(key);
    } catch (error) {
      console.error('Cache delete error:', error);
    }
  }
};

// Cache middleware
export const withCache = (handler, ttl = 3600) => {
  return async (req, context) => {
    const cacheKey = `api:${req.url}`;
    const cached = await cache.get(cacheKey);
    
    if (cached) {
      return new Response(JSON.stringify(cached), {
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    const response = await handler(req, context);
    const data = await response.json();
    
    await cache.set(cacheKey, data, ttl);
    
    return new Response(JSON.stringify(data), {
      headers: { 'Content-Type': 'application/json' }
    });
  };
};

Database Optimisation

-- Database indexes for performance
CREATE INDEX idx_members_organization_id ON members(organization_id);
CREATE INDEX idx_bookings_date_time ON bookings(date, time);
CREATE INDEX idx_transactions_created_at ON transactions(created_at);
CREATE INDEX idx_users_email ON users(email);

-- Query optimisation
EXPLAIN ANALYZE SELECT * FROM bookings 
WHERE date >= '2024-01-01' 
AND organization_id = 'org_123'
ORDER BY date, time;

-- Database maintenance
VACUUM ANALYZE;
REINDEX DATABASE readygolf_prod;

📋 Deployment Checklist

Pre-Deployment

  • All tests passing
  • Code review completed
  • Security scan passed
  • Performance benchmarks met
  • Database migrations tested
  • Environment variables configured
  • SSL certificates valid
  • Backup strategy in place

Deployment

  • Deploy to staging environment
  • Run smoke tests
  • Deploy to production
  • Verify health checks
  • Monitor error rates
  • Check performance metrics
  • Validate functionality
  • Update DNS if needed

Post-Deployment

  • Monitor application logs
  • Check database performance
  • Verify external integrations
  • Monitor user feedback
  • Update documentation
  • Archive deployment artifacts

Need help with deployment? Check out our troubleshooting guide for common deployment issues and solutions.