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
| minimum | recommended | why | |
|---|---|---|---|
| RAM | 2 GB | 4 GB | each .NET engine uses 300-600 MB |
| CPU | 1 core | 2 cores | cBots are event-driven: near 0% idle, short spikes at bar close |
| Disk | 20 GB | 40 GB | the Docker image is ~1 GB, the rest is logs |
| OS | Ubuntu 22.04 | Ubuntu 22.04/24.04 LTS | where the CLI is tested |
| Location | — | close to your broker | London 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 InvalidStopLossTakeProfit — 1,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:
--memorytogether 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
| mistake | what you will see | fix |
|---|---|---|
SYMBOL the broker does not have | container starts and dies in a loop | copy it from symbols |
:latest instead of a pinned version | one day stops stop being modified, or crashes | :certified |
Restart=always without StartLimitBurst | infinite silent retries | see §8 |
No --memory | one leak takes the whole server down | see §6 |
No max-size on logs | full disk within weeks | see §6 |
| Monitoring by log lines | false positives and negatives | watch usage |
Trusting systemctl is-active | five weeks idle, still "active" | watch the account |
| Expecting Telegram from cTrader Cloud | HTTP 503 | not possible — use a VPS |
PERIOD different from the certified one | a curve that differs from the published one | see §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.