apiVersion: v1 kind: Namespace metadata: name: trading labels: name: trading environment: production --- # OPTIONAL: Use this if you want to persist IB Gateway settings/logs # across pod restarts. For most use cases, this is NOT needed since # IB Gateway is mostly stateless and credentials are in Secrets. # # Only create this PV/PVC if you need to persist: # - TWS session data # - Custom workspace layouts # - Historical API usage logs apiVersion: v1 kind: PersistentVolume metadata: name: ib-gateway-data labels: type: local app: ib-gateway spec: capacity: storage: 5Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage local: path: /mnt/local-ssd/ib-gateway # Adjust to your local SSD path nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - hetzner-2 --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: ib-gateway-data namespace: trading spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi storageClassName: local-storage selector: matchLabels: app: ib-gateway # To use this PVC, add to Deployment volumeMounts: # - name: data # mountPath: /root/Jts # And to volumes: # - name: data # persistentVolumeClaim: # claimName: ib-gateway-data --- apiVersion: v1 kind: Secret metadata: name: ib-credentials namespace: trading type: Opaque stringData: # IMPORTANT: Replace these with your actual IB credentials # For paper trading, use your paper trading account username: "saladin85" password: "3Lcd@05041985" # Trading mode: "paper" or "live" trading-mode: "paper" # IB Gateway config (jts.ini equivalent) # This enables headless mode and configures ports ibgateway.conf: | [IBGateway] TradingMode=paper ApiOnly=true ReadOnlyApi=false TrustedIPs=127.0.0.1 [IBGatewayAPI] ApiPortNumber=4002 [Logon] UseRemoteSettings=no Locale=en ColorPaletteName=dark [Display] ShowSplashScreen=no --- apiVersion: v1 kind: ConfigMap metadata: name: ib-gateway-config namespace: trading data: # Startup script to configure IB Gateway for headless operation startup.sh: | #!/bin/bash set -e echo "Starting IB Gateway in headless mode..." echo "Trading Mode: ${TRADING_MODE}" echo "Port: ${TWS_PORT}" # Configure based on trading mode if [ "${TRADING_MODE}" == "live" ]; then export TWS_PORT=4001 echo "⚠️ LIVE TRADING MODE - USE WITH CAUTION ⚠️" else export TWS_PORT=4002 echo "📝 Paper Trading Mode (Safe)" fi # IMPORTANT: use the env vars provided by the Deployment export IB_USERNAME="${TWS_USERID}" export IB_PASSWORD="${TWS_PASSWORD}" # Start IB Gateway exec /opt/ibgateway/ibgateway-latest-standalone-linux-x64.sh \ --tws-path=/root/Jts \ --tws-settings-path=/root \ --user="${IB_USERNAME}" \ --pw="${IB_PASSWORD}" \ --mode="${TRADING_MODE}" \ --port="${TWS_PORT}" # Health check script healthcheck.sh: | #!/bin/bash # Check if TWS API port is listening # PORT=${TWS_PORT:-4002} # nc -z localhost $PORT # exit $? #!/bin/sh # Pure-python TCP check (no nc required) PORT="${TWS_PORT:-4002}" python - <<'PY' import os, socket, sys port = int(os.environ.get("TWS_PORT", os.environ.get("PORT", "4002"))) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(2) try: s.connect(("127.0.0.1", port)) sys.exit(0) except Exception: sys.exit(1) finally: s.close() PY --- # apiVersion: apps/v1 # kind: Deployment # metadata: # name: ib-gateway # namespace: trading # labels: # app: ib-gateway # component: trading-infrastructure # spec: # replicas: 1 # IB Gateway should only have 1 instance per account # strategy: # type: Recreate # Avoid multiple simultaneous logins # selector: # matchLabels: # app: ib-gateway # template: # metadata: # labels: # app: ib-gateway # annotations: # prometheus.io/scrape: "false" # No metrics endpoint by default # spec: # # Pin to hetzner-2 (matches your existing pattern) # nodeSelector: # kubernetes.io/hostname: hetzner-2 # # Security context # securityContext: # runAsNonRoot: false # IB Gateway requires root for VNC (even if unused) # fsGroup: 1000 # containers: # - name: ib-gateway # # Using community-maintained IB Gateway image # # Alternative: waytrade/ib-gateway:latest # image: ghcr.io/gnzsnz/ib-gateway:stable # imagePullPolicy: IfNotPresent # env: # - name: TWS_USERID # valueFrom: # secretKeyRef: # name: ib-credentials # key: username # - name: TWS_PASSWORD # valueFrom: # secretKeyRef: # name: ib-credentials # key: password # - name: TRADING_MODE # valueFrom: # secretKeyRef: # name: ib-credentials # key: trading-mode # - name: TWS_PORT # value: "4002" # Default to paper trading # - name: READ_ONLY_API # value: "no" # # Ports # ports: # - name: paper-trading # containerPort: 4002 # protocol: TCP # - name: live-trading # containerPort: 4001 # protocol: TCP # - name: vnc # containerPort: 5900 # protocol: TCP # VNC (not exposed externally) # # Resource limits # resources: # requests: # memory: "1Gi" # cpu: "500m" # limits: # memory: "2Gi" # cpu: "1000m" # # Liveness probe (check if API port is responsive) # startupProbe: # tcpSocket: # port: 4002 # initialDelaySeconds: 60 # Wait 60s before first check # periodSeconds: 10 # Check every 10s # timeoutSeconds: 5 # failureThreshold: 18 # 60s + (10s * 18) = 240s total startup time # livenessProbe: # tcpSocket: # port: 4002 # initialDelaySeconds: 0 # IB Gateway takes time to start # periodSeconds: 60 # timeoutSeconds: 5 # failureThreshold: 3 # # Readiness probe # readinessProbe: # tcpSocket: # port: 4002 # initialDelaySeconds: 0 # periodSeconds: 10 # timeoutSeconds: 5 # failureThreshold: 2 # # Volume mounts for config # volumeMounts: # - name: ib-config # mountPath: /root/Jts/jts.ini # subPath: ibgateway.conf # - name: startup-script # mountPath: /startup.sh # subPath: startup.sh # - name: data # mountPath: /root/Jts # # Logging to stdout (Fluent Bit will collect) # # IB Gateway logs go to /root/Jts/log by default # lifecycle: # postStart: # exec: # command: # - /bin/sh # - -c # - | # mkdir -p /root/Jts/log # ln -sf /dev/stdout /root/Jts/log/ibgateway.log || true # volumes: # - name: ib-config # secret: # secretName: ib-credentials # defaultMode: 0644 # - name: startup-script # configMap: # name: ib-gateway-config # defaultMode: 0755 # - name: data # persistentVolumeClaim: # claimName: ib-gateway-data # # Restart policy # restartPolicy: Always # # DNS policy for internal cluster resolution # dnsPolicy: ClusterFirst apiVersion: apps/v1 kind: Deployment metadata: name: ib-gateway namespace: trading labels: app: ib-gateway component: trading-infrastructure spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: ib-gateway template: metadata: labels: app: ib-gateway annotations: prometheus.io/scrape: "false" spec: nodeSelector: kubernetes.io/hostname: hetzner-2 securityContext: runAsNonRoot: false fsGroup: 1000 # Seed writable jts.ini into the PVC once initContainers: - name: seed-jts-config image: busybox:1.36 command: - sh - -c - | set -e mkdir -p /data if [ ! -f /data/jts.ini ]; then echo "Seeding jts.ini into PVC" cp /config/ibgateway.conf /data/jts.ini chmod 644 /data/jts.ini else echo "jts.ini already exists in PVC" fi volumeMounts: - name: ib-config mountPath: /config readOnly: true - name: data mountPath: /data containers: # ------------------------------------------------------------------ # IB Gateway # ------------------------------------------------------------------ - name: ib-gateway image: ghcr.io/gnzsnz/ib-gateway:stable imagePullPolicy: IfNotPresent env: - name: TWS_USERID valueFrom: secretKeyRef: name: ib-credentials key: username - name: TWS_PASSWORD valueFrom: secretKeyRef: name: ib-credentials key: password - name: TRADING_MODE valueFrom: secretKeyRef: name: ib-credentials key: trading-mode - name: TWS_PORT value: "4002" - name: READ_ONLY_API value: "no" ports: - name: ib-api-local containerPort: 4002 protocol: TCP - name: live-trading containerPort: 4001 protocol: TCP - name: vnc containerPort: 5900 protocol: TCP resources: requests: memory: "1Gi" cpu: "500m" limits: memory: "2Gi" cpu: "1000m" # IMPORTANT: Probes should check the local IB port (4002) startupProbe: tcpSocket: port: 4002 initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 18 livenessProbe: tcpSocket: port: 4002 periodSeconds: 60 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: tcpSocket: port: 4002 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 2 volumeMounts: - name: data mountPath: /root/Jts lifecycle: postStart: exec: command: - sh - -c - | mkdir -p /root/Jts/log ln -sf /dev/stdout /root/Jts/log/ibgateway.log || true # ------------------------------------------------------------------ # Sidecar TCP proxy: accepts cluster traffic, forwards to localhost:4002 # ------------------------------------------------------------------ - name: ib-api-proxy image: alpine/socat:1.8.0.0 imagePullPolicy: IfNotPresent args: - "-d" - "-d" - "TCP-LISTEN:4003,fork,reuseaddr" - "TCP:127.0.0.1:4002" ports: - name: ib-api containerPort: 4003 protocol: TCP resources: requests: memory: "32Mi" cpu: "10m" limits: memory: "128Mi" cpu: "100m" # basic probe: is proxy listening readinessProbe: tcpSocket: port: 4003 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3 volumes: - name: ib-config secret: secretName: ib-credentials defaultMode: 0644 - name: data persistentVolumeClaim: claimName: ib-gateway-data restartPolicy: Always dnsPolicy: ClusterFirst --- # apiVersion: v1 # kind: Service # metadata: # name: ib-gateway # namespace: trading # labels: # app: ib-gateway # spec: # type: ClusterIP # Internal-only, not exposed publicly # clusterIP: None # Headless service (optional, remove if you want a stable ClusterIP) # selector: # app: ib-gateway # ports: # - name: paper-trading # port: 4002 # targetPort: 4002 # protocol: TCP # - name: live-trading # port: 4001 # targetPort: 4001 # protocol: TCP # sessionAffinity: ClientIP # Stick to same pod (important for stateful TWS sessions) # sessionAffinityConfig: # clientIP: # timeoutSeconds: 3600 # 1 hour session stickiness apiVersion: v1 kind: Service metadata: name: ib-gateway namespace: trading labels: app: ib-gateway spec: type: ClusterIP selector: app: ib-gateway ports: - name: paper-trading port: 4002 targetPort: 4003 # <-- proxy sidecar, not the gateway directly protocol: TCP - name: live-trading port: 4001 targetPort: 4001 protocol: TCP sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 3600