| name | railway |
| description | Reference for deploying, configuring, and troubleshooting applications on Railway (railway.com). Covers config-as-code (`railway.json` / `railway.toml` schema), services, environments (persistent + PR), variables + reference variables, cron jobs, Functions, builds (Dockerfile + Railpack), deployments (healthchecks, restart policy, pre-deploy, regions, scaling), and a Reflex-on-Railway troubleshooting guide including an Alpine Dockerfile pattern that survives free-tier memory limits. Invoke explicitly (e.g. via `/railway`) when working on Railway config — the skill does not auto-trigger on incidental Railway mentions. |
Railway
Railway is a Heroku-style PaaS. A project groups one or more services; each service is deployed into one or more environments. Every service has build + deploy settings that can live in a railway.json or railway.toml file alongside the code — this is the recommended way to configure Railway for an AI agent, because it's reviewable, diffable, and reproducible across PR environments.
Mental model
Project ──▶ Environment (production | staging | pr-123 | ...)
└─▶ Service (container from GitHub repo | Docker image | local dir)
└─▶ Deployment (one attempt: build → pre-deploy → start → healthcheck → active)
- Project — top-level container. Holds Shared Variables usable across services.
- Environment — isolated copy of all services.
production exists by default. Persistent envs (e.g. staging) stick around; PR envs are ephemeral per pull request.
- Service — deploy target. Sources: GitHub repo, Docker Hub / ghcr.io / quay.io / gcr image, or local via
railway up. Service names: max 32 chars.
- Deployment — one build+run attempt. Lifecycle:
INITIALIZING → BUILDING → DEPLOYING → SUCCESS | FAILED | CRASHED | REMOVED.
Any service change creates a staged change that must be reviewed and deployed.
Config as code (quickstart)
Drop a railway.toml at the repo root. Config-in-code always overrides dashboard settings — but only for that deployment; the dashboard itself isn't updated.
Minimal railway.toml:
[build]
builder = "DOCKERFILE"
watchPatterns = ["src/**", "Dockerfile"]
[deploy]
startCommand = "python main.py"
healthcheckPath = "/health"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
drainingSeconds = 30
preDeployCommand = ["python manage.py migrate"]
[env]
NODE_ENV = "production"
The equivalent JSON form:
{
"$schema": "https://railway.com/railway.schema.json",
"build": { "builder": "DOCKERFILE" },
"deploy": {
"startCommand": "python main.py",
"healthcheckPath": "/health",
"restartPolicyType": "ON_FAILURE"
}
}
Per-environment overrides (the env name must match a real env; pr is a magic name for ephemeral PR envs):
[environments.staging.deploy]
startCommand = "python main.py --env staging"
[environments.pr.deploy]
startCommand = "python main.py --env pr"
Resolution priority (highest first): environment-specific config → base config → dashboard service settings. For PR deploys: named-env config → pr config → base-env config → base config → dashboard.
Full schema, every field, every value: references/config-schema.md.
Deploying a Reflex app (the primary workflow this skill is tuned for)
Reflex has Railway-specific sharp edges. Internalize these before editing anything:
- Reflex listens on
$PORT. Railway injects PORT; reflex run --env prod reads it. Don't hardcode 3000 in the start command.
- Reflex builds the frontend on first startup in prod mode (not during
docker build). Peak memory is during first boot, not image build. Plan memory accordingly — this is the #1 cause of Railway OOMs for Reflex apps.
- Healthchecks against
/ are fragile on Reflex because / serves a SPA. Options: leave healthcheckPath unset for the first deploy, use a short healthcheckTimeout only after verifying the app responds, or add an explicit /ping endpoint. Reflex does not guarantee /ping by default, so verify before trusting it.
- Use Alpine-based Python images — and install the bun-installer runtime deps. Slim images exhaust the free-tier 512MB build budget. Alpine is the right base, but
python:3.11-alpine is missing bash, curl, libstdc++, and libgcc — Reflex shells out to bash <script> on first boot to download bun, and bun itself needs libstdc++/libgcc on musl. Install those as a non-virtual package (don't put them in .build-deps — they need to persist to runtime). See references/troubleshooting-reflex.md for the full Dockerfile.
- Commit
uv.lock. Many Reflex starter templates gitignore it (treating the app like a library). railway up honors .gitignore, so the file silently doesn't reach the builder, and uv sync --frozen fails on "Unable to find lockfile". Remove from .gitignore, commit, COPY pyproject.toml uv.lock ./.
- Postgres wiring. Add a Postgres service via "New → Database → PostgreSQL", then reference it from the Reflex service as
DATABASE_URL=${{Postgres.DATABASE_URL}}. Always use the private URL for service-to-service — it's free, faster, and doesn't leave Railway's network. The public URL incurs egress.
- Single-service, one process vs two. The simplest deploy runs
reflex run --env prod in one container (Python serves backend on :8000 and frontend on :3000). For a more robust production setup — faster cold starts, explicit $PORT binding, no runtime Next.js build — use a single service that runs two processes via an entrypoint script: reflex run --backend-only plus Caddy serving a pre-exported frontend from /srv. This is still one Railway service; the Caddyfile already in many Reflex starter repos is designed for this pattern. See references/troubleshooting-reflex.md → "Production pattern: Caddy + static frontend export".
Example railway.toml for a typical single-service Reflex app:
[build]
builder = "DOCKERFILE"
watchPatterns = ["Dockerfile", "pyproject.toml", "uv.lock", "web/**"]
[deploy]
startCommand = ".venv/bin/reflex run --env prod"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 5
drainingSeconds = 30
healthcheckPath = "/"
healthcheckTimeout = 30
[env]
REFLEX_ENV = "prod"
PYTHONUNBUFFERED = "1"
When to read which reference
Load only the file(s) relevant to the task:
- Editing
railway.json / railway.toml → references/config-schema.md — every field, value, nesting rule.
- Services, environments, PR envs, Focused PR envs, monorepos → references/services-and-environments.md.
- Variables, reference syntax, shared vars, sealed vars, Railway-provided vars → references/variables.md.
- Build issues: Dockerfile detection, Railpack, cache mounts, watch patterns, build args → references/builds.md.
- Deploy behavior: healthchecks, restart policy, pre-deploy, regions, scaling, draining, multi-region → references/deployments.md.
- Cron jobs (schedule format, exit requirements) and TypeScript Bun Functions → references/cron-and-functions.md.
- OOM, slow builds, "No start command could be found", SIGTERM handling, and the Reflex Dockerfile war story → references/troubleshooting-reflex.md.
CLI quick reference
railway login # auth
railway link # link cwd to a project + service
railway up # deploy current dir to the linked service
railway run <cmd> # run cmd locally with project env vars injected
railway shell # interactive shell with env loaded
railway logs # stream service logs
railway variables # list env vars (sealed values are hidden)
railway status # show current project / env / service
railway redeploy # redeploy the current active deployment
railway down # remove the most recent deployment
railway deployment list # list recent deployments for this service
Run railway <cmd> --help for full options.
Non-obvious behaviors to remember
- Dockerfile auto-wins. If a file literally named
Dockerfile (capital D) exists, Railway uses it regardless of builder setting. Use RAILWAY_DOCKERFILE_PATH=Dockerfile.backend to pick a different one.
- Healthcheck hostname is
healthcheck.railway.app — if your app filters hosts, allow this one or you'll see "service unavailable".
- Healthchecks are one-shot — only run at deploy, not continuous. For continuous, add an external monitor.
- Start-command
$VAR expansion requires a shell wrap for Dockerfile/image services: "/bin/sh -c \"exec python main.py --port $PORT\"". Railpack-built services already run via shell.
- Volumes ⇒ deploy downtime. Railway refuses to run two deployments with the same volume mounted, so zero-downtime isn't possible there.
- Cron semantics. Min frequency 5 min. Schedules are UTC. If the previous run is still active when the next fires, Railway skips it — the process MUST exit cleanly.
- Sealed variables are one-way. Cannot be un-sealed, not in
railway variables, not copied to PR envs, not copied when duplicating services/envs.
- Free plan caps. ~512MB RAM,
restartPolicyType = ALWAYS unavailable, ON_FAILURE capped at 10 retries, private Docker registries disabled.
- Ephemeral storage limit is 1GB free / 100GB paid. Exceeding it forces a restart. Use a volume for anything that must persist or exceed that.
Operating style for this skill
When asked to edit Railway config:
- Read the current
railway.toml / railway.json and Dockerfile first.
- Before changing
healthcheckPath, verify the endpoint returns 200 in the actual running app — a wrong path wedges deploys for 5 minutes per attempt.
- Before suggesting
restartPolicyType = ALWAYS, check the plan — it silently no-ops on free.
- When adding env-specific overrides, confirm the env name exists (
railway environment or dashboard); a typo becomes a silent no-op rather than an error.
- Prefer editing
railway.toml/railway.json over dashboard changes — code-as-config is diffable and reviewable.