Production Setup - RumenDamyanov/js-chess GitHub Wiki
Production Setup
Complete guide for deploying the JS Chess Framework Showcase to production environments.
Overview
This guide covers production deployment strategies for:
- Static hosting (Netlify, Vercel, GitHub Pages)
- Container orchestration (Docker, Kubernetes)
- Cloud platforms (AWS, Google Cloud, Azure)
- CDN configuration and optimization
- Monitoring and logging setup
Pre-Deployment Checklist
Code Quality
- All tests passing (unit, integration, E2E)
- Code coverage > 80%
- ESLint and Prettier checks passing
- TypeScript compilation without errors
- Bundle size optimization completed
- Security audit passed
Performance Optimization
- Images optimized and compressed
- Bundle splitting configured
- Tree shaking enabled
- Lazy loading implemented
- Service worker configured
- Caching strategies implemented
Security
- Environment variables secured
- API endpoints protected
- HTTPS enforced
- Security headers configured
- Content Security Policy implemented
- XSS and CSRF protection enabled
Build Configuration
Environment Variables
# .env.production
NODE_ENV=production
CHESS_API_URL=https://api.yourchess.com
ANALYTICS_ID=GA_MEASUREMENT_ID
SENTRY_DSN=your_sentry_dsn
CDN_URL=https://cdn.yourchess.com
Production Build Scripts
{
"scripts": {
"build:prod": "npm run build:all && npm run optimize",
"build:all": "npm run build:angular && npm run build:react && npm run build:vue",
"build:angular": "cd angular-chess && npm run build:prod",
"build:react": "cd react-chess && npm run build",
"build:vue": "cd vue-chess && npm run build",
"optimize": "npm run compress && npm run analyze",
"compress": "gzip -r dist/ && brotli -r dist/",
"analyze": "npm run analyze:angular && npm run analyze:react && npm run analyze:vue"
}
}
Webpack Production Configuration
// webpack.prod.js
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
}),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
chess: {
test: /[\\/]src[\\/]chess[\\/]/,
name: 'chess-logic',
chunks: 'all',
},
},
},
},
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
}),
new CompressionPlugin({
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
}),
process.env.ANALYZE && new BundleAnalyzerPlugin(),
].filter(Boolean),
};
Static Hosting Deployment
Netlify Deployment
netlify.toml Configuration
[build]
publish = "dist"
command = "npm run build:prod"
[build.environment]
NODE_VERSION = "18"
NPM_VERSION = "9"
[redirects](/RumenDamyanov/js-chess/wiki/redirects)
from = "/angular/*"
to = "/angular/index.html"
status = 200
[redirects](/RumenDamyanov/js-chess/wiki/redirects)
from = "/react/*"
to = "/react/index.html"
status = 200
[redirects](/RumenDamyanov/js-chess/wiki/redirects)
from = "/vue/*"
to = "/vue/index.html"
status = 200
[headers](/RumenDamyanov/js-chess/wiki/headers)
for = "/*.js"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[headers](/RumenDamyanov/js-chess/wiki/headers)
for = "/*.css"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[headers](/RumenDamyanov/js-chess/wiki/headers)
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
Deployment Script
#!/bin/bash
# deploy-netlify.sh
set -e
echo "Building for production..."
npm run build:prod
echo "Running final tests..."
npm run test:prod
echo "Deploying to Netlify..."
netlify deploy --prod --dir=dist
echo "Deployment complete!"
echo "URL: https://js-chess-showcase.netlify.app"
Vercel Deployment
vercel.json Configuration
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"distDir": "dist"
}
}
],
"routes": [
{
"src": "/angular/(.*)",
"dest": "/angular/index.html"
},
{
"src": "/react/(.*)",
"dest": "/react/index.html"
},
{
"src": "/vue/(.*)",
"dest": "/vue/index.html"
}
],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
}
]
}
]
}
GitHub Pages Deployment
GitHub Actions Workflow
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build for production
run: npm run build:prod
env:
NODE_ENV: production
CHESS_API_URL: ${{ secrets.CHESS_API_URL }}
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: 'dist'
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
Container Deployment
Multi-Stage Dockerfile
# Dockerfile.prod
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY */package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Build all frameworks
RUN npm run build:prod
# Production stage
FROM nginx:alpine AS production
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Copy built applications
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy SSL certificates (if using)
COPY ssl/ /etc/nginx/ssl/
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]
Nginx Configuration
# nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# Performance
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
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;
# Brotli compression (if available)
brotli on;
brotli_comp_level 6;
brotli_types
text/plain
text/css
application/json
application/javascript
text/xml
application/xml
application/xml+rss
text/javascript;
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none';" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=5r/s;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Rate limiting
limit_req zone=general burst=20 nodelay;
# Main application routes
location / {
try_files $uri $uri/ /index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
}
# Framework-specific routes
location /angular/ {
alias /usr/share/nginx/html/angular/;
try_files $uri $uri/ /angular/index.html;
}
location /react/ {
alias /usr/share/nginx/html/react/;
try_files $uri $uri/ /react/index.html;
}
location /vue/ {
alias /usr/share/nginx/html/vue/;
try_files $uri $uri/ /vue/index.html;
}
# API proxy (if needed)
location /api/ {
limit_req zone=api burst=10 nodelay;
proxy_pass $CHESS_API_URL;
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;
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Error pages
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
# HTTPS configuration (if using SSL)
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# SSL configuration
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;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
# Same location blocks as HTTP server
# ... (inherit from above)
}
}
Docker Compose for Production
# docker-compose.prod.yml
version: '3.8'
services:
chess-frontend:
build:
context: .
dockerfile: Dockerfile.prod
container_name: chess-frontend
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- CHESS_API_URL=${CHESS_API_URL}
volumes:
- ./ssl:/etc/nginx/ssl:ro
- ./logs/nginx:/var/log/nginx
networks:
- chess-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
chess-api:
image: rumendamyanov/go-chess:latest
container_name: chess-api
restart: unless-stopped
expose:
- "8080"
environment:
- GIN_MODE=release
- DATABASE_URL=${DATABASE_URL}
networks:
- chess-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
# Monitoring services
prometheus:
image: prom/prometheus:latest
container_name: chess-prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
networks:
- chess-network
grafana:
image: grafana/grafana:latest
container_name: chess-grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana:/etc/grafana/provisioning
networks:
- chess-network
networks:
chess-network:
driver: bridge
volumes:
prometheus_data:
grafana_data:
Cloud Platform Deployment
AWS S3 + CloudFront
#!/bin/bash
# deploy-aws.sh
# Build the application
npm run build:prod
# Sync to S3
aws s3 sync dist/ s3://your-chess-bucket --delete --cache-control "max-age=31536000"
# Invalidate CloudFront cache
aws cloudfront create-invalidation --distribution-id YOUR_DISTRIBUTION_ID --paths "/*"
echo "Deployed to AWS successfully!"
CloudFormation Template
# infrastructure.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Chess Showcase Infrastructure'
Parameters:
DomainName:
Type: String
Default: chess.yourdomain.com
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub '${DomainName}-static'
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
PublicAccessBlockConfiguration:
BlockPublicAcls: false
BlockPublicPolicy: false
IgnorePublicAcls: false
RestrictPublicBuckets: false
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Aliases:
- !Ref DomainName
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
Compress: true
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # Managed-CachingOptimized
Origins:
- Id: S3Origin
DomainName: !GetAtt S3Bucket.RegionalDomainName
S3OriginConfig:
OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${OriginAccessIdentity}'
Enabled: true
DefaultRootObject: index.html
PriceClass: PriceClass_100
OriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Sub 'OAI for ${DomainName}'
Outputs:
CloudFrontURL:
Description: 'CloudFront Distribution URL'
Value: !GetAtt CloudFrontDistribution.DomainName
Google Cloud Platform
#!/bin/bash
# deploy-gcp.sh
# Build the application
npm run build:prod
# Deploy to Cloud Storage
gsutil -m rsync -r -d dist/ gs://your-chess-bucket
# Set up load balancer (if needed)
gcloud compute backend-buckets create chess-backend --gcs-bucket-name=your-chess-bucket
echo "Deployed to GCP successfully!"
Azure Static Web Apps
# .github/workflows/azure-static-web-apps.yml
name: Azure Static Web Apps CI/CD
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: "upload"
app_location: "/"
api_location: ""
output_location: "dist"
app_build_command: "npm run build:prod"
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
action: "close"
Kubernetes Deployment
Kubernetes Manifests
# k8s/namespace.yml
apiVersion: v1
kind: Namespace
metadata:
name: chess-showcase
---
# k8s/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: chess-frontend
namespace: chess-showcase
spec:
replicas: 3
selector:
matchLabels:
app: chess-frontend
template:
metadata:
labels:
app: chess-frontend
spec:
containers:
- name: chess-frontend
image: your-registry/chess-frontend:latest
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 5
---
# k8s/service.yml
apiVersion: v1
kind: Service
metadata:
name: chess-frontend-service
namespace: chess-showcase
spec:
selector:
app: chess-frontend
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
---
# k8s/ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: chess-frontend-ingress
namespace: chess-showcase
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- chess.yourdomain.com
secretName: chess-tls
rules:
- host: chess.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: chess-frontend-service
port:
number: 80
Helm Chart
# helm/chess-showcase/Chart.yaml
apiVersion: v2
name: chess-showcase
description: A Helm chart for Chess Showcase
type: application
version: 0.1.0
appVersion: "1.0.0"
# helm/chess-showcase/values.yaml
replicaCount: 3
image:
repository: your-registry/chess-frontend
pullPolicy: IfNotPresent
tag: "latest"
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: chess.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: chess-tls
hosts:
- chess.yourdomain.com
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
Monitoring and Logging
Application Monitoring
// monitoring/metrics.js
import { createPrometheusMetrics } from './prometheus';
class ApplicationMetrics {
constructor() {
this.metrics = createPrometheusMetrics();
}
recordGameStart() {
this.metrics.gamesStarted.inc();
}
recordGameComplete(duration) {
this.metrics.gamesCompleted.inc();
this.metrics.gameDuration.observe(duration);
}
recordAIMove(difficulty, duration) {
this.metrics.aiMoves.inc({ difficulty });
this.metrics.aiThinkTime.observe({ difficulty }, duration);
}
recordError(type) {
this.metrics.errors.inc({ type });
}
}
export const metrics = new ApplicationMetrics();
Error Tracking with Sentry
// monitoring/sentry.js
import * as Sentry from '@sentry/browser';
import { Integrations } from '@sentry/tracing';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
integrations: [
new Integrations.BrowserTracing(),
],
tracesSampleRate: 0.1,
beforeSend(event) {
// Filter out non-critical errors in production
if (event.level === 'warning' && process.env.NODE_ENV === 'production') {
return null;
}
return event;
}
});
export { Sentry };
Health Check Endpoint
// health.js
export function createHealthCheck() {
return {
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || '1.0.0',
uptime: process.uptime(),
memory: process.memoryUsage(),
checks: {
api: await checkAPIHealth(),
database: await checkDatabaseHealth(),
cache: await checkCacheHealth()
}
};
}
Performance Optimization
CDN Configuration
// CDN cache headers configuration
const cacheHeaders = {
// Static assets - long cache
'**/*.{js,css,png,jpg,jpeg,gif,ico,svg,woff,woff2}': {
'Cache-Control': 'public, max-age=31536000, immutable',
'ETag': true
},
// HTML files - short cache
'**/*.html': {
'Cache-Control': 'public, max-age=300, must-revalidate',
'ETag': true
},
// API responses - no cache
'/api/**': {
'Cache-Control': 'no-store, no-cache, must-revalidate',
'Pragma': 'no-cache'
}
};
Service Worker
// sw.js - Service Worker for caching
const CACHE_NAME = 'chess-showcase-v1';
const urlsToCache = [
'/',
'/static/js/bundle.js',
'/static/css/main.css',
'/static/media/pieces.svg'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
);
});
Security Considerations
Content Security Policy
<!-- CSP Meta Tag -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline' https://analytics.google.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.chess.com;
frame-ancestors 'none';">
Environment Security
# Secure environment variables
CHESS_API_KEY=your_secure_api_key
DATABASE_PASSWORD=your_secure_db_password
JWT_SECRET=your_jwt_secret
SENTRY_DSN=your_sentry_dsn
# Never commit these to version control!
# Use secret management services in production
Rollback Strategy
Blue-Green Deployment
#!/bin/bash
# blue-green-deploy.sh
CURRENT_ENV=$(kubectl get service chess-frontend -o jsonpath='{.spec.selector.version}')
NEW_ENV=$([ "$CURRENT_ENV" = "blue" ] && echo "green" || echo "blue")
echo "Current environment: $CURRENT_ENV"
echo "Deploying to: $NEW_ENV"
# Deploy new version
kubectl set image deployment/chess-frontend-$NEW_ENV chess-frontend=your-registry/chess-frontend:$NEW_VERSION
# Wait for rollout
kubectl rollout status deployment/chess-frontend-$NEW_ENV
# Run health checks
if curl -f http://chess-frontend-$NEW_ENV/health; then
echo "Health check passed, switching traffic..."
kubectl patch service chess-frontend -p '{"spec":{"selector":{"version":"'$NEW_ENV'"}}}'
echo "Traffic switched to $NEW_ENV"
else
echo "Health check failed, keeping current environment"
exit 1
fi
Backup and Recovery
Database Backup
#!/bin/bash
# backup-database.sh
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="chess_db_backup_$TIMESTAMP.sql"
# Create backup
pg_dump $DATABASE_URL > $BACKUP_FILE
# Upload to cloud storage
aws s3 cp $BACKUP_FILE s3://your-backup-bucket/database/
# Keep only last 30 days of backups
aws s3 ls s3://your-backup-bucket/database/ | head -n -30 | awk '{print $4}' | xargs -I {} aws s3 rm s3://your-backup-bucket/database/{}
echo "Database backup completed: $BACKUP_FILE"
Next Steps
- Environment Configuration - Managing environment variables
- Nginx Configuration - Advanced web server setup
- Monitoring - Advanced monitoring and alerting
- Security - Security best practices