Target Scenario: Server with existing reverse proxy (Nginx, Traefik, Apache)
What's Included:
- Blue-green deployment for zero-downtime updates
- All infrastructure services
- No Caddy (you manage your own reverse proxy)
- Ubuntu/Debian server with Docker installed
- Existing reverse proxy (Nginx, Traefik, Apache, etc.)
- Domain configured in your reverse proxy
- SSL certificate management (Let's Encrypt, Cloudflare, etc.)
cd ~
git clone https://github.com/YOUR_USERNAME/chord.git
cd chord
# Generate configs
chmod +x generate-configs.sh
./generate-configs.shFollow prompts to generate .env, livekit.yaml, and turnserver.conf.
docker compose -f docker-compose.deploy.yml --profile infra up -dExposed ports:
- SQL Server: 1433 (consider firewall restrictions)
- Redis: 6379 (consider firewall restrictions)
- MinIO API: 9000
- MinIO Console: 9001
- LiveKit WebSocket: 7880
- LiveKit RTC: 7881 (UDP/TCP)
- Coturn: 3478 (UDP/TCP)
# Install mc client
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc && sudo mv mc /usr/local/bin/
# Configure
mc alias set chord http://localhost:9000 YOUR_MINIO_USER YOUR_MINIO_PASSWORD
# Create bucket
mc mb chord/chord-uploads
mc anonymous set download chord/chord-uploadsdocker compose -f docker-compose.deploy.yml --profile blue up -dServices:
- API: Port 5002
- Frontend: Port 3002
curl http://localhost:5002/health
curl http://localhost:3002/healthCreate /etc/nginx/sites-available/chord:
upstream chord_api {
server localhost:5002;
}
upstream chord_frontend {
server localhost:3002;
}
upstream chord_livekit {
server localhost:7880;
}
server {
listen 80;
listen [::]:80;
server_name your-domain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your-domain.com;
# SSL configuration (adjust paths)
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
# Frontend
location / {
proxy_pass http://chord_frontend;
proxy_http_version 1.1;
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;
}
# API
location /api/ {
proxy_pass http://chord_api/api/;
proxy_http_version 1.1;
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;
client_max_body_size 25M;
}
# SignalR WebSocket (for real-time chat)
# IMPORTANT: Frontend requests /api/hubs but backend expects /hubs
# So we strip the /api prefix here
location /api/hubs/ {
proxy_pass http://chord_api/hubs/;
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_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# LiveKit WebSocket (for voice/video)
location /livekit/ {
proxy_pass http://chord_livekit/;
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_read_timeout 3600s;
}
# MinIO file uploads (optional, can use direct port access)
location /uploads/ {
proxy_pass http://localhost:9000/;
proxy_http_version 1.1;
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;
client_max_body_size 100M;
}
}Enable and reload:
sudo ln -s /etc/nginx/sites-available/chord /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxIf using Traefik with Docker labels, add to docker-compose.deploy.yml:
api-blue:
labels:
- "traefik.enable=true"
- "traefik.http.routers.chord-api.rule=Host(`your-domain.com`) && PathPrefix(`/api`)"
- "traefik.http.routers.chord-api.entrypoints=websecure"
- "traefik.http.routers.chord-api.tls.certresolver=letsencrypt"
- "traefik.http.services.chord-api.loadbalancer.server.port=80"
frontend-blue:
labels:
- "traefik.enable=true"
- "traefik.http.routers.chord-frontend.rule=Host(`your-domain.com`)"
- "traefik.http.routers.chord-frontend.entrypoints=websecure"
- "traefik.http.routers.chord-frontend.tls.certresolver=letsencrypt"
- "traefik.http.services.chord-frontend.loadbalancer.server.port=80"Create /etc/apache2/sites-available/chord.conf:
<VirtualHost *:80>
ServerName your-domain.com
Redirect permanent / https://your-domain.com/
</VirtualHost>
<VirtualHost *:443>
ServerName your-domain.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
# Frontend
ProxyPass / http://localhost:3002/
ProxyPassReverse / http://localhost:3002/
# API
ProxyPass /api http://localhost:5002/api
ProxyPassReverse /api http://localhost:5002/api
# WebSockets
ProxyPass /hubs ws://localhost:5002/hubs
ProxyPassReverse /hubs ws://localhost:5002/hubs
ProxyPass /livekit ws://localhost:7880/
ProxyPassReverse /livekit ws://localhost:7880/
# Upload size
LimitRequestBody 26214400
</VirtualHost>Enable:
sudo a2enmod proxy proxy_http proxy_wstunnel ssl
sudo a2ensite chord
sudo systemctl reload apache2docker compose -f docker-compose.deploy.yml --profile green up -dServices start on:
- API: Port 5003
- Frontend: Port 3003
Nginx: Change upstream ports in config (5002→5003, 3002→3003):
upstream chord_api {
server localhost:5003; # Changed
}
upstream chord_frontend {
server localhost:3003; # Changed
}Reload: sudo systemctl reload nginx
Traefik: Update labels or use weighted services
Apache: Update ProxyPass directives
docker compose -f docker-compose.deploy.yml --profile blue downDeploy to blue (now inactive), update proxy, stop green.
Use scripts/deploy.sh for automated blue-green deployment:
./scripts/deploy.sh \
--image-tag YOUR_GIT_SHA \
--registry ghcr.io \
--repo YOUR_USERNAME/chordNote: The script includes Caddy configuration. You'll need to modify update_caddy() function to update your specific reverse proxy instead.
Consider restricting SQL Server and Redis to localhost only. Edit docker-compose.deploy.yml:
sqlserver:
ports:
- "127.0.0.1:1433:1433" # Localhost only
redis:
ports:
- "127.0.0.1:6379:6379" # Localhost onlyOr use a firewall:
# Allow only from localhost
sudo ufw deny 1433
sudo ufw deny 6379Applications can access SQL/Redis via Docker network without exposing ports:
sqlserver:
# ports: [] # No port mapping, internal only
networks:
- chord-networkAPI will connect via sqlserver:1433 internally.
# API (Blue stack)
curl http://localhost:5002/health
# Frontend (Blue stack)
curl http://localhost:3002/health
# API (Green stack)
curl http://localhost:5003/health
# Frontend (Green stack)
curl http://localhost:3003/health
# LiveKit
curl http://localhost:7880/docker statsdocker compose -f docker-compose.deploy.yml logs -f
docker logs -f chord-api-blueError: bind: address already in use
# Check what's using the port
sudo ss -tlnp | grep ':5002\|:3002\|:5003\|:3003'
# Stop conflicting service or change ports in .envError: 502 Bad Gateway
# Check containers are running
docker ps | grep chord
# Check container health
docker inspect chord-api-blue | grep Health
# Test direct connection (use current active stack port)
curl http://localhost:5002/health # Blue stack
# or
curl http://localhost:5003/health # Green stackError: WebSocket connection failed
Ensure your proxy supports WebSocket upgrades:
Nginx:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";Apache:
ProxyPass /hubs ws://localhost:5002/hubs # Blue stack
# or
ProxyPass /hubs ws://localhost:5003/hubs # Green stack