Deployment

How to run cTrader cBots 24/5 on your own VPS with Docker

cTrader Cloud blocks outbound network access, so your cBot cannot send you Telegram alerts. Here is the exact setup we use in production to run five cBots continuously on a VPS.

If you run a cBot on your laptop, you stop trading every time you close the lid, the power goes out, or Windows decides to restart. This article explains how to set up a server that runs it continuously — isolated, monitored and reproducible.

It is not theory. It is the same setup we use to keep five cBots running 24/5, feeding the live statistics on our product pages. Every recommendation here comes from something that cost us an incident.

Why not cTrader Cloud, since it is free?

Fair question, and it deserves a direct answer. cTrader Cloud works well for many cases, but it has one limitation that may matter to you:

cBots executed on cTrader Cloud do not have internet access.

That means any external notification — Telegram, webhooks, your own endpoint — will not work. In our logs it looks like this:

alerts active -> chat 123456789 · budget 200/day
notification NOT delivered: HTTP 503

The configuration is correct; what fails is the outbound request. Notifications.SendEmail() does not work there either.

If you are happy checking the platform now and then, Cloud is perfect and you can stop reading. If you want the bot to message your phone when it opens, closes, or trips a risk guard, you need to run it yourself.

The three options with network access are: your own computer, cTrader CLI on your own server (what this guide covers), and a VPS running desktop cTrader over remote desktop (more expensive and more fragile).

What you need

minimumrecommendedwhy
RAM2 GB4 GBeach .NET engine uses 300-600 MB
CPU1 core2 corescBots are event-driven: near 0% idle, short spikes at bar close
Disk20 GB40 GBthe Docker image is ~1 GB, the rest is logs
OSUbuntu 22.04Ubuntu 22.04/24.04 LTSwhere the CLI is tested
Locationclose to your brokerLondon or New York, depending on the broker

That is 5 to 15 EUR/month at any provider.

You do not need historical data. Unlike a backtest, the live engine requests warm-up bars from the broker on connect.

1. Install Docker

ssh root@YOUR_IP

apt update && apt upgrade -y
apt install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
  > /etc/apt/sources.list.d/docker.list
apt update && apt install -y docker-ce docker-ce-cli containerd.io

docker run --rm hello-world

2. The image — and pin it

cTrader publishes an official CLI that runs cBots headlessly.

docker pull spotware/ctrader-console:latest
docker tag spotware/ctrader-console:latest ctrader-console:certified
docker save ctrader-console:certified | gzip > /root/ctrader-console-backup.tar.gz

This is the single most important advice in this guide: freeze the version.

Here is what happened to us. Version 5.8.3 started rejecting stop-loss modifications with InvalidStopLossTakeProfit1,090 rejections out of 2,676 attempts, with the exact same input that produced 0 out of 3,780 on 5.7.10. Version 5.9 crashes the instance deterministically. We are still pinned to 5.7.10, and that version is no longer in the public registry: had we not saved a copy, we would have lost it.

An automatic docker pull can change the engine underneath a bot that is trading your money. Use :certified, never :latest.

3. File layout

/opt/cbots/mybot/
├── bot.algo           <- the file you bought or compiled
├── params.cbotset     <- parameter preset (optional)
└── ctrader-cli.pwd    <- your cTID password, mode 600
mkdir -p /opt/cbots/mybot
install -m600 /dev/stdin /opt/cbots/mybot/ctrader-cli.pwd <<< 'YOUR_CTID_PASSWORD'

Keep exactly one copy of the password. We once had the same password duplicated across five directories. Every copy is one more place it can leak from, and one more to update when it changes.

4. Per-bot configuration

/opt/cbots/mybot/run.env:

[email protected]
BROKER=Skilling            # the EXACT broker name in cTrader
ACCOUNT=1234567

SYMBOL=GOLD                # see the warning below
PERIOD=m1

MEMORY=1g
CPUSET=1

The SYMBOL is your broker's literal name, not the one the cBot uses

This is the mistake that kills most deployments. The host chart is resolved by the platform, not by the cBot: if you name a symbol your broker does not have, the container will not start — and with automatic restart, you get an infinite loop.

On Skilling, gold is GOLD, not XAUUSD. The DAX is Germany 40, with a space. On IC Markets it is DE40, and crude oil is XTIUSD while Skilling calls it OIL WTI. Look it up in your platform's symbol search and copy it verbatim.

We had four out of five configurations pointing at XAUUSD on a broker that does not list it. It was caught in review, before launch — which is exactly what the next step is for.

PERIOD must match the certified backtest

If the result you bought was measured on m1, use m1. Changing it alters how often the bot evaluates and it stops reproducing the published curve.

5. Pre-flight: validate before you start

The CLI ships two commands that take seconds and save hours:

docker run --rm -v /opt/cbots/mybot:/mnt ctrader-console:certified \
  accounts --ctid [email protected] --password-file /mnt/ctrader-cli.pwd

docker run --rm -v /opt/cbots/mybot:/mnt ctrader-console:certified \
  symbols --ctid [email protected] --password-file /mnt/ctrader-cli.pwd \
          --broker Skilling --account 1234567

The first confirms your credentials work. The second gives you your broker's exact symbol list — copy SYMBOL from there, and check that the markets your cBot trades actually exist.

A bot started against a misconfigured account does not fail with a clear error. It loops.

6. The launcher

/opt/cbots/run_cbot.sh:

#!/usr/bin/env bash
set -euo pipefail
BOT="$1"
DIR="/opt/cbots/$BOT"
set -a; source "$DIR/run.env"; set +a

docker rm -f "cbot_$BOT" 2>/dev/null || true

exec docker run --rm --name "cbot_$BOT" \
  -v "$DIR:/mnt/Robots:ro" \
  --memory="${MEMORY:-1g}" --memory-swap="${MEMORY:-1g}" \
  --cpuset-cpus="${CPUSET:-0}" --cpu-shares=128 \
  --log-driver=json-file --log-opt max-size=50m --log-opt max-file=3 \
  ctrader-console:certified \
  run /mnt/Robots/bot.algo \
      --ctid "$CTID" --password-file /mnt/Robots/ctrader-cli.pwd \
      --broker "$BROKER" --account "$ACCOUNT" \
      --symbol "$SYMBOL" --period "$PERIOD" \
      --exit-on-stop

Details that matter:

  • --memory together with --memory-swap — without it, a leaking container eats the swap and takes the whole server down.
  • --cpu-shares=128 (default is 1024) — low weight, so the bot yields under contention instead of choking the machine.
  • max-size=50m — without a log cap, Docker fills your disk in weeks.
  • :ro — the container never needs to write to your files.

7. Pilot run: measure, do not estimate

Before making it a service, run it by hand:

free -h
bash /opt/cbots/run_cbot.sh mybot

In another SSH session:

docker stats --no-stream cbot_mybot
free -h

What a good pilot looks like:

  • Connects without authentication errors.
  • Every market the cBot trades resolves. Check the symbol-resolution lines in the log. If one says the symbol was not found, that strategy is disabled and will not trade — while the bot keeps running with the rest, producing a result that is not the one you bought.
  • Container RAM stable between 300 and 600 MB.
  • CPU near zero when idle.

Zero trades in the first few hours is not a failure. A portfolio cBot on 1h-to-8h timeframes may take fewer than two trades a day. To know whether it is alive, watch the container's resource usage — not the log.

8. Make it a service

/etc/systemd/system/[email protected]:

[Unit]
Description=cTrader cBot %i
After=docker.service
Requires=docker.service

[Service]
Type=simple
ExecStart=/opt/cbots/run_cbot.sh %i
ExecStop=/usr/bin/docker stop -t 30 cbot_%i
TimeoutStopSec=45
Restart=always
RestartSec=30
StartLimitBurst=5
StartLimitIntervalSec=600

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now cbot@mybot
journalctl -u cbot@mybot -f

StartLimitBurst=5 is not decoration. With a bare Restart=always, a misconfigured account retries forever in silence. With the limit, after 5 attempts in 10 minutes the service enters failed — and a failed service is something you can monitor.

One template serves all your bots: cbot@gold, cbot@nasdaq. All the difference lives in each run.env, so there are no five files drifting apart.

9. Monitoring: the part that will actually save you

Here is the most expensive lesson we learned. We had a bot with OnFailure configured to alert us if the service went down. It went five weeks without trading and never alerted, because the service never went down: its token had expired, so it started, failed to authenticate, retried — and systemd saw it as "active".

An "active" service is not a trading bot. And a silent monitor reads as health.

Watch two different things:

a) That the process is alive — by resource usage, not logs

#!/usr/bin/env bash
# /opt/cbots/watchdog.sh — every 20 minutes via cron
for BOT in mybot; do
  if ! docker ps --format '{{.Names}}' | grep -q "^cbot_$BOT$"; then
    curl -s "https://api.telegram.org/bot$TG_TOKEN/sendMessage" \
      -d chat_id="$TG_CHAT" -d text="cbot_$BOT is NOT running"
    continue
  fi
  docker stats --no-stream --format '{{.Name}} {{.NetIO}}' "cbot_$BOT" \
    >> /opt/cbots/logs/heartbeat.log
done

Compare received bytes between readings: if they stop growing, the bot is not talking to the broker even though the container exists. Counting log lines does not work — a healthy bot can go hours without writing anything.

b) That the account is moving — from outside

Ideally a second process queries your account through the broker's API (read-only) and sends you a daily report: balance, open positions, trades taken. That is what catches the dangerous case: the bot runs, but does not trade.

10. Common mistakes

mistakewhat you will seefix
SYMBOL the broker does not havecontainer starts and dies in a loopcopy it from symbols
:latest instead of a pinned versionone day stops stop being modified, or crashes:certified
Restart=always without StartLimitBurstinfinite silent retriessee §8
No --memoryone leak takes the whole server downsee §6
No max-size on logsfull disk within weekssee §6
Monitoring by log linesfalse positives and negativeswatch usage
Trusting systemctl is-activefive weeks idle, still "active"watch the account
Expecting Telegram from cTrader CloudHTTP 503not possible — use a VPS
PERIOD different from the certified onea curve that differs from the published onesee §4

11. Running several cBots

One container per cBot, always. Bundling them is tempting, but:

  • If one fails, they all go down.
  • You cannot restart one without touching the others.
  • You cannot cap memory per bot.
  • Logs mix and stop being diagnosable.

With one container each, systemctl restart cbot@gold does not disturb cbot@nasdaq. We run five that way, in about 860 MB total.

Split cores with CPUSET in each run.env, and start them one at a time, leaving the first running stable for 24 hours before adding the second.

Summary

1. Docker                    apt install docker-ce
2. PINNED image              docker tag ...:latest ctrader-console:certified
3. Files                     bot.algo + run.env + ctrader-cli.pwd (600)
4. Validate FIRST            accounts + symbols
5. Foreground pilot          measure real RAM/CPU
6. systemd                   cbot@mybot with StartLimitBurst
7. Watch TWO things          process alive + account moving

That gives you cBots running continuously, isolated from each other, with a frozen engine version, resource limits, and alerts that reach your phone — which is exactly what cTrader Cloud cannot give you.

Based on the production setup behind the live statistics on our product pages: five cBots, five accounts, running 24/5 since July 2026.

Published Aug 19, 2026 · realbacktesting · Educational content and market commentary — not financial advice. Trading involves risk; past performance does not guarantee future results.