genesis-railway
Deploys a genesis monorepo to Railway using the Railway MCP tools. The layout it expects: services/<app>-server (Go API with a Dockerfile and /health), services/<app>-web (nginx-served React app with a Dockerfile), and Postgres.
Railway project: <app>
├── Postgres # Railway Postgres template
├── <app>-server # builds services/<app>-server/Dockerfile
└── <app>-web # builds services/<app>-web/Dockerfile, gets the public domain
web ──▶ server ──▶ Postgres (private networking)
Prerequisites
Railway MCP server (mcp__railway__* tools). If they are not available, stop and tell the user — do not fall back to guessing CLI commands.
Railway CLI (npm i -g @railway/cli or brew install railway), used only for railway login, railway init, railway add, railway link, and reading resolved variables.
Rules
Use MCP tools for every Railway mutation. Settings, variables, volumes, sources, and domains go through mcp__railway__*. The CLI is for login, link, and railway add --database postgres only.
Never add railway.json / railway.toml. Config-as-code is deprecated and stops working 2026-12-01, and no MCP tool writes it. All build/deploy settings are set on the service via update-service (step 3). Its successor, .railway/railway.ts (railway config pull/plan/apply, CLI ≥ 5), is out of scope.
Create services in dependency order. A ${{Service.VAR}} reference is empty until the referenced service exists: Postgres, then server, then web.
Find required variables before creating anything. Grep cmd/<app>/main.go for config checks that os.Exit — each needs a variable. Config keys map to env with . → _ (http.port → HTTP_PORT).
Use the Railway Postgres template. It ships with a volume, the Database tab, backups, and DATABASE_URL / PG* variables. Only if a migration runs CREATE EXTENSION postgis fall back to a postgis/postgis image service (see PostGIS exception) — the template image lacks it.
Resolve the project ID first. If the repo root is linked, read ~/.railway/config.json (.projects["<abs repo path>"].project). Otherwise list-projects, or railway init --name <app> from the repo root.
Steps
- Create services —
railway add --database postgres from the linked repo root (the remote MCP has no template tool; the CLI names the service Postgres), then create-service <app>-server, then <app>-web (no image).
- Variables via
set-variables with skipDeploys: true:
- Server:
PORT=8080, HTTP_PORT=${{PORT}} (both — the server reads HTTP_PORT; without it the healthcheck probes the wrong port), DB_URL=${{Postgres.DATABASE_URL}}?sslmode=disable, LOG_LEVEL=info, plus anything found in rule 4 (generate secrets, report them to the user).
- Web:
BACKEND_URL=http://${{<app>-server.RAILWAY_PRIVATE_DOMAIN}}:8080
- Settings via
update-service on server and web: rootDirectory: services/<app>-{server,web}, dockerfilePath: Dockerfile, watchPatterns: ["services/<app>-{server,web}/**"] (repo-root relative; a stale pattern matches nothing, so auto-deploy silently never fires), restartPolicyType: ON_FAILURE, restartPolicyMaxRetries: 1; server also healthcheckPath: /health.
- Source —
connect-service-source on server, then web, with repo: <owner>/<repo>, branch: main. This attaches GitHub auto-deploy and starts the first build, so do it after steps 2–3.
- Domain —
generate-domain on web.
- Link subdirs so the CLI works from inside them (absolute paths for
cd):cd <abs>/services/<app>-server && railway link -p <app> -e production -s <app>-server
cd <abs>/services/<app>-web && railway link -p <app> -e production -s <app>-web
Additional infra
If docker-compose.yml has more infra than Postgres (Redis, MinIO, etc.), create each from the official Railway template, not a plain image, and do it before the server so its variables can be referenced. Fall back to create-service with image: only when no template exists.
PostGIS exception
Replaces step 1's Postgres and adds what the template would have provided:
create-service Postgres with image: postgis/postgis:<tag from docker-compose.yml>.
set-variables (skipDeploys: true): POSTGRES_DB=<app_underscore>, POSTGRES_USER=postgres, POSTGRES_PASSWORD=<openssl rand -hex 24>, PGDATA=/var/lib/postgresql/data/pgdata (volume root isn't empty), PGHOST=${{RAILWAY_PRIVATE_DOMAIN}}, PGPORT=5432, PGUSER=${{POSTGRES_USER}}, PGPASSWORD=${{POSTGRES_PASSWORD}}, PGDATABASE=${{POSTGRES_DB}} (the PG* set turns on the dashboard's Database tab), DATABASE_URL=postgres://${{POSTGRES_USER}}:${{POSTGRES_PASSWORD}}@${{RAILWAY_PRIVATE_DOMAIN}}:5432/${{POSTGRES_DB}}.
create-volume on Postgres, mountPath: /var/lib/postgresql/data.
- No Database tab in the dashboard; that only comes with the template.
Verification procedure
get-status until every service reports SUCCESS.
curl https://<web domain>/health returns the server's health response through the nginx proxy.
- If proxied paths 500, check
BACKEND_URL from a linked subdir with railway variables --json (MCP list-variables redacts references). If it renders as http://:8080, web built before server existed — redeploy web.
Common mistakes to watch for
- Setting only
PORT. The server reads HTTP_PORT; without it the healthcheck probes the wrong port and every deploy fails.
- Skipping
PGDATA on a postgis image service. The volume root isn't empty, so Postgres refuses to init without a subdirectory.
- Connecting the source before variables/settings. The first build fires immediately and runs with missing config.
- Using a plain image service when the template would do. You lose the Database tab and backups for nothing.
- Using the placeholder
project-00 in watchPatterns or service names. References like ${{project-00-server.RAILWAY_PRIVATE_DOMAIN}} resolve to empty.
1---2name: genesis-railway3description: Use when deploying a project scaffolded with genesis (services/<app>-server Go API, services/<app>-web React app, Postgres) to Railway — creating the Railway project, services, variables, volume, GitHub auto-deploy, and public domain via the Railway MCP server.4---56# genesis-railway78Deploys a genesis monorepo to Railway using the Railway MCP tools. The layout it expects: `services/<app>-server` (Go API with a `Dockerfile` and `/health`), `services/<app>-web` (nginx-served React app with a `Dockerfile`), and Postgres.910```11Railway project: <app>12├── Postgres # Railway Postgres template13├── <app>-server # builds services/<app>-server/Dockerfile14└── <app>-web # builds services/<app>-web/Dockerfile, gets the public domain15 web ──▶ server ──▶ Postgres (private networking)16```1718## Prerequisites1920**Railway MCP server** (`mcp__railway__*` tools). If they are not available, stop and tell the user — do not fall back to guessing CLI commands.2122**Railway CLI** (`npm i -g @railway/cli` or `brew install railway`), used only for `railway login`, `railway init`, `railway add`, `railway link`, and reading resolved variables.2324## Rules25261. **Use MCP tools for every Railway mutation.** Settings, variables, volumes, sources, and domains go through `mcp__railway__*`. The CLI is for login, link, and `railway add --database postgres` only.27282. **Never add `railway.json` / `railway.toml`.** Config-as-code is deprecated and stops working 2026-12-01, and no MCP tool writes it. All build/deploy settings are set on the service via `update-service` (step 3). Its successor, `.railway/railway.ts` (`railway config pull/plan/apply`, CLI ≥ 5), is out of scope.29303. **Create services in dependency order.** A `${{Service.VAR}}` reference is empty until the referenced service exists: Postgres, then server, then web.31324. **Find required variables before creating anything.** Grep `cmd/<app>/main.go` for config checks that `os.Exit` — each needs a variable. Config keys map to env with `.` → `_` (`http.port` → `HTTP_PORT`).33345. **Use the Railway Postgres template.** It ships with a volume, the Database tab, backups, and `DATABASE_URL` / `PG*` variables. Only if a migration runs `CREATE EXTENSION postgis` fall back to a `postgis/postgis` image service (see PostGIS exception) — the template image lacks it.35366. **Resolve the project ID first.** If the repo root is linked, read `~/.railway/config.json` (`.projects["<abs repo path>"].project`). Otherwise `list-projects`, or `railway init --name <app>` from the repo root.3738## Steps39401. **Create services** — `railway add --database postgres` from the linked repo root (the remote MCP has no template tool; the CLI names the service `Postgres`), then `create-service` `<app>-server`, then `<app>-web` (no image).412. **Variables** via `set-variables` with `skipDeploys: true`:42 - Server: `PORT=8080`, `HTTP_PORT=${{PORT}}` (both — the server reads `HTTP_PORT`; without it the healthcheck probes the wrong port), `DB_URL=${{Postgres.DATABASE_URL}}?sslmode=disable`, `LOG_LEVEL=info`, plus anything found in rule 4 (generate secrets, report them to the user).43 - Web: `BACKEND_URL=http://${{<app>-server.RAILWAY_PRIVATE_DOMAIN}}:8080`443. **Settings** via `update-service` on server and web: `rootDirectory: services/<app>-{server,web}`, `dockerfilePath: Dockerfile`, `watchPatterns: ["services/<app>-{server,web}/**"]` (repo-root relative; a stale pattern matches nothing, so auto-deploy silently never fires), `restartPolicyType: ON_FAILURE`, `restartPolicyMaxRetries: 1`; server also `healthcheckPath: /health`.454. **Source** — `connect-service-source` on server, then web, with `repo: <owner>/<repo>`, `branch: main`. This attaches GitHub auto-deploy and starts the first build, so do it after steps 2–3.465. **Domain** — `generate-domain` on web.476. **Link subdirs** so the CLI works from inside them (absolute paths for `cd`):48 ```sh49 cd <abs>/services/<app>-server && railway link -p <app> -e production -s <app>-server50 cd <abs>/services/<app>-web && railway link -p <app> -e production -s <app>-web51 ```5253## Additional infra5455If `docker-compose.yml` has more infra than Postgres (Redis, MinIO, etc.), create each from the official Railway template, not a plain image, and do it before the server so its variables can be referenced. Fall back to `create-service` with `image:` only when no template exists.5657## PostGIS exception5859Replaces step 1's Postgres and adds what the template would have provided:6061- `create-service` `Postgres` with `image: postgis/postgis:<tag from docker-compose.yml>`.62- `set-variables` (`skipDeploys: true`): `POSTGRES_DB=<app_underscore>`, `POSTGRES_USER=postgres`, `POSTGRES_PASSWORD=<openssl rand -hex 24>`, `PGDATA=/var/lib/postgresql/data/pgdata` (volume root isn't empty), `PGHOST=${{RAILWAY_PRIVATE_DOMAIN}}`, `PGPORT=5432`, `PGUSER=${{POSTGRES_USER}}`, `PGPASSWORD=${{POSTGRES_PASSWORD}}`, `PGDATABASE=${{POSTGRES_DB}}` (the `PG*` set turns on the dashboard's Database tab), `DATABASE_URL=postgres://${{POSTGRES_USER}}:${{POSTGRES_PASSWORD}}@${{RAILWAY_PRIVATE_DOMAIN}}:5432/${{POSTGRES_DB}}`.63- `create-volume` on Postgres, `mountPath: /var/lib/postgresql/data`.64- No Database tab in the dashboard; that only comes with the template.6566## Verification procedure67681. `get-status` until every service reports `SUCCESS`.692. `curl https://<web domain>/health` returns the server's health response through the nginx proxy.703. If proxied paths 500, check `BACKEND_URL` from a linked subdir with `railway variables --json` (MCP `list-variables` redacts references). If it renders as `http://:8080`, web built before server existed — `redeploy` web.7172## Common mistakes to watch for7374- **Setting only `PORT`.** The server reads `HTTP_PORT`; without it the healthcheck probes the wrong port and every deploy fails.75- **Skipping `PGDATA` on a postgis image service.** The volume root isn't empty, so Postgres refuses to init without a subdirectory.76- **Connecting the source before variables/settings.** The first build fires immediately and runs with missing config.77- **Using a plain image service when the template would do.** You lose the Database tab and backups for nothing.78- **Using the placeholder `project-00` in `watchPatterns` or service names.** References like `${{project-00-server.RAILWAY_PRIVATE_DOMAIN}}` resolve to empty.