This guide covers deploying the Poker Engine to production environments.
Table of Contents
- Prerequisites
- Quick Start
- Environment Configuration
- Docker Deployment
- Kubernetes Deployment
- Manual Deployment
- SSL/TLS Setup
- Database Setup
- Monitoring
- Backup & Recovery
- Scaling
- Troubleshooting
Prerequisites
System Requirements
| Component | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4+ cores |
| RAM | 4 GB | 8+ GB |
| Storage | 20 GB SSD | 50+ GB SSD |
| OS | Ubuntu 20.04+ | Ubuntu 22.04 LTS |
Software Requirements
- Docker with Docker Compose v2
- Node.js, jq and curl on the host.
install.shuses them to verify the signed release manifest, download the image and run the health check - OpenSSL on the host, to generate secrets and the initial self-signed certificate
PostgreSQL 16, Redis 7 and Nginx run as containers in the Docker setup. For a manual deployment you install them yourself and need Node.js 20.19 or newer; Redis is optional there.
Network Requirements
- Ports 80 (HTTP) and 443 (HTTPS) available publicly. The app’s port 3000 is bound to
127.0.0.1only. The installer checks all three for conflicts and warns if any are in use - Domain name with DNS configured
- Outbound HTTPS to
www.onlinepokerscript.comfor license checks and release downloads - SSL certificate (Let’s Encrypt or commercial) — the installer auto-generates a self-signed certificate if none exists, but you should replace it for production
Quick Start
Using Docker (Recommended)
# 1. Extract the release tarball
tar xzf poker-engine-X.Y.Z-main.tar.gz
cd poker-engine-X.Y.Z-main
# 2. Create the production environment file
cp .env.prod.example .env.prod
# 3. Edit .env.prod and replace every placeholder
# (See Environment Configuration section)
openssl rand -hex 64 # once each for JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, JWT_2FA_TEMP_SECRET
openssl rand -hex 32 # once each for POSTGRES_PASSWORD, REDIS_PASSWORD, CSRF_SECRET, ENCRYPTION_KEY
# Also set LICENSE_KEY, LICENSE_DOMAIN, CORS_ORIGINS and ADMIN_INITIAL_PASSWORD
# 4. Optional: add your SSL certificate as nginx/ssl/fullchain.pem and nginx/ssl/privkey.pem
# (The installer generates a self-signed certificate if none exists.
# Replace it with a real certificate for production — see SSL/TLS Setup section.)
# 5. Run the installer
./install.sh
# 6. Start the scheduled backup service (the installer does not start it)
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --build backup
# 7. Verify deployment (add -k while the self-signed certificate is in place)
curl https://your-domain.com/health
The installer validates .env.prod, verifies your license, verifies and loads the signed Docker image, starts PostgreSQL and Redis, runs database migrations, starts the app and Nginx, and waits for the health check to pass.
On an empty database, the app’s first startup creates the admin account with ADMIN_INITIAL_PASSWORD. The password must be 12-128 characters with upper and lower case, a number and a special character, and must not contain a known default such as password or admin123. The app refuses to start otherwise.
Kubernetes Deployment
Enterprise Kubernetes deployments are supported through the Helm chart in deploy/helm/poker-engine. See Kubernetes and Helm Deployment for values examples, secret options, ingress/TLS, migrations, upgrades, rollback, and Compose-to-Kubernetes migration steps.
Environment Configuration
Create .env.prod from .env.prod.example and configure all values. It holds infrastructure and secrets only.
Game settings, feature toggles, SMTP and payment providers are not environment variables. They are stored in the database and managed in the Admin Panel under Settings, so variables such as SMTP_HOST, STRIPE_SECRET_KEY or COINPAYMENTS_PUBLIC_KEY have no effect.
For the complete list of variables, see the Configuration Reference.
Application Settings
NODE_ENV=production
PORT=3000
# Match your real proxy topology. The bundled Nginx is exactly one proxy hop.
TRUST_PROXY=1
# Keep docker for Compose. The Helm chart sets kubernetes.
DEPLOYMENT_MODE=docker
# Image reference used by docker-compose.prod.yml
GHCR_OWNER=aronprins
POKER_ENGINE_VERSION=latest
The release tarball pins the default image version in its docker-compose.prod.yml.
License
# From your purchase confirmation email
LICENSE_KEY=your-license-key
# The bare domain players use, for example poker.example.com
LICENSE_DOMAIN=your-domain.com
Database
# Used by the PostgreSQL container
POSTGRES_USER=poker
POSTGRES_PASSWORD=YOUR_SECURE_PASSWORD
POSTGRES_DB=poker_engine
# Connection pool
DATABASE_POOL_SIZE=50
DATABASE_POOL_TIMEOUT=10
docker-compose.prod.yml builds DATABASE_URL from these values, so don’t set it in .env.prod. See Database Pooling for pool sizing.
Security Note: Generate a strong password:
openssl rand -hex 32
Authentication & Encryption
# JWT secrets: unique values of at least 64 characters
JWT_ACCESS_SECRET=
JWT_REFRESH_SECRET=
JWT_2FA_TEMP_SECRET=
# First-install admin bootstrap password (required for empty production/staging databases)
ADMIN_INITIAL_PASSWORD=
# Token Expiration
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
# Password Hashing
BCRYPT_ROUNDS=12
# CSRF secret: at least 32 characters
CSRF_SECRET=
# Encryption key for sensitive data at rest (REQUIRED): exactly 64 hex characters
ENCRYPTION_KEY=
Security Note: Always generate unique secrets for production:
# JWT secrets
openssl rand -hex 64
# CSRF_SECRET and ENCRYPTION_KEY
openssl rand -hex 32
Important:
ENCRYPTION_KEYis required — the application will crash on startup without it. Generate it withopenssl rand -hex 32.
CORS
# Allowed origins (comma-separated). localhost and 127.0.0.1 are rejected in production.
CORS_ORIGINS=https://your-domain.com,https://www.your-domain.com
Client Configuration
The released client calls /api/v1 and connects its WebSocket on the origin it was loaded from, so no client configuration is needed when Nginx serves both.
Only if you build the frontend yourself and host the API on another origin, set VITE_API_URL at build time to the full API base URL. The WebSocket connects to that URL’s origin:
# Client environment (in client/.env.production)
VITE_API_URL=https://api.your-domain.com/api/v1
Note: In development, the Vite dev server proxies requests to the backend, so this is not needed. The proxy is configured in client/vite.config.ts.
Redis
REDIS_PASSWORD=YOUR_REDIS_PASSWORD
docker-compose.prod.yml always sets REDIS_ENABLED=true and builds REDIS_URL from this password, so REDIS_PASSWORD is required.
Logging
LOG_LEVEL=info
Production Log Levels:
error– Errors only (minimal)warn– Warnings and errorsinfo– Standard production logging (default)debug– Verbose debugging (not recommended for production)
Docker Deployment
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Internet │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Nginx (ports 80, 443) │
│ SSL termination, rate limiting │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Poker Engine App (port 3000) │
│ Node.js + Express + Socket.io │
└─────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ PostgreSQL (5432) │ │ Redis (6379) │
│ Primary data store │ │ Session/cache store │
└─────────────────────────────┘ └─────────────────────────────┘
Services Overview
| Service | Container | Port | Purpose |
|---|---|---|---|
| nginx | poker-engine-nginx | 80, 443 | Reverse proxy, SSL |
| app | poker-engine-app | 3000 (127.0.0.1 only) | Application server |
| postgres | poker-engine-db-prod | 5432 (internal) | Database |
| redis | poker-engine-redis-prod | 6379 (internal) | Cache and Socket.IO adapter |
| backup | poker-engine-backup | – | Automated backups (start separately, see Automated Backups) |
Commands
# Start all services
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d
# Stop all services
docker compose --env-file .env.prod -f docker-compose.prod.yml down
# View logs
docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f
# View specific service logs
docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f app
# Restart a service
docker compose --env-file .env.prod -f docker-compose.prod.yml restart app
# Run migrations
docker compose --env-file .env.prod -f docker-compose.prod.yml --profile migrations up migrations
Building the Image
Licensed installs load the signed image from the release package. To build from a source checkout instead:
# Build production image
docker build -t poker-engine:latest .
# Build with specific tag
docker build -t poker-engine:v1.0.0 .
# Build and push to registry
docker build -t your-registry.com/poker-engine:latest .
docker push your-registry.com/poker-engine:latest
Resource Limits
The production compose file includes resource limits:
| Service | CPU Limit | Memory Limit | CPU Reserved | Memory Reserved |
|---|---|---|---|---|
| app | 2 cores | 2 GB | 0.5 cores | 512 MB |
| postgres | 1 core | 1 GB | 0.25 cores | 256 MB |
| redis | 0.5 cores | 512 MB | 0.1 cores | 128 MB |
| nginx | 0.5 cores | 256 MB | 0.1 cores | 64 MB |
| backup | 0.25 cores | 256 MB | 0.1 cores | 64 MB |
Adjust these in docker-compose.prod.yml based on your server capacity.
Manual Deployment
If you prefer not to use Docker, you can run the app from a source checkout:
1. Install Dependencies
# Install Node.js 20.x (20.19 or newer)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# Install PostgreSQL 16
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt-get update
sudo apt-get -y install postgresql-16
# Install Nginx
sudo apt-get install -y nginx
2. Setup Database
# Create database user and database
sudo -u postgres psql <<EOF
CREATE USER poker WITH PASSWORD 'your-secure-password';
CREATE DATABASE poker_engine OWNER poker;
GRANT ALL PRIVILEGES ON DATABASE poker_engine TO poker;
EOF
3. Build
Create a .env file in the project root with NODE_ENV=production, DATABASE_URL, the secrets above, CORS_ORIGINS, LICENSE_KEY, LICENSE_DOMAIN and ADMIN_INITIAL_PASSWORD.
# Install dependencies
npm ci
# Generate Prisma client
npx prisma generate
# Run migrations
npx prisma migrate deploy
# Build TypeScript
npm run build
# Build frontend (output in client/dist)
npm run build:client
4. Setup Process Manager
# Install PM2
npm install -g pm2
# Start application
pm2 start dist/index.js --name poker-engine
# Setup auto-start on boot
pm2 startup
pm2 save
5. Configure Nginx
Copy the nginx configuration and the frontend. The certificate paths are /etc/nginx/ssl/fullchain.pem and /etc/nginx/ssl/privkey.pem:
sudo cp nginx/nginx.conf /etc/nginx/nginx.conf
sudo cp nginx/conf.d/default.conf nginx/conf.d/security-headers.inc /etc/nginx/conf.d/
sudo cp -r client/dist/. /var/www/html/
The poker_app upstream in nginx.conf points at the Compose hostname. Change it to server 127.0.0.1:3000;, then:
sudo nginx -t
sudo systemctl restart nginx
SSL/TLS Setup
Auto-Generated Self-Signed Certificate
The installer (install.sh) automatically generates a self-signed SSL certificate at nginx/ssl/ if no certificate files are found. This allows the application to start immediately, but browsers will show a security warning.
For production use, replace the self-signed certificate with a real one using Let’s Encrypt or a commercial provider.
Using Let’s Encrypt (Recommended)
In the Docker setup Nginx runs in a container, so obtain the certificate with Certbot’s standalone mode and copy it into nginx/ssl/:
# Install Certbot
sudo apt-get install certbot
# Port 80 must be free while Certbot runs
docker compose --env-file .env.prod -f docker-compose.prod.yml stop nginx
sudo certbot certonly --standalone -d your-domain.com -d www.your-domain.com
sudo cp /etc/letsencrypt/live/your-domain.com/fullchain.pem nginx/ssl/fullchain.pem
sudo cp /etc/letsencrypt/live/your-domain.com/privkey.pem nginx/ssl/privkey.pem
docker compose --env-file .env.prod -f docker-compose.prod.yml start nginx
Repeat the stop, copy and start steps when the certificate renews.
Using Custom Certificate
Place your certificate files in nginx/ssl/:
nginx/ssl/
├── fullchain.pem # Full certificate chain
└── privkey.pem # Private key
Ensure proper permissions:
chmod 600 nginx/ssl/privkey.pem
chmod 644 nginx/ssl/fullchain.pem
SSL Configuration
The nginx configuration includes modern SSL settings:
- TLS 1.2 and 1.3 only
- Strong cipher suites
- HSTS enabled
- Session caching
Database Setup
Initial Setup
# Run migrations
docker compose --env-file .env.prod -f docker-compose.prod.yml --profile migrations up migrations
# Or manually
npx prisma migrate deploy
Bootstrap Data
You don’t need to seed a production database. On first startup against an empty database, the app creates the admin account, default groups, settings, tables and bot profiles.
Database Maintenance
# Connect to database
docker compose --env-file .env.prod -f docker-compose.prod.yml exec postgres psql -U poker -d poker_engine
# Vacuum and analyze
docker compose --env-file .env.prod -f docker-compose.prod.yml exec postgres psql -U poker -d poker_engine -c "VACUUM ANALYZE;"
Monitoring
Health Check Endpoints
| Endpoint | Purpose | Expected Response |
|---|---|---|
/health | Full health check | 200 when healthy or degraded, 503 when unhealthy |
/health/live | Liveness probe | 200 if process running |
/health/ready | Readiness probe | 200 if ready for traffic; 503 if the database is down, the license is not configured or the app is draining |
Checking Health
# Full health check
curl https://your-domain.com/health
# Response:
{
"status": "ok",
"version": "0.0.42",
"maintenance": false,
"liveMigrationEnabled": false,
"draining": false,
"activeHands": 0,
"drainDeadlineAt": null,
"license": { "status": "valid", "lastChecked": "2026-01-15T10:30:00.000Z" }
}
Requests from an admin also get the database and memory checks, uptime and license failure details.
Metrics Endpoint
The bundled Nginx doesn’t proxy /metrics, so call these on the server itself. They require an authenticated admin session:
# Performance metrics
curl -H "Authorization: Bearer $ADMIN_TOKEN" http://127.0.0.1:3000/metrics
# Slow requests
curl -H "Authorization: Bearer $ADMIN_TOKEN" http://127.0.0.1:3000/metrics/slow
For Prometheus, set METRICS_SCRAPE_TOKEN in .env.prod and scrape /metrics/prometheus with that token as a bearer token. The endpoint returns 404 until a token is set.
Sentry Integration
Error tracking is integrated with Sentry. Configure in .env.prod:
SENTRY_ENABLED=true
SENTRY_DSN=https://your-key@sentry.io/project-id
Log Monitoring
# View all logs
docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f
# View application logs only
docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f app
# Filter errors
docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f app | grep -i error
Backup & Recovery
Automated Backups
install.sh doesn’t start the backup service. Start it once after installing:
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --build backup
It then runs with these defaults:
- Schedule: 2:00 AM daily
- Retention: 30 days
- Location:
./backups/postgres/
Configure in .env.prod:
BACKUP_SCHEDULE="0 2 * * *" # Cron format
BACKUP_RETENTION_DAYS=30
Manual Backup
# Create immediate backup
docker compose --env-file .env.prod -f docker-compose.prod.yml exec -T postgres pg_dump -U poker poker_engine > backup-$(date +%Y%m%d-%H%M%S).sql
# Compressed backup
docker compose --env-file .env.prod -f docker-compose.prod.yml exec -T postgres pg_dump -U poker poker_engine | gzip > backup-$(date +%Y%m%d-%H%M%S).sql.gz
Restore from Backup
# Stop application
docker compose --env-file .env.prod -f docker-compose.prod.yml stop app
# Restore database
cat backup.sql | docker compose --env-file .env.prod -f docker-compose.prod.yml exec -T postgres psql -U poker poker_engine
# Or from compressed backup
gunzip -c backup.sql.gz | docker compose --env-file .env.prod -f docker-compose.prod.yml exec -T postgres psql -U poker poker_engine
# Run any pending migrations
docker compose --env-file .env.prod -f docker-compose.prod.yml --profile migrations up migrations
# Restart application
docker compose --env-file .env.prod -f docker-compose.prod.yml start app
To rehearse a restore into a disposable database, using the latest backup:
docker compose --env-file .env.prod -f docker-compose.prod.yml run --rm backup restore --latest --rehearsal
Backup Best Practices
- Test restores regularly – Verify backups work before you need them
- Offsite storage – Copy backups to S3, GCS, or another location
- Encrypt backups – Especially if storing offsite
- Monitor backup success – Alert on failed backups
Scaling
Horizontal Scaling
Run one application instance. Live table state is held in the memory of the app process, so multiple instances would split gameplay state. docker-compose.prod.yml also sets a fixed container_name, so --scale app=N doesn’t work.
Scale vertically instead by raising the app CPU and memory limits in docker-compose.prod.yml.
Redis
Redis is enabled by the production compose file. It backs the Socket.IO adapter, the Developer API request-ID replay protection and its rate limits. It doesn’t store table state.
Database Scaling
See Database Pooling. The app connects through a single DATABASE_URL and has no read-replica support.
Troubleshooting
Common Issues
Application Won’t Start
# Check logs
docker compose --env-file .env.prod -f docker-compose.prod.yml logs app
# Common causes:
# - Database not ready
# - Missing or invalid environment variables (the log lists each one; ENCRYPTION_KEY is required)
# - ADMIN_INITIAL_PASSWORD missing or too weak on an empty database
# - Port already in use (installer now checks ports 80, 443, 3000 before starting)
Docker Image Pull Fails
Install and upgrade scripts load the licensed deployment artifact after validating the signed release manifest and its SHA-256 checksum. A bundled GHCR token is not part of this flow. Download failures stop the update before extraction or image loading. Check license access and connectivity to the configured trusted download origin.
Database Connection Failed
# Check database is running
docker compose --env-file .env.prod -f docker-compose.prod.yml ps postgres
# Test connection
docker compose --env-file .env.prod -f docker-compose.prod.yml exec postgres pg_isready
# Check DATABASE_URL format
# postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public
WebSocket Connection Issues
- Check nginx WebSocket configuration
- Verify
UpgradeandConnectionheaders are passed - Check firewall allows WebSocket connections
- Verify SSL certificate is valid
# Test WebSocket connection
wscat -c wss://your-domain.com/socket.io/
SSL Certificate Issues
# Test certificate
openssl s_client -connect your-domain.com:443 -servername your-domain.com
# Check certificate expiry
echo | openssl s_client -connect your-domain.com:443 2>/dev/null | openssl x509 -noout -dates
High Memory Usage
# Check container memory
docker stats
# Increase memory limits in docker-compose.prod.yml
deploy:
resources:
limits:
memory: 4G
Useful Commands
# Restart everything
docker compose --env-file .env.prod -f docker-compose.prod.yml restart
# Clean up unused resources (without -a, which would delete the loaded app image)
docker system prune
# View resource usage
docker stats
# Shell into container
docker compose --env-file .env.prod -f docker-compose.prod.yml exec app sh
# Check nginx config
docker compose --env-file .env.prod -f docker-compose.prod.yml exec nginx nginx -t
Getting Help
- Check the logs first
- Review this documentation
- Generate a diagnostic bundle with
./support-bundle.sh - Contact support with:
- Error messages
- Steps to reproduce
- Environment details
Security Checklist
Before going live, verify:
- [ ] All default passwords changed
- [ ]
ADMIN_INITIAL_PASSWORDis unique to this install - [ ] JWT secrets are unique and secure
- [ ]
ENCRYPTION_KEYset (required — generate withopenssl rand -hex 32) - [ ] SSL/TLS properly configured (replace auto-generated self-signed cert with a real one)
- [ ] Database not exposed to internet
- [ ] Redis password set
- [ ] CORS origins restricted to your domain
- [ ] Rate limiting configured
- [ ] Backups enabled and tested
- [ ] Monitoring/alerting configured
- [ ] Firewall rules in place
- [ ]
.envfiles not in version control
Maintenance Windows
Planned Downtime
To enable maintenance mode, go to Settings → Features in the Admin Panel and turn on Maintenance Mode. While it’s on, the app blocks non-admin API requests. Turn it off when you’re done.
An admin session can do the same with POST /api/v1/admin/general/maintenance/enable and POST /api/v1/admin/general/maintenance/disable.
Upgrading
# Self-contained: downloads the update through the license server
./docker-upgrade.sh /opt/poker-engine
# Or from an extracted update tarball
./upgrade.sh /opt/poker-engine
Replace /opt/poker-engine with your installation path. Add --yes to skip the confirmation prompt.
Both scripts verify your license, back up the database, apply the new image, run migrations and check /health. If an upgrade fails after the backup, they roll back to the previous version.
You can also manage updates in the Admin Panel under License & Updates.
To drain active hands and keep players connected through an upgrade, set LIVE_MIGRATION_ENABLED=true in .env.prod. It’s off by default.