ci(railway): wire up frontend + mcp_server services for Hermes deployment

Adds the two missing pieces so all three services (backend, frontend, MCP)
can be deployed independently on Railway. Hermes agents need the MCP server
on a public URL, which itself proxies to the backend over the private network.

mcp_server/
  - New Dockerfile (no Dockerfile existed) installs the package via pip and
    defaults transport=streamable-http on 0.0.0.0:7800 so the server is
    reachable to remote MCP clients out of the box. stdio still works for
    local pipx/uvx invocations because those don't inherit container ENV.
  - .dockerignore drops tests/__pycache__ from the build context.
  - railway.json pins the DOCKERFILE builder.

frontend/
  - nginx.conf -> nginx.conf.template: proxy_pass now reads ${BACKEND_URL}.
    The official nginx:alpine entrypoint runs envsubst on /etc/nginx/templates
    at start; NGINX_ENVSUBST_FILTER=BACKEND_URL keeps nginx's own $host,
    $remote_addr, etc. literal.
  - Dockerfile drops the `frontend/` path prefix on COPY, so the build context
    can be the frontend/ subdirectory itself (Railway Root Directory pattern).
    Default BACKEND_URL=http://backend:5000 keeps docker-compose unchanged.
  - railway.json pins DOCKERFILE + /health probe.

docker-compose.yml
  - Frontend service: context=./frontend, BACKEND_URL env var (default still
    resolves to the in-network `backend` hostname).
This commit is contained in:
Claude
2026-05-06 22:55:53 +00:00
parent 7460e1d92c
commit 50c64b3070
7 changed files with 85 additions and 8 deletions
+7 -2
View File
@@ -135,14 +135,19 @@ services:
# ========================
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
context: ./frontend
dockerfile: Dockerfile
args:
RUNTIME_IMAGE: ${IMAGE_PREFIX:-}nginx:1.25-alpine
container_name: quantdinger-frontend
restart: unless-stopped
ports:
- "${FRONTEND_PORT:-8888}:80"
environment:
# nginx envsubst will inject this into the proxy_pass directive at start.
# Override with the backend's reachable URL when deploying outside compose
# (e.g. Railway: http://${{Backend.RAILWAY_PRIVATE_DOMAIN}}:5000).
- BACKEND_URL=${BACKEND_URL:-http://backend:5000}
depends_on:
- backend
networks:
+10 -4
View File
@@ -1,7 +1,7 @@
# QuantDinger Frontend
# Build context: repository root (see docker-compose.yml)
# Build context: ./frontend (see docker-compose.yml)
#
# Serves the pre-built SPA from `frontend/dist/` (nginx). Vue source lives in a
# Serves the pre-built SPA from `dist/` (nginx). Vue source lives in a
# separate private repository: build there, then copy `dist/` into this tree.
ARG RUNTIME_IMAGE=nginx:1.25-alpine
@@ -10,8 +10,14 @@ FROM ${RUNTIME_IMAGE}
RUN apk add --no-cache curl
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
COPY frontend/dist/ /usr/share/nginx/html/
# nginx:alpine auto-runs envsubst on /etc/nginx/templates/*.template at start.
# Pin the filter so only ${BACKEND_URL} is substituted — without it, nginx's
# own $-variables ($host, $remote_addr, ...) would also get clobbered.
ENV NGINX_ENVSUBST_FILTER=BACKEND_URL \
BACKEND_URL=http://backend:5000
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
COPY dist/ /usr/share/nginx/html/
EXPOSE 80
@@ -23,9 +23,13 @@ server {
access_log off;
}
# API proxy to backend
# API proxy to backend. ${BACKEND_URL} is substituted at container start by
# the official nginx image's envsubst step (NGINX_ENVSUBST_FILTER pins it
# to BACKEND_URL so $host/$remote_addr/etc. below stay literal).
# docker-compose default: http://backend:5000
# Railway: set BACKEND_URL=http://${{Backend.RAILWAY_PRIVATE_DOMAIN}}:5000
location /api/ {
proxy_pass http://backend:5000/api/;
proxy_pass ${BACKEND_URL}/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile"
},
"deploy": {
"healthcheckPath": "/health",
"healthcheckTimeout": 30,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}
+13
View File
@@ -0,0 +1,13 @@
__pycache__/
*.pyc
*.pyo
*.pyd
*.egg-info/
.pytest_cache/
.tox/
.venv/
venv/
.env
tests/
.git
.DS_Store
+25
View File
@@ -0,0 +1,25 @@
ARG BASE_IMAGE=python:3.12-slim-bookworm
FROM ${BASE_IMAGE}
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY pyproject.toml README.md LICENSE ./
COPY src/ ./src/
RUN pip install --no-cache-dir .
# Cloud defaults: bind streamable-http to all interfaces on the platform-supplied
# port. Override any of these on the platform side; stdio still works locally
# because pipx/uvx invocations don't inherit these.
ENV QUANTDINGER_MCP_TRANSPORT=streamable-http \
QUANTDINGER_MCP_HOST=0.0.0.0 \
QUANTDINGER_MCP_PORT=7800 \
PYTHONUNBUFFERED=1
EXPOSE 7800
CMD ["quantdinger-mcp"]
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile"
},
"deploy": {
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}