Python + in-JVM agent replacement for IBC on IB Gateway — headless Docker automation for login, 2FA, config, and monitoring
ibg-controller
A Python + in-JVM Java agent drop-in replacement for IBC on IB Gateway, targeted at the headless Docker use case. Launches Gateway, drives the login dialog (including TOTP 2FA), applies post-login API config, monitors for re-authentication events, and exposes IBC's TCP command server.
IBC is being deprecated in September 2026. This is one of the
community paths forward — written in Python so the maintainer
community of gnzsnz/ib-gateway-docker can read, patch, and extend
it without a JVM or Rust toolchain.
Scope
ibg-controller targets headless Docker IB Gateway and **favors gnzsnz/ib-gateway-docker** as its base: the shipped image builds FROM it and docker/run.sh extends its scaffolding. UPSTREAM_IMAGE is a build arg so it isn't hard-locked, but gnzsnz's image is the path ibg-controller is built, tested, and maintained for.
Quick start
The fastest way to use ibg-controller is the pre-built image on GitHub Container Registry:
docker pull ghcr.io/code-hustler-ft3d/ibg-controller:latest
docker run -d --name ibkr \ --env-file /path/to/your/.env \ -e TRADING_MODE=paper \ -e TWSSERVERPAPER=cdc1.ibllc.com \ -p 127.0.0.1:4002:4004 \ ghcr.io/code-hustler-ft3d/ibg-controller:latest
Tags published: :latest, :<major>.<minor> (e.g. :0.5), and :v<major>.<minor>.<patch> (e.g. :v0.7.0). Every tag is signed with cosign via Sigstore keyless signing — see SECURITY.md for the verification recipe. For reproducible deployments, pin to a digest (ghcr.io/code-hustler-ft3d/ibg-controller@sha256:...) — the digest is printed in each release's CI log.
IMPORTANT: required consumer config (v0.5.11+). If you run
ibg-controller under docker compose, you must set
stopgraceperiod: 90son theib-gatewayservice. Docker's
default of 10s is too short for the clean-logout chain to complete
and you will strand IBKR session slots on every container restart.
See docs/MIGRATION.md
for the timing math.>
> ib-gateway: > image: ghcr.io/code-hustler-ft3d/ibg-controller:latest > stopgraceperiod: 90s # required — see MIGRATION.md > environment: > TRADING_MODE: paper > TWSSERVERPAPER: cdc1.ibllc.com > USEIBGCONTROLLER: "yes" > # ... your other env vars >> services:
If you'd rather build the image yourself (or compose ibg-controller into a larger image of your own):
# In the Dockerfile's setup stage:
COPY ./ibg-controller /root/ibg-controller
RUN cd /root/ibg-controller && \
make install DESTDIR=/root && \
cd / && rm -rf /root/ibg-controller /root/build
In the production stage, add these packages:
python3 matchbox-window-manager
and follow docs/MIGRATION.md for the rest.
Then at docker run time:
docker run -d --name ibkr \
--env-file /path/to/your/.env \
-e TRADING_MODE=paper \
-e USEIBGCONTROLLER=yes \
-e TWSSERVERPAPER=cdc1.ibllc.com \
-e TWOFACTOR_CODE=<your TOTP secret> \
-p 127.0.0.1:4002:4004 \
your-ib-gateway-image
Full Dockerfile migration instructions: docs/MIGRATION.md. Already running IBC? Converting your config.ini to ibg-controller env vars: docs/FROM_IBC.md. Finding your regional server (TWSSERVER): docs/BOOTSTRAP.md. Why each piece is necessary: docs/ARCHITECTURE.md.
Using the shipped Dockerfile
A ready-to-use Dockerfile is shipped at the repo root. It extends gnzsnz/ib-gateway-docker, adds the small set of runtime packages the controller needs (python3
matchbox-window-manager), and drops the controller artifacts from
dist/ into /home/ibgateway/:
# Build the agent jar and stage the controller
make
Build the image. UPSTREAM_IMAGE defaults to ghcr.io/gnzsnz/ib-gateway:stable
(a moving tag). For reproducible production builds, pin a digest:
docker build -t ibg-controller:local \
--build-arg UPSTREAM_IMAGE=ghcr.io/gnzsnz/ib-gateway:10.45.1g@sha256:... \
.
Use this when you want a single ready-to-run image with no further customization. If you're composing ibg-controller into a larger image of your own, use the make install DESTDIR=... path shown in the Quick start above instead.
System requirements
| Requirement | Notes | |---|---| | Linux | amd64 and arm64. Ubuntu 24.04 base tested. | | IB Gateway | Release images pin 10.45.1g (gnzsnz :stable line); dialog behavior validated on 10.45.1c of the same line. Should work on any 10.x with a compatible install4j launcher and a Zulu JRE 17+. | | Python 3.10+ | For f-strings with = and type hints. | | JDK 17+ | Only at build time, for the agent. Runtime uses Gateway's bundled Zulu JRE. | | Ubuntu packages | python3 matchbox-window-manager (matchbox provides focus routing for Xvfb; Xvfb itself ships in the upstream gnzsnz/ib-gateway image) | | JRE config | None. Pre-v0.6.2 the image wrote accessibility.properties and copied libatk-wrapper.so into the JRE; both were dropped after v0.5.12 disabled the AT-SPI bridge in the JVM. |
Compatibility table
| Feature | Gateway | TWS | Notes | |---|---|---|---| | Single-mode paper cold-start | ✅ verified | ⚠️ code in place | TWS code-path unit-tested; app-name match needs real TWS to validate | | Single-mode live cold-start | ✅ verified | ⚠️ code in place | | | Dual mode (TRADINGMODE=both) | ✅ verified | ⚠️ code in place | Per-instance state isolation, agent sockets, ready files, JVM-PID-scoped findapp | | TOTP 2FA (single method) | ✅ verified | ⚠️ code in place | | | IB Key push 2FA (single method) | ✅ wait mode | ✅ wait mode | Controller detects the 2FA dialog, logs "approve on your phone", polls for dialog dismissal. User approves via IB Key mobile app. Same approach as ibctl. | | Multi-method 2FA | ⚠️ partial (v0.7.0) | ⚠️ partial | Account with >1 method: Gateway defaults the dialog to one method. If that's the method matching TWOFACTORCODE, login works. If not, the controller now fails clearly (no silent mis-type) and tells you to set your IBKR preferred method — automated method-switching is not yet implemented. See issue #7. | | Existing-session dialog | ✅ verified | ⚠️ code in place | Clicks Continue Login; late-arrival handler catches the dialog if it shows during the 2FA wait | | TWSMASTERCLIENT_ID | ✅ verified | ⚠️ untested | Set + read back | | READONLYAPI | ✅ verified | ⚠️ untested | Set + read back via JCHECK | | AUTOLOGOFFTIME / AUTORESTARTTIME | ✅ verified | ⚠️ untested | Gateway shows one or the other based on account state; controller tries both labels | | STOP command | ✅ verified | ⚠️ untested | | | RESTART command (in-place) | ✅ code verified | ⚠️ untested | Full tear-down / re-launch / re-drive login pipeline — verified against live Gateway | | RECONNECTACCOUNT | ✅ verified | ⚠️ untested | | | ENABLEAPI | ✅ verified | ⚠️ untested | | | RECONNECTDATA | ❌ no Gateway menu item | ⚠️ dispatch in place | TWS-only |
"✅ verified" = run end-to-end against a real IB account with logged evidence in the parent repo's spike/ directory. "⚠️ code in place" = written, unit-tested where possible, not yet run against the corresponding product.
What it replaces
- IBC — Java, ~8000 lines. Replaced entirely for Gateway.
GATEWAYORTWS=tws) that's
unit-tested for the launcher-discovery path but not yet live-tested
against real TWS.
- oathtool — stdlib-only TOTP generation.
- xdotool — not needed for input; the in-JVM agent handles text entry.
Architecture at a glance
┌────────────────────────────────────────┐
│ Docker container (headless) │
│ │
│ Xvfb :1 ← matchbox WM │
│ │ │
│ ↓ │
│ IB Gateway JVM │
│ └─ -javaagent:gateway-input-agent.jar │
│ │ │
│ ↓ │
│ Unix socket (/tmp/gateway-input-{mode}.sock)
│ ↑ │
│ gateway_controller.py (Python) │
│ ├─ agent socket: discover windows, │
│ │ type text, click, navigate │
│ │ JTree │
│ ├─ state machine: login → 2FA → │
│ │ config → ready → monitor │
│ ├─ IBC-compat command server (TCP) │
│ └─ signals /tmp/gatewayready{mode} │
│ │
└────────────────────────────────────────┘
- Python controller runs the state machine, the re-auth monitor
- Java agent (~750 lines) loaded via
-javaagent:into Gateway's JVM
EditableText writes return false). The agent
uses Swing's own JTextField.setText(), AbstractButton.doClick(),
JTree.setSelectionPath(), and JToggleButton.doClick() — the only
things that actually work — plus Window.getWindows() for component
discovery.
- No AT-SPI / ATK in the runtime path. Earlier versions used the
AtkWrapper bridge for component discovery and button
clicks. v0.5.12 disabled the bridge in the JVM after a thread-dump
showed AtkWrapper$5.propertyChange deadlocking on
JProgressBar.setValue calls during login (surfaced as a misleading
CCP LOCKOUT DETECTED warning); v0.6.2 removed the install-time
ATK packages and JRE configuration entirely. See
CHANGELOG.md v0.5.12 / v0.6.2 for the full record.
Full diagnostic history and architectural reasoning: docs/ARCHITECTURE.md
Env vars
Credentials
| Var | Notes | |---|---| |TWSUSERID | Your IB username (or paper username if TRADINGMODE=paper and TWSUSERIDPAPER isn't set) |
| TWS_PASSWORD | Your IB password |
| TWSPASSWORDFILE | Alternative: read the password from a file at this path (Docker secrets pattern). run.sh loads it via file_env before launching the controller. |
| TWSUSERIDPAPER | Paper account username — used when TRADING_MODE=paper |
| TWSPASSWORDPAPER | Paper account password — used when TRADING_MODE=paper |
| TRADING_MODE | live, paper, or both (default: paper). both runs two Gateway JVMs in parallel with isolated state. |
| TWOFACTOR_CODE | Base32 TOTP secret from IBKR Mobile Authenticator setup. When set, the controller generates a TOTP code and enters it into the Second Factor Authentication dialog. |
| TWOFACTORCODEFILE | Alternative: read the TOTP secret from a file (Docker secrets pattern). |
| TWOFADEVICE | IBC-compatible. Only relevant if your IBKR account has more than one 2FA method enabled. Names the method TWOFACTORCODE satisfies — defaults to Mobile Authenticator app. The controller checks Gateway's 2FA prompt against this; if Gateway defaulted to a different method it fails clearly rather than mis-typing the code. Ignored on single-method accounts. v0.7.0+ (issue #7). |
2FA setup for unattended use (read this if you have a choice of method)
The controller is built for unattended, headless operation, and that constrains which 2FA method works:
- Use Mobile Authenticator (TOTP) — provide the base32 secret via
TWOFACTOR_CODE. This is the only method a headless container can
satisfy on its own.
- IB Key push is not suitable for unattended use. It sends a
TWOFACTOR_CODE unset and it polls
for the dialog to clear), but that only helps if a human is actually
there to approve. For true automation, it's a dead end.
- If your account has more than one method enabled, Gateway
ALERT2FAFAILED reason="2FA method mismatch" rather
than silently entering the code in the wrong place (see
issue #7).
The resolution is on the IBKR side: get the login the controller uses
to default to (or only have) Mobile Authenticator. Manage your
methods in **Client Portal → Settings → User Settings → Security →
Secure Login System, or contact IBKR Client Services (Secure
Login dept)**. How IBKR's multi-method defaulting and device
management behave is governed entirely by IBKR — confirm against your
own account; this project can't change it.
2FA timeout behavior (IBC-compat)
| Var | Notes | |---|---| |TWOFAEXITINTERVAL | Seconds to wait for the Second Factor Authentication dialog to appear. Default 120. Matches IBC's env var name. |
| TWOFATIMEOUTACTION | What to do if the wait expires: exit (controller exits non-zero), restart (in-place Gateway re-launch via the RESTART command path), or none (fall through to waitforapi_port — matches Phase 1 behavior, useful for paper accounts / autorestart tokens that skip 2FA entirely). Default none. |
| RELOGINAFTERTWOFATIMEOUT | yes/no. If yes, re-drive the login form once before dispatching TWOFATIMEOUT_ACTION. Matches IBC. |
Disclaimer dismissal (IBC-compat)
| Var | Notes | |---|---| |BYPASS_WARNING | Comma- or semicolon-separated list of extra button labels to auto-dismiss in the post-login disclaimer loop. Extends the built-in allowlist (I understand and accept, I Accept, Acknowledge, Accept and Continue). Bare OK is permanently refused — clicking OK on Gateway's "Connecting to server..." progress modal cancels the in-progress login. |
| TWSCOLDRESTART | yes/no. If yes, applywarmstate() skips any GATEWAYWARMSTATE copy and forces Gateway to cold-start. Useful for debugging stuck state. Matches IBC's env var. |
Server (the bootstrap knob)
| Var | Notes | |---|---| |TWSSERVER | IBKR regional server hostname (e.g. ndc1.ibllc.com, cdc1.ibllc.com). Default: Gateway's built-in default. See docs/BOOTSTRAP.md for how to find your server. |
| TWSSERVERPAPER | Same but for paper mode. Some users have live on one region and paper on another. |
Dialog behavior
| Var | Notes | |---|---| |EXISTINGSESSIONDETECTED_ACTION | primary (default) / primaryoverride / secondary / manual. Matches IBC's setting of the same name. Gateway 10.45.1c's dialog uses Continue Login for primary and Cancel for secondary. |
Post-login API config (v0.2)
| Var | Notes | |---|---| |TWSMASTERCLIENT_ID | Integer. Sets Master API client ID in Configure → Settings → API. |
| READONLYAPI | yes/no. Toggles Read-Only API checkbox. |
| AUTOLOGOFFTIME | HH:MM. Sets Configure → Settings → Lock and Exit → Set Auto Log Off Time. |
| AUTORESTARTTIME | HH:MM AM/PM. Sets Configure → Settings → Lock and Exit → Set Auto Restart Time. |
Gateway's Lock and Exit panel shows either the Auto Log Off Time field or the Auto Restart Time field depending on whether the account has an active autorestart daily-token cycle. The controller tries both labels and sets the one Gateway is currently displaying. If the user sets the one Gateway isn't showing, a clear warning is logged. Setting both env vars makes the controller handle whichever Gateway is displaying in any given session.
Command server (v0.2)
| Var | Notes | |---|---| |CONTROLLERCOMMANDSERVER_PORT | TCP port to listen on for IBC-compat commands. Unset (default) disables the server. Set to 7462 to match IBC's default. In dual mode, the paper instance auto-offsets to port+1 to avoid a bind collision. |
| CONTROLLERCOMMANDSERVER_HOST | Bind address. Default 0.0.0.0 so Docker port forwarding works; restrict exposure with Docker's -p 127.0.0.1:7462:7462 for loopback-only external access. |
Supported commands: STOP, RESTART (in-place Gateway JVM re-launch
- full re-login),
RECONNECTACCOUNT,ENABLEAPI.RECONNECTDATA
**Optional auth token (recommended if you expose the port beyond localhost)**: set CONTROLLERCOMMANDSERVERAUTHTOKEN=<random-secret>. When set, clients must send AUTH <token>\n as their first line before a command:
AUTH <token>
STOP
The token is checked with hmac.compare_digest to resist timing side-channels. Without the token set, the command server runs in IBC-compat no-auth mode and logs a loud WARNING at startup.
Recovery tunables (v0.5.10)
| Var | Notes | |---|---| |CCPMAINTENANCERECOVERYDELAYSECONDS | Seconds to sleep after a JVM code-0 exit (or cold start) inside IBKR's daily maintenance window (~23:30-00:30 America/NewYork) before re-auth. Default 480 (8 min). The delay lets IBKR's auth server drain the cooperatively-shutdown session; re-auth'ing too quickly during this window is silently dropped → CCP LOCKOUT cascade. See docs/DISCONNECTRECOVERY.md for the 2026-04-20/21 incident that motivated this. |
| CCPLOCKOUTMAXJVMRESTARTS | Cap on CCP-lockout-triggered JVM restart cycles. Default 0 (halt on first persistent lockout — see ALERTCCPPERSISTENT_HALT). Set to a positive integer (e.g. 5) to restore the pre-v0.5.9 auto-recovery loop. |
Observability (v0.4.9)
| Var | Notes | |---|---| |CONTROLLERHEALTHSERVER_PORT | TCP port for the HTTP /health endpoint. Default 8080 in the shipped image, unset on source checkout. In dual mode, paper auto-offsets to port+1. Set to empty to disable the health server entirely. |
| CONTROLLERHEALTHSERVER_HOST | Bind address. Default 0.0.0.0 so Docker port forwarding works. Restrict external exposure with Docker's -p 127.0.0.1:8080:8080 on the host side. |
GET /health returns JSON with the controller's state, Gateway JVM liveness, API port status, CCP lockout streak, and the timestamp of the most recent successful auth. HTTP 200 if the controller is logged in and serving (state==MONITORING AND apiportopen AND jvm_alive); 503 otherwise. GET /ready returns 200 while the process is running (for Kubernetes-style readiness).
The controller also emits stable grep-contract log tokens (ALERTCCPPERSISTENT, ALERTJVMRESTART_EXHAUSTED, ALERT2FAFAILED, ALERTPASSWORDEXPIRED, ALERTLOGINFAILED, ALERT_SHUTDOWN) that external monitors can pattern-match on regardless of log level. ALERTPASSWORDEXPIRED fires in two flavors — status=warning (with days_remaining=N when the dialog reports it, actionable before lockout) and status=expired (login is already blocked, rotate the password in IBKR's web portal). ALERTLOGINFAILED fires when Gateway surfaces a credential-rejection modal or when the launcher.log fingerprint matches a bad-credentials auth flow (reason=bad-credentials), so monitors can tell a stale-password account lockout apart from IBKR's silent cooldown (ALERTCCPPERSISTENT) without waiting for the CCP-retry ceiling. ALERT_SHUTDOWN is the lifecycle complement — INFO-level and emitted on SIGTERM/SIGINT with graceful=true|false so dashboards can distinguish operator-initiated shutdowns from Gateway-JVM crashes, and flag stuck-JVM graceful=false restarts that need attention.
The shipped Dockerfile includes a HEALTHCHECK that curls /health every 30s with a 180s start-period. In DUAL_MODE=yes it probes both the live and paper ports; either side being unhealthy marks the container unhealthy.
Full protocol, field semantics, and integration examples (cron, Prometheus blackboxexporter, jq): docs/OBSERVABILITY.md.
Playbook for operator-action scenarios (CCP lockout, 2FA failure, JVM restart exhaustion, Gateway JVM crash): docs/DISCONNECT_RECOVERY.md.
Product selector (v0.2)
| Var | Notes | |---|---| |GATEWAYORTWS | gateway (default) or tws. Switches launcher discovery ($TWSPATH/ibgateway/... vs $TWSPATH/tws/...) and the agent's window-title match between IB Gateway and Trader Workstation. Code path unit-tested; live validation pending a TWS-with-controller Dockerfile variant. |
Dual-mode per-instance (v0.2.1)
| Var | Notes | |---|---| |TWSSETTINGSPATH | In dual mode, run.sh sets this per instance (e.g. /home/ibgateway/Jts_live) so live and paper have isolated state. Single-mode users don't need to set it. |
| CONTROLLERREADYFILE | Override the readiness signal file. Defaults to /tmp/gatewayready (single mode) or /tmp/gatewayready_{mode} (dual mode, set automatically by run.sh). |
Other
| Var | Notes | |---|---| |GATEWAYWARMSTATE | Optional path. If set, the controller copies files from this directory into Gateway's settings dir before launch. Useful for seeding a fresh container with a previously-captured jts.ini + encrypted state dir (for autorestart token support). |
| GATEWAYINPUTAGENT_JAR | Override path to the agent jar. Default: ~/gateway-input-agent.jar. |
| GATEWAYINPUTAGENT_SOCKET | Override Unix socket path. Default: /tmp/gateway-input.sock (single mode) or /tmp/gateway-input-{mode}.sock (dual mode, set automatically by run.sh). |
| CONTROLLER_DEBUG | Set to 1 to enable debug-level logging. |
| CONTROLLERTESTMODE | Set to 1 to exit immediately after clicking Log In (for smoke tests). |
Docker integration
This tool is designed to drop into a gnzsnz/ib-gateway-docker image via a Dockerfile.template addition, replacing IBC. Install via the Makefile target (make install DESTDIR=...) that drops gateway-input-agent.jar and scripts/gateway_controller.py into the ibgateway user's home.
See docs/MIGRATION.md for swapping out IBC in a gnzsnz/ib-gateway-docker-style Dockerfile.
If you already have an IBC config.ini, the ibcconfigto_env.py one-shot tool converts it to the equivalent ibg-controller env vars (.env, docker run -e, or docker-compose format), warning on unsupported keys instead of silently dropping them. Full mapping table and cutover recipe: docs/FROM_IBC.md.
./ibcconfigto_env.py /path/to/your/IBC/config.ini > .env
or: ./ibcconfigto_env.py --format compose config.ini
Building
# Build the agent jar and stage the controller into dist/
make
Syntax-check the Python controller + validate the agent jar manifest
make test
Create a release tarball (dist/ibg-controller-0.6.2.tar.gz)
make release VERSION=0.6.2
Install directly into a running ibgateway home (for dev on host, or
as called by the Docker image's setup stage)
make install DESTDIR=/home/ibgateway
Build requires a JDK 17+ (javac + jar) and make. No Maven, no Gradle.
Installing from a release tarball (for consumers who don't build)
VER=0.6.2
curl -sSLO https://github.com/code-hustler-ft3d/ibg-controller/releases/download/v${VER}/ibg-controller-${VER}.tar.gz
tar -xzf ibg-controller-${VER}.tar.gz
cd ibg-controller-${VER}
DESTDIR=/home/ibgateway ./install.sh
The tarball layout is flat:
ibg-controller-0.6.2/ ├── gateway-input-agent.jar ← installed to $DESTDIR/gateway-input-agent.jar ├── gatewaycontroller.py ← installed to $DESTDIR/scripts/gatewaycontroller.py ├── ibcconfigto_env.py ← one-shot IBC config.ini → env migration tool ├── install.sh ├── README.md, CHANGELOG.md, LICENSE, SECURITY.md └── docs/ ├── ADR-001-in-jvm-dialog-dispatcher.md ├── ARCHITECTURE.md ├── BOOTSTRAP.md ├── DISCONNECT_RECOVERY.md ├── FROM_IBC.md ├── MIGRATION.md ├── OBSERVABILITY.md └── UPGRADING.md
Troubleshooting
IBKR auth lockouts and the controller's automatic backoff (v0.3.2+)
Important triage note (v0.5.12+): the CCP LOCKOUT DETECTED
warning name is misleading. Pre-v0.5.12 the most common cause was
an intra-JVM AtkWrapper deadlock that fired the
AuthTimeoutMonitor-CCP 20-second timer locally without IBKR ever
being reached. v0.5.12 disables the AT-SPI bridge in the JVM and
the deadlock is gone — so if you're seeing this on v0.5.12+, it's
much more likely a real broker-side lockout. Triage:>
-launcher.logcontainsNSAUTHSTARTfor the affected mode →
real broker-side lockout, follow the backoff playbook below
and/or DISCONNECT_RECOVERY.md →
CCP lockout.
- No NSAUTHSTART and you're on a pre-v0.5.12 image → it was
the deadlock; upgrade.
- No NSAUTHSTART on v0.5.12+ → check
/tmp/jvmconsole${TRADINGMODE}.logafterkill -3 <jvmpid>
for an AtkUtil.invokeInSwing parked thread (smoking gun for a
bridge regression — open an issue).
IBKR's auth server occasionally stops responding to fresh password logins for several minutes (and occasionally hours) after a burst of failed attempts from the same account. There are two visible failure modes:
- CCP silent timeout — Gateway logs a 20-second silent
AuthTimeoutMonitor-CCP: Timeout! in launcher.log with no dialog
on-screen. Gateway 10.45.1c also hides the error dialog that
10.44.1g surfaces in the same state, making this worse.
- Stuck-connecting retry loop — Gateway's login dialog stays up
Attempt N: connecting to server (trying for another XX
seconds). The auth protocol never starts, so no Timeout! line
appears in launcher.log at all — the only visible signal is the
dialog text.
What the controller does automatically: as of v0.4.0, the controller detects both modes, applies an exponential backoff between retries (60s → 120s → 240s → 480s → 600s cap), and recovers by **re-driving Log In on the existing Gateway JVM** — never by killing and relaunching it. This matches IBC's LoginManager.initiateLogin pattern: IBKR's auth server treats each new JVM as a fresh handshake and keeps the CCP limiter armed, so the previous v0.2.2–v0.3.2 "backoff + dorestartin_place" design never let the lockout clear. You'll see these lines:
CCP LOCKOUT DETECTED — IBKR's auth server silently dropped the auth request
CCP backoff: waiting 60s before next auth attempt
Retrying auth in-JVM after CCP backoff (attempt 1/8)
In-JVM relogin attempt (no JVM restart — matches IBC's LoginManager.initiateLogin semantics)
or
Login dialog stuck in 'connecting to server' retry loop — IBKR auth server isn't accepting sessions right now. Applying CCP backoff before retry.
CCP backoff: waiting 120s before next auth attempt
The backoff counter is per-trading-mode (live and paper run as separate processes in dual mode, so they don't share state), and resets on genuine 2FA-success. Up to 8 in-JVM retries per controller lifetime; past that the controller exits and the container orchestrator's restart policy takes over. Just let it run — the controller will keep retrying with increasing delays until IBKR's rate limiter clears, often 5–60 minutes total.
If you're still stuck after an hour of patient backoff, double-check:
- You're sending the right username for the trading mode (the
TWSUSERIDPAPER when
TRADING_MODE=paper, but double-check your env file)
- Your
TWSSERVER/TWSSERVER_PAPERmatches the regional
docs/BOOTSTRAP.md
- If you have a previously working container's
/home/ibgateway/Jts
GATEWAYWARMSTATE — autorestart
token reauth goes through a different code path than fresh-password
auth and bypasses the cooldown
If a clean retry from a known-good config still fails after the backoff has run its course, the issue is almost certainly account-side (wrong server, wrong userid, account locked), not the controller. Gateway's launcher.log at /home/ibgateway/Jts/launcher.log will confirm the CCP-Timeout case — you'll see the Authenticating → Timeout! pattern with nothing in between. The stuck-connecting case won't show in launcher.log; check the controller's own logs for the "stuck in 'connecting to server'" warning instead.
"Gateway PID unknown (agent never reported one) — cannot proceed without a JVM identity"
App discovery relies on the input agent reporting its JVM PID through the Unix socket. If you see this error, the agent itself failed to start. Check:
- The
-javaagent:/home/ibgateway/gateway-input-agent.jar=...
cat /proc/$(pgrep -f java)/cmdline | tr '\0' '\n' | grep agent.
- The agent socket exists and is writable:
ls -l /tmp/gateway-input-${TRADING_MODE}.sock (or
/tmp/gateway-input.sock in single-mode).
/tmp/jvmconsole${TRADING_MODE}.log— added in v0.5.12. Check
Pre-v0.5.12 deployments only: if you're still on a release that
used the AT-SPI desktop tree for app discovery and you see "IBKR
Gateway never appeared in AT-SPI desktop tree within 120s",
upgrade. v0.5.12+ doesn't use AT-SPI at all and v0.6.2 removed the
ATK install steps from the image; both error modes are gone.
"Existing session detected" dialog keeps appearing in a loop
The controller's EXISTINGSESSIONDETECTED_ACTION=primary clicks Continue Login, which tells IBKR to kick the other session. If something else keeps reconnecting as that account (another container, the mobile app, TWS on your desktop), you'll ping-pong forever. Shut down the other session first.
The controller logs "Gateway JVM PID (from agent): None"
The Java agent's GET_PID command returned nothing. This usually means the agent's Unix socket is the old IBC default path (check GATEWAYINPUTAGENT_SOCKET in the container's env) or the ProcessHandle API isn't available (you're running on JRE < 17). Verify the JRE is Zulu 17 or newer.
Post-login config: "Auto Log Off Time" label not found
Gateway's Lock and Exit panel shows either "Set Auto Log Off Time (HH:MM)" or "Set Auto Restart Time (HH:MM)" depending on account state. The controller tries the label matching the env var you set. If it can't find it, it warns which label Gateway is currently showing and suggests the other env var. Set both AUTOLOGOFFTIME and AUTORESTARTTIME if you want the controller to handle whichever Gateway is displaying.
Security
Full supply chain details (cosign verification, SBOM extraction, pinning to digests, vuln reporting): SECURITY.md. Quick version of the deployment hygiene below.
This is a narrow threat model — single-user container running a trading tool. But there are real things to get right.
If you're running with CONTROLLERCOMMANDSERVER_PORT set:
- Bind to loopback only via Docker:
-p 127.0.0.1:7462:7462.
0.0.0.0 by default so Docker port
forwarding works at all; Docker's mapping is what controls
external exposure. Never use a bare -p 7462:7462 (which
binds the host's external interface) unless you also set an
auth token.
- Set
CONTROLLERCOMMANDSERVERAUTHTOKENto a random
127.0.0.1 on the host, or if the host is multi-tenant.
Without it, anyone who can reach the port can send
STOP/RESTART/RECONNECTACCOUNT.
- The controller logs a loud WARNING at startup if the command
Credentials:
- Pass them via
docker run --env-file /path/to/.env, never on the
launcher.log.
- Set your
.envfile to600on the host. Docker Engine only
docker run time; what's in /proc/<pid>/environ
after that depends on your Docker Engine version.
- For additional safety, consider Docker secrets (
_FILEenv vars
TWSPASSWORDFILE) — the controller delegates those to
run.sh's file_env helper which loads them from a separate
file path.
Logs and sharing them:
- Window/dialog dumps (
CONTROLLERDEBUG=1, theCONTROLLERTEST_MODE
DU\d{5,10} / U\d{5,10} (IBKR account format), and as of
v0.6.3 the in-JVM agent masks password-field contents at the
source — a JPasswordField is emitted as <redacted password
len=N>, never its value. (Before v0.6.3 the login-failure dump
could log your IBKR password in plaintext; see CHANGELOG.md v0.6.3.
Upgrade, and if you ran an older version, treat any saved
login-failure logs as sensitive.) Other identifying information
like your username can still appear in window titles. **Review
logs before posting them publicly**.
- Gateway's own
/home/ibgateway/Jts/launcher.logis NOT
Warm state and file inputs:
GATEWAYWARMSTATEis trusted — only set it to a directory
$JTSCONFIGDIR.
TWSSERVER/TWSSERVER_PAPERare validated as hostnames
What's NOT protected:
- The Java agent's Unix socket is reachable by any process in the
/tmp/gateway-input-*.sock
(owner-only perms are set, but the container is single-user so
this is mostly for defense-in-depth).
- The command server has no rate limiting.
- No TLS on the command server. Mitigated by recommending
-p 127.0.0.1:....
License
MIT — see LICENSE. Builds on work from IBC, ibctl, and gnzsnz/ib-gateway-docker, all credited below.
Acknowledgements
- @rlktradewright for IBC. Most
- Lcstyle/ibctl for the Java-agent
- @gnzsnz for steering the tool's architecture in
ib-gateway-docker image this is meant to drop
into.