Deployment

This guide covers deploying the Poker Engine to production environments.

Table of Contents

  1. Prerequisites
  2. Quick Start
  3. Environment Configuration
  4. Docker Deployment
  5. Kubernetes Deployment
  6. Manual Deployment
  7. SSL/TLS Setup
  8. Database Setup
  9. Monitoring
  10. Backup & Recovery
  11. Scaling
  12. Troubleshooting

Prerequisites

System Requirements

ComponentMinimumRecommended
CPU2 cores4+ cores
RAM4 GB8+ GB
Storage20 GB SSD50+ GB SSD
OSUbuntu 20.04+Ubuntu 22.04 LTS

Software Requirements

  • Docker with Docker Compose v2
  • Node.js, jq and curl on the host. install.sh uses 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.1 only. The installer checks all three for conflicts and warns if any are in use
  • Domain name with DNS configured
  • Outbound HTTPS to www.onlinepokerscript.com for 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

# 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_KEY is required — the application will crash on startup without it. Generate it with openssl 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 errors
  • info – 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

ServiceContainerPortPurpose
nginxpoker-engine-nginx80, 443Reverse proxy, SSL
apppoker-engine-app3000 (127.0.0.1 only)Application server
postgrespoker-engine-db-prod5432 (internal)Database
redispoker-engine-redis-prod6379 (internal)Cache and Socket.IO adapter
backuppoker-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:

ServiceCPU LimitMemory LimitCPU ReservedMemory Reserved
app2 cores2 GB0.5 cores512 MB
postgres1 core1 GB0.25 cores256 MB
redis0.5 cores512 MB0.1 cores128 MB
nginx0.5 cores256 MB0.1 cores64 MB
backup0.25 cores256 MB0.1 cores64 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.

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

EndpointPurposeExpected Response
/healthFull health check200 when healthy or degraded, 503 when unhealthy
/health/liveLiveness probe200 if process running
/health/readyReadiness probe200 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

  1. Test restores regularly – Verify backups work before you need them
  2. Offsite storage – Copy backups to S3, GCS, or another location
  3. Encrypt backups – Especially if storing offsite
  4. 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

  1. Check nginx WebSocket configuration
  2. Verify Upgrade and Connection headers are passed
  3. Check firewall allows WebSocket connections
  4. 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

  1. Check the logs first
  2. Review this documentation
  3. Generate a diagnostic bundle with ./support-bundle.sh
  4. Contact support with:
    • Error messages
    • Steps to reproduce
    • Environment details

Security Checklist

Before going live, verify:

  • [ ] All default passwords changed
  • [ ] ADMIN_INITIAL_PASSWORD is unique to this install
  • [ ] JWT secrets are unique and secure
  • [ ] ENCRYPTION_KEY set (required — generate with openssl 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
  • [ ] .env files 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.

Try the live demo Pricing

Have questions about Poker Engine? We reply fast.