Add Railway deployment config for 24/7 cloud paper trading

- Dockerfile: add templates copy, ENV PORT, update CMD to live engine
- Create src/live/health.py: threaded HTTP health server (/health, /state)
- Wire health server into src/live/run.py before engine loop
- Dashboard: add unauthenticated /health endpoint, use PORT env var
- Create railway.toml with Dockerfile builder and health check config
- docker-compose.yml: rename service to live-engine, add PORT env vars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Brent Neale
2026-02-19 15:53:54 +10:00
parent 65ac3b296a
commit 4e2db68049
6 changed files with 82 additions and 4 deletions
+3 -1
View File
@@ -12,8 +12,10 @@ RUN pip install --no-cache-dir -r requirements.docker.txt
COPY config/system.yaml config/system.yaml
COPY src/ src/
COPY sql/ sql/
COPY templates/ templates/
RUN mkdir -p logs models
ENV PYTHONUNBUFFERED=1
CMD ["python", "src/order_executor.py"]
ENV PORT=8080
CMD ["python", "src/live/run.py"]
+8 -2
View File
@@ -1,13 +1,17 @@
services:
fx-quant:
live-engine:
build: .
container_name: fx-quant
container_name: fx-quant-engine
env_file:
- config/.env
environment:
- PORT=8080
volumes:
- ./config:/app/config
- ./logs:/app/logs
- ./models:/app/models
ports:
- "8080:8080"
restart: unless-stopped
dashboard:
@@ -16,6 +20,8 @@ services:
command: python src/dashboard.py
env_file:
- config/.env
environment:
- PORT=5000
volumes:
- ./config:/app/config
- ./logs:/app/logs:ro
+9
View File
@@ -0,0 +1,9 @@
[build]
builder = "dockerfile"
dockerfilePath = "Dockerfile"
[deploy]
healthcheckPath = "/health"
healthcheckTimeout = 30
restartPolicyType = "on_failure"
restartPolicyMaxRetries = 5
+6 -1
View File
@@ -161,6 +161,11 @@ def validate_config(cfg):
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@app.route("/health")
def health():
return jsonify({"status": "ok"})
@app.route("/")
@requires_auth
def index():
@@ -589,4 +594,4 @@ def _parse_int_list(s):
# Main
# ---------------------------------------------------------------------------
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=False)
+53
View File
@@ -0,0 +1,53 @@
"""
Lightweight threaded HTTP health server for Railway liveness checks.
Runs as a daemon thread so it never blocks the main trading engine loop.
"""
import json
import os
import time
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
_start_time = time.time()
STATE_FILE = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
"logs", "live_state.json",
)
class _HealthHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
body = json.dumps({
"status": "ok",
"uptime_seconds": round(time.time() - _start_time, 1),
})
self._respond(200, body)
elif self.path == "/state":
try:
with open(STATE_FILE) as f:
body = f.read()
except FileNotFoundError:
body = json.dumps({"error": "state file not found"})
self._respond(200, body)
else:
self._respond(404, json.dumps({"error": "not found"}))
def _respond(self, code: int, body: str):
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(body.encode())
def log_message(self, format, *args):
pass # suppress request logs to keep engine output clean
def start_health_server():
port = int(os.environ.get("PORT", 8080))
server = HTTPServer(("0.0.0.0", port), _HealthHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
print(f"Health server listening on :{port}")
+3
View File
@@ -89,6 +89,9 @@ def build_slots(config: dict) -> list[StrategySlot]:
def main():
from src.live.health import start_health_server
start_health_server()
config = load_config()
if not config:
print("ERROR: No phase2 config found in config/system.yaml")