This document covers swapping IBC for ibg-controller inside a
gnzsnz/ib-gateway-docker-style
image. If you're running vanilla IBC today, the swap requires:
- Adding a couple of runtime packages to your Docker image
- Compiling and installing the controller in your Dockerfile (or downloading a release)
- Setting one new env var (
TWS_SERVER) for the bootstrap - Flipping one flag (
USE_IBG_CONTROLLER=yes) at runtime
Existing env vars (TWS_USERID, TWS_PASSWORD, TRADING_MODE,
TWOFACTOR_CODE, EXISTING_SESSION_DETECTED_ACTION, etc.) work
unchanged. The controller reads env vars directly — it doesn't parse
config.ini. If you already have an IBC config.ini that isn't
already mirrored into env vars, FROM_IBC.md documents
the IBC-key → env-var mapping and ships a one-shot
ibc_config_to_env.py that does the conversion for you.
# Add make + default-jdk-headless to the existing curl/ca-certificates/unzip line:
RUN apt-get update -y && \
apt-get install --no-install-recommends --yes \
curl ca-certificates unzip default-jdk-headless make && \
...
# After the existing IB Gateway install + scripts copy, add:
COPY ./controller /root/controller
RUN cd /root/controller && \
make install DESTDIR=/root && \
cd / && rm -rf /root/controller /root/buildThis compiles the agent jar and drops:
/root/gateway-input-agent.jar/root/scripts/gateway_controller.py
Into the setup-stage filesystem. The existing COPY --chown ... --from=setup /root/ copies them into /home/ibgateway/ in the production stage, so no changes needed there.
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install --no-install-recommends --yes \
gettext-base socat xvfb x11vnc sshpass openssh-client sudo telnet \
python3 matchbox-window-manager && \
...The new runtime packages:
python3— runsgateway_controller.py(stdlib-only at module load as of v0.5.14 — no PyGObject/gi/Atspi)matchbox-window-manager— a minimal window manager (Xvfb has no WM by default and Gateway's input routing depends on a focus owner)
No JRE accessibility-bridge setup is required. Pre-v0.5.13 the image installed
python3-gi gir1.2-atspi-2.0 at-spi2-core libatk-wrapper-java libatk-wrapper-java-jni dbus-x11and wrote$JAVA_HOME/conf/accessibility.propertiesso the JVM could load the AT-SPIAtkWrapperbridge for component discovery. v0.5.12 disabled the bridge after it was found to deadlock onJProgressBar.setValuecalls during login (intra-JVM, not broker-side); v0.5.13 removed the JVM-side install steps; v0.5.14 removed the residualpython3-gi gir1.2-atspi-2.0 at-spi2-coreinstall along with the dead Python helpers that referencedgi.repository.Atspi. All UI work now goes through the in-JVM agent socket. If you're rebasing from a fork that has the old install block, drop it.
The gnzsnz run.sh gains a branch: when USE_IBG_CONTROLLER=yes,
start the window manager and launch the controller instead of IBC.
When unset, fall through to the IBC path unchanged. Full diff:
# New helper functions
start_window_manager() { ... } # matchbox-window-manager
start_controller() { ... } # python3 gateway_controller.py
wait_for_controller_ready() { ... } # block on /tmp/gateway_ready
# In the Common Start section, after start_xvfb:
if [ "$USE_IBG_CONTROLLER" = "yes" ]; then
wait_x_socket
start_window_manager
fi
# In start_process(), replace the unconditional start_IBC with:
if [ "$USE_IBG_CONTROLLER" = "yes" ]; then
start_controller
wait_for_controller_ready
port_forwarding # socat starts AFTER readiness
else
port_forwarding
start_IBC
fiThe controller path gates port_forwarding on /tmp/gateway_ready,
which fixes a long-standing issue in the IBC path where socat races the
Gateway login and the first few API client connection attempts hit a
closed port.
USE_IBG_CONTROLLER=yes— opts into the new path. Default (unset) uses IBC as before. (Pre-v0.6.0 the toggle was namedUSE_PYATSPI2_CONTROLLER; that alias was removed in v0.6.0 — seeUPGRADING.mdif you're migrating from v0.5.x.)TWS_SERVER/TWS_SERVER_PAPER— your IBKR regional server. See BOOTSTRAP.md for how to find it.
These keep working the same way IBC used them:
TWS_USERID,TWS_PASSWORDTWS_USERID_PAPER,TWS_PASSWORD_PAPERTRADING_MODE—live,paper, orboth(v0.2 adds full dual-mode support with per-instance state isolation)TWOFACTOR_CODE(the base32 TOTP secret)EXISTING_SESSION_DETECTED_ACTION— clicksContinue Loginfor primary on Gateway 10.45.1cVNC_SERVER_PASSWORDTIME_ZONESSH_TUNNEL,SSH_OPTIONS, ...
The controller drives Gateway's Configure → Settings → API dialog after login completes. These IBC env vars now take effect:
TWS_MASTER_CLIENT_ID— integer, sets the Master API client IDREAD_ONLY_API—yes/no, toggles the Read-Only API checkboxBYPASS_ORDER_PRECAUTIONS—yes/no, toggles API → Precautions → Bypass Order Precautions for API OrdersAUTO_LOGOFF_TIME—HH:MM, sets Lock and Exit → Set Auto Log Off TimeAUTO_RESTART_TIME—HH:MM AM/PM, sets Lock and Exit → Set Auto Restart Time
Gateway shows either Auto Log Off Time or Auto Restart Time in the Lock and Exit panel depending on whether the account has an autorestart daily-token cycle active. Setting both env vars is safe — the handler tries both labels and sets whichever is visible, and logs a clear warning for the one that's not.
IBC's telnet command server can be re-enabled with:
CONTROLLER_COMMAND_SERVER_PORT— TCP port (e.g.7462for IBC compat). Unset = disabled. In dual mode the paper instance auto-offsets toport+1to avoid a bind collision.CONTROLLER_COMMAND_SERVER_HOST— bind address, default0.0.0.0so Docker port forwarding works. Restrict exposure viadocker run -p 127.0.0.1:7462:7462.
Supported commands: STOP, RESTART (in-place Gateway JVM
re-launch with full re-login), RECONNECTACCOUNT, ENABLEAPI.
RECONNECTDATA returns a clean error on Gateway (no File →
Reconnect Data menu item) and dispatches against TWS.
GATEWAY_OR_TWS—gateway(default) ortws. Switches launcher discovery, the agent's window-title match, and the API port the controller checks (API_PORTwhen set, otherwise 7496 live / 7497 paper for TWS). TWS is experimental: the image ships IB Gateway only, and TWS hasn't been validated live with the controller.
TWS_ACCEPT_INCOMING—manual(default),acceptorreject, as IBC'sAcceptIncomingConnectionAction. Gateway and TWS ask "Accept incoming connection attempt from …?" when a client connects from an address outside Trusted IPs;acceptclicks OK or Yes,rejectclicks No,manualleaves it to you. Adding the client to Trusted IPs is safer and avoids the dialog.
Gateway's config dialog doesn't expose these, so the controller warns and ignores them:
ALLOW_BLIND_TRADING— TWS Precautions tab; Gateway's simplified config has no equivalentSAVE_TWS_SETTINGS— not a Gateway knob
If you need these, keep IBC for now.
CUSTOM_CONFIG— no IBC config file is written; the controller reads env vars directly.- IB Key push as an unattended method — the controller can wait for
a phone approval, but nobody is there to tap it in a headless
deployment. Use TOTP via
TWOFACTOR_CODE(andTWOFA_DEVICEon multi-method accounts, honored since v0.7.0).
Required for v0.5.11 and later. If you run ibg-controller under
docker compose, set stop_grace_period: 90s on the ib-gateway
service. The default of 10s is too short — docker will SIGKILL the
container before the clean-logout chain finishes, and IBKR will hold
the brokerage session slot open for hours after every restart.
services:
ib-gateway:
image: ghcr.io/code-hustler-ft3d/ibg-controller:latest
stop_grace_period: 90s
environment:
TRADING_MODE: both
USE_IBG_CONTROLLER: "yes"
# ... rest of your configWhen docker sends SIGTERM, run.sh's stop_ibc() walks this
sequence (v0.5.11+):
| Step | Budget | What happens |
|---|---|---|
| 1. SIGTERM controllers | up to 60s | Each controller calls _attempt_clean_logout(), which uses the in-JVM input agent to dispatch a WINDOW_CLOSING AWT event to Gateway's main window. Gateway's WindowListener runs a real CCP session-close handshake and the JVM exits. Per-controller timeout _CLEAN_LOGOUT_TIMEOUT_SECONDS = 15s per instance — in dual mode (TRADING_MODE=both) two instances run sequentially, so 30s of clean-logout is normal. The 60s outer budget covers JVM shutdown-hook work that runs after the WindowListener returns. |
| 2. Tear down x11vnc / Xvfb / matchbox / socat | a few seconds | Only after controllers have exited (or 60s elapsed). |
| 3. IBKR FIN-ACK margin | a few seconds | Server-side acknowledgement of the session-close TCP teardown. |
Total budget: ~90s. Setting stop_grace_period: 90s matches that
budget. Setting it lower means docker SIGKILLs the container partway
through step 1, the WindowListener never gets to run, and IBKR
treats the session as a network drop rather than a clean logout —
holding the slot open until the server-side timeout (>15 min).
If you're running single-mode (TRADING_MODE=live or
TRADING_MODE=paper, not both), 60s would technically suffice, but
we recommend keeping 90s for symmetry with dual-mode and for the
FIN-ACK margin.
After running docker compose stop (or sending SIGTERM directly),
your container logs should include:
ALERT_CLEAN_LOGOUT mode=<live|paper> pid=<n> status=succeeded
reason="JVM exited cleanly within 15s of WINDOW_CLOSING"
ALERT_SHUTDOWN mode=<live|paper> signal=SIGTERM graceful=true
If you see status=failed_timeout, status=failed_unreachable, or
no ALERT_CLEAN_LOGOUT line at all, the slot probably leaked. Check
that stop_grace_period: 90s is set and that you're running
v0.5.11+ (the underlying clean-logout pipeline didn't actually work
end-to-end before that release — see CHANGELOG.md).
-
Build your Docker image with the new Dockerfile.
-
Keep your existing
docker-compose.yml— addUSE_IBG_CONTROLLER: yesandTWS_SERVER: <your-server>to the environment section. -
Leave all existing env vars as-is.
-
docker compose up -dand watch logs. -
Expected log sequence (single paper mode, no post-login config):
.> Starting Gateway controller in paper mode. .> agent-socket: /tmp/gateway-input-paper.sock .> ready-file: /tmp/gateway_ready_paper .> jts-config: /home/ibgateway/Jts [INFO] Loading input agent [INFO] Input agent is up [INFO] Gateway JVM PID (from agent): 37 [INFO] App registered: 'IBKR Gateway' (pid=37) [INFO] Login dialog detected [INFO] set_text on text 'Username': ok via agent (verified) [INFO] click on push button:'Paper Log In': ok [INFO] 2FA dialog detected: (...Second Factor Authentication...) (if TOTP) [INFO] Typing TOTP code into the 2FA dialog [INFO] API port 4002 accepting connections after 0s [INFO] Post-login config: no supported env vars set, skipping [INFO] Readiness signal: /tmp/gateway_ready_paper [INFO] Command server: CONTROLLER_COMMAND_SERVER_PORT not set, skipping [INFO] Login complete. Entering monitor loop.Dual-mode (
TRADING_MODE=both) runs the same sequence twice in sequence, first for live then for paper (separated by a 15-second sleep). Each instance uses its ownJts_live/Jts_papersettings directory, agent socket, and ready file.With post-login config set (e.g.
TWS_MASTER_CLIENT_ID=5,READ_ONLY_API=yes), you'll also see:[INFO] Applying post-login configuration from env vars [INFO] Navigating to API → Settings [INFO] Setting Master API client ID = 5 [INFO] Setting Read-Only API = True [INFO] Post-login config applied and dialog closedWith
CONTROLLER_COMMAND_SERVER_PORT=7462set, you'll see:[INFO] Command server: listening on 0.0.0.0:7462 -
Verify your API client can connect through the forwarded port (4001 for live, 4002 for paper, or whatever you expose).
If the controller fails for you, flip USE_IBG_CONTROLLER back to
unset (or no). The image still contains IBC and all its dependencies.
run.sh's default branch uses IBC unchanged.
You can also run both side-by-side (two containers, one IBC, one controller) on different account credentials to compare behavior before committing.
Please include:
- The full controller log (
docker logs <container>) - Gateway's
launcher.logfrom inside the container (docker exec <container> cat /home/ibgateway/Jts/launcher.log) - Your
TWS_SERVERvalue and whether you're running live or paper - Gateway version (
TWS_MAJOR_VRSN) - Container architecture (amd64 / arm64)
Never include your credentials, TOTP secret, or account numbers. The controller never logs your password or TOTP secret, and masks account numbers and your configured usernames in its own logs. Gateway's launcher.log, which the controller also copies into its logs when Gateway loses its connection, may include other fragments. Sanitize before sharing.