Crypto Exchange Server Setup & Deployment: The Technical Go-Live Guide
Table of Contents
- Before You Begin
- Server Architecture Overview
- Step 1: Provision and Harden Servers
- Step 2: Install the Web Stack
- Step 3: Configure Nginx as Reverse Proxy
- Step 4: PHP-FPM Tuning
- Step 5: MySQL Database Setup and Tuning
- Step 6: Redis Cache and Queue Configuration
- Step 7: Wallet Daemon Deployment
- Step 8: WebSocket Server Setup
- Step 9: SSL/TLS and Domain Configuration
- Step 10: Cron Jobs and Background Workers
- Step 11: Monitoring and Alerting
- Step 12: Backup and Disaster Recovery
- Step 13: Load Testing
- Step 14: Go-Live Checklist
- Post-Launch Operations
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:
| Server | Role | Minimum Specs |
|---|---|---|
| App Server | Nginx + PHP-FPM + application code | 8 vCPU, 32 GB RAM, 200 GB NVMe |
| Database Server | MySQL 8.0 primary | 16 vCPU, 64 GB RAM, 500 GB NVMe RAID-10 |
| Cache/Queue Server | Redis 7.x | 4 vCPU, 16 GB RAM |
| WebSocket Server | Real-time data feeds | 4 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:
| Record | Type | Value |
|---|---|---|
| exchange.example.com | A | App server public IP |
| www.exchange.example.com | CNAME | exchange.example.com |
| api.exchange.example.com | A | App 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.datto 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
| Component | RPO (data loss) | RTO (downtime) |
|---|---|---|
| Database | 1 hour | 30 minutes |
| Application | 0 (stateless, redeploy from source) | 10 minutes |
| Wallet daemons | 0 (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:
| Endpoint | Target | Acceptable |
|---|---|---|
| API response (order placement) | <100ms p95 | <200ms p99 |
| WebSocket message latency | <50ms | <100ms |
| Page load (trading view) | <2s | <3s |
| Concurrent WebSocket connections | 5,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_childrenconsistently 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.
Exchange Business Analyst
James writes about exchange business models, go-to-market strategy, and growth tactics. He has helped hundreds of operators plan and launch profitable exchanges.