Documentation Developer Guide Deployment Guide

Deployment Guide

This guide covers deploying Online Casino Script to a production Ubuntu 22.04 LTS server, including server provisioning, Nginx configuration, SSL, systemd services, monitoring, backups, and scaling.

For the initial Docker-based setup or a step-by-step bare-metal install, see the Installation Guide. This guide focuses on production operations.


Table of Contents

  1. Server provisioning
  2. Application deployment
  3. Nginx configuration
  4. PHP-FPM tuning
  5. MySQL tuning
  6. Redis configuration
  7. Systemd services (Horizon + Reverb)
  8. Cron schedule
  9. SSL certificate
  10. Environment variables for production
  11. Health and monitoring
  12. Log management
  13. Backup strategy
  14. Zero-downtime deploys
  15. Horizontal scaling
  16. Security checklist
  17. Pre-launch checklist

1. Server provisioning

Traffic level CPU RAM Disk Bandwidth
Up to 500 concurrent players 4 vCPUs 8 GB 50 GB SSD 100 Mbps
Up to 5,000 concurrent players 8 vCPUs 16 GB 100 GB NVMe 1 Gbps
5,000+ concurrent players See Section 15 — Horizontal scaling

Operating system: Ubuntu 22.04 LTS

Recommended providers: Hetzner, DigitalOcean, AWS EC2, OVH, Linode. Verify the provider allows gambling-related workloads — some providers prohibit this in their terms of service.

Software versions

Software Minimum version
PHP 8.3+
Nginx 1.24+
MySQL 8.0+ (or MariaDB 10.6+)
Redis 7.0+
Node.js 20+ (build step only)
Certbot Latest

Install system dependencies

sudo apt update && sudo apt upgrade -y

# PHP 8.3 and required extensions
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
sudo apt install -y php8.3-fpm php8.3-cli php8.3-mysql php8.3-redis 
    php8.3-bcmath php8.3-curl php8.3-fileinfo php8.3-gd php8.3-mbstring 
    php8.3-openssl php8.3-pcntl php8.3-tokenizer php8.3-xml php8.3-zip

# Nginx
sudo apt install -y nginx

# MySQL 8
sudo apt install -y mysql-server
sudo mysql_secure_installation

# Redis 7
sudo apt install -y redis-server

# Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

# Node.js 20 (build step only)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Certbot (SSL)
sudo apt install -y certbot python3-certbot-nginx

2. Application deployment

# Create application directory
sudo mkdir -p /var/www/casino
sudo chown -R www-data:www-data /var/www/casino

# Clone the repository
cd /var/www/casino
git clone <your-repo-url> . --depth=1

# Install PHP dependencies (production — no dev packages)
composer install --no-dev --optimize-autoloader

# Build frontend assets
npm ci
npm run build

# Set file permissions
sudo chown -R www-data:www-data /var/www/casino
sudo find /var/www/casino -type f -exec chmod 644 {} ;
sudo find /var/www/casino -type d -exec chmod 755 {} ;
sudo chmod -R 775 /var/www/casino/storage
sudo chmod -R 775 /var/www/casino/bootstrap/cache
sudo chmod 600 /var/www/casino/.env

# Configure environment (see Section 10)
cp .env.example .env
# Edit .env with your production values

# Generate application secrets
php artisan key:generate
php artisan jwt:secret

# Run migrations and seed initial data (first deploy only)
php artisan migrate --force
php artisan db:seed --force

# Cache config, routes, views, and events
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

# Create storage symlink
php artisan storage:link

# Publish Horizon assets
php artisan horizon:publish

3. Nginx configuration

Save as /etc/nginx/sites-available/casino:

upstream php-fpm {
    server unix:/run/php/php8.3-fpm.sock;
}

# Redirect HTTP → HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# Main HTTPS server block
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    root /var/www/casino/public;
    index index.php;

    # SSL — managed by Certbot (see Section 9)
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml image/svg+xml;
    gzip_min_length 1000;

    # Max upload size for KYC documents
    client_max_body_size 10M;

    # Laravel SPA entry point
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP handling
    location ~ .php$ {
        fastcgi_pass php-fpm;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 60;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
    }

    # WebSocket proxy — Laravel Reverb
    location /app/ {
        proxy_pass http://127.0.0.1:8080;
        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 86400;
        proxy_send_timeout 86400;
    }

    # Static asset caching (Vite generates content-hashed filenames)
    location /build/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Deny access to hidden and sensitive files
    location ~ /. { deny all; }
    location ~ .(env|log|htaccess)$ { deny all; }
}

Enable the site:

sudo ln -s /etc/nginx/sites-available/casino /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

4. PHP-FPM tuning

Create a dedicated pool at /etc/php/8.3/fpm/pool.d/casino.conf:

[casino]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500

php_admin_value[error_log] = /var/log/php8.3-fpm-casino.log
php_admin_flag[log_errors] = on
php_value[memory_limit] = 256M
php_value[max_execution_time] = 60
php_value[upload_max_filesize] = 10M
php_value[post_max_size] = 12M

; OPcache — critical for production performance
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.memory_consumption] = 256
php_admin_value[opcache.interned_strings_buffer] = 16
php_admin_value[opcache.max_accelerated_files] = 20000
php_admin_value[opcache.revalidate_freq] = 0
php_admin_value[opcache.validate_timestamps] = 0
php_admin_value[opcache.save_comments] = 1
sudo systemctl restart php8.3-fpm

Tuning pm.max_children: Start at 50. Raise to (available RAM - 2 GB) / 30 MB as a rough guide. Monitor with ps aux | grep php-fpm and raise if workers are always at maximum.


5. MySQL tuning

Create /etc/mysql/mysql.conf.d/casino.cnf:

[mysqld]
# InnoDB buffer pool — 50–70% of available RAM
innodb_buffer_pool_size = 4G
innodb_buffer_pool_instances = 4

# Redo log
innodb_log_file_size = 512M
innodb_log_buffer_size = 64M

# ACID compliance — required for financial data
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT

# Connections
max_connections = 300
wait_timeout = 600

# Binary log for point-in-time recovery
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 7
binlog_format = ROW

Create the database and user:

CREATE DATABASE online_casino CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'casino'@'localhost' IDENTIFIED BY 'strong-password-here';
GRANT ALL PRIVILEGES ON online_casino.* TO 'casino'@'localhost';
FLUSH PRIVILEGES;
sudo systemctl restart mysql

6. Redis configuration

Edit /etc/redis/redis.conf:

# Bind to localhost only — never expose Redis publicly
bind 127.0.0.1

# Strong password
requirepass your-strong-redis-password

# Memory limit — leave 1–2 GB for the OS
maxmemory 2gb
maxmemory-policy allkeys-lru

# RDB persistence snapshots
save 900 1
save 300 10
save 60 10000

# AOF persistence for durability
appendonly yes
appendfsync everysec

# Slow log
slowlog-log-slower-than 10000
slowlog-max-len 128
sudo systemctl restart redis-server

7. Systemd services (Horizon + Reverb)

Two long-running processes must be kept alive by systemd.

Laravel Horizon (queue workers)

Save as /etc/systemd/system/casino-horizon.service:

[Unit]
Description=Online Casino Script — Laravel Horizon
After=network.target mysql.service redis.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/casino
ExecStart=/usr/bin/php artisan horizon
Restart=on-failure
RestartSec=5s
KillSignal=SIGTERM
TimeoutStopSec=60
StandardOutput=journal
StandardError=journal
SyslogIdentifier=casino-horizon

[Install]
WantedBy=multi-user.target

Laravel Reverb (WebSocket server)

Save as /etc/systemd/system/casino-reverb.service:

[Unit]
Description=Online Casino Script — Laravel Reverb WebSocket Server
After=network.target mysql.service redis.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/casino
ExecStart=/usr/bin/php artisan reverb:start --host=127.0.0.1 --port=8080
Restart=on-failure
RestartSec=5s
KillSignal=SIGTERM
TimeoutStopSec=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=casino-reverb

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable casino-horizon casino-reverb
sudo systemctl start casino-horizon casino-reverb

# Verify both are running
sudo systemctl status casino-horizon casino-reverb

8. Cron schedule

Add to /etc/cron.d/casino:

# Laravel scheduler — runs every minute
* * * * * www-data cd /var/www/casino && php artisan schedule:run >> /dev/null 2>&1

# Daily database backup at 02:00 UTC
0 2 * * * www-data cd /var/www/casino && bash scripts/backup/backup-database.sh >> /var/log/casino-backup-db.log 2>&1

# Daily Redis backup at 02:30 UTC
30 2 * * * www-data cd /var/www/casino && bash scripts/backup/backup-redis.sh >> /var/log/casino-backup-redis.log 2>&1

The Laravel scheduler manages:

  • 03:00 UTC — Daily ledger balance audit (LEDGER_AUDIT_TIME)
  • 04:00 UTC — Multi-account detection scan (MULTI_ACCOUNT_DETECTION_TIME)
  • 05:00 UTC — Bonus abuse detection (BONUS_ABUSE_DETECTION_TIME)
  • 06:00 UTC — Betting velocity check (VELOCITY_CHECK_TIME)
  • Every 5 minutes — Horizon queue metrics snapshot

Adjust these times in .env to avoid peak traffic hours for your timezone.


9. SSL certificate

# Obtain a Let's Encrypt certificate (Certbot auto-configures Nginx)
sudo certbot --nginx -d example.com -d www.example.com

# Verify auto-renewal
sudo certbot renew --dry-run
sudo systemctl status certbot.timer

Certbot installs a systemd timer that renews certificates automatically 30 days before expiry.


10. Environment variables for production

A minimal production-ready .env:

APP_NAME="Your Casino"
APP_ENV=production
APP_KEY=                    # php artisan key:generate
APP_DEBUG=false             # MUST be false in production
APP_URL=https://example.com
FORCE_HTTPS=true
CSP_ENABLED=true
CORS_ALLOWED_ORIGINS=https://example.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=online_casino
DB_USERNAME=casino
DB_PASSWORD=

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=
REDIS_PORT=6379
REDIS_CACHE_DB=1
REDIS_SESSION_DB=2

SESSION_DRIVER=redis
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=strict

JWT_SECRET=                 # php artisan jwt:secret

REVERB_APP_ID=online-casino
REVERB_APP_KEY=             # openssl rand -hex 16
REVERB_APP_SECRET=          # openssl rand -hex 32
REVERB_HOST=example.com
REVERB_PORT=443
REVERB_SCHEME=https
REVERB_ALLOWED_ORIGINS=https://example.com
VITE_REVERB_APP_KEY=        # same as REVERB_APP_KEY
VITE_REVERB_HOST=example.com
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=https

MAIL_MAILER=smtp
MAIL_HOST=
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@example.com
MAIL_FROM_NAME="Your Casino"

LOG_LEVEL=warning
CACHE_STORE=redis
QUEUE_CONNECTION=redis

After filling in values:

php artisan config:cache
php artisan route:cache
php artisan view:cache

Important: After any .env change in production, always run php artisan config:clear && php artisan config:cache to apply the change.

For the full reference of all available variables, see the Configuration Reference.


11. Health and monitoring

Health endpoint

curl https://example.com/api/health

Expected response:

{
  "status": "ok",
  "database": "ok",
  "redis": "ok",
  "horizon": "ok",
  "timestamp": "2026-01-01T00:00:00Z"
}

Configure your uptime monitor (UptimeRobot, Pingdom, Better Stack) to alert on any non-200 response or a non-"ok" status field.

Horizon dashboard

Access at https://example.com/horizon (requires admin login).

Monitor:

  • Queue throughput and wait times
  • Failed job count — alert if non-zero for more than a few minutes
  • Worker process count

Log locations

Log Path
Application storage/logs/laravel.log
Structured JSON storage/logs/structured.json
Nginx access /var/log/nginx/access.log
Nginx errors /var/log/nginx/error.log
PHP-FPM /var/log/php8.3-fpm-casino.log
Horizon journalctl -u casino-horizon -f
Reverb journalctl -u casino-reverb -f

Alerting

Configure the following in .env to receive critical alerts:

ALERT_SLACK_WEBHOOK=https://hooks.slack.com/services/...
ALERT_EMAIL_TO=ops@example.com

Alerts fire for: failed queue jobs, health endpoint failures, AML rule triggers, and high error rates.


12. Log management

Add a logrotate configuration at /etc/logrotate.d/casino:

/var/www/casino/storage/logs/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0664 www-data www-data
    sharedscripts
    postrotate
        kill -USR1 $(cat /run/php/php8.3-fpm.pid) 2>/dev/null || true
    endscript
}

This keeps 14 days of compressed logs and prevents unbounded disk growth.


13. Backup strategy

Database

Run scripts/backup/backup-database.sh (included in the repository) daily via cron. It creates a timestamped compressed MySQL dump.

# Manual backup
bash scripts/backup/backup-database.sh

Redis

Run scripts/backup/backup-redis.sh daily. It copies the Redis RDB snapshot.

Restoring

# Restore MySQL
mysql -u casino -p online_casino < /path/to/backup.sql

# Restore Redis
sudo systemctl stop redis-server
cp /path/to/dump.rdb /var/lib/redis/dump.rdb
sudo chown redis:redis /var/lib/redis/dump.rdb
sudo systemctl start redis-server

Store backups off-server — upload to S3, Backblaze B2, or equivalent. Retain at least 30 days. Test your restore procedure before going live.


14. Zero-downtime deploys

cd /var/www/casino
git pull
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache && php artisan event:cache
php artisan horizon:terminate    # Horizon drains the queue and restarts via systemd
sudo systemctl restart php8.3-fpm

PHP-FPM restarts gracefully — in-flight requests complete before workers are replaced. Horizon terminates after draining its current jobs (systemd restarts it automatically). No downtime for players.


15. Horizontal scaling

When to scale

Concurrent players Setup
< 500 1 app server, 1 MySQL, 1 Redis
500–2,000 2 app servers, 1 MySQL + 1 replica, 1 Redis
2,000–10,000 3–5 app servers, MySQL + 2 replicas, Redis Cluster, 2 Reverb nodes
> 10,000 Autoscaling group, MySQL Galera / RDS Multi-AZ, Redis Sentinel

Architecture

Players → Load Balancer → App Server 1 ─┐
                        → App Server N ─┤── Shared Redis
                                         └── MySQL Primary ← MySQL Replica(s)
                                         └── Reverb Server(s)

Required .env changes for multi-server

# Read replicas (comma-separated)
DB_READ_HOST=replica1.example.com,replica2.example.com

# Redis pub/sub for WebSocket scaling
REVERB_SCALING_ENABLED=true

# Session must be Redis-backed on all servers
SESSION_DRIVER=redis

# File storage must be shared (S3 for multi-server)
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=eu-west-1
AWS_BUCKET=your-bucket

# Trust the load balancer's IP
APP_TRUSTED_PROXIES=10.0.0.0/8

Per-server deployment steps

Run on each app server:

php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan storage:link

Run on one designated server only:

php artisan migrate --force

Load balancer configuration

  • Use round-robin (the API is stateless — no sticky sessions required)
  • Health check target: GET /api/health — expect HTTP 200
  • Forward X-Real-IP, X-Forwarded-For, X-Forwarded-Proto headers
  • Terminate TLS at the load balancer

16. Security checklist

  • [ ] APP_DEBUG=false in production
  • [ ] APP_KEY is 32 characters, randomly generated
  • [ ] JWT_SECRET is set and unique per installation
  • [ ] REVERB_APP_KEY and REVERB_APP_SECRET are random and unique
  • [ ] Redis has a strong password (requirepass in redis.conf)
  • [ ] Redis is bound to 127.0.0.1 — never publicly accessible
  • [ ] MySQL user has only the required privileges (no SUPER, no FILE)
  • [ ] Firewall: only ports 80, 443, and 22 (SSH) are publicly accessible
  • [ ] SSL certificate is valid and auto-renews
  • [ ] SESSION_SECURE_COOKIE=true (requires HTTPS)
  • [ ] FORCE_HTTPS=true set
  • [ ] CSP_ENABLED=true (Content-Security-Policy header active)
  • [ ] .env permissions: chmod 600 .env
  • [ ] php artisan config:cache run after each .env change
  • [ ] SSH key-only authentication (password login disabled)
  • [ ] ClamAV enabled for KYC file uploads (CLAMAV_ENABLED=true) if required
  • [ ] Fail2ban protecting SSH and Nginx login endpoints

17. Pre-launch checklist

Infrastructure

  • [ ] DNS records pointing to server IP
  • [ ] SSL certificate issued and valid — https://example.com loads without browser warning
  • [ ] Both systemd services running: sudo systemctl status casino-horizon casino-reverb
  • [ ] Cron running: systemctl status cron

Application

  • [ ] All migrations applied: php artisan migrate:status (no pending)
  • [ ] Database seeded with initial data (admin, games, payment methods)
  • [ ] Config cached: php artisan config:cache
  • [ ] Routes cached: php artisan route:cache
  • [ ] Storage symlink created: ls -la public/storage
  • [ ] Frontend assets built: ls public/build/manifest.json

Configuration

  • [ ] Admin password changed (not password)
  • [ ] 2FA enabled on all admin accounts
  • [ ] At least one payment method enabled and tested
  • [ ] At least one game enabled
  • [ ] SMTP email tested — test email sent from Admin → Settings → Email
  • [ ] Site name, currency, and timezone set in Admin → Settings → General
  • [ ] Legal pages reviewed and customised for your jurisdiction

Monitoring

  • [ ] Health endpoint returns 200: curl https://example.com/api/health
  • [ ] Horizon dashboard accessible and showing workers: https://example.com/horizon
  • [ ] Log rotation configured
  • [ ] Backup scripts tested with a successful restore verification
  • [ ] Alerting configured (ALERT_SLACK_WEBHOOK or ALERT_EMAIL_TO)
  • [ ] Uptime monitor configured (external check of /api/health)

Security

  • [ ] All items in Section 16 checked
  • [ ] Responsible gaming features enabled: deposit limits, self-exclusion, session reminders
  • [ ] Penetration test completed (recommended before accepting real players)

For the initial install, see Installation Guide. For all .env variables, see Configuration Reference. For API integration, see API Reference.