- Fixed INWX API authentication method (per-request, not session-based) - Deployed DNS failover monitor on Hostinger secondary - Service active and monitoring primary every 30s - Will auto-failover after 3 consecutive health check failures - Updated documentation with correct API usage pattern Key Discovery: INWX API uses per-request authentication (pass user/pass with every call), NOT session-based login (account.login). This resolves all error 2002 issues. Source: 2013 Bash-INWX-DynDNS script revealed correct authentication pattern. Files changed: - DNS failover monitor: /usr/local/bin/dns-failover-monitor.py - Systemd service: /etc/systemd/system/dns-failover.service - Setup script: /root/setup-inwx-direct.sh - Documentation: docs/DEPLOY_SECONDARY_MANUAL.md
11 KiB
Manual Deployment to Secondary Server (Hostinger VPS)
Status: COMPLETED ✅
Last Updated: November 25, 2025
Deployed Components
- ✅ PostgreSQL streaming replication (port 55432, async mode)
- ✅ Trading bot container with all dependencies
- ✅ nginx reverse proxy with HTTPS and HTTP Basic Auth
- ✅ Certificate synchronization (hourly from srvrevproxy02)
- ✅ DNS failover monitor (active and monitoring)
- Service running: systemctl status dns-failover
- INWX API working with per-request authentication
- DNS record: flow.egonetix.de → 95.216.52.28 (primary)
- Will auto-failover to 72.62.39.24 after 3 health check failures
Active Services
- PostgreSQL: Streaming from primary (95.216.52.28:55432)
- Trading Bot: Running on port 3001
- nginx: HTTPS with flow.egonetix.de certificate
- Certificate Sync: Hourly cron on srvrevproxy02
- Failover Monitor: ✅ ACTIVE - Running and monitoring primary health every 30s
Quick Start - Deploy Secondary Now
Step 1: Complete the Code Sync (if not finished)
# Wait for rsync to complete or run it manually
rsync -avz --delete \
--exclude 'node_modules' \
--exclude '.next' \
--exclude '.git' \
--exclude 'logs/*' \
--exclude 'postgres-data' \
/home/icke/traderv4/ root@72.62.39.24:/home/icke/traderv4/
Step 2: Backup and Sync Database
# Dump database from primary
docker exec trading-bot-postgres pg_dump -U postgres trading_bot_v4 > /tmp/trading_bot_backup.sql
# Copy to secondary
scp /tmp/trading_bot_backup.sql root@72.62.39.24:/tmp/trading_bot_backup.sql
Step 3: Deploy on Secondary
# SSH to secondary
ssh root@72.62.39.24
cd /home/icke/traderv4
# Start PostgreSQL
docker compose up -d postgres
# Wait for PostgreSQL to be ready
sleep 10
# Restore database
docker exec -i trading-bot-postgres psql -U postgres -c "DROP DATABASE IF EXISTS trading_bot_v4; CREATE DATABASE trading_bot_v4;"
docker exec -i trading-bot-postgres psql -U postgres trading_bot_v4 < /tmp/trading_bot_backup.sql
# Verify database
docker exec trading-bot-postgres psql -U postgres trading_bot_v4 -c "SELECT COUNT(*) FROM \"Trade\";"
# Build trading bot
docker compose build trading-bot
# Start trading bot (but keep it inactive - secondary waits in standby)
docker compose up -d trading-bot
# Check logs
docker logs -f trading-bot-v4
Step 4: Verify Everything Works
# Check all containers running
docker ps
# Should see:
# - trading-bot-v4 (your bot)
# - trading-bot-postgres
# - n8n (already running)
# Test health endpoint
curl http://localhost:3001/api/health
# Check database connection
docker exec trading-bot-postgres psql -U postgres -c "\l"
Ongoing Sync Strategy
Option A: PostgreSQL Streaming Replication (Best)
Setup once, sync forever in real-time (1-2 second lag)
See HA_DATABASE_SYNC_STRATEGY.md for complete setup guide.
Quick version:
# On PRIMARY
docker exec trading-bot-postgres psql -U postgres -c "
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'ReplPass2024!';
"
docker exec trading-bot-postgres bash -c "cat >> /var/lib/postgresql/data/postgresql.conf << CONF
wal_level = replica
max_wal_senders = 3
wal_keep_size = 64
CONF"
docker exec trading-bot-postgres bash -c "echo 'host replication replicator 72.62.39.24/32 md5' >> /var/lib/postgresql/data/pg_hba.conf"
docker restart trading-bot-postgres
# On SECONDARY
docker compose down postgres
rm -rf postgres-data/
mkdir -p postgres-data
docker run --rm \
-v $(pwd)/postgres-data:/var/lib/postgresql/data \
-e PGPASSWORD='ReplPass2024!' \
postgres:16-alpine \
pg_basebackup -h <hetzner-ip> -p 5432 -U replicator -D /var/lib/postgresql/data -P -R
docker compose up -d postgres
# Verify
docker exec trading-bot-postgres psql -U postgres -c "SELECT * FROM pg_stat_wal_receiver;"
Option B: Cron Job Backup (Simple but 6hr lag)
# On PRIMARY - Create sync script
cat > /root/sync-to-secondary.sh << 'SCRIPT'
#!/bin/bash
LOG="/var/log/secondary-sync.log"
echo "[$(date)] Starting sync..." >> $LOG
# Sync code
rsync -avz --delete \
--exclude 'node_modules' --exclude '.next' --exclude '.git' \
/home/icke/traderv4/ root@72.62.39.24:/home/icke/traderv4/ >> $LOG 2>&1
# Sync database
docker exec trading-bot-postgres pg_dump -U postgres trading_bot_v4 | \
ssh root@72.62.39.24 "docker exec -i trading-bot-postgres psql -U postgres -c 'DROP DATABASE IF EXISTS trading_bot_v4; CREATE DATABASE trading_bot_v4;' && docker exec -i trading-bot-postgres psql -U postgres trading_bot_v4" >> $LOG 2>&1
echo "[$(date)] Sync complete" >> $LOG
SCRIPT
chmod +x /root/sync-to-secondary.sh
# Test it
/root/sync-to-secondary.sh
# Schedule every 6 hours
crontab -e
# Add: 0 */6 * * * /root/sync-to-secondary.sh
Health Monitor Setup
Create health monitor to automatically switch DNS on failure:
# Create health monitor script (run on laptop or third server)
cat > ~/trading-bot-monitor.py << 'SCRIPT'
#!/usr/bin/env python3
import requests
import time
import os
CLOUDFLARE_API_TOKEN = "your-token"
CLOUDFLARE_ZONE_ID = "your-zone-id"
CLOUDFLARE_RECORD_ID = "your-record-id"
PRIMARY_IP = "hetzner-ip"
SECONDARY_IP = "72.62.39.24"
PRIMARY_URL = f"http://{PRIMARY_IP}:3001/api/health"
SECONDARY_URL = f"http://{SECONDARY_IP}:3001/api/health"
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
current_active = "primary"
def send_telegram(message):
try:
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
requests.post(url, json={"chat_id": TELEGRAM_CHAT_ID, "text": message}, timeout=10)
except:
pass
def check_health(url):
try:
response = requests.get(url, timeout=10)
return response.status_code == 200
except:
return False
def update_cloudflare_dns(ip):
url = f"https://api.cloudflare.com/client/v4/zones/{CLOUDFLARE_ZONE_ID}/dns_records/{CLOUDFLARE_RECORD_ID}"
headers = {"Authorization": f"Bearer {CLOUDFLARE_API_TOKEN}", "Content-Type": "application/json"}
data = {"type": "A", "name": "flow.egonetix.de", "content": ip, "ttl": 120, "proxied": False}
response = requests.put(url, json=data, headers=headers, timeout=10)
return response.status_code == 200
print("Health monitor started")
send_telegram("🏥 Trading Bot Health Monitor Started")
while True:
primary_healthy = check_health(PRIMARY_URL)
secondary_healthy = check_health(SECONDARY_URL)
print(f"Primary: {'✅' if primary_healthy else '❌'} | Secondary: {'✅' if secondary_healthy else '❌'}")
if current_active == "primary" and not primary_healthy and secondary_healthy:
print("FAILOVER: Switching to secondary")
if update_cloudflare_dns(SECONDARY_IP):
current_active = "secondary"
send_telegram(f"🚨 FAILOVER: Primary DOWN, switched to Secondary ({SECONDARY_IP})")
elif current_active == "secondary" and primary_healthy:
print("RECOVERY: Switching back to primary")
if update_cloudflare_dns(PRIMARY_IP):
current_active = "primary"
send_telegram(f"✅ RECOVERY: Primary restored ({PRIMARY_IP})")
time.sleep(30)
SCRIPT
chmod +x ~/trading-bot-monitor.py
# Run in background
nohup python3 ~/trading-bot-monitor.py > ~/monitor.log 2>&1 &
Verification Checklist
- Secondary server has all code from primary
- Secondary has same .env file (same wallet key!)
- PostgreSQL running on secondary
- Database streaming replication active (229 trades synced)
- Trading bot built successfully
- Trading bot starts without errors
- Health endpoint responds on secondary
- n8n running on secondary (already was)
- Sync strategy chosen and configured (streaming replication)
- nginx reverse proxy with HTTPS and Basic Auth
- Certificate sync from srvrevproxy02 (hourly)
- DNS failover monitor configured and active
- Test failover scenario completed
Certificate Synchronization (ACTIVE)
Status: ✅ Operational - Hourly sync from srvrevproxy02 to Hostinger
# Location on srvrevproxy02
/usr/local/bin/cert-push-to-hostinger.sh
# Cron job
0 * * * * root /usr/local/bin/cert-push-to-hostinger.sh
# View sync logs
ssh root@srvrevproxy02 'tail -f /var/log/cert-push-hostinger.log'
# Manual sync test
ssh root@srvrevproxy02 '/usr/local/bin/cert-push-to-hostinger.sh'
What syncs:
- Source:
/etc/letsencrypt/on srvrevproxy02 (all Let's Encrypt certificates) - Target:
/home/icke/traderv4/nginx/ssl/on Hostinger - Method: rsync with SSH key authentication
- Includes: flow.egonetix.de + all other domain certificates
- Auto-reload: nginx on Hostinger reloads after sync
DNS Failover Monitor (READY TO ACTIVATE)
Status: ✅ ACTIVE - Service running, monitoring primary health every 30s
Key Discovery: INWX API uses per-request authentication (pass user/pass with every call), NOT session-based login. This resolves all error 2002 issues.
# SSH to Hostinger
ssh root@72.62.39.24
# Run setup script with INWX credentials
bash /root/setup-inwx-direct.sh Tomson lJJKQqKFT4rMaye9
# Start monitoring service
systemctl start dns-failover
# Check status
systemctl status dns-failover
# View logs
tail -f /var/log/dns-failover.log
CRITICAL: INWX API Authentication
INWX uses per-request authentication (NOT session-based):
- ❌ WRONG: Call
account.login()first, then use session → This gives error 2002 - ✅ CORRECT: Pass
userandpasswith every API call
Example from the working monitor script:
api = ServerProxy("https://api.domrobot.com/xmlrpc/")
# Pass user/pass directly with each call (no login session needed)
result = api.nameserver.info({
'user': username,
'pass': password,
'domain': 'egonetix.de',
'name': 'flow',
'type': 'A'
})
How it works:
- Monitors primary server health every 30 seconds
- 3 consecutive failures (90s) triggers automatic failover
- Updates DNS via INWX API: flow.egonetix.de → 72.62.39.24
- Deploys dual-domain nginx config
- Automatic recovery when primary returns online
Configuration:
- Script:
/usr/local/bin/dns-failover-monitor.py - Service:
/etc/systemd/system/dns-failover.service - State:
/var/lib/dns-failover-state.json - Logs:
/var/log/dns-failover.log
Test Failover
# Option 1: Automatic (if dns-failover running)
# Stop primary reverse proxy
ssh root@srvrevproxy02 "systemctl stop nginx"
# Monitor will detect failure in ~90s and switch DNS automatically
# Option 2: Manual
# 1. Update INWX DNS: flow.egonetix.de → 72.62.39.24
# 2. Wait for DNS propagation (5-10 minutes)
# 3. Deploy nginx config on Hostinger
ssh root@72.62.39.24 '/home/icke/traderv4/deploy-flow-domain.sh'
# 4. Test endpoints
curl -u admin:TradingBot2025Secure https://flow.egonetix.de/api/health
# 5. Restart primary
ssh root@srvrevproxy02 "systemctl start nginx"
ssh root@hetzner-ip "cd /home/icke/traderv4 && docker compose start trading-bot"
Summary
Your secondary server is now a full replica:
- ✅ Same code as primary
- ✅ Same database (snapshot)
- ✅ Same configuration (.env)
- ✅ Ready to take over if primary fails
Choose sync strategy:
- 🔄 PostgreSQL Streaming Replication - Real-time, 1-2s lag (BEST)
- ⏰ Cron Job - Simple, 6-hour lag (OK for testing)
Enable automated failover:
- 🤖 Run health monitor script (switches DNS automatically)
- 📱 Gets Telegram alerts on failover/recovery
- ⚡ 30-60 second failover time