Deployment
ibkr-httpapi runs as a Docker Compose stack built from docker-compose.yml.example. There is no published image of the whole thing: the api image builds locally from Dockerfile.api, and the gateway image is one you build or choose (see Getting started).
run.sh copies the example to docker-compose.yml on the first make up and never touches it again, so later changes to the example do not reach an existing deployment. Diff the two after you pull.
Services
| Service | Image | Job |
|---|---|---|
ibgateway | ${IBKR_GATEWAY_IMAGE:-ghcr.io/gnzsnz/ib-gateway:stable} | IB Gateway plus IBC. Logs in with .env.ibkr, serves the TWS API socket. |
api | ibkr-httpapi:local, built from Dockerfile.api | The FastAPI app (python -m ibkrapi) on port 8889, REST under /v1 and MCP at /mcp. |
nginx | nginx 1.27-alpine, pinned by digest | Publishes the API on the host and proxies every path to api:8889. |
wickworks | psyb0t/docker-wickworks:latest | TA sidecar for POST .../rates/ta. Remove the service and clear wickworks.url if you do not want it. |
cloudflared | commented out | Tunnel to http://nginx:80 for public access without a host port. |
tailscale | commented out | Tailnet sidecar. |
Start order follows health checks: api waits for ibgateway to be healthy, nginx waits for api.
Networks
| Network | Members | Notes |
|---|---|---|
front | nginx, api | The only network with a published port. |
backend | api, ibgateway | ibgateway reaches IBKR’s servers from here. |
internal | api, wickworks | internal: true: no egress, so wickworks cannot reach the internet. |
The TWS API socket is never published on the host. Only api can reach it.
Ports
| Host port | Service | Notes |
|---|---|---|
${API_HOST_BIND:-127.0.0.1}:${API_HOST_PORT:-8889} | nginx | The API. Loopback by default. |
127.0.0.1:5900 | ibgateway | VNC to the gateway desktop. Compose sets VNC_SERVER_PASSWORD to empty; set a password in the service environment to use it. |
127.0.0.1:8001 | ibgateway | Published on loopback by the example file. |
Inside the stack the API listens on api:8889 and the gateway on 4002 (paper) or 4001 (live), set by gateway.port in config.yaml.
State on disk
| Host path | Mounted at | Holds | Back it up |
|---|---|---|---|
config/ | /app/config (read-only) in api | config.yaml | Yes, it holds the API token. |
.env.ibkr | env file of ibgateway | IBKR username, password, trading mode | Keep it out of Git. It already is. |
.data/ibgateway/ | /home/ibgateway/Jts in ibgateway | Gateway settings | Optional. |
.data/nginx/nginx.conf | /etc/nginx/nginx.conf | nginx config, copied in by run.sh | No, it is regenerated. |
logs/ | /app/logs in api | full.log, every JSON log line | Optional. |
data/ | /app/data in api | history/: bars, tick snapshots, chains, executions, completed orders, cached contract metadata | Yes. Nothing else can recreate it. See Pacing and history. |
.env | read by compose | Values run.sh derives from config.yaml | No, run.sh rewrites it on every start. |
make clean stops the stack and deletes .data/, logs/ and run.log. It leaves data/ alone.
Container hardening
api,nginxandwickworksrun with a read-only root filesystem,tmpfsfor/tmp(noexec,nosuid),cap_drop: [ALL]andno-new-privileges. nginx gets back onlyNET_BIND_SERVICE,CHOWN,SETUIDandSETGID.ibgatewaykeeps a writable filesystem and its capabilities because Xvfb, the JVM and IBC write all over it. It still getsno-new-privileges.- Every service has memory, CPU and PID limits and
json-filelogs capped at 5 files of 10 MB. - The
apiimage runs as the unprivilegedappuser undertini, installs dependencies with--require-hashes, and uses a base image pinned by digest.
make audit-compose runs scripts/audit_compose.sh over every compose file in the repository root. It fails on privileged: true, host PID, IPC, network, user namespace or cgroup mode, a mounted Docker socket, or SYS_ADMIN. It warns about ports published without a bind address, image tags without a digest, and services missing hardening keys.
Daily restart and 2FA
IBKR logs every session out once a day. The compose file sets AUTO_RESTART_TIME: "11:59 PM" (in TIME_ZONE: Etc/UTC), so IBC restarts the gateway itself, plus TWOFA_TIMEOUT_ACTION: restart and RELOGIN_AFTER_TWOFA_TIMEOUT: "yes" so an unanswered 2FA prompt leads to another login attempt instead of a dead gateway. Expect a 2FA push on IBKR Mobile about once a week.
While the gateway is away, the API keeps reconnecting with exponential backoff (gateway.reconnect_backoff up to gateway.reconnect_max_backoff). Requests that arrive during the gap wait for the reconnect instead of failing; see troubleshooting.
Make targets for the stack
| Target | Runs |
|---|---|
make up | ./run.sh (described in Getting started) |
make down | docker compose down |
make logs | docker compose logs -f |
make status | docker compose ps |
make build | docker compose build --no-cache |
make clean | make down, then delete .data/, logs/ and run.log |
Upgrading
git pull
diff docker-compose.yml.example docker-compose.yml
make build
make up
make up asks before it restarts a running stack. Read the changelog first: minor versions before 1.0 can change the REST API and say so there.
Browser access to the gateway desktop
Dockerfile.novnc builds a small noVNC proxy (websockify on port 8080, target ibgateway:5900 by default, VNC_PASSWORD filled into the page so it connects without a prompt). It is not part of the compose file. Add it as a service on the backend network if you want to watch the gateway from a browser.
Public exposure
The default binds the API to 127.0.0.1. Read Security before you change API_HOST_BIND or enable the tunnel sidecars.