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
+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")