Running a Windows MT5 Trading Bot on a Linux VPS
This is the story of a deployment that should have taken 30 minutes and took an entire evening. Here's everything that went wrong, how we fixed it, and the exact commands that finally made it work.
The Problem
MetaTrader 5 is a Windows application. The Python MetaTrader5 package only works on Windows — or under Wine on Linux. If you want to run an automated MT5 trading bot 24/7 on a cheap Linux VPS, you have to make Wine work in a headless server environment.
That's harder than it sounds.
The Stack
- Ubuntu 24.04 (Contabo VPS)
- Wine (default Ubuntu package — WineHQ Staging doesn't support Noble yet)
- Xvfb — virtual display so MT5 thinks it has a screen
- Supervisord — keeps all processes running and restarts them on crash
- systemd — keeps supervisord alive across reboots
- Dokploy + Traefik — reverse proxy with automatic SSL
- Python 3.8 (Windows, under Wine) — required by the MetaTrader5 package
- Python 3.12 (Linux) — for the FastAPI bridge
Step 1 — Why WineHQ Staging Fails on Ubuntu 24.04
The first thing everyone tries is WineHQ Staging. Don't. As of early 2026, WineHQ only publishes packages up to Ubuntu 22.04 (Jammy). On Ubuntu 24.04 (Noble), the install fails with:
winehq-staging : Depends: wine-staging (= 11.5~jammy-1)
E: Unable to correct problems, you have held broken packages.
The fix: use Ubuntu's default Wine package instead.
dpkg --add-architecture i386
apt-get update
apt-get install -y wine wine64 wine32:i386
It's not as bleeding-edge as Staging, but it runs MT5 reliably.
Step 2 — Headless Wine Needs a Virtual Display
MT5 has a GUI. Even in /auto install mode, it tries to open windows. On a headless VPS with no display, it crashes silently.
The fix is Xvfb — a virtual framebuffer that pretends to be a screen:
apt-get install -y xvfb
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
Every Wine command needs DISPLAY=:99 set, or it crashes with no useful error message.
Also disable Wine's crash dialog popup — otherwise MT5 crashes block the process forever:
wine reg add "HKCU\\Software\\Wine\\WineDbg" /v ShowCrashDialog /t REG_DWORD /d 0 /f
Step 3 — Python 3.8 Type Hints on Python 3.12 Syntax
The bot was written with modern Python 3.10+ type hint syntax:
def get_account_info() -> dict | None:
...
Wine runs Python 3.8, which doesn't support X | Y union syntax at runtime. This causes:
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'
The fix — add from __future__ import annotations to every affected file. This makes Python 3.8 treat all annotations as strings (lazy evaluation) instead of evaluating them at import time:
find /opt/regimetrader/trading-bot2 -name "*.py" \
-not -path "*/__pycache__/*" | while read f; do
if ! grep -q "from __future__ import annotations" "$f"; then
sed -i '1s/^/from __future__ import annotations\n/' "$f"
fi
done
Module-level type annotations like PAIR_MIN_ATR: dict[str, float] = {...} are different — those are runtime values, not hints. Fix those manually:
sed -i 's/PAIR_MIN_ATR: dict\[str, float\]/PAIR_MIN_ATR/' filters/market_quality.py
Step 4 — Supervisord Path Issues (Docker vs Host)
The supervisord config was written for Docker, where the app lives at /app. On the host VPS it lives at /opt/regimetrader/trading-bot2. Every log path, working directory, and command path needs updating:
[program:bot_api]
command=/opt/regimetrader/venv/bin/python -m uvicorn bot_api.main:app --host 0.0.0.0 --port 8000
directory=/opt/regimetrader/trading-bot2
stdout_logfile=/opt/regimetrader/trading-bot2/logs/api.log
Also — Wine uses Windows-style paths. The run_bot.sh script needs to pass the main.py path using the Wine Z: drive mapping (which maps to /):
exec wine "$PYEXE" "Z:\\opt\\regimetrader\\trading-bot2\\main.py"
Step 5 — Ubuntu 24.04 Blocks System pip
error: externally-managed-environment
Ubuntu 24.04 prevents installing packages into the system Python. Use a venv:
apt-get install -y python3.12-venv
python3 -m venv /opt/regimetrader/venv
/opt/regimetrader/venv/bin/pip install -r requirements.txt
Update supervisord to use the venv Python:
command=/opt/regimetrader/venv/bin/python -m uvicorn ...
Step 6 — Keeping Supervisord Alive with systemd
Supervisord kept dying whenever a managed process crashed hard. The fix is using Type=simple with the -n (nodaemon) flag so systemd directly owns the process:
[Unit]
Description=RegimeTrader MT5 Bot
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStartPre=/bin/rm -f /var/run/supervisor.sock
ExecStart=/usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Restart=always + RestartSec=5 means if supervisord ever dies, systemd brings it back within 5 seconds.
Step 7 — Exposing the Bot API Through Traefik (Dokploy)
The VPS runs Dokploy, which uses Traefik as a reverse proxy owning ports 80 and 443. You can't run a separate nginx — Traefik owns those ports.
Creating a Dokploy "Application" service doesn't work for host processes (only Docker containers). The solution is Traefik's dynamic file provider.
Find where Traefik loads dynamic configs:
docker inspect dokploy-traefik | grep -A5 "Mounts"
# Returns: /etc/dokploy/traefik/dynamic
Create a config file there:
# /etc/dokploy/traefik/dynamic/bot-api.yml
http:
routers:
bot-api:
rule: "Host(`bot.regimetrader.devsamk.com`)"
service: bot-api
entryPoints:
- websecure
tls:
certResolver: letsencrypt
services:
bot-api:
loadBalancer:
servers:
- url: "http://172.17.0.1:8000"
172.17.0.1 is the Docker host gateway — how Traefik (running in Docker) reaches services on the host machine.
Traefik picks up file changes automatically. No restart needed. SSL certificate is issued automatically by Let's Encrypt.
The Final Architecture
Browser / Dashboard (Vercel)
↓ HTTPS
bot.regimetrader.devsamk.com
↓ Traefik (Docker, port 443)
↓ 172.17.0.1:8000
FastAPI bot_api (Linux Python, supervisord)
↓ reads/writes config
Trading Bot (Wine Python, supervisord)
↓ MetaTrader5 package
MT5 Terminal (Wine + Xvfb)
↓ broker connection
Live MT5 Demo/Live Account
Step 8 — Wine Python Can't Do HTTPS (SSL Certificate Store)
The bot validates its license key by calling the dashboard API over HTTPS. Wine Python doesn't have SSL certificates configured, so every HTTPS call fails silently with "error": "unknown".
The fix — replace requests with Python's built-in urllib and disable SSL verification for the Wine environment:
import ssl, urllib.request, json
def _validate_with_server(key: str) -> dict:
try:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(
f"{DASHBOARD_URL}/api/license/validate",
data=json.dumps({"key": key}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=15, context=ctx) as resp:
return json.loads(resp.read())
except Exception as e:
return {"valid": False, "error": str(e)}
Also save the license key directly to the license file as a fallback, so the bot doesn't need to re-validate on every restart:
python3 -c "
import json, os
LICENSE_FILE = os.path.join(os.path.expanduser('~'), '.regimetrader_license')
with open(LICENSE_FILE, 'w') as f:
json.dump({'key': 'YOUR-LICENSE-KEY'}, f)
"
# Copy to Wine user home too
cp ~/.regimetrader_license "/root/.wine/drive_c/users/root/.regimetrader_license"
Step 9 — MT5 Authorization Failed
Once the license is validated, the bot tries to connect to MT5:
MT5 failed to connect after 3 attempts: (-6, 'Terminal: Authorization failed')
This just means MT5 has no broker credentials. You don't need SSH for this — go to your dashboard → Accounts page → fill in your MT5 demo account number, password, and broker server → Save. The bot API writes the credentials to config/settings.json and restarts the trading bot automatically.
No SSH needed. Everything from this point is managed from the dashboard.
- Check your Ubuntu version before installing Wine. WineHQ Staging only supports up to 22.04.
- Every Wine process needs
DISPLAY=:99. No display = silent crash. from __future__ import annotationsfixes most Python 3.8/3.10+ type hint issues — but not module-level runtime annotations.- Traefik's dynamic file provider is the cleanest way to expose host services when you're already running Dokploy.
172.17.0.1is how Docker containers reach the host. Notlocalhost, not127.0.0.1.- Use
tmuxfor long-running setup commands on a VPS. SSH drops will kill your install mid-way otherwise.
Quick Reference Commands
# Check all 4 processes are running
supervisorctl status
# Watch bot logs live
tail -f /opt/regimetrader/trading-bot2/logs/bot.log
# Restart just the trading bot
supervisorctl restart trading_bot
# Update code from local machine
rsync -avz --checksum --progress \
--exclude='.git' --exclude='__pycache__' --exclude='*.pyc' \
--exclude='.venv' --exclude='installers/*.exe' \
trading-bot2/ root@YOUR_VPS_IP:/opt/regimetrader/trading-bot2/
# Then restart on VPS
supervisorctl restart trading_bot bot_api
# Test the API is reachable
curl https://bot.regimetrader.devsamk.com/health
Built with RegimeTrader — an SMC/ICT algorithmic trading bot for MetaTrader 5.
