PaaS Deploys skill for Claude Code
Render, Railway or Heroku, configured from the repo. Claude picks the platform that fits the stack, then writes render.yaml, railway.json or a Procfile so services, databases and variables live in the repo. Apps bind to $PORT with real health checks, migrations run once in the release phase and stay backward compatible, database pools fit plan limits, and the free-tier traps such as sleeping instances and expiring databases are called out early.
- name
paas-deploys - size235 lines · ≈2,500 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Deploys and operates apps on Heroku-style platforms - Render, Railway and Heroku - with config as code (render.yaml Blueprints, railway.json, Procfile and app.json), web and worker services, $PORT binding and health checks, managed Postgres and Redis with connection limits, environment groups and variable references, release or pre-deploy phases for migrations, preview environments, zero-downtime deploys and rollback, logs, scaling, and the free-tier and pricing limits that bite. Use when deploying to Render, Railway or Heroku, choosing between them, writing their config files, or debugging failed builds, crashed services, sleeping apps or migration problems. Try asking
- Deploy this Django app with a Celery worker and Postgres to Render using a Blueprint
- Our Railway service crashes with no open ports detected. Fix the deployment
- Should we move from Heroku to Render or Railway? Compare cost and migration effort
Use it when
- Deploying a web app, worker or cron job to Render, Railway or Heroku
- Writing render.yaml, railway.json, Procfile or app.json
- Builds fail, services crash, or apps sleep on a PaaS
- Running migrations safely during PaaS deploys
Skip it when
- You deploy to Fly.io Machines and volumes — use fly-io
- You want to run everything on your own server — use vps-deployment
How to check the result
The deploy reaches live status with the health check returning 200, the release or pre-deploy migration ran exactly once, total database connections stay under the plan limit, and the previous release is recorded for rollback.
The common mistake it prevents
Rolling back a release after a destructive migration, which restores the old code but not the old schema and breaks the app again.
A PaaS deployment is done when the service config lives in the repo, the app binds to
$PORT and passes a health check, migrations run once per release before new code takes
traffic, secrets live in the platform’s variables, the database plan’s connection limit
is respected, and rollback to the previous release is a known one-step action.
Ground rules
- Config as code:
render.yaml,railway.json/railway.toml, orProcfile+app.json. Dashboard-only settings are undocumented and drift. - The filesystem is ephemeral on all three (except attached volumes/disks). Uploads go to object storage (see s3-object-storage); sessions go to the database or Redis.
- Migrations must be backward compatible with the currently running release (expand, deploy, contract; see schema-migrations), because old and new code overlap during zero-downtime deploys.
- Production deploys, rollbacks, database plan changes, deleting services or databases,
and
heroku pg:reset-style commands need explicit confirmation. Database deletion is usually irreversible. - Free tiers sleep or expire. Never run anything with users or data you need on them.
Choose the platform
| Need | Render | Railway | Heroku |
|---|---|---|---|
| Config file | render.yaml Blueprint (whole stack) | railway.json/.toml per service | Procfile, app.json |
| Migrations before traffic | preDeployCommand (paid instances) | deploy.preDeployCommand | release: process |
| Zero-downtime | Yes with health check (not with an attached disk) | Health-checked deploys | Preboot feature |
| Preview environments | Preview environments from Blueprints | PR environments | Review apps (pipelines) |
| Background workers / cron | type: worker, type: cron | Separate services, cron schedule per service | worker: dynos, Scheduler add-on |
| Private networking | Private services on internal hostnames | *.railway.internal | Private Spaces (enterprise) |
| Pricing shape | Per instance type, flat monthly | Usage-based (CPU/RAM/egress) | Per dyno type, flat monthly |
Default pick: Render for a conventional web + worker + Postgres stack defined in one file; Railway for quick multi-service setups and usage-based billing; Heroku when the team already runs on it or needs its add-on ecosystem. For global placement and VMs, see fly-io; for full control at lowest cost, see vps-deployment.
Workflow
- [ ] 1. Inspect the app: start command, port, build, migrations, background work
- [ ] 2. Write the platform config file
- [ ] 3. Provision database/cache; wire variables by reference
- [ ] 4. Health check, graceful shutdown, release phase
- [ ] 5. Previews and promotion
- [ ] 6. Deploy, verify, record rollback target
1. App contract
- Listens on
0.0.0.0:$PORT(not 127.0.0.1, not a hardcoded port). /healthzreturns 200 quickly without depending on slow downstreams (a separate readiness check can test the database).- Handles SIGTERM: stop accepting, finish in-flight requests within ~25 s.
- Logs to stdout/stderr, one line per event (JSON if possible).
- Build is deterministic: lockfile committed, runtime version pinned
(
engines.node,.python-version,.ruby-version).
2. Config files
Render (render.yaml):
services:
- type: web
name: api
runtime: node
region: frankfurt
plan: starter
buildCommand: npm ci && npm run build
startCommand: node dist/server.js
preDeployCommand: npm run migrate
healthCheckPath: /healthz
envVars:
- key: NODE_VERSION
value: "22"
- key: DATABASE_URL
fromDatabase: { name: db, property: connectionString }
- key: REDIS_URL
fromService: { type: keyvalue, name: cache, property: connectionString }
- key: SESSION_SECRET
generateValue: true
- key: STRIPE_SECRET_KEY
sync: false # prompt in dashboard; never committed
- type: worker
name: jobs
runtime: node
plan: starter
buildCommand: npm ci && npm run build
startCommand: node dist/worker.js
envVars:
- fromGroup: shared-settings
- type: keyvalue
name: cache
plan: starter
ipAllowList: [] # internal only
databases:
- name: db
postgresMajorVersion: "17"
ipAllowList: []
Check plan names and service types against the current Blueprint spec before applying; Render renamed some (for example Redis to Key Value) over time.
Railway (railway.json, per service):
{
"$schema": "https://railway.com/railway.schema.json",
"build": { "builder": "RAILPACK", "buildCommand": "npm run build" },
"deploy": {
"startCommand": "node dist/server.js",
"preDeployCommand": ["npm run migrate"],
"healthcheckPath": "/healthz",
"healthcheckTimeout": 100,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 5
}
}
Railway variables reference other services: DATABASE_URL=${{Postgres.DATABASE_URL}};
prefer the private URL (*.railway.internal) for service-to-service and database
traffic to avoid egress charges. Railpack replaced Nixpacks as the default builder; a
Dockerfile in the root takes precedence.
Heroku (Procfile):
web: gunicorn app.wsgi --bind 0.0.0.0:$PORT --workers 3 --timeout 25
worker: celery -A app worker --concurrency 4
release: python manage.py migrate --noinput
heroku features:enable preboot -a acme-api # zero-downtime dyno swap
heroku config:set DJANGO_SETTINGS_MODULE=app.settings.prod -a acme-api
heroku ps:scale web=2 worker=1 -a acme-api
3. Databases and connections
- Managed Postgres plans cap connections. Size pools so
instances x workers x pool_sizestays under ~80% of the limit; add PgBouncer (platform-provided or sidecar) when web concurrency grows. - Use internal connection strings inside the platform; external URLs only for admin tools, with IP allowlists.
- Backups: confirm plan includes daily backups/PITR; test a restore into a scratch database before you need it (see backup-restore).
- Free Postgres on Render expires after 30 days (then a short grace period before deletion); never keep real data there.
4. Release phase and health
| Platform | Migration hook | Failure behavior |
|---|---|---|
| Render | preDeployCommand | Deploy aborts, old instance keeps serving |
| Railway | preDeployCommand | Deploy fails, previous deployment stays live |
| Heroku | release: process | Release fails, dynos not restarted with new slug |
Keep hooks idempotent and fast (< a few minutes). Long data backfills run as one-off
jobs (render jobs, railway run, heroku run) after the deploy, not in the hook.
5. Previews and promotion
- Render: enable preview environments in the Blueprint (
previews.generation) so each PR gets the full stack; set preview plans small and expiration short. - Railway: PR environments clone services and variables; seed databases explicitly.
- Heroku: pipeline with review apps -> staging -> production; promote the exact slug
(
heroku pipelines:promote -a acme-staging), which skips rebuilding.
6. Deploy, verify, roll back
# Render: deploys on push (auto-deploy) or via deploy hook / API; logs in dashboard or CLI
render deploys list <service-id> # CLI commands vary by version; check render --help
# Railway
railway up --service api # deploy current directory
railway logs --service api
railway status
# Heroku
git push heroku main
heroku releases -a acme-api
heroku logs --tail -a acme-api
heroku releases:rollback v142 -a acme-api # confirm first; does not roll back DB migrations
Rollback restores code and config, never the database schema. That is why migrations must be backward compatible.
Debugging
| Symptom | Cause | Fix |
|---|---|---|
| “No open ports detected” / H10 crash / health check timeout | App binds to localhost or a fixed port | Bind 0.0.0.0:$PORT |
| Heroku H12 request timeout | Request > 30 s at the router | Move work to a worker; stream responses |
| Heroku R14/R15 memory | Exceeds dyno memory | Fewer workers, fix leak, bigger dyno |
| Works then data vanishes | Writing to ephemeral filesystem | Object storage or attached disk/volume |
too many connections | Pool sizes x instances > plan limit | Shrink pools, add PgBouncer, upgrade plan |
| First request takes ~30-60 s | Free/sleeping instance cold start | Paid always-on instance |
| Build fails only on platform | Different runtime version, missing build var, devDependencies pruned | Pin versions; set build-time vars; adjust build command |
| Deploy succeeded but old code served | Health check path missing so platform kept old instance, or CDN cache | Check deploy events; purge CDN |
Gotchas
- Render services with an attached persistent disk cannot scale horizontally and deploy with brief downtime; prefer managed databases and object storage.
- Heroku dynos restart roughly daily (cycling); anything held in memory or on disk is lost. Schedulers that rely on uptime miss runs.
- Railway’s usage-based billing grows with idle memory; set resource limits per service and remove abandoned PR environments.
- Variable changes trigger redeploys on most platforms; batch them.
- Platform-managed TLS and custom domains need DNS pointed correctly (CNAME or ALIAS/flattening at the apex; see dns-tls-certificates).
- Outbound IPs are shared and change; databases elsewhere cannot allowlist them without a static egress add-on.
Output
**Platform:** Render (render.yaml Blueprint, Frankfurt)
**Services:** api (web, starter, /healthz, preDeploy migrate), jobs (worker), cache (key value), db (Postgres 17)
**Variables:** DATABASE_URL/REDIS_URL by reference; secrets sync:false; shared group for workers
**Connections:** 2 api instances x pool 10 + worker pool 5 = 25 of 97 allowed
**Previews:** per-PR environments, 3-day expiry
**Verified:** deploy live, health 200, migration ran once, rollback target = deploy dep-cq1... (code only) About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.