# Trading bot hosting: VPS, Docker, restarts and latency

URL: https://fincodelabs.com/en/blog/trading-bot-hosting · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Backtesting and risk · Published: 2026-11-06 · Updated: 2026-11-06

Trading bot hosting without a sales pitch: when a VPS is worth it, what a ping measures, tested Docker files, graceful shutdown and restarts that reconcile.

**Short answer:** **Trading bot hosting** means a server that keeps a bot running, restarts it safely, keeps its clock accurate and alerts a person when something breaks. A VPS is worth paying for once the bot must run while your machine is off. A lower ping alone is a weak reason: on 5 November 2026, one venue's two API hosts differed by a factor of 28 in TCP connect time but by only 12% in request time.

No host is named or ranked here, and there are no affiliate links. The Docker files and the shutdown and restart code below ran on 5 November 2026 on Docker 29.4.0 (Docker Desktop for Windows, shared with other containers) against a local mock exchange, so no key or real order was involved. A Linux server was not tested.

Server location does not change where you are or which rules apply. Rules depend on your jurisdiction and each platform's terms. Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized. Automated trading can lose money.

## Why do you need a VPS for trading, and when not?

You need a server when the bot must keep working while your machine sleeps, reboots or goes offline, and not before.

**What each situation asks of a server**

| Situation | Your own machine? | What matters |
| --- | --- | --- |
| Building and testing on a [testnet](https://fincodelabs.com/en/blog/paper-trading-api) | Fine | Logs and clean restarts |
| Rules on hourly or daily candles, orders resting on the exchange | Your call: what can an outage cost? | Restart, reconcile, alerts; a stopped bot manages nothing |
| Receiving webhooks from another service | Rarely | A stable public HTTPS address |
| API key restricted by IP | Rarely | A stable outbound address; see [API key security](https://fincodelabs.com/en/blog/api-key-security-trading-bots) |
| Reacting within milliseconds | No, and a VPS may not help | Measure first; a public VPS is not colocation |

Size it by measuring with `docker stats`: the drill bot and the mock exchange here used about 15 MiB each; a bot holding full order books needs far more. Choose a location for reliability and measured latency, never for access. A VPS is not a way round a geographic block, KYC or sanctions: if an exchange returns 403 or 451 from a location, the service is not available there, so stop.

## Latency trading bot checks: what a ping really measures

A ping or TCP connect to a public exchange hostname often measures only the distance to the nearest CDN edge, so measure the request itself before paying for a location.

Setup: one desktop PC in Turkey, 5 November 2026 from 19:45 UTC; per host, 50 new TCP connections, then 50 requests on one reused HTTPS connection; public time endpoints, no account. Not a server test or a venue comparison; hosts are alphabetical.

**Medians in milliseconds, 95th percentile in brackets; edge from response headers, addresses checked against AWS's published IP ranges**

| Host | Edge seen | TCP connect | Request, reused connection |
| --- | --- | ---: | ---: |
| `api.binance.com` | CloudFront, Istanbul | 10.4 (20.5) | 255.4 (266.3) |
| `api.bybit.com` | CloudFront, Istanbul | 9.9 (22.9) | 190.3 (200.6) |
| `api.kraken.com` | Cloudflare, Amsterdam | 47.9 (54.2) | 75.0 (91.2) |
| `data-api.binance.vision` | None seen; AWS Tokyo addresses | 289.2 (302.3) | 285.6 (289.1) |
| `openapi.okx.com` | Cloudflare, Istanbul | 46.2 (54.0) | 293.4 (354.3) |

The two Binance hosts show the trap: TCP connect suggests one is 28 times closer, yet requests differ by 12%. `X-Cache: Miss from cloudfront` shows the edge forwarding the call, and Binance documents the second host for public market data only. Repeat this from any candidate server:

```bash
curl -s -o /dev/null -o /dev/null -w 'connect=%{time_connect} first_byte=%{time_starttransfer}\n' \
  https://testnet.binance.vision/api/v3/time https://testnet.binance.vision/api/v3/time
curl -s -D - -o /dev/null https://testnet.binance.vision/api/v3/time | grep -i -E 'x-amz-cf-pop|x-cache'
```

The second line shows the reused connection: `connect=0.000000`. Neither number is order acknowledgment time: an order also passes authentication and matching, which needs a real key and was not run.

## Crypto trading bot Docker setup: Dockerfile and compose file

Run as a non-root user on a read-only filesystem, keep state on a named volume, and pass the key as a mounted file so that it never enters the image.

```
FROM python:3.12-slim
RUN useradd --system --uid 10001 --no-create-home --shell /usr/sbin/nologin bot \
    && mkdir /data && chown bot /data
WORKDIR /app
COPY bot.py rules.py ./
USER bot
ENV PYTHONUNBUFFERED=1
HEALTHCHECK --interval=15s --timeout=3s --start-period=20s --retries=3 \
  CMD ["python", "-c", "import urllib.request as u; u.urlopen('http://127.0.0.1:8080/', timeout=2)"]
CMD ["python", "bot.py"]
```

```
services:
  bot:
    build: .
    restart: on-failure:5
    stop_grace_period: 30s
    read_only: true
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
    environment:
      EXCHANGE_URL: http://exchange:9000     # a testnet URL; live only by explicit change
      API_KEY_FILE: /run/secrets/exchange_api_key
      ON_STOP: leave                         # or cancel: your written decision
    secrets: [exchange_api_key]
    volumes: ["bot-data:/data"]
    logging:
      driver: json-file
      options: {max-size: "10m", max-file: "5"}
    depends_on: [exchange]
  exchange:                                  # local mock for the test; remove for real use
    image: python:3.12-slim
    command: ["python", "/m/mock_exchange.py"]
    volumes: ["./mock_exchange.py:/m/mock_exchange.py:ro"]
secrets:
  exchange_api_key:
    file: ./secrets/exchange_api_key.txt     # on the host, outside git
volumes:
  bot-data:
```

In the running container: `id` returned uid 10001, writing to `/app` failed, and the mock key appeared 0 times in `env`, `docker inspect`, `docker history` and the logs. Docker's Compose documentation warns that API keys in environment variables risk exposure, and OWASP's Secrets Management Cheat Sheet advises against them unless other methods are impossible. Compose bind-mounts the secret file, which showed as root-owned mode 777 on this host, so check its owner and mode on a Linux server. List `secrets/` and `.env` in `.dockerignore`. A secrets manager is stronger still, as the [API key guide](https://fincodelabs.com/en/blog/api-key-security-trading-bots) covers.

## Graceful shutdown and a restart that reconciles first

On SIGTERM stop sending new orders and exit cleanly; on every start, compare the exchange with your journal before sending anything.

Excerpt of `bot.py`, standard library only. Not shown: `main()` runs `reconcile()` before the loop and, on a problem, logs `reconcile_break` with an alert and waits for SIGTERM without trading; `watchdog()` exits with code 70 when the loop stalls for 30 seconds, because an `unhealthy` status restarts nothing; `api()` has a 10-second timeout and raises on HTTP errors; `lookup()` returns an order's status, or `never_arrived` on the mock's 404 (check your venue's code); `record()` appends a JSON line with fsync; `journal()` replays the file into the latest state per ID; `cancel()` returns the new status, or alerts and returns None if the venue refuses.

```python
UNSETTLED = {"sending", "canceling", "NEW", "PARTIALLY_FILLED"}  # as your venue spells them

def send(order):
    cid = uuid.uuid4().hex
    record({"cid": cid, "state": "sending"})  # written before the request leaves
    status = api("POST", "/order", {**order, "cid": cid}).get("status")
    if not status:                            # odd reply: leave "sending" for reconcile
        raise RuntimeError(f"no status for {cid}")
    record({"cid": cid, "state": status})
    log("order_sent", cid=cid, status=status)

def reconcile():                              # returns a problem, or None
    known = journal()
    live = api("GET", "/open-orders")
    strangers = [o.get("cid") or "(no client ID)" for o in live
                 if known.get(o.get("cid"), {}).get("state") not in UNSETTLED]
    if strangers:                             # a list, so two missing IDs count twice
        return f"open orders the journal does not expect: {strangers}"
    for cid, entry in known.items():
        if entry["state"] in UNSETTLED:       # no reply yet, or open when the bot stopped
            status = lookup(cid)
            if not status:
                return f"no status for {cid}"
            if status == entry["state"]:
                continue
            record({"cid": cid, "state": status})
            log("reconcile_resolved", cid=cid, status=status)
    return None

def shutdown():
    state.update(ready=False, reason="stopping")
    on_stop = os.environ.get("ON_STOP", "leave")
    log("shutdown_begin", on_stop=on_stop)
    if on_stop == "cancel":
        mine = journal()
        try:
            for order in api("GET", "/open-orders"):
                cid = order.get("cid")
                if cid in mine:               # never cancel an order this bot did not place
                    record({"cid": cid, "state": "canceling"})
                    record({"cid": cid, "state": cancel(cid) or "canceling"})
        except Exception as err:              # network, JSON or odd replies
            log("cancel_incomplete", error=str(err), alert=True)
    log("shutdown_done")
```

The journal entry is written before the request leaves, so a crash at the worst moment leaves a record to look up by client order ID. On a real venue, an empty lookup after a timeout is not final: Binance's request security page says a timeout reply does not always mean the request failed and says to query its status, so keep checking by client order ID. Whether a stop cancels open orders is your written decision, because canceling a resting exit during a deploy leaves a position unprotected; either way, the bot cancels only orders in its own journal. Positions and balances need the same check, as [position reconciliation](https://fincodelabs.com/en/blog/position-reconciliation) explains; other failure modes are in [trading bot development](https://fincodelabs.com/en/blog/trading-bot-development).

## Docker restart policy: what the drills showed

Docker restarts a process that exits with an error, and little else, so the bot has to turn a hang into an exit.

**Drills against the mock exchange, 5 November 2026, with the code above**

| Drill | Result |
| --- | --- |
| `docker stop`, 11 times | SIGTERM reached Python as PID 1: 0.9 to 2.9 seconds, exit code 0, both shutdown lines logged; with `ON_STOP: cancel` it canceled only its own orders. Under `sh -c "python ..."` the shell stayed PID 1 and Python never got the signal: 11 seconds at a 10-second timeout, then exit code 137 |
| Reply delayed past the client's 10-second timeout | `TimeoutError`, exit code 1, one restart, then `reconcile_resolved` by client order ID. 16 order requests, 16 unique IDs: no duplicate |
| `docker kill` during a delayed reply | Exit code 137, no restart, as for a manual stop; a manual start resolved it the same way (18 requests, 18 unique IDs) |
| Loop hung by a test hook | `stalled_exit` after about 36 seconds, one restart, orders resumed |
| Unexpected open orders at start: one placed by hand, two without a client ID | `reconcile_break` with an alert listing all three; no new order in 80 seconds; health `unhealthy`, no restart |
| Exchange unreachable at start | Exit code 1, five restarts, then stopped for good, all within about 36 seconds |

From the documents: a restart first waits 100 ms and the wait doubles up to a minute, so `on-failure:5` can run out within a short outage, as it did here in 36 seconds; alert on it. `on-failure` does not restart after the daemon restarts, while `always` and `unless-stopped` differ on containers you stopped by hand, so a reboot needs your decision; neither was tested, as other containers shared this host. Restart loops also spend connection limits, such as Binance's 300 WebSocket connection attempts per 5 minutes per IP.

## Clock sync for signed requests

Sync the host clock with NTP and alert on drift, because exchanges reject signed requests whose timestamp falls outside a narrow window.

Binance's documentation, last modified 30 October 2026, accepts a signed request only if its timestamp is less than 1 second ahead of server time and no more than `recvWindow` behind it (default 5,000 ms, maximum 60,000); Bybit's rule has the same shape and default. Containers read the host's clock, so sync on the host: chrony's `makestep` steps the clock at startup, and slewing corrects it afterwards, and `chronyc tracking` reports the offset as System time. A time endpoint is only a rough check: at 19:53 UTC on 5 November 2026 this PC read about 1,045 ms behind the Spot Testnet with a 255 ms round trip, so the true gap was 915 to 1,170 ms: accepted, but worth an alert.

> **Hypothetical example.** With the default window, a clock 6 seconds behind is rejected, and raising `recvWindow` to 10,000 ms hides the fault. A clock 1.5 seconds ahead is rejected at any window, so alert on drift well under 1 second.

## Monitoring that reaches a person

A health endpoint on a dead server tells nobody anything, so the bot must check in with a monitor on another machine that alarms when it goes quiet.

- **Log alerts.** Route lines carrying `alert: true`, such as a reconcile break, to a channel someone reads.
- **Restart count.** Alert on a rising `RestartCount`, since a quiet restart loop looks healthy.
- **Drill it.** Stop the bot on purpose and time how long a person takes to hear.

Who gets the alert and what they do are in [trading bot risk management](https://fincodelabs.com/en/blog/trading-bot-risk-management). By default, software commissioned from Fincode runs on your own server or cloud account with your own keys, as the [algorithmic trading](https://fincodelabs.com/en/services/algorithmic-trading) page says; naming exchanges here does not mean Fincode integrates them or has built on them. Tests show how software behaves, not future results.

## Frequently asked questions

### Do I need a VPS to run a trading bot?

Not to build or test one. You need one when the bot must run while your machine is off, or when webhooks or IP-restricted keys need a stable address.

### Will a VPS closer to the exchange make my bot faster?

Measure it. Many API hostnames sit behind CDNs, so a ping may only reach the nearest edge. Time real requests from the candidate server.

### Should I run a trading bot in Docker?

Docker gives repeatable builds, restart policies and isolation, but a restart must still reconcile orders before trading.

### Can a VPS in another country get around an exchange block?

No. Server location does not change where you are or which rules apply. If an exchange returns 403 or 451, the service is not available there, so stop.

## Sources

- [Docker documentation: Start containers automatically (restart policies)](https://docs.docker.com/engine/containers/start-containers-automatically/)
- [Docker documentation: docker container run (restart delay)](https://docs.docker.com/reference/cli/docker/container/run/)
- [Docker documentation: Dockerfile reference (HEALTHCHECK, ENTRYPOINT, USER)](https://docs.docker.com/reference/dockerfile/)
- [Docker documentation: docker container stop](https://docs.docker.com/reference/cli/docker/container/stop/)
- [Docker documentation: Secrets in Compose](https://docs.docker.com/compose/how-tos/use-secrets/)
- [OWASP: Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
- [chrony documentation: chrony.conf (makestep)](https://chrony-project.org/doc/4.6/chrony.conf.html)
- [chrony documentation: chronyc (tracking)](https://chrony-project.org/doc/4.6/chronyc.html)
- [Binance Spot API documentation: Request security (timing, recvWindow)](https://developers.binance.com/docs/binance-spot-api-docs/rest-api/request-security)
- [Bybit V5 API: Integration guidance (timestamp and recv_window)](https://bybit-exchange.github.io/docs/v5/guide)
- [Binance Spot API documentation: WebSocket Streams (connection limits)](https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md)
- [Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)
- [Binance Spot API documentation: General API information (base endpoints)](https://developers.binance.com/docs/binance-spot-api-docs/rest-api/general-api-information)
- [OKX API v5 documentation (production REST host)](https://www.okx.com/docs-v5/en/)
- [AWS: published IP address ranges (ip-ranges.json)](https://ip-ranges.amazonaws.com/ip-ranges.json)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/trading-bot-hosting

Cite as: Fincode, "Trading bot hosting: VPS, Docker, restarts and latency", updated November 6, 2026, https://fincodelabs.com/en/blog/trading-bot-hosting
