From 4040df350b40ee555893ab3c9d74c0e33534f2dc Mon Sep 17 00:00:00 2001 From: Alex Wegener Date: Sun, 26 Apr 2026 22:53:22 +0200 Subject: [PATCH] feat: add railway-deploy + butler-deploy skills (v0.0.2) - railway-deploy: consolidates 3 imported skills (railway-cli, railway-deploy, railway-deploy-nextjs) into one lean SKILL.md (165L) plus references/cli-reference.md (full CLI surface) and references/nextjs-prisma.md (Next.js+Prisma recipe). Establishes lean-SKILL+references pattern for tool-heavy skills. - butler-deploy: itch.io publish workflow, generified from a game project (removed pixelrealm/pixelforgestudios hardcoding and project-specific npm-script narrative; now ships generic patterns + CI auth note). - Docs aligned: README "What's in it" -> 6 skills, skills/README.md table, plugin.json + marketplace.json descriptions/keywords (added railway, itch.io). - .ytstack/STATE.md + DECISIONS.md updated; lean-SKILL+references pattern recorded as locked decision for future yopstack skills. - Patch bump 0.0.1 -> 0.0.2. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude-plugin/marketplace.json | 6 +- .claude-plugin/plugin.json | 6 +- .ytstack/DECISIONS.md | 32 ++ .ytstack/STATE.md | 18 +- README.md | 12 +- skills/README.md | 4 + skills/butler-deploy/SKILL.md | 136 ++++++ skills/railway-deploy/SKILL.md | 165 +++++++ .../references/cli-reference.md | 431 ++++++++++++++++++ .../references/nextjs-prisma.md | 185 ++++++++ 10 files changed, 979 insertions(+), 16 deletions(-) create mode 100644 skills/butler-deploy/SKILL.md create mode 100644 skills/railway-deploy/SKILL.md create mode 100644 skills/railway-deploy/references/cli-reference.md create mode 100644 skills/railway-deploy/references/nextjs-prisma.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 17c9ae4..01e78c9 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,8 +8,8 @@ "plugins": [ { "name": "yopstack", - "description": "Claude Code plugin. opentofu + land-and-deploy + canary + setup-deploy. Provisioning, deploy, observability for agent-driven infra. No Yesterday-infra deps.", - "version": "0.0.1", + "description": "Claude Code plugin. opentofu + railway-deploy + butler-deploy + land-and-deploy + canary + setup-deploy. Provisioning, deploy, observability for agent-driven infra. No Yesterday-infra deps.", + "version": "0.0.2", "source": "./", "author": { "name": "Yesterday", @@ -22,6 +22,8 @@ "infrastructure", "observability", "opentofu", + "railway", + "itch.io", "yesterday-ai" ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2f01c8a..5408a4c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "yopstack", - "version": "0.0.1", - "description": "Yesterday Ops Stack -- generic infrastructure-provisioning, deployment, and observability skills for Claude Code agents. opentofu + land-and-deploy + canary + setup-deploy. Public, no Yesterday-infra dependencies.", + "version": "0.0.2", + "description": "Yesterday Ops Stack -- generic infrastructure-provisioning, deployment, and observability skills for Claude Code agents. opentofu + railway-deploy + butler-deploy + land-and-deploy + canary + setup-deploy. Public, no Yesterday-infra dependencies.", "author": { "name": "Yesterday", "url": "https://github.com/Yesterday-AI" @@ -16,6 +16,8 @@ "infrastructure", "observability", "opentofu", + "railway", + "itch.io", "yesterday-ai" ], "dependencies": [ diff --git a/.ytstack/DECISIONS.md b/.ytstack/DECISIONS.md index 971f875..29f1984 100644 --- a/.ytstack/DECISIONS.md +++ b/.ytstack/DECISIONS.md @@ -53,3 +53,35 @@ Plus: `opentofu` originally placed in yastack (agentic-foundation migration list **How to apply:** - yopstack skill list excludes `qa`. - If a future need for ops-specific testing emerges (e.g. infra smoke tests post-deploy), reassess then. + +--- + +## 2026-04-26: Lean SKILL.md + `references/` subfolder for tool-heavy skills + +**Context:** During the railway-deploy consolidation (2026-04-26), three imported skills (`railway-cli`, `railway-deploy`, `railway-deploy-nextjs`) overlapped: ~700 lines combined, ~50% redundancy, ~3 contradictions on the actual CLI command shape. User asked to consolidate without making the resulting skill bloated. + +**Options considered:** + +- A) One monolithic SKILL.md with all CLI ref + Next.js recipe inline. Simple, but ~600+ lines load every invocation -- bloats agent context for routine "redeploy this service" tasks. +- B) Lean SKILL.md (decision-oriented: when-to-use, top gotchas, core workflows, command index) plus `references/` subfolder (full CLI reference, framework-specific recipes) loaded on demand. +- C) Keep three separate skills. Rejected -- selection conflicts (which skill wins for "deploy nextjs to railway"?), gotchas drift across copies. + +**Chose:** B. + +**Reason:** Matches how agents actually consume skills -- they need quick orientation + the 5-10 most common workflows in-context, and only need the full CLI surface area for edge cases. Keeping references separate also makes the gotchas/CLI table the single source of truth (one file to update when the upstream tool changes). 165-line SKILL.md vs 600+ inline is the difference between "always loaded" and "loaded when needed". + +**How to apply:** + +- New yopstack skills that wrap a CLI / framework with substantial surface area follow this layout: + + ```text + skills// + ├── SKILL.md # lean: when-to-use + gotchas + core workflows + command index + └── references/ # loaded on demand + ├── -reference.md # full CLI ref / API surface / config schema + └── -recipe.md # framework- or stack-specific deploy patterns + ``` + +- SKILL.md must include an explicit "References" section pointing to each `references/*.md` so the agent discovers them. +- Skills with a small surface area (single-purpose tool, no framework variants) can stay single-file -- this pattern is for tool-heavy skills, not a blanket rule. +- Same pattern is portable to yastack / ytstack core skills if they ever bloat past ~250 lines. diff --git a/.ytstack/STATE.md b/.ytstack/STATE.md index ebbd001..01f6770 100644 --- a/.ytstack/STATE.md +++ b/.ytstack/STATE.md @@ -1,21 +1,23 @@ --- project: yopstack slug: yopstack -last_updated: 2026-04-25T00:00:00Z -current_milestone: none (pre-migration, reactive mode) +last_updated: 2026-04-26T00:00:00Z +current_milestone: none (reactive mode -- skills landing ad-hoc) active_slice: none active_task: none --- # State -**Status:** v0.0.1 scaffold. Plugin manifest + self-marketplace + documentation in place. **No skills migrated or wrapped yet.** Pre-work reactive mode -- waiting for `opentofu` migration from yastack + `vendor/gstack` subtree-add. +**Status:** v0.0.2. `opentofu` migrated (Phase 2, 2026-04-26). `railway-deploy` and `butler-deploy` added 2026-04-26 as native yopstack skills (not part of the agentic-foundation migration). 3 skills shipped, 3 pending gstack subtree. ## Skill source plan | Skill | Source | Status | |---|---|---| -| opentofu | yastack/skills/opentofu (originally agentic-foundation) | pending migration | +| opentofu | yastack/skills/opentofu (originally agentic-foundation) | shipped 2026-04-26 | +| railway-deploy | added in yopstack (consolidated from 3 imported skills) | shipped 2026-04-26 | +| butler-deploy | added in yopstack (generified from a game project) | shipped 2026-04-26 | | land-and-deploy | vendor/gstack/land-and-deploy (subtree TBD) | pending wrap | | canary | vendor/gstack/canary (subtree TBD) | pending wrap | | setup-deploy | vendor/gstack/setup-deploy (subtree TBD) | pending wrap | @@ -34,10 +36,12 @@ active_task: none ## Recent changes +- 2026-04-26: `railway-deploy` consolidated from three imported skills (`railway-cli` + `railway-deploy` + `railway-deploy-nextjs`) into one lean SKILL.md + `references/cli-reference.md` (full CLI) + `references/nextjs-prisma.md` (framework recipe). Established the lean-SKILL+references pattern as a yopstack convention (DECISIONS 2026-04-26). +- 2026-04-26: `butler-deploy` audited and generified -- removed pixelrealm/pixelforgestudios hardcoding and project-specific npm-script narrative; now ships generic butler push patterns + CI auth note + channel-per-platform recipe. +- 2026-04-26: docs aligned -- README "What's in it" table grew to 6 skills, skills/README.md table updated, plugin.json + marketplace.json descriptions/keywords updated (added `railway` + `itch.io`). Patch bump 0.0.1 -> 0.0.2. +- 2026-04-26: `opentofu` landed (Phase 2 migration from yastack/agentic-foundation). - 2026-04-25: scaffolded as standalone public repo (analog yastack pattern). Plugin manifest, self-marketplace, README/CLAUDE/LICENSE/NOTICE, Yesterday wordmark logo, minimal `.ytstack/` (PROJECT + DECISIONS + STATE) per dogfooding precedent. Listed in both `Yesterday-AI/ystacks` (public catalog) and own self-marketplace. ## Next action -Awaiting `opentofu` migration from yastack + `vendor/gstack` subtree-add. When ready: scaffold M001-Migration milestone here, sliced by skill-cluster (provisioning, deploy-execution, observability), executed via TDD/verify/summarize loop per migrated/wrapped skill. - -No user-side actions blocking; no agent-side actions queued until migration begins. +`vendor/gstack` subtree-add still pending -- needed before `land-and-deploy`, `canary`, `setup-deploy` can be wrapped via the ytstack-style shell-exec inject pattern. No user-side actions blocking; reactive mode continues for ad-hoc skill drops. diff --git a/README.md b/README.md index 36e9e24..4350dbb 100644 --- a/README.md +++ b/README.md @@ -29,14 +29,16 @@ The ops layer of the Yesterday plugin family. Skills for the deploy-arc that com ## What's in it -4 generic skills: +6 generic skills: | Skill | Source | Purpose | |---|---|---| | opentofu | **migrated from agentic-foundation (Phase 2)** | Autonomous infrastructure provisioning | -| land-and-deploy | gstack | Merge + deploy + canary-checks prod + offer revert | -| canary | gstack | Post-deploy visual monitor (console errors, perf regressions, broken links) | -| setup-deploy | gstack | Initialize deploy infrastructure | +| railway-deploy | added in yopstack | Deploy/manage/debug services on Railway via CLI | +| butler-deploy | added in yopstack | Publish HTML5 game builds to itch.io via butler | +| land-and-deploy | gstack (pending) | Merge + deploy + canary-checks prod + offer revert | +| canary | gstack (pending) | Post-deploy visual monitor (console errors, perf regressions, broken links) | +| setup-deploy | gstack (pending) | Initialize deploy infrastructure | Plus one cross-marketplace dependency (auto-pulled via plugin.json): @@ -44,7 +46,7 @@ Plus one cross-marketplace dependency (auto-pulled via plugin.json): |---|---| | [skill-creator](https://github.com/Yesterday-AI/ystacks/tree/main/plugins/skill-creator) | Meta-skill for creating new agent skills | -**Status:** Phase 2 of the agentic-foundation -> ystacks migration. `opentofu` landed 2026-04-26. The three gstack ops skills (`land-and-deploy`, `canary`, `setup-deploy`) wait on the `vendor/gstack` subtree. +**Status:** Phase 2 of the agentic-foundation -> ystacks migration. `opentofu` landed 2026-04-26; `railway-deploy` and `butler-deploy` added 2026-04-26 as native yopstack skills. The three gstack ops skills (`land-and-deploy`, `canary`, `setup-deploy`) wait on the `vendor/gstack` subtree. ## Install diff --git a/skills/README.md b/skills/README.md index fb39b54..cbcf947 100644 --- a/skills/README.md +++ b/skills/README.md @@ -3,10 +3,14 @@ | Skill | Status | One-liner | |---|---|---| | `opentofu` | migrated 2026-04-26 (Phase 2) | Autonomous IaC via OpenTofu (Terraform fork) -- init, plan, apply, destroy, import | +| `railway-deploy` | added 2026-04-26 | Deploy/manage/debug services on Railway via CLI -- auth, projects, services, env, networking, logs | +| `butler-deploy` | added 2026-04-26 | Publish HTML5 game builds to itch.io via butler CLI -- install, login, push, channel-per-platform | | `land-and-deploy` | pending gstack vendor | Merge + deploy + canary checks + revert flow | | `canary` | pending gstack vendor | Post-deploy visual monitor | | `setup-deploy` | pending gstack vendor | Initialize deploy infrastructure | +`railway-deploy` keeps its bulk in `references/` (full CLI ref + Next.js+Prisma recipe) so `SKILL.md` stays lean. + See parent [README.md](../README.md#whats-in-it) for cross-marketplace plugin imports (`skill-creator`) auto-pulled via `plugin.json`. Per the [agentic-foundation MIGRATION-TRIAGE.md](https://github.com/Yesterday-AI/agentic-foundation/blob/main/MIGRATION-TRIAGE.md) (FROZEN v8). diff --git a/skills/butler-deploy/SKILL.md b/skills/butler-deploy/SKILL.md new file mode 100644 index 0000000..8373d61 --- /dev/null +++ b/skills/butler-deploy/SKILL.md @@ -0,0 +1,136 @@ +--- +name: butler-deploy +description: > + Publish or update an HTML5 game on itch.io via the butler CLI. Covers + install, login, channel push with versioning, and first-time page setup. + Use when deploying a web game build (e.g. dist/) to an itch.io channel. +--- + +# Butler Deploy + +Push HTML5 game builds to itch.io with the [butler CLI](https://itchio.itch.io/butler). +Butler does delta uploads (only changed files after the first push) and the +game is live the moment the push completes. + +## When to use + +- First-time publish of a web build to itch.io +- Updating an existing itch.io page with a new build +- Wiring an itch.io deploy step into CI + +## Prerequisites + +### 1. Install butler (one-time, per machine) + +```bash +# macOS +brew install butler + +# Linux +curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default +unzip butler.zip +mv butler /usr/local/bin/butler + +# Windows +scoop install butler +# or download from https://itchio.itch.io/butler +``` + +### 2. Authenticate (one-time, per machine) + +```bash +butler login +# Opens browser → authorize → return to terminal. +``` + +For CI: set `BUTLER_API_KEY` instead (generate at +). No `butler login` needed. + +### 3. Create the itch.io page (one-time, in browser) + +Butler refuses the first push if the target page does not exist. + +1. → **Create new project** +2. Settings: + - **Kind:** HTML + - **Viewport size:** match your build (e.g. 1280×720) + - **"This file will be played in the browser"**: checked +3. Save as **Draft** (publish after verifying the first push). + +After the page exists, all future deploys are fully scripted. + +## Core command + +```bash +butler push /: --userversion +``` + +| Argument | Example | Meaning | +|----------|---------|---------| +| `` | `dist/` | local directory to upload | +| `/` | `acme/spacelander` | itch.io target | +| `` | `html5` | upload channel (`html5`, `windows`, `linux`, `osx`, …) | +| `--userversion` | `1.4.2` | stamps the upload (optional but recommended) | + +Example: + +```bash +butler push dist/ acme/spacelander:html5 --userversion 1.4.2 +``` + +## Common patterns + +### Build then push + +Wire build + push together — pick whichever fits your stack: + +```bash +# Direct +npm run build && butler push dist/ acme/spacelander:html5 --userversion "$(node -p "require('./package.json').version")" + +# Make +make build && butler push build/web acme/spacelander:html5 + +# Project npm script (optional convenience) +# package.json: "deploy": "butler push dist/ $ITCH_GAME:html5 --userversion $npm_package_version" +ITCH_GAME=acme/spacelander npm run deploy +``` + +### Channel-per-platform + +```bash +butler push dist/web acme/spacelander:html5 +butler push dist/win acme/spacelander:windows +butler push dist/linux acme/spacelander:linux +butler push dist/osx acme/spacelander:osx +``` + +### Verify after push + +```bash +butler status acme/spacelander # all channels, latest version + state +butler status acme/spacelander:html5 # one channel +``` + +## Useful butler commands + +```bash +butler login # interactive auth (browser) +butler logout +butler push /: [--userversion VER] [--ignore PATTERN] +butler status /[:] +butler fetch /: # download a build +butler validate # sanity-check a build dir +butler --version +``` + +## Troubleshooting + +| Symptom | Likely cause | Fix | +|---------|--------------|-----| +| `butler: command not found` | not installed | install per step 1 | +| `404 Not found` on first push | itch.io page doesn't exist | create the project page in browser first | +| `401 Unauthorized` | not logged in / bad API key | `butler login`, or fix `BUTLER_API_KEY` | +| `403 Forbidden` | account doesn't own the target | check `/` slug matches a project you own | +| Push silently uploads everything every time | `dist/` content non-deterministic (timestamps, build hashes) | rebuild with stable filenames, or `--ignore` volatile files | +| `--userversion` shows up empty on itch.io | flag value missing/empty | confirm the version env / `package.json` field is set before invoking | diff --git a/skills/railway-deploy/SKILL.md b/skills/railway-deploy/SKILL.md new file mode 100644 index 0000000..554d1e5 --- /dev/null +++ b/skills/railway-deploy/SKILL.md @@ -0,0 +1,165 @@ +--- +name: railway-deploy +description: > + Deploy, manage, and debug services on Railway via the Railway CLI. Covers + authentication, project + service creation, env variables, private + networking, logs, and config-as-code. Use whenever creating, deploying, or + troubleshooting Railway services. See references/ for the full CLI command + reference and a Next.js+Prisma deploy recipe. +--- + +# Railway Deploy + +Operate Railway from the CLI: create projects, add services + databases, set +variables, deploy, debug. Optimised for agent + CI use (token-based auth, +non-interactive flags). + +## When to use + +- Creating a new Railway project / service / database +- Deploying local code or a Docker image to Railway +- Setting env vars or wiring services together via private networking +- Tailing logs or SSH-ing into a running container +- Debugging a failing deploy (build, healthcheck, migration) + +## References + +Read these on demand — do not pre-load. + +- [`references/cli-reference.md`](references/cli-reference.md) — complete CLI: + every command, flag, template-variable syntax, `railway.toml` schema, + practical patterns. Open when you need an exact command or flag. +- [`references/nextjs-prisma.md`](references/nextjs-prisma.md) — Next.js + (standalone) + Prisma + PostgreSQL deploy recipe: Dockerfile, entrypoint + with `prisma migrate deploy`, healthcheck route, common pitfalls. Open + only when deploying a Next.js+Prisma app. + +## Authentication + +CI / agents → set one env var, no `railway login` needed: + +| Variable | Scope | Use case | +|----------|-------|----------| +| `RAILWAY_TOKEN` | single project | agent deploys, CI | +| `RAILWAY_API_TOKEN` | full account | account-wide automation | + +Interactive (humans only): `railway login` (or `--browserless` in headless +shells). Verify with `railway whoami` / `railway status`. + +Also set `RAILWAY_NO_TELEMETRY=1` to silence the telemetry prompt. + +## Top gotchas (read before acting) + +These bite repeatedly. The full list is in the CLI reference. + +1. **`railway init` creates a project, not a service.** Always follow with + `railway add --service ""` to create the app service. +2. **`railway add --service` is interactive** unless you pass `--variables` + inline. Pass them to skip the TUI. +3. **Variable command is `railway variable` (singular).** Plural exists in + older docs/skills but is wrong in current CLI. Subcommands: `list`, + `set`, `delete`. There is **no `variable get`** — use + `railway variable list --kv` and grep. +4. **`--variables KEY=VAL` without quotes** in `spawnSync`/`execFileSync`. + Quotes get parsed as part of the key. +5. **`railway link` always re-prompts.** To switch service inside an + already-linked project, use `railway service link `. +6. **`VOLUME` is banned in Dockerfiles** on Railway — build will fail. Strip + any `VOLUME` instructions. +7. **`railway up` uploads the working directory** (respecting + `.railwayignore` → `.gitignore`). It is **not** git-based. +8. **`railway run` is local; `railway ssh` is in-container.** Use `ssh` for + migrations, bootstrap, or anything touching container fs. +9. **Template variables use `${{Service.VAR}}`** (double braces). Resolved + by Railway at runtime, not by your shell — escape `$` in heredocs/scripts. + +## Core workflows + +### Deploy a new service from local code + +```bash +railway init --name "my-app" +railway add --service "api" \ + --variables PORT=3000 \ + --variables NODE_ENV=production \ + --variables 'DATABASE_URL=${{Postgres.DATABASE_URL}}' +railway add --database postgres +railway service link api +railway up --detach +railway domain --port 3000 +railway logs +``` + +### Add a service to an existing project + +```bash +railway add --service "worker" \ + --variables 'DATABASE_URL=${{Postgres.DATABASE_URL}}' \ + --variables 'API_URL=http://api.railway.internal:3000' +railway service link worker +railway up --detach +``` + +### Service-to-service networking + +Internal DNS, zero config, free egress, encrypted: + +``` +http://.railway.internal: +``` + +Always prefer this over the public domain for service-to-service calls. + +### Debug a failing deploy + +```bash +railway status # what's linked? +railway logs -s # runtime logs +railway logs -s --build # build logs +railway ssh -s # poke around in the container +railway variable list -s # confirm env +``` + +Healthcheck failing? Verify the endpoint returns 200 and the port matches +the one Railway is probing (set in `railway.toml` `[deploy] healthcheckPath` ++ exposed `PORT`). + +### Config as code + +Place `railway.toml` at project root — it overrides dashboard settings: + +```toml +[build] +builder = "DOCKERFILE" +dockerfilePath = "Dockerfile" + +[deploy] +healthcheckPath = "/api/health" +healthcheckTimeout = 30 +restartPolicyType = "ON_FAILURE" +restartPolicyMaxRetries = 3 +``` + +Full schema (env overrides, cron, watchPatterns, preDeployCommand, …) in the +CLI reference. + +## Quick command index + +```bash +railway status [--json] # current link +railway list [--json] # all projects +railway add --service "" --variables K=V +railway add --database postgres|mysql|redis|mongo +railway service link # switch service in project +railway variable list [--kv] [-s ] +railway variable set K=V [K2=V2 …] +railway up [--detach] [--ci] [-s ] +railway redeploy [-s ] +railway domain [--port N] [-s ] [] +railway logs [-s ] [--build] [-n N] +railway ssh [-s ] # in-container shell +railway run # local cmd with railway env +railway connect # db shell +``` + +For anything not listed here, open `references/cli-reference.md`. diff --git a/skills/railway-deploy/references/cli-reference.md b/skills/railway-deploy/references/cli-reference.md new file mode 100644 index 0000000..299ec4d --- /dev/null +++ b/skills/railway-deploy/references/cli-reference.md @@ -0,0 +1,431 @@ +# Railway CLI Reference + +Complete reference for the Railway CLI. Covers every command needed to +deploy, manage, and debug services. Read on demand from `SKILL.md`. + +## Gotchas (full list) + +1. **`railway init` only creates a project, NOT a service.** Create the app + service separately with `railway add --service "name"`. +2. **`railway add --service` is interactive** unless you pass `--variables` + inline. Always pass variables to skip the TUI prompt. +3. **`railway link` always re-prompts** for workspace/project. Use + `railway service link ` to switch service within an already-linked + project. +4. **`railway service list --json` does not exist.** Use `railway list --json` + to get project/service info programmatically. +5. **`VOLUME` is banned in Dockerfiles** on Railway. Remove any `VOLUME` + instructions or the build will fail. +6. **The CLI command is `railway variable`** (singular), not `railway + variables`. Subcommands: `list`, `set`, `delete`. **There is no + `variable get`** — use `railway variable list --kv` and grep. +7. Set `RAILWAY_NO_TELEMETRY=1` to suppress the telemetry prompt. +8. Use `--browserless` for `login` in scripts or headless environments. + Browser login often fails. +9. **Template variables** use `${{ServiceName.VARIABLE_NAME}}` syntax + (double curly braces). Resolved by Railway at runtime, not by your shell. +10. **`railway up` uploads the current directory.** It does NOT use git — it + sends all files (respecting `.railwayignore` or `.gitignore`). +11. **`--variables "KEY=VAL"` with quotes breaks `spawnSync`/`execFileSync`.** + The quotes become part of the key name (`"KEY` instead of `KEY`). Use + `--variables KEY=VAL` without quotes. +12. **`railway run` is LOCAL only** — it injects env vars into a local + command. **`railway ssh`** runs commands inside the actual container. + Use `ssh` for bootstrap, migrations, or anything that needs the + container's filesystem. + +--- + +## Authentication + +```bash +railway login # interactive (browser) +railway login --browserless # pairing code (scripts/headless) +railway whoami +railway logout +``` + +### Token-based auth (CI/CD & agents) + +| Variable | Scope | Use case | +|----------|-------|----------| +| `RAILWAY_TOKEN` | single project | agent deployments, CI/CD | +| `RAILWAY_API_TOKEN` | full account | account-wide automation | + +```bash +RAILWAY_TOKEN=xxx railway up --detach +``` + +--- + +## Project management + +```bash +railway init # create project (interactive workspace pick) +railway init --name "My Project" +railway link # link cwd to existing project (interactive) +railway link -p -s -e production # non-interactive +railway status [--json] +railway list [--json] +railway open # open dashboard +railway unlink +railway project delete -p -y +``` + +--- + +## Service management + +### Create services + +```bash +# Empty service (interactive) +railway add --service + +# Empty service with variables (non-interactive) +railway add --service "api" \ + --variables PORT=3000 \ + --variables NODE_ENV=production \ + --variables 'DATABASE_URL=${{Postgres.DATABASE_URL}}' + +# Database +railway add --database postgres +railway add --database mysql +railway add --database redis +railway add --database mongo + +# From GitHub repo +railway add --repo user/repo + +# From Docker image +railway add --image nginx:latest +``` + +### Link / switch services + +```bash +railway service link # preferred — same project +railway service link # interactive picker +railway service status +railway service status --all +``` + +### Service operations + +```bash +railway service redeploy -s # redeploy latest +railway service restart -s +railway service scale -s # multi-region +railway service delete # destructive +``` + +--- + +## Environment variables + +```bash +railway variable list # current service +railway variable list --json +railway variable list --kv # KEY=VALUE format +railway variable list -s Postgres # specific service + +railway variable set KEY=value +railway variable set API_KEY=secret123 DEBUG=true PORT=8080 + +# stdin (long values) +echo "long-value" | railway variable set SECRET_KEY --stdin + +railway variable delete MY_VAR +``` + +There is no `railway variable get`. To read one value: + +```bash +railway variable list --kv -s Postgres | grep '^DATABASE_URL=' +``` + +### Template variables + +Reference other services' variables with `${{}}`: + +```bash +railway variable set 'DATABASE_URL=${{Postgres.DATABASE_URL}}' +``` + +Built-in database references: + +``` +${{Postgres.DATABASE_URL}} +${{Postgres.DATABASE_PUBLIC_URL}} +${{Redis.REDIS_URL}} +${{MySQL.MYSQL_URL}} +${{Mongo.MONGO_URL}} +``` + +Cross-service: + +``` +${{api.PORT}} +${{worker.API_KEY}} +``` + +Built-in runtime: + +``` +${{RAILWAY_PUBLIC_DOMAIN}} # this service's public domain +${{RAILWAY_PRIVATE_DOMAIN}} # this service's internal domain +${{RAILWAY_SERVICE_NAME}} +${{RAILWAY_ENVIRONMENT_NAME}} +``` + +### Bulk-set from .env file + +```bash +while IFS='=' read -r key value; do + [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue + railway variable set "$key=$value" +done < .env +``` + +--- + +## Deployment + +```bash +railway up # streams build logs +railway up --detach # returns immediately +railway up -s +railway up --ci # streams build, exits when build done +railway redeploy # redeploy without new code +railway service redeploy -s +railway down # remove latest deployment +``` + +### What `railway up` does + +1. Reads `.railwayignore` (falls back to `.gitignore`). +2. Compresses and uploads the directory. +3. Detects build method: Dockerfile or Railpack (Nixpacks). +4. Builds and deploys the container. + +--- + +## Domains & networking + +### Public domains + +```bash +railway domain # *.up.railway.app +railway domain --port 8080 +railway domain example.com # custom +railway domain example.com --port 8080 +railway domain --port 3000 -s api +``` + +Custom domain → add a CNAME pointing at the `*.up.railway.app` URL. Railway +auto-provisions SSL. + +### Private networking + +Same-project services communicate over encrypted Wireguard tunnels via +internal DNS — zero config: + +``` +http://.railway.internal: +``` + +Examples: + +``` +http://api.railway.internal:3000 +http://worker.railway.internal:8080 +``` + +- Each environment has its own isolated network. +- Free egress, low latency. +- Always use this for service-to-service calls. + +--- + +## Logs & debugging + +```bash +railway logs # current service +railway logs -s +railway logs -n 100 # last 100 lines +railway logs --build # build logs +railway logs -s api 2>&1 | grep -i error +``` + +### Shell access + +```bash +railway ssh # in-container shell +railway connect # db shell (Postgres/Mysql/Redis/Mongo) +``` + +### Run with Railway env + +```bash +railway run npm start # local cmd, Railway env injected +railway run python manage.py migrate +railway shell # interactive shell with vars +``` + +### Common debug recipes + +| Symptom | First check | +|---------|-------------| +| Service won't start | `railway logs -s ` — runtime crash? | +| Healthcheck fails | endpoint returns 200? port matches `[deploy]`? | +| DB connection refused | `railway variable list -s ` — `DATABASE_URL` set? | +| Service-to-service 404 | use `.railway.internal:`, not public URL | +| Build fails | `railway logs --build` — Docker/Nixpacks output | +| "No project linked" | `railway link -p -s ` | +| "Unauthorized" | `RAILWAY_TOKEN` expired — regenerate in dashboard | +| Build OOM | optimise Dockerfile (multi-stage, .dockerignore), or upgrade plan | +| Deploy stuck | `railway redeploy -s `; if no help, check healthcheck | + +--- + +## Config as code (`railway.toml`) + +Place at project root. **Config in code overrides dashboard settings.** + +```toml +[build] +builder = "DOCKERFILE" # or "RAILPACK" (default) +dockerfilePath = "Dockerfile.railway" +buildCommand = "yarn build" +watchPatterns = ["src/**", "package.json"] # only redeploy on these changes + +[deploy] +startCommand = "node server.js" # override CMD +preDeployCommand = "npm run db:migrate" # run before start +healthcheckPath = "/api/health" +healthcheckTimeout = 60 +restartPolicyType = "ON_FAILURE" # ON_FAILURE | ALWAYS | NEVER +restartPolicyMaxRetries = 5 +cronSchedule = "*/15 * * * *" +drainingSeconds = 10 # SIGTERM → SIGKILL grace +``` + +### Environment overrides + +```toml +[environments.staging.deploy] +startCommand = "npm run staging" + +[environments.pr.deploy] +startCommand = "npm run preview" +``` + +--- + +## Environments + +```bash +railway environment # switch (interactive) +railway environment new staging +railway environment delete dev +``` + +--- + +## Volumes + +```bash +railway volume list +railway volume add +railway volume delete +``` + +--- + +## Practical patterns + +### Programmatic project info (JSON) + +```bash +railway list --json | node -e " + let d=''; process.stdin.on('data',c=>d+=c); + process.stdin.on('end',()=>{ + const projects = JSON.parse(d); + for (const p of projects) { + const svcs = p.services?.edges?.map(e => e.node.name) || []; + console.log(p.name + ': ' + svcs.join(', ')); + } + });" + +railway status --json +``` + +### Database access from local machine + +```bash +# Connection URL +railway variable list --kv -s Postgres | grep '^DATABASE_PUBLIC_URL=' + +# Direct shell +railway connect + +# Or pipe into psql +psql "$(railway variable list --kv -s Postgres | grep '^DATABASE_PUBLIC_URL=' | cut -d= -f2-)" +``` + +### Full-stack deploy + +```bash +railway init --name "my-app" + +railway add --service "api" \ + --variables PORT=3000 \ + --variables NODE_ENV=production \ + --variables 'DATABASE_URL=${{Postgres.DATABASE_URL}}' + +railway add --service "web" \ + --variables PORT=8080 \ + --variables 'API_URL=http://api.railway.internal:3000' + +railway add --database postgres + +railway service link api && (cd api && railway up --detach) +railway service link web && (cd web && railway up --detach) + +railway service link api && railway domain --port 3000 +railway service link web && railway domain --port 8080 +``` + +--- + +## Cost management + +Railway bills by resource usage (CPU, RAM, egress). Open +`railway open` → Usage tab. Tips: + +- Use private networking between services — no egress charges. +- Scale dev/staging down when idle. +- Set memory/CPU limits in service settings to cap runaway costs. +- `[build] watchPatterns` to avoid unnecessary rebuilds. + +--- + +## Secrets best practices + +- Never hardcode secrets in code or Dockerfiles. +- Always use `railway variable set` (or dashboard) for secrets. +- Reference shared secrets across services with `${{Service.VAR}}` template + syntax — single source of truth. + +--- + +## Global flags + +| Flag | Short | Purpose | +|------|-------|---------| +| `--service ` | `-s` | Target a specific service | +| `--environment ` | `-e` | Target a specific environment | +| `--json` | | JSON output | +| `--yes` | `-y` | Skip confirmations | +| `--help` | `-h` | Show help | +| `--version` | `-V` | Show version | diff --git a/skills/railway-deploy/references/nextjs-prisma.md b/skills/railway-deploy/references/nextjs-prisma.md new file mode 100644 index 0000000..8b853b9 --- /dev/null +++ b/skills/railway-deploy/references/nextjs-prisma.md @@ -0,0 +1,185 @@ +# Next.js + Prisma + PostgreSQL on Railway + +Recipe for deploying a Next.js (App Router, standalone output) + Prisma + +PostgreSQL project to Railway via a multi-stage Dockerfile. Read on demand +from `SKILL.md` — only relevant when the project is Next.js+Prisma. + +## Prerequisites + +- Next.js with `output: "standalone"` in `next.config.mjs` +- Prisma + PostgreSQL with migrations in `prisma/migrations/` +- pnpm package manager +- Railway project linked (`railway link` or GitHub-connected) + +## Files to add + +### 1. `railway.toml` + +```toml +[build] +builder = "DOCKERFILE" +dockerfilePath = "Dockerfile" + +[deploy] +healthcheckPath = "/api/health" +healthcheckTimeout = 30 +restartPolicyType = "ON_FAILURE" +restartPolicyMaxRetries = 3 +``` + +### 2. `docker-entrypoint.sh` + +Runs migrations before the server starts. **Critical** — without it, the +healthcheck passes (`SELECT 1` doesn't need tables) but real queries 500. + +```sh +#!/bin/sh +set -e + +echo "Running database migrations..." +node node_modules/prisma/build/index.js migrate deploy --schema=./prisma/schema.prisma +echo "Migrations complete." + +echo "Starting server..." +exec node server.js +``` + +Make executable: `chmod +x docker-entrypoint.sh`. + +### 3. `Dockerfile` + +Multi-stage: deps → build → runner. + +- **Base**: `node:20-alpine` + `openssl` (Prisma needs it). +- **Deps**: install all deps, generate Prisma client, flatten the prisma CLI + + engines from pnpm's store into `/prisma-cli/` so the runner image can + copy them as a flat tree. +- **Build**: `next build` → standalone output. +- **Runner**: minimal image with standalone server, Prisma client, Prisma + CLI (for migrations), schema + migrations, entrypoint. + +```dockerfile +# syntax=docker/dockerfile:1 + +# --- Base --- +FROM node:20-alpine AS base +RUN apk add --no-cache openssl +RUN corepack enable && corepack prepare pnpm@latest --activate +WORKDIR /app + +# --- Dependencies --- +FROM base AS deps +COPY package.json pnpm-lock.yaml ./ +COPY prisma ./prisma/ +RUN pnpm install --frozen-lockfile +RUN pnpm exec prisma generate +# Flat prisma CLI dir for the production image +RUN mkdir -p /prisma-cli/node_modules && \ + cp -rL node_modules/.pnpm/prisma@*/node_modules/prisma /prisma-cli/node_modules/prisma && \ + cp -rL node_modules/.pnpm/prisma@*/node_modules/@prisma /prisma-cli/node_modules/@prisma + +# --- Build --- +FROM base AS build +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +RUN pnpm build + +# --- Production --- +FROM base AS runner +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +COPY --from=build /app/public ./public +COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=deps /app/node_modules/.pnpm/@prisma+client*/node_modules/@prisma/client ./node_modules/@prisma/client +COPY --from=deps /app/node_modules/.pnpm/@prisma+client*/node_modules/.prisma ./node_modules/.prisma +COPY --from=deps /prisma-cli/node_modules/prisma ./node_modules/prisma +COPY --from=deps /prisma-cli/node_modules/@prisma/engines ./node_modules/@prisma/engines +COPY prisma ./prisma/ +COPY docker-entrypoint.sh ./docker-entrypoint.sh + +USER nextjs + +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["sh", "./docker-entrypoint.sh"] +``` + +Note: no `VOLUME` instructions — Railway rejects them. + +### 4. Healthcheck route — `src/app/api/health/route.ts` + +```ts +import { prisma } from "@/lib/prisma"; +import { NextResponse } from "next/server"; + +export async function GET() { + try { + await prisma.$queryRaw`SELECT 1`; + return NextResponse.json({ status: "ok", db: "connected" }); + } catch { + return NextResponse.json( + { status: "error", db: "unreachable" }, + { status: 503 } + ); + } +} +``` + +## Prisma schema + +`binaryTargets` must include the Alpine musl targets: + +```prisma +generator client { + provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"] +} +``` + +## Railway env vars + +Set via dashboard or `railway variable set`: + +| Variable | Value | Notes | +|----------|-------|-------| +| `DATABASE_URL` | `${{Postgres.DATABASE_URL}}` | matches `env()` in `prisma/schema.prisma` | +| `NEXTAUTH_URL` | `https://your-app.up.railway.app` | NextAuth callbacks | +| `NEXTAUTH_SECRET` | `openssl rand -base64 32` | random 32+ chars | + +## Common issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Healthcheck passes, queries 500 | migrations not applied | ensure entrypoint runs `prisma migrate deploy` before `node server.js` | +| `Can't find Prisma schema` at startup | schema not in runner | `COPY prisma ./prisma/` in runner stage | +| `Query engine not found` | missing binary target | add `linux-musl-openssl-3.0.x` to `binaryTargets` | +| `prisma: not found` in entrypoint | CLI missing in runner | use the flat `/prisma-cli/` copy pattern | +| `Schema engine not found` for migrations | `@prisma/engines` not copied | `cp -rL` resolves pnpm symlinks; COPY engines into runner | +| `pnpm install` fails on Railway | lock file mismatch | run `pnpm install` locally, commit updated `pnpm-lock.yaml` | + +## Checklist + +1. [ ] `next.config.mjs` has `output: "standalone"` +2. [ ] Prisma schema has Alpine musl `binaryTargets` +3. [ ] `railway.toml` with Dockerfile builder +4. [ ] `Dockerfile` follows the multi-stage pattern above +5. [ ] `docker-entrypoint.sh` runs `prisma migrate deploy` before server start +6. [ ] `/api/health` route exists and pings the DB +7. [ ] `DATABASE_URL`, `NEXTAUTH_URL`, `NEXTAUTH_SECRET` set in Railway +8. [ ] Railway project linked or GitHub repo connected + +## Debugging on Railway + +- Build logs: dashboard → Deployments → Build Logs (or `railway logs --build`) +- Runtime logs: dashboard → Deploy Logs (or `railway logs`) +- Env vars: Settings → Variables (or `railway variable list`) +- After this setup, failed migrations crash on startup (visible in deploy + logs) instead of silently serving 500s on every query.