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.