Crypto Exchange Server Setup & Deployment: The Technical Go-Live Guide
DevOps Deployment Server

Crypto Exchange Server Setup & Deployment: The Technical Go-Live Guide

C
Codono Team
| | 15 min read

Table of Contents

This guide is for sysadmins, DevOps engineers, and CTOs who have already made the business decision to launch an exchange and now need a deployment playbook. If you are still evaluating the business side — licensing, costs, revenue models — read the crypto exchange business plan or the complete startup guide first.

Here we cover every technical step: from bare-metal server provisioning to a production-ready exchange handling real trades and real money.

Before You Begin

Make sure you have:

  • A licensed copy of Codono exchange software with full source code access
  • Root SSH access to your server(s)
  • A registered domain with DNS control
  • SSL certificates (or plan to use Let’s Encrypt)
  • Access credentials for your chosen KYC provider (SumSub or similar)

The instructions below assume Ubuntu 22.04 LTS. Codono also supports CentOS/RHEL and Debian, but package names may differ.

Server Architecture Overview

A production crypto exchange is not a single server. At minimum, plan for this topology:

ServerRoleMinimum Specs
App ServerNginx + PHP-FPM + application code8 vCPU, 32 GB RAM, 200 GB NVMe
Database ServerMySQL 8.0 primary16 vCPU, 64 GB RAM, 500 GB NVMe RAID-10
Cache/Queue ServerRedis 7.x4 vCPU, 16 GB RAM
WebSocket ServerReal-time data feeds4 vCPU, 8 GB RAM
Wallet Server(s)Blockchain daemons (bitcoind, geth, etc.)8 vCPU, 32 GB RAM, 1 TB+ SSD per chain

For a small-to-medium exchange (under 5,000 concurrent users), you can combine App + WebSocket on one server and Cache + Queue on another. Never combine the database or wallet daemons with anything else.

Recommended providers: Hetzner (best price-performance in EU), OVH, AWS EC2 (if you need multi-region), DigitalOcean. Avoid shared hosting.

Step 1: Provision and Harden Servers

Start with a fresh Ubuntu 22.04 LTS installation on each server.

Initial hardening:

# Update packages
apt update && apt upgrade -y

# Create a non-root deploy user
adduser deploy
usermod -aG sudo deploy

# Disable root SSH login
sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd

# Configure UFW firewall
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp    # SSH
ufw allow 80/tcp    # HTTP
ufw allow 443/tcp   # HTTPS
ufw enable

On wallet servers, also restrict RPC ports to only accept connections from the app server’s internal IP:

ufw allow from 10.0.0.2 to any port 8332  # Bitcoin RPC
ufw allow from 10.0.0.2 to any port 8545  # Ethereum RPC

Install fail2ban for brute-force protection:

apt install fail2ban -y
systemctl enable fail2ban

Set up unattended security updates:

apt install unattended-upgrades -y
dpkg-reconfigure --priority=low unattended-upgrades

Step 2: Install the Web Stack

On the App Server, install Nginx, PHP 8.2, and required extensions:

# Nginx
apt install nginx -y

# PHP 8.2 + extensions required by Codono
apt install php8.2-fpm php8.2-mysql php8.2-redis php8.2-curl \
  php8.2-gd php8.2-mbstring php8.2-xml php8.2-zip php8.2-bcmath \
  php8.2-intl php8.2-soap php8.2-gmp -y

Deploy the Codono source code:

mkdir -p /var/www/exchange
# Upload source code via rsync or git clone
rsync -avz ./codono-source/ deploy@app-server:/var/www/exchange/
chown -R www-data:www-data /var/www/exchange
chmod -R 755 /var/www/exchange
chmod -R 775 /var/www/exchange/runtime  # Writable cache dir

Step 3: Configure Nginx as Reverse Proxy

Create the main site configuration:

# /etc/nginx/sites-available/exchange.conf

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

upstream websocket {
    server 127.0.0.1:9502;
}

server {
    listen 443 ssl http2;
    server_name exchange.example.com;

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

    # SSL (see Step 9)
    ssl_certificate /etc/letsencrypt/live/exchange.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/exchange.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # Gzip
    gzip on;
    gzip_types text/plain application/json application/javascript text/css;
    gzip_min_length 1000;

    # Rate limiting zone (defined in nginx.conf)
    limit_req zone=api burst=20 nodelay;

    # Main application
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass php-fpm;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 30s;
    }

    # WebSocket proxy
    location /ws {
        proxy_pass http://websocket;
        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_read_timeout 86400s;
    }

    # Block sensitive paths
    location ~ /\.(git|env|htaccess) {
        deny all;
    }

    location ~ /(runtime|application|vendor) {
        deny all;
    }

    # Static asset caching
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

server {
    listen 80;
    server_name exchange.example.com;
    return 301 https://$host$request_uri;
}

Add the rate limiting zone in /etc/nginx/nginx.conf inside the http block:

limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

Enable and test:

ln -s /etc/nginx/sites-available/exchange.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Step 4: PHP-FPM Tuning

Edit /etc/php/8.2/fpm/pool.d/www.conf for production workloads:

; Process management
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000

; Timeouts
request_terminate_timeout = 30s

; Logging
slowlog = /var/log/php-fpm-slow.log
request_slowlog_timeout = 5s

Edit /etc/php/8.2/fpm/php.ini for exchange-specific settings:

memory_limit = 256M
max_execution_time = 30
upload_max_filesize = 10M
post_max_size = 12M
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0   ; Set to 1 during development
session.gc_maxlifetime = 7200

Restart PHP-FPM:

systemctl restart php8.2-fpm

Key point: Setting opcache.validate_timestamps = 0 in production means you must run systemctl restart php8.2-fpm after every code deploy. This gives a significant performance boost because PHP skips file stat checks on every request.

Step 5: MySQL Database Setup and Tuning

On the Database Server, install MySQL 8.0:

apt install mysql-server-8.0 -y
mysql_secure_installation

Create the exchange database and user:

CREATE DATABASE exchange_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'exchange_user'@'10.0.0.%' IDENTIFIED BY 'strong_random_password';
GRANT ALL PRIVILEGES ON exchange_db.* TO 'exchange_user'@'10.0.0.%';
FLUSH PRIVILEGES;

Edit /etc/mysql/mysql.conf.d/mysqld.cnf for exchange workloads:

[mysqld]
# InnoDB tuning (assuming 64 GB RAM server)
innodb_buffer_pool_size = 40G
innodb_buffer_pool_instances = 8
innodb_log_file_size = 2G
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000

# Connection handling
max_connections = 500
thread_cache_size = 50
table_open_cache = 4000

# Query cache (disabled in MySQL 8, use Redis instead)
# query_cache_type = 0

# Slow query logging
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1

# Network
bind-address = 10.0.0.3    # Internal IP only
max_allowed_packet = 64M

# Binary logging for replication
log_bin = mysql-bin
server-id = 1
binlog_expire_logs_seconds = 604800

Import the Codono database schema:

mysql -u exchange_user -p exchange_db < /var/www/exchange/database/schema.sql

Performance tip: The innodb_buffer_pool_size should be 60-70% of total server RAM. This is the single most important MySQL tuning parameter for an exchange because the order book, balances, and trade history tables are read/write intensive.

Step 6: Redis Cache and Queue Configuration

On the Cache Server, install Redis 7:

apt install redis-server -y

Edit /etc/redis/redis.conf:

bind 10.0.0.4              # Internal IP
maxmemory 8gb
maxmemory-policy allkeys-lru
save 900 1                  # RDB snapshots
save 300 10
appendonly yes              # AOF for durability

Codono uses Redis for:

  • Session storage — faster than MySQL sessions, supports horizontal app server scaling
  • Cache layer — coin prices, ticker data, trading pair configs
  • Queue backend — withdrawal processing, email sending, KYC callbacks
  • Pub/Sub — real-time order book and trade updates pushed to the WebSocket server

Test connectivity from the app server:

redis-cli -h 10.0.0.4 ping
# Should return: PONG

Step 7: Wallet Daemon Deployment

This is the most complex part. Each blockchain you support needs its own daemon running on the Wallet Server.

Bitcoin (bitcoind)

# Install
wget https://bitcoincore.org/bin/bitcoin-core-27.0/bitcoin-27.0-x86_64-linux-gnu.tar.gz
tar xzf bitcoin-27.0-x86_64-linux-gnu.tar.gz
cp bitcoin-27.0/bin/* /usr/local/bin/

Configure /home/bitcoin/.bitcoin/bitcoin.conf:

server=1
rpcuser=exchange_btc
rpcpassword=strong_random_rpc_password
rpcallowip=10.0.0.2/32
rpcbind=10.0.0.5
txindex=1
maxconnections=50
dbcache=4096

Ethereum (geth)

apt install -y software-properties-common
add-apt-repository -y ppa:ethereum/ethereum
apt install geth -y

Run as a systemd service:

geth --http --http.addr 10.0.0.5 --http.port 8545 \
  --http.api eth,net,web3,personal \
  --http.corsdomain "*" \
  --http.vhosts "10.0.0.5" \
  --syncmode snap \
  --datadir /data/ethereum \
  --maxpeers 50

Other Chains

Repeat the pattern for each supported blockchain. Codono supports 50+ chains — see the blockchain integration docs for daemon-specific configurations. Popular chains to enable at launch:

  • BNB Chain (BSC node or RPC endpoint)
  • Tron (java-tron full node)
  • Solana (solana-validator or RPC endpoint)
  • Polygon (bor + heimdall or RPC endpoint)

Hot/cold wallet split: Configure the exchange wallet system so that only 5-10% of total deposits stay in the hot wallet. The rest moves to cold storage addresses protected by multi-signature authorization. Codono’s admin dashboard provides threshold alerts and manual cold-to-hot replenishment controls.

Step 8: WebSocket Server Setup

Real-time price feeds, order book updates, and trade notifications require WebSocket connections. Codono includes a Swoole-based WebSocket server.

Install Swoole:

pecl install swoole
echo "extension=swoole.so" > /etc/php/8.2/cli/conf.d/20-swoole.ini

Start the WebSocket server:

cd /var/www/exchange
php artisan websocket:serve --host=0.0.0.0 --port=9502

Create a systemd service for auto-restart:

# /etc/systemd/system/exchange-ws.service
[Unit]
Description=Exchange WebSocket Server
After=network.target redis.service

[Service]
User=www-data
WorkingDirectory=/var/www/exchange
ExecStart=/usr/bin/php artisan websocket:serve --host=0.0.0.0 --port=9502
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable and start:

systemctl enable exchange-ws
systemctl start exchange-ws

The WebSocket server subscribes to Redis pub/sub channels. When a trade executes or the order book changes, the matching engine publishes the update to Redis, and the WebSocket server broadcasts it to all connected clients within milliseconds.

Step 9: SSL/TLS and Domain Configuration

Use Let’s Encrypt for free, auto-renewing certificates:

apt install certbot python3-certbot-nginx -y
certbot --nginx -d exchange.example.com -d www.exchange.example.com

Verify auto-renewal:

certbot renew --dry-run

DNS records to configure:

RecordTypeValue
exchange.example.comAApp server public IP
www.exchange.example.comCNAMEexchange.example.com
api.exchange.example.comAApp server public IP (if separate API subdomain)

Security headers — add to the Nginx server block:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Step 10: Cron Jobs and Background Workers

Codono requires several scheduled tasks. Add to the www-data crontab:

crontab -u www-data -e
# Process pending withdrawals (every 2 minutes)
*/2 * * * * php /var/www/exchange/artisan queue:withdrawal

# Update coin prices from market data (every minute)
* * * * * php /var/www/exchange/artisan market:prices

# Process deposit confirmations (every minute)
* * * * * php /var/www/exchange/artisan wallet:deposits

# Clean expired orders (every 5 minutes)
*/5 * * * * php /var/www/exchange/artisan orders:cleanup

# Generate daily reports (daily at midnight UTC)
0 0 * * * php /var/www/exchange/artisan reports:daily

# Database backup (daily at 3 AM UTC)
0 3 * * * /usr/local/bin/exchange-backup.sh

# SSL certificate renewal check (twice daily)
0 */12 * * * certbot renew --quiet --post-hook "systemctl reload nginx"

For queue workers that need to run continuously (email notifications, KYC callbacks), use Supervisor:

apt install supervisor -y
# /etc/supervisor/conf.d/exchange-worker.conf
[program:exchange-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/exchange/artisan queue:work redis --tries=3 --timeout=90
autostart=true
autorestart=true
numprocs=4
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/exchange-worker.log
supervisorctl reread && supervisorctl update

Step 11: Monitoring and Alerting

A production exchange needs three layers of monitoring:

Infrastructure Monitoring (Prometheus + Grafana)

Install node_exporter on every server. Track:

  • CPU utilization (alert above 80% sustained)
  • RAM usage (alert above 85%)
  • Disk I/O and free space (alert below 20%)
  • Network throughput and packet loss

Application Monitoring

  • PHP-FPM pool status (pm.status_path = /fpm-status)
  • Nginx stub_status for active connections
  • MySQL slow query count per hour
  • Redis memory usage and key count
  • Queue depth (pending jobs should stay near zero)

Wallet Monitoring (critical)

  • Hot wallet balance per coin (alert when below threshold)
  • Pending deposit count (spike indicates daemon sync issues)
  • Pending withdrawal count (spike indicates processing failure)
  • Daemon block height vs. network height (alert if more than 10 blocks behind)

Alerting channels: PagerDuty or Opsgenie for critical alerts (wallet, database down), Slack or Telegram for warnings (high CPU, slow queries). The admin dashboard also shows real-time system health.

Step 12: Backup and Disaster Recovery

Database Backups

#!/bin/bash
# /usr/local/bin/exchange-backup.sh
DATE=$(date +%Y%m%d_%H%M)
BACKUP_DIR=/backup/mysql

mysqldump --single-transaction --routines --triggers \
  -u backup_user -p exchange_db | gzip > $BACKUP_DIR/exchange_$DATE.sql.gz

# Retain 30 days
find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -delete

# Sync to offsite storage
aws s3 sync $BACKUP_DIR s3://exchange-backups/mysql/ --storage-class STANDARD_IA

Wallet Backups

  • Bitcoin: back up wallet.dat to encrypted offsite storage after any new address generation
  • Ethereum: back up the keystore directory
  • Store cold wallet seeds in geographically separated, fireproof safes

Recovery Targets

ComponentRPO (data loss)RTO (downtime)
Database1 hour30 minutes
Application0 (stateless, redeploy from source)10 minutes
Wallet daemons0 (resync from blockchain)2-24 hours (chain dependent)

Set up MySQL replication to a standby server for near-zero RPO. Test failover quarterly.

Step 13: Load Testing

Before opening to real users, load test every critical path:

# Install k6
apt install k6 -y

# Test login endpoint
k6 run --vus 100 --duration 60s login-test.js

# Test order placement
k6 run --vus 50 --duration 120s order-test.js

# Test WebSocket connections
k6 run --vus 500 --duration 60s ws-test.js

Baseline targets for a production exchange:

EndpointTargetAcceptable
API response (order placement)<100ms p95<200ms p99
WebSocket message latency<50ms<100ms
Page load (trading view)<2s<3s
Concurrent WebSocket connections5,000+2,000+
Orders per second (matching engine)1,000+500+

If any endpoint exceeds thresholds, profile with php-fpm-slow.log, MySQL slow query log, or EXPLAIN on hot queries before scaling hardware.

Step 14: Go-Live Checklist

Run through this checklist before opening registration:

Infrastructure:

  • All servers hardened (SSH keys only, firewall rules, fail2ban)
  • SSL certificates installed and auto-renewal verified
  • DNS propagation complete
  • DDoS protection active (Cloudflare or equivalent)
  • Backup scripts running and verified with test restore

Application:

  • Environment config set to production (debug off, error display off)
  • OPcache enabled, validate_timestamps off
  • All sensitive paths blocked in Nginx (.git, .env, runtime/)
  • Security headers configured
  • Rate limiting active on API endpoints
  • CORS configured for your domain only

Wallets:

  • All wallet daemons synced to current block height
  • Hot wallet funded with initial operating balances
  • Cold wallet addresses generated and keys secured offline
  • Deposit address generation tested for each coin
  • Withdrawal flow tested end-to-end for each coin
  • Hot wallet thresholds and alerts configured

Trading:

  • Trading pairs configured with correct price precision
  • Fee schedules set (maker/taker rates)
  • Matching engine tested with simulated orders
  • Order book depth displaying correctly
  • WebSocket feeds streaming live data

Compliance:

  • KYC provider integrated and tested
  • Verification tiers configured
  • Withdrawal limits per tier enforced
  • AML transaction monitoring active

Monitoring:

  • All dashboards live in Grafana
  • Alert channels configured and tested (send test alert)
  • On-call rotation established

Post-Launch Operations

The first 30 days after launch require close attention:

Daily tasks:

  • Review hot wallet balances across all chains
  • Check pending withdrawal queue (should clear within minutes)
  • Monitor error logs for new patterns
  • Review slow query log for optimization opportunities

Weekly tasks:

  • Verify backup integrity (test restore to staging)
  • Review security logs (failed logins, blocked IPs)
  • Update wallet daemon software if new versions are available
  • Review PHP-FPM and Nginx access patterns for capacity planning

Scaling triggers:

  • PHP-FPM max_children consistently at limit → add app server behind load balancer
  • MySQL CPU above 70% sustained → add read replicas or upgrade hardware
  • Redis memory above 80% → increase maxmemory or add cluster nodes
  • WebSocket server above 3,000 concurrent → add second WS node with Redis pub/sub

This guide covers the technical deployment. For the business side — licensing, costs, revenue models, and go-to-market strategy — read the crypto exchange business plan. For a complete end-to-end overview, see how to start a crypto exchange.

Ready to deploy? Grab the live demo to test the full stack, review pricing for license options, or contact the team for deployment support.

DevOps Deployment Server Exchange Technical

Build Your Exchange with Codono

Complete crypto exchange software with spot, futures, P2P, and 15+ blockchains.