mcpbeat Sign in

Railway Agent Skill

Use when deploying an app to Railway — pushing a repo or running railway up, provisioning a managed database, wiring env and reference variables across services, attaching a volume, or fixing a deploy that builds but will not serve. NOT Render (that is `render`), NOT Fly machines (that is `fly-io`), NOT a self-hosted PaaS on your own box (that is `coolify`).

8k tokens
context cost
the whole folder, loaded on every use
6
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/ericrisco/rsc-harness --skill railway

What comes with it

20 001 bytes besides the instruction
evals/README.md
evals/cases.yaml
references/cli-cookbook.md
references/config-as-code.md
scripts/verify.sh

The instruction itself

16 sections, as written by the author

Railway — ship a repo to a managed PaaS

Railway is an opinionated PaaS: connect a GitHub repo or run railway up, and it builds your

code (Railpack by default, or your Dockerfile), runs the container, networks it, and hands you a

domain. You manage *services, environments, variables, volumes, and databases* — not servers,

not an OS. This skill makes you fast and correct on Railway's specific surface, not generic

deploy theory.

repo / `railway up`  →  Railpack (or Dockerfile) build  →  container runs (binds 0.0.0.0:$PORT)
   →  variables + reference vars injected  →  private net (*.railway.internal) + public domain

Is Railway the right target?

Settle this before touching config. If the user named the platform, honor it and route.

| Want | Use | Why / route |

| --- | --- | --- |

| Push a repo, get a built+running app with zero ops | Railway (this skill) | Managed PaaS, Railpack build, by-the-minute billing |

| render.yaml Blueprint, free static sites, their dashboard model | render | Different PaaS — route to the render skill |

| fly.toml, Firecracker microVMs, fly machine, multi-region edge | fly-io | Railway has no fly machine equivalent — route to fly-io |

| Self-host the PaaS on your own Hetzner/DO box | coolify | Railway is fully managed; Coolify is BYO-server |

| Raw VPS, you manage the OS | hetzner / digitalocean | IaaS, not PaaS |

| Platform-agnostic release strategy (rolling, blue-green theory) | deployment | This skill is Railway mechanics, not strategy |

The 60-second path

# Install the CLI (pick one). npm form needs Node 16+.
brew install railway                          # macOS
npm i -g @railway/cli                          # any Node 16+ host
bash <(curl -fsSL railway.com/install.sh)      # shell installer

railway login            # opens browser; for CI use RAILWAY_TOKEN instead (no login)
railway init             # create a NEW project from this dir, OR:
railway link             # link this dir to an EXISTING project/service
railway up               # build + deploy the current dir; streams build/deploy logs

Why link vs init: init makes a fresh project; link attaches an already-created project

(the common case once the project exists in the dashboard). railway up deploys whatever is in

the working dir — no git push required.

Two ways to deploy — pick one per service

  • GitHub auto-deploy (default for most teams): connect the repo in the dashboard; every push

to the watched branch triggers a build + deploy. Best when you want CI-style "merge to ship".

  • CLI railway up: deploy the working tree directly. Best for first setup, hotfixes, or

hosts without git. Use railway up --detach to not block on logs, --service api to target a

specific service in a multi-service project (otherwise it prompts).

railway up --detach --service api --environment production

There is no railway deploy --prod — that command does not exist. Use railway up (or push to

the connected branch). To re-run the last deploy unchanged: railway redeploy.

Bind 0.0.0.0:$PORT — the #1 first-deploy failure

Railway's edge proxy and private network reach your container over an injected $PORT. If you

bind localhost/127.0.0.1, the build succeeds but every request 502s and healthchecks fail.

// Bad — only reachable from inside the container; proxy gets connection refused -> 502
app.listen(3000, "127.0.0.1");

// Good — listen on the injected port, bind all interfaces
const port = Number(process.env.PORT) || 3000;
app.listen(port, "0.0.0.0", () => console.log(`up on :${port}`));

Same rule for every stack: read $PORT, bind 0.0.0.0 (or :: — see private networking).

Never hardcode the port the proxy talks to.

Config as code — railway.json (or railway.toml)

Commit railway.json to make build/deploy reproducible. **Code config overrides the

dashboard** for the fields it sets. Per-environment overrides go under environments.<name>.

{
  "$schema": "https://railway.com/railway.schema.json",
  "build": {
    "builder": "RAILPACK",
    "buildCommand": "npm run build"
  },
  "deploy": {
    "startCommand": "node dist/server.js",
    "healthcheckPath": "/healthz",
    "healthcheckTimeout": 300,
    "restartPolicyType": "ON_FAILURE",
    "restartPolicyMaxRetries": 10
  },
  "environments": {
    "production": {
      "deploy": { "numReplicas": 2 }
    }
  }
}

builder is RAILPACK (default; Railway's successor to Nixpacks) or DOCKERFILE (a Dockerfile

is also auto-detected). restartPolicyType is ON_FAILURE | ALWAYS | NEVER. The full field

list — watchPatterns, dockerfilePath, railpackVersion, preDeployCommand,

multiRegionConfig, cronSchedule, overlapSeconds, drainingSeconds, and TOML variants —

lives in references/config-as-code.md.

Variables & reference variables

Set plain variables via CLI or dashboard; never inline secrets into railway.json (it's

committed).

railway variables                         # list
railway variables --set "LOG_LEVEL=info"  # set (older: railway variable set KEY=value)

Wire one service's value into another with template syntax instead of copying it. References

stay correct across credential rotations and across environments.

# Bad — hardcoded, breaks the moment Railway rotates the DB password
DATABASE_URL=postgresql://user:[email protected]:6543/railway

# Good — reference the Postgres service's own variable
DATABASE_URL=${{Postgres.DATABASE_URL}}

Also available: ${{shared.SENTRY_DSN}} (project-shared vars) and Railway-provided vars like

${{RAILWAY_PUBLIC_DOMAIN}}. The left side of the dot is the *service name* exactly as it

appears in the project.

Managed databases

Add Postgres / MySQL / Redis / MongoDB as services from Railway's official templates, then

reference their connection vars into the app.

railway add            # interactive: pick a database template (or use + New / cmd-K in dashboard)
railway connect        # open a db shell (psql / mongosh / redis-cli) against the service

The DB service exposes vars (e.g. DATABASE_URL, PGHOST) on its Variables tab. Reference them

into the app service as ${{Postgres.DATABASE_URL}} — do not paste the literal URL. Schema

design and SQL live in postgresdb; migrations live in db-migrations. This skill only

provisions and wires.

Volumes

Attach a volume to a service via the dashboard or CLI. Railway auto-injects

RAILWAY_VOLUME_NAME and RAILWAY_VOLUME_MOUNT_PATH at runtime — read them, never define them

yourself.

const dataDir = process.env.RAILWAY_VOLUME_MOUNT_PATH ?? "/data";

Gotcha: a volume pins the service to a single replica — a volume blocks horizontal scaling.

If you need many replicas, keep state in a managed DB or object storage, not a volume.

Environments

A project has production plus any environments you add (staging, PR environments). Each gets

its own variables and its own environments.<name> config override. Switch with

railway environment <name>; target a deploy with railway up --environment staging. Variables

and reference targets resolve *within* the active environment.

Private networking

Every service gets a DNS name under *.railway.internal. Talk service-to-service over it —

internal egress is free, and traffic never leaves Railway.

# from the app service, reach the API service privately
curl http://api.railway.internal:8080/internal/ping

IPv6 gotcha: environments created before 2025-10-16 are IPv6-only internally — bind ::

(not just 0.0.0.0) or recreate the environment. Newer environments resolve both IPv4 and IPv6.

Cross-project and cross-environment private traffic is blocked by design.

Domains

railway domain         # generate a *.up.railway.app domain for the current service

For a custom domain, add it in the service settings and Railway gives you a CNAME target. The

registrar-side DNS record work (CNAME/ALIAS at your provider) belongs to domains-dns — this

skill stops at "here is the CNAME target".

Healthcheck & failed-deploy triage

Set healthcheckPath in railway.json so Railway gates the deploy on a real readiness route.

When a deploy builds but won't go live, read railway logs and walk these in order:

| Symptom | Likely cause | Fix |

| --- | --- | --- |

| 502 on every request, healthcheck times out | App bound localhost, not 0.0.0.0:$PORT | Bind 0.0.0.0 (or ::), listen on $PORT |

| "no start command" / container exits 0 immediately | No startCommand and Railpack can't infer one | Set deploy.startCommand |

| App crashes on boot referencing a config key | Missing variable / unresolved ${{...}} reference | Set the var; check the service name in the reference |

| Healthcheck 404s | healthcheckPath points at a route that doesn't exist | Point it at an existing route or remove it |

Cost awareness

There is no permanent free tier. New accounts get a one-time $5 trial credit. Hobby is

$5/mo (includes $5 usage), Pro is $20/mo (includes $20 usage); usage is billed by the minute

and the included subscription credit is consumed first, then you pay the delta. An idle service

left running still bills. Don't leave throwaway preview services up; delete environments you're

done with.

Anti-patterns

| Anti-pattern | Why it bites | Do instead |

| --- | --- | --- |

| Binding localhost / 127.0.0.1 | Proxy + private net can't reach it → 502 | Bind 0.0.0.0 (or ::), listen on $PORT |

| Inlining a secret/connection string in railway.json | It's committed → leaked secret | Put secrets in variables; reference them |

| Hardcoding DATABASE_URL into the app's vars | Breaks on credential rotation / env change | Use ${{Postgres.DATABASE_URL}} |

| One mega-service running web + worker + cron | No independent scaling/restart, tangled logs | Split into services in one project |

| Assuming a free tier exists | Surprise — trial credit runs out, deploys stop | Plan for Hobby/Pro; watch by-the-minute usage |

| Assuming IPv4 on a pre-2025-10-16 environment | Private DNS resolves IPv6-only → connection refused | Bind :: or recreate the environment |

| Committing the .railway link as if it were config | It's local link state, not portable config | Config-as-code goes in railway.json |

Verification

  • App reads $PORT and binds 0.0.0.0 (or ::) — not a hardcoded port on localhost.
  • No secret/connection string is inlined in railway.json; secrets are variables.
  • Cross-service values use ${{Service.VAR}} references, not copied literals.
  • builder ∈ {RAILPACK, DOCKERFILE}; restartPolicyType ∈ {ON_FAILURE, ALWAYS, NEVER}.
  • A real healthcheckPath route exists if one is configured.

Run bash scripts/verify.sh from the target dir to structurally lint a present railway.json.

It is a no-op pass when no config file exists (CLI/dashboard-only use is valid). For the deeper

CLI recipe catalog (CI deploys with RAILWAY_TOKEN, railway run for local dev against prod

vars, db shells, multi-service deploys), see references/cli-cookbook.md.

How to use it

Copy the folder

Take ericrisco/railway from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npm, brew. Without those the skill loads but fails at the first command.