Deploying Your Algorithm to Railway
Step-by-step guide to deploying a Python trading bot to Railway — from repo setup and IB Gateway configuration to environment variables, health checks, and auto-restart.
Why Railway?
Railway is a Platform-as-a-Service (PaaS) that runs your code on managed servers. For algorithmic trading, it offers three things you need: always-on uptime (your algo runs 24/7 even when your laptop is off), easy environment variable management for IBKR credentials, and automatic restarts if your process crashes. It is the fastest path from "working locally" to "running in production" for solo developers.
| Feature | Railway | AWS EC2 / DigitalOcean | Local Machine |
|---|---|---|---|
| Setup time | ~15 minutes | 1–2 hours | Immediate but not always-on |
| Cost | $5–$20/month | $10–$40/month | $0 (but laptop must stay on) |
| Uptime | 99.9% SLA | 99.9% SLA (with config) | Depends on your machine |
| Deployment | Push to GitHub | SSH + manual setup | python scheduler.py |
| Auto-restart | Built-in | Requires systemd/PM2 | No |
| Logs | Live in dashboard | CloudWatch / journald | Terminal only |
Architecture Overview
Your Railway project will have two services: (1) IB Gateway running headless in a Docker container, and (2) your Python trading bot connecting to it via the internal Railway network.
Railway Project
├── Service: ib-gateway (Docker image: ghcr.io/gnzsnz/ib-gateway)
│ ├── Port 4002 (paper) ← your bot connects here
│ ├── VNC port 5900 ← optional remote desktop
│ └── Env: TWS_USERID, TWS_PASSWORD, TRADING_MODE=paper
│
└── Service: sma-bot (your GitHub repo)
├── scheduler.py ← entry point
├── sma_strategy.py ← signal + order logic
├── requirements.txt
└── Env: IB_HOST, IB_PORT=4002, SYMBOL, FAST_PERIOD, SLOW_PERIODStep 1 — Prepare Your Repository
Create a new GitHub repo with the following structure. Keep your IBKR username and password out of the code — they go in Railway environment variables.
my-sma-bot/
├── sma_strategy.py ← signal + IBKR execution logic
├── scheduler.py ← daily run scheduler
├── requirements.txt ← Python dependencies
├── Procfile ← tells Railway what command to run
└── .gitignore ← must include .env# requirements.txt
ib_insync==0.9.86
pandas==2.2.2
schedule==1.2.1
pytz==2024.1# Procfile — Railway reads this to know what to run
worker: python scheduler.pyNever Commit Credentials
Add .env to your .gitignore immediately. One accidental push of your IBKR username/password to a public repo will result in unauthorized account access. Use Railway environment variables exclusively.
Step 2 — Update Your Strategy to Use Environment Variables
Replace hardcoded values in your strategy with os.environ reads. This lets you change symbols or parameters without modifying code.
# sma_strategy.py — top of file
import os
SYMBOL = os.environ.get('SYMBOL', 'AAPL')
FAST_PERIOD = int(os.environ.get('FAST_PERIOD', '10'))
SLOW_PERIOD = int(os.environ.get('SLOW_PERIOD', '30'))
IB_HOST = os.environ.get('IB_HOST', '127.0.0.1')
IB_PORT = int(os.environ.get('IB_PORT', '4002')) # 4002 = IB Gateway paper
CLIENT_ID = int(os.environ.get('CLIENT_ID', '1'))
CAPITAL_PCT = float(os.environ.get('CAPITAL_PCT', '0.95'))Step 3 — Create a Railway Account and Project
- Go to railway.app and sign up with your GitHub account
- Click "New Project" → "Deploy from GitHub repo"
- Select your my-sma-bot repository
- Railway auto-detects the Procfile and starts a build
Step 4 — Add the IB Gateway Service
IB Gateway needs to run as a separate service in the same Railway project. The community maintains a Docker image that runs IB Gateway headlessly.
- In your Railway project, click "+ New Service" → "Docker Image"
- Image: ghcr.io/gnzsnz/ib-gateway:latest
- Click on the service → "Variables" tab → add: TWS_USERID (your IBKR username), TWS_PASSWORD (your IBKR password), TRADING_MODE=paper, TWS_ACCEPT_INCOMING=yes
- Under "Settings" → expose internal port 4002
- Deploy the service — first startup takes ~60 seconds as it initializes IB Gateway
Use Paper Trading First
Set TRADING_MODE=paper in the IB Gateway service to connect to IBKR's paper trading environment. Only switch to TRADING_MODE=live after 30+ days of consistent results. The paper environment is completely separate from your real account.
Step 5 — Configure Environment Variables for Your Bot
In your sma-bot service → Variables tab, add the following. IB_HOST uses the Railway internal hostname for the ib-gateway service (Railway automatically handles internal service discovery).
# Railway Environment Variables — sma-bot service
SYMBOL=AAPL
FAST_PERIOD=10
SLOW_PERIOD=30
CAPITAL_PCT=0.95
IB_PORT=4002
CLIENT_ID=1
# Railway internal hostname for ib-gateway service:
# Go to ib-gateway service → Settings → copy "Private Domain"
# It looks like: ib-gateway.railway.internal
IB_HOST=ib-gateway.railway.internalStep 6 — Add Reconnection Logic to Your Bot
In production, IBKR connections drop. Your scheduler must handle this gracefully instead of crashing.
# Add this to sma_strategy.py run() function
def run_with_retry(max_attempts: int = 3):
"""Wrap run() with retry logic for dropped connections."""
for attempt in range(1, max_attempts + 1):
try:
run()
return # Success — exit retry loop
except Exception as exc:
log.error('Attempt %d/%d failed: %s', attempt, max_attempts, exc)
if attempt < max_attempts:
log.info('Retrying in 30 seconds...')
time.sleep(30)
else:
log.critical('All %d attempts failed. Alerting and exiting.', max_attempts)
# Optional: send an alert via email/SMS here
raise# In scheduler.py, replace run() with run_with_retry()
from sma_strategy import run_with_retry
def job():
if not is_market_day():
return
print(f'Running strategy...')
run_with_retry(max_attempts=3)Step 7 — Deploy and Verify
- Push your final code to GitHub — Railway auto-deploys on every push to main
- In Railway dashboard → sma-bot → "Logs" tab — confirm "Scheduler started" appears
- Wait for 4:05 PM ET — check logs for the signal evaluation and any orders
- In IBKR paper account (via IBKR mobile or web) — verify any BUY orders appear in trade history
- If no orders: check logs for errors; confirm IB Gateway service shows "Connected" status
Step 8 — Monitoring in Production
Set up basic monitoring so you know when something breaks. At minimum, log every strategy run result and set Railway to alert you on crashes.
# Add to scheduler.py — simple log-based monitoring
import smtplib
from email.mime.text import MIMEText
ALERT_EMAIL = os.environ.get('ALERT_EMAIL', '')
SMTP_HOST = os.environ.get('SMTP_HOST', '')
def send_alert(subject: str, body: str):
"""Send email alert on critical failures."""
if not ALERT_EMAIL or not SMTP_HOST:
return
try:
msg = MIMEText(body)
msg['Subject'] = f'[SMA Bot] {subject}'
msg['From'] = ALERT_EMAIL
msg['To'] = ALERT_EMAIL
with smtplib.SMTP(SMTP_HOST) as server:
server.send_message(msg)
except Exception:
pass # Don't let alert failure crash the process
def job():
if not is_market_day():
return
try:
run_with_retry(max_attempts=3)
send_alert('Daily Run OK', f'Strategy ran at {datetime.now(ET).strftime("%H:%M ET")}. Check IBKR for orders.')
except Exception as exc:
send_alert('CRITICAL — Bot Failed', str(exc))Common Issues and Fixes
| Error | Likely Cause | Fix |
|---|---|---|
| Connection refused on port 4002 | IB Gateway not yet started | Add a 90-second sleep before connecting; IB Gateway takes ~60s to initialize |
| Authentication failed | Wrong credentials in env vars | Check TWS_USERID and TWS_PASSWORD; IBKR usernames are case-sensitive |
| No historical data returned | Market closed; requesting during off-hours | Use durationStr with a buffer; IB Gateway returns empty on weekends for TRADES |
| Order rejected: Outside RTH | Trying to trade outside 9:30–4:00 ET | Add market hours check before placing orders (useRTH=True already on data) |
| Client ID already in use | Another instance still connected | Change CLIENT_ID env var; use different IDs per Railway instance |
You Are Now Running Live
Once your Railway deployment shows the scheduler running, IB Gateway connected, and you see a logged signal evaluation at 4:05 PM ET — your algorithmic trading system is live. Start with paper trading for 30 days. Monitor logs daily. After 30 days of consistent, expected behavior, you are ready to flip TRADING_MODE=live and allocate real capital — starting at 10% of your intended size.
- Railway runs your bot as a persistent long-running process on a managed server
- IB Gateway runs headless alongside your Python process using the ib-gateway Docker image
- Environment variables store credentials — never commit them to your repository
- Health checks and auto-restart protect against crashes and TWS disconnects