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.