Back to Blog
April 2, 20265 min readBy Samuel Kaweesi

Running a Windows MT5 Trading Bot on a Linux VPS — The Hard Way (That Actually Works)

A real-world guide to deploying a MetaTrader 5 Python trading bot on Ubuntu 24.04 using Wine, Xvfb, Supervisord, and Traefik — without a single Windows machine.

Running MT5 Trading Bot on Ubuntu Linux VPS with Wine and Traefik

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.


  1. Check your Ubuntu version before installing Wine. WineHQ Staging only supports up to 22.04.
  2. Every Wine process needs DISPLAY=:99. No display = silent crash.
  3. from __future__ import annotations fixes most Python 3.8/3.10+ type hint issues — but not module-level runtime annotations.
  4. Traefik's dynamic file provider is the cleanest way to expose host services when you're already running Dokploy.
  5. 172.17.0.1 is how Docker containers reach the host. Not localhost, not 127.0.0.1.
  6. Use tmux for 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.

Ready to automate your MT5 trading?

RegimeTrader runs 24/5 on Smart Money Concepts — no coding required. Start your free trial today.