Claude Code for Deployment: CLAUDE.md Rules for 6 Hosts
TL;DR
- Claude Code writes deployment config that runs on your laptop and then fails on the platform, because it cannot see the platform's contract: which port to bind, where secrets live, when migrations run, and which runtime executes the code.
- One CLAUDE.md fixes that for any host. Put the shared rules (port contract, secrets, release step, process split, scheduled jobs, no unattended production deploys) in one skeleton, then add a short block for the platform you use.
- The platforms differ most in their config surface and runtime model: Vercel and Netlify run functions, Railway, Render and Fly.io run long-lived containers, and Cloudflare Workers runs V8 isolates with bindings instead of a filesystem.
- They also differ in where migrations belong: Render has a
preDeployCommand, Fly.io hasrelease_command, Railway haspreDeployCommand, and the function platforms have no release phase at all. - Every platform fact in this guide was checked against that platform's documentation on 21 October 2026.
Claude Code can take a project from repository to production on any of these six platforms, as long as your CLAUDE.md tells it which rules the platform enforces. This guide gives you that file. It starts with the skeleton every deployment shares, compares Vercel, Netlify, Railway, Render, Fly.io and Cloudflare Workers side by side, and then lists only the rules specific to each platform, so you copy one shared block plus one platform block instead of six overlapping templates.
If CLAUDE.md is new to you, our guide to CLAUDE.md explains how Claude Code loads it at the start of every session. For the release discipline that sits above any single platform (pre-ship checks, rollback, verification), see Claude Code deployment, and for container builds see Claude Code with Docker.
Why Claude Code needs deployment rules in CLAUDE.md
A deployment is a contract between your code and a platform you cannot see from inside the repository. Claude reads your source, your package.json and maybe a Dockerfile. It does not read the platform's documentation unless you put the relevant parts where it looks, so it writes the configuration that tutorials taught it.
Across all six platforms, the same seven failure patterns show up when there is no CLAUDE.md:
- A hardcoded port or host.
app.listen(3000)works locally and receives no traffic on a platform that tells the app which port to use, or that requires binding to0.0.0.0rather thanlocalhost. - Secrets in committed config. Claude puts a
DATABASE_URLor API key intovercel.json,netlify.toml,render.yamlorwrangler.jsoncbecause that is the file it was editing. - Migrations in the wrong phase. Run in the start command, a migration re-runs on every restart and every scaled instance. Run with no gate at all, a failed migration ships alongside the code that needs it.
- One process doing two jobs. Asked for "a web server and a worker", Claude starts both from one entry point, so the worker dies whenever the web process restarts and scales with HTTP traffic it has nothing to do with.
- The wrong runtime for the code. Node-only libraries end up in edge or isolate runtimes, and long jobs end up in request handlers with a hard duration limit.
- Unattended production deploys. Claude has a terminal. If the CLI can deploy to production, Claude can run that command when it decides you asked for it.
- Whole-file config rewrites. Asked to add one setting, Claude regenerates the entire config file and silently drops settings it was not told about.
None of these are exotic. They are the defaults. The fix is to make the correct pattern the only one Claude sees.
Which deployment platform should you use with Claude Code?
All six work well with Claude Code; the right one depends on what you are deploying. The table compares the things that change how Claude should write your configuration. Limits and plan details are from each platform's documentation as of 21 October 2026 and change often, so treat them as a starting point.
| Vercel | Netlify | Railway | Render | Fly.io | Cloudflare Workers | |
|---|---|---|---|---|---|---|
| Compute model | Functions (Node.js default; Edge, Python, Go and more) | Functions (Node) plus Deno edge functions | Long-lived containers per service | Long-lived services: web, worker, cron, private | Firecracker Machines from your Docker image | V8 isolates at the edge |
| Config surface | vercel.json plus framework config |
netlify.toml (plus optional _redirects) |
.railway/railway.ts (Infrastructure as Code); railway.json/railway.toml read until 2026-12-01 |
render.yaml Blueprint |
fly.toml |
wrangler.jsonc (recommended) or wrangler.toml |
| Port contract | None; the platform invokes your handler | None for functions | Listen on injected PORT, bind 0.0.0.0 |
Bind 0.0.0.0 on PORT (default 10000) |
internal_port in fly.toml (default 8080) |
None; export a fetch handler |
| Release step for migrations | None built in | None built in | preDeployCommand |
preDeployCommand (paid services) |
release_command (temporary Machine, no volumes) |
None; D1 migrations via Wrangler |
| Scheduled jobs | crons in vercel.json, UTC, once a day on Hobby |
schedule in function config, UTC, 30 second limit |
cronSchedule per service |
cron service with schedule |
A dedicated [processes] group |
triggers.crons, UTC, scheduled handler |
| Choose it when | Next.js and frontend-led apps | Static and Jamstack sites with light functions | Several services plus managed databases in one project | Heroku-style services described as code | Containers close to users in many regions | Latency-critical APIs on Cloudflare's network |
Sources are listed per platform in the sections below and in full at the end of this page.
A quick way to choose:
- Vercel if the app is Next.js or another frontend framework and most of your backend is request and response.
- Netlify if the site is mostly static and the dynamic parts are small functions, forms and redirects.
- Railway if you want an API, a worker, Postgres and Redis wired together in one project with almost no YAML.
- Render if you want the classic web plus worker plus cron service model, declared in one Blueprint file.
- Fly.io if you ship a Docker image and care about which regions it runs in.
- Cloudflare Workers if the code fits an isolate and you want KV, R2, D1 and Durable Objects as bindings.
The shared CLAUDE.md skeleton for deployment
This block is platform-neutral. Put it in your project's CLAUDE.md as-is, then append the platform block from the sections further down.
# Deployment rules
## Port and host
- Read the port from the environment; NEVER hardcode 3000, 8000 or 8080
- Bind to 0.0.0.0, never localhost, on container platforms
- Every web service exposes GET /health returning 200 without touching slow dependencies
## Secrets and configuration
- Secrets live in the platform's secret store, NEVER in committed config files
- Every variable the app reads is listed in .env.example with a one-line comment
- Validate required variables at startup and exit non-zero if one is missing
- Browser-exposed variables use the framework's public prefix; server secrets never do
## Release phase
- Database migrations run in the platform's release or pre-deploy step, never in the start command
- A failed migration must fail the deploy
## Processes
- One process type per service: web, worker and scheduler are deployed separately
- Long-running work leaves the request path (queue, worker or background function)
## Scheduled jobs
- Cron schedules are UTC and use five fields
- Every scheduled job is idempotent: running twice or skipping once is safe
- Scheduled endpoints authenticate the caller
## Config files
- Edit the specific keys you were asked to change; NEVER regenerate a config file wholesale
- NEVER remove an existing binding, service, route or redirect without explicit instruction
## Hard rules
- NEVER deploy to production from the terminal without an explicit request in this session
- Production deploys go through git push to the production branch or CI
- NEVER run destructive platform commands (delete volume, destroy machine, drop database) without confirmation
Each line exists because of one of the seven failure patterns above. Four of them need more than a one-line rule.
The port contract
Container platforms route traffic to a port they expect your process to bind. Railway injects PORT and asks you to bind 0.0.0.0; Render's default PORT is 10000 and a deploy fails if it detects no bound port; Fly.io routes to whatever internal_port says in fly.toml. The safe pattern is the same everywhere:
// src/server.ts
const port = Number(process.env.PORT ?? 3000); // fallback for local dev only
app.listen(port, '0.0.0.0', () => console.info(`listening on ${port}`));
Tell Claude in CLAUDE.md that the fallback exists for local development only, then search for literal port numbers before every deploy. A single listen(3000) in a file Claude wrote outside the main server breaks a service.
Secrets: one store per environment
Every platform here keeps secrets outside the repository, and every one separates environments differently: Vercel scopes variables to Production, Preview, Development and custom environments; Netlify scopes them to deploy contexts; Railway and Render attach them to services and environment groups; Fly.io stores them per app; Workers stores them per Worker and environment. The rule Claude needs is that a secret never appears in a file that gets committed, even inside a context or environment block that looks private. Add a second rule for adding variables: a new variable is not done until it is in .env.example, set in every environment that needs it, and validated at startup.
Migrations belong in the release step
The release step runs once per deploy, after the build and before the new version takes traffic, and a non-zero exit stops the deploy. That is exactly the semantics a migration needs. Running migrations in the start command instead means they run on every restart and on every instance at once, which is safe only if every migration is perfectly idempotent and concurrency-safe. Railway, Render and Fly.io each have a release hook (details below). Vercel, Netlify and Workers do not, so run migrations from CI before the deploy step or from a protected script you trigger deliberately.
Production deploys need a human
Claude Code can run any CLI you can. vercel --prod, netlify deploy --prod, fly deploy and wrangler deploy all ship to production when run with production credentials. Put them on the deny list so Claude has to ask:
{
"permissions": {
"allow": [
"Bash(vercel build*)",
"Bash(netlify build*)",
"Bash(fly config validate*)",
"Bash(wrangler dev*)"
],
"deny": [
"Bash(vercel --prod*)",
"Bash(vercel deploy --prod*)",
"Bash(netlify deploy --prod*)",
"Bash(fly deploy*)",
"Bash(fly volumes destroy*)",
"Bash(fly machines destroy*)",
"Bash(wrangler deploy*)",
"Bash(railway up*)"
]
}
}
Put this in .claude/settings.local.json. Our guide to Claude Code permissions explains how allow and deny rules interact.
Vercel rules for Claude Code
Vercel runs your framework's server code as Vercel Functions. The rules Claude needs that are specific to Vercel:
- Default to the Node.js runtime. Vercel's Edge runtime documentation now says it recommends migrating from edge to Node.js for improved performance and reliability, and that both runtimes run on Fluid compute. It also notes that from Next.js 16.3,
runtime = 'edge'is no longer supported in Next.js, so routes and pages run on Node.js (Vercel Edge runtime). Tell Claude not to addexport const runtime = 'edge'to new routes. - Know the duration limits. With Fluid compute, functions default to 300 seconds on every plan; Hobby caps at 300 seconds, Pro and Enterprise at 800 seconds, with a 30 minute beta for some runtimes (max duration). In the Next.js App Router, set it per route with
export const maxDuration = 60. Anything longer belongs in a queue or workflow, not a request handler. - Env var changes need a redeploy. Changes apply only to new deployments, not to ones already live (environment variables). Use
vercel env pullfor local development, and scope every variable to Production, Preview or Development deliberately. - Secure every cron endpoint. Crons are declared in
vercel.jsonas apathandschedule, run in UTC, and are invoked with a GET to the production deployment. If you set aCRON_SECRETvariable, Vercel sends it asAuthorization: Bearer <secret>, so the handler can reject other callers (managing cron jobs). - Expect missed and duplicate cron runs. Vercel describes cron delivery as best effort: a run can be missed or invoked more than once, failed runs are not retried, and redirects are not followed. Make each job idempotent and reconciliation-based.
- Respect the Hobby cron limit. On Hobby, cron jobs can run once per day and may fire anywhere within the scheduled hour; a more frequent expression fails the deployment (cron usage and pricing).
## Vercel
- Runtime: Node.js for every route; NEVER add runtime = 'edge' to new code
- Long work: maxDuration per route (Hobby max 300s, Pro max 800s); beyond that use a queue
- Env vars: scoped per environment, `vercel env pull` locally, redeploy after any change
- Cron: vercel.json crons (path + schedule, UTC); every cron route checks Authorization: Bearer ${CRON_SECRET}
- Cron jobs are idempotent; missed or duplicate runs must be safe
- Production deploys ONLY via git push to main; NEVER `vercel --prod` from Claude
- vercel.json: edit specific keys, never rewrite the file
A cron route that refuses other callers:
// app/api/cron/sync/route.ts
export async function GET(request: Request) {
if (request.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
await reconcileSinceLastRun(); // idempotent: safe to run twice
return Response.json({ ok: true });
}
Netlify rules for Claude Code
Netlify combines a static site deploy with serverless functions and Deno-based edge functions, and most Claude mistakes come from mixing those layers:
netlify.tomlvariables do not reach functions. Netlify's docs state that environment variables declared innetlify.tomlare not available to serverless functions (functions environment variables). Runtime secrets for functions go in the Netlify UI or CLI. This is the rule Claude breaks most often, because the build environment block looks like the natural place for every variable.- Redirect order is deterministic. Rules in
_redirectsare always processed first, then rules innetlify.toml, and the first matching rule wins, reading top to bottom (redirects overview). Pick one file as the source of truth, and review every inserted rule for position: a catch-all placed above a specific proxy rule silently shadows it. - Edge functions are not Node. They run on a Deno-based runtime with a 50 ms CPU limit per request and a 40 second response header timeout (edge function limits). Keep them to request inspection, rewrites, headers and personalization, and put database drivers and heavy SDKs in regular functions.
- Long work goes to background functions. A function becomes a background function with
background: truein its config (the-backgroundfilename suffix still works); it returns an empty 202 immediately and runs for up to 15 minutes (background functions). - Scheduled functions are short and production-only. A
schedulein the function config (cron or shortcuts such as@hourly) runs in UTC with a 30 second execution limit, and only on published deploys, not Deploy Previews or branch deploys (scheduled functions). If the job takes longer, have the scheduled function enqueue a background function.
## Netlify
- netlify.toml at the repo root; edit sections, never rewrite the file
- Function secrets: Netlify UI or CLI, NEVER [build.environment] (functions cannot read it)
- Redirects: ONE source of truth; _redirects is processed before netlify.toml; first match wins
- Edge functions (netlify/edge-functions): Deno runtime, 50 ms CPU, no Node-only packages
- Functions (netlify/functions): Request in, Response out, config.path for routing
- Work over a few seconds: config.background = true (202 now, up to 15 minutes)
- Scheduled functions: config.schedule, UTC, 30 s limit, production deploys only
- NEVER `netlify deploy --prod` from Claude without an explicit request
A modern Netlify function with its routing declared in code:
// netlify/functions/report.mts
import type { Config } from '@netlify/functions';
export default async (req: Request) => {
const body = await req.json();
await enqueueReport(body); // hand off; do not render the report inline
return Response.json({ queued: true }, { status: 202 });
};
export const config: Config = { path: '/api/report' };
Railway rules for Claude Code
Railway runs each service as a container inside a project, with managed databases alongside. Its rules are mostly about wiring services together without hardcoding anything:
- Config as code is changing. Railway's config-as-code reference marks
railway.jsonandrailway.tomlas deprecated in favour of Infrastructure as Code, and says existing files keep working for legacy services until 2026-12-01 (config as code). The replacement is a single.railway/railway.tsfile that declares services withservice("web", { build, start, healthcheck, env })and is applied withrailway config planthenrailway config apply;railway config migrategenerates it from your existing files (Infrastructure as Code). Tell Claude which one your project uses so it does not edit a file Railway is about to ignore. - Bind the injected port. Railway injects
PORT, the server should bind0.0.0.0, and the target port of the public domain must match the port you listen on (application failed to respond). - Reference variables, never paste them. Use reference syntax such as
${{ Postgres.DATABASE_URL }}for another service's variable and${{ shared.KEY }}for shared ones (variables). A pasted literal connection string stops matching the moment the database credentials change. - Talk to sibling services privately. Services in a project reach each other at
SERVICE_NAME.railway.internalover an encrypted private network (private networking). Internal calls should use that hostname, not the public domain. - Know what the healthcheck does. It runs only at deploy time, waits up to 300 seconds by default for any 2xx, and is sent from the hostname
healthcheck.railway.app, which host-restricted apps must allow (healthchecks). It is a deploy gate, not uptime monitoring. - Migrations in the pre-deploy step, cron on its own service. In the legacy config these are
deploy.preDeployCommandanddeploy.cronSchedule, alongside arestartPolicyTypeofON_FAILURE,ALWAYSorNEVER. The Infrastructure as Code page does not yet document keys for cron or the builder, so runrailway config pull --forceand check what it generates before Claude invents a key. A cron service should exit when its work is done.
## Railway
- Config: .railway/railway.ts (IaC); legacy railway.json is read only until 2026-12-01
- One service per process type: web, worker, cron are separate services
- Listen on process.env.PORT, host 0.0.0.0; public domain target port matches
- Variables: ${{ Postgres.DATABASE_URL }}, ${{ Redis.REDIS_URL }}, ${{ shared.KEY }}; NEVER literal connection strings
- Internal calls: http://<service>.railway.internal:<port>, never the public domain
- Healthcheck: /health returns 2xx; allow host healthcheck.railway.app
- Migrations: the pre-deploy command (legacy key deploy.preDeployCommand); NEVER in the start command
- Cron: a separate service with a cron schedule (legacy key deploy.cronSchedule), exits on completion
The most common Railway mistake is the monolith service: one container starting both node server.js and node worker.js. Railway's model is one process per service, so split them, give each its own start command, and let the worker reach the API over the private network.
Render rules for Claude Code
Render describes infrastructure in a render.yaml Blueprint, which makes it easy for Claude to edit and easy for Claude to get subtly wrong:
- Bind
0.0.0.0onPORT. Every web service must bind a port on host0.0.0.0, the defaultPORTis 10000, and if Render detects no bound port the deploy fails (web services). - Use current Blueprint keys. Service
typeis one ofweb,pserv,worker,cron,keyvalue(withredisas a deprecated alias) orworkflow;runtimereplaces the deprecatedenvfield;autoDeployTrigger(commit,checksPassoroff) replaces the deprecatedautoDeploy(Blueprint spec). Claude trained on older examples writesenv: nodeandautoDeploy: true; tell it which keys are current. - Wire credentials by reference.
fromDatabaseandfromServicepull connection strings from the resource,generateValue: truecreates a random 256-bit value, andsync: falseprompts for a secret once at Blueprint creation and is ignored on later syncs. Environment groups live in the rootenvVarGroupslist and cannot reference service properties. - Migrations go in
preDeployCommand. It runs after the build and before the start command, on a separate instance with no access to persistent disks, and it is available for paid web services, private services and background workers (deploys). On a free service, run migrations from CI instead. - Know the free-tier behaviour. Free web services spin down after 15 minutes without traffic and take about one minute to spin back up; free Postgres databases expire 30 days after creation; background workers and cron jobs do not run on free instances (free instances). Document this in CLAUDE.md so Claude does not add keep-alive pings to a preview service.
- Cron uses five fields. The
scheduleon acronservice is a standard cron expression. Libraries that accept a seconds field have taught Claude six-field expressions; reject them.
## Render
- render.yaml is the source of truth; edit services, never regenerate the file
- Keys: type, runtime (not env), autoDeployTrigger (not autoDeploy), healthCheckPath on web
- Web services bind 0.0.0.0 on process.env.PORT (Render default 10000)
- DATABASE_URL via fromDatabase; service URLs via fromService; secrets via sync: false
- Migrations: preDeployCommand (paid services); free services migrate from CI
- Cron: type cron + five-field schedule, script exits when done
- Free web services sleep after 15 minutes; NEVER add keep-alive pings
A Blueprint fragment in the current dialect:
services:
- type: web
name: api
runtime: node
buildCommand: npm ci && npm run build
preDeployCommand: npm run db:migrate
startCommand: node dist/server.js
healthCheckPath: /health
autoDeployTrigger: checksPass
envVars:
- key: DATABASE_URL
fromDatabase:
name: main-db
property: connectionString
- key: SESSION_SECRET
generateValue: true
- key: STRIPE_SECRET_KEY
sync: false
Fly.io rules for Claude Code
Fly.io runs your Docker image as Machines in the regions you choose. Its rules are about regions, volumes and the release step:
- Pin the primary region.
primary_regioninfly.tomldecides wherefly deploycreates Machines and setsPRIMARY_REGIONinside them (fly.toml reference). - Run migrations in
release_command. It runs once per deploy in a temporary Machine built from the new image, with no volumes attached, and a non-zero exit fails the deploy; the default timeout is 5 minutes. Migrations that need a volume cannot run here. - Choose the stop behaviour on purpose.
auto_stop_machinesaccepts"off","stop"or"suspend"and defaults to"off";min_machines_runningkeeps Machines alive in the primary region when stopping is on. Claude will otherwise copy whatever a template had. - Treat volumes as local disks. A volume attaches to exactly one Machine, lives on one physical host in one region, and is not replicated; Fly.io keeps daily snapshots for five days by default and says to always provision at least two volumes per app (volumes). Replication is your application's job.
- Replay writes with the right region variable. Fly Proxy honours a
fly-replayresponse header (region=,instance=orapp=) and replays the request elsewhere; requests over 1 MB cannot be replayed (dynamic request routing). CompareFLY_REGION, the environment variable naming where the Machine runs, withPRIMARY_REGION. TheFly-Regionrequest header is the edge region that accepted the connection, which is a different thing (request headers). - Setting a secret restarts Machines.
fly secrets setupdates every Machine, which restarts it and resets its ephemeral filesystem;--stagesaves the secret without restarting, and secrets are runtime-only, not available during the Docker build (secrets).
## Fly.io
- fly.toml: app, primary_region, [http_service] internal_port matches the server port
- Migrations: [deploy] release_command (temporary Machine, no volumes, 5 min default timeout)
- auto_stop_machines set explicitly; min_machines_running for anything latency-sensitive
- Volumes: one per Machine, at least two per app, replication handled by the app
- Writes from non-primary regions: respond with fly-replay: region=${PRIMARY_REGION}
- Region check uses process.env.FLY_REGION, NOT the Fly-Region request header
- Secrets: `fly secrets set --stage` then `fly secrets deploy` to batch restarts
- Validate before pushing: `fly config validate`; production deploys from CI only
A write-replay middleware that compares the right values:
// middleware/replay-writes.ts
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
export function replayWrites(req, res, next) {
const here = process.env.FLY_REGION;
const primary = process.env.PRIMARY_REGION;
if (WRITE_METHODS.has(req.method) && here && primary && here !== primary) {
res.set('fly-replay', `region=${primary}`);
return res.status(409).end();
}
next();
}
Cloudflare Workers rules for Claude Code
Workers run in V8 isolates, not containers, so the rules are about bindings, limits and compatibility settings:
- Use
wrangler.jsoncfor new projects. Cloudflare recommends it overwrangler.toml, and some newer Wrangler features only work with a JSON config (Wrangler configuration). Bindings such askv_namespaces,r2_bucketsandvarsare not inherited by environments and must be redeclared under eachenv, whilecompatibility_dateandcompatibility_flagsare inherited. - The compatibility date decides what Node APIs exist. With a compatibility date of 2026-08-04 or later, built-in Node.js APIs and polyfills are available without extra configuration; between 2024-09-23 and 2026-08-03 you need the
nodejs_compatflag (Node.js compatibility). Put your date in CLAUDE.md, because it changes which imports are valid. - Know the CPU limits. Free Workers get 10 ms of CPU per HTTP request; paid Workers default to 30 seconds and can be configured up to 5 minutes; memory is 128 MB per isolate; the free plan allows 100,000 requests a day (Workers limits). CPU time excludes waiting on I/O, so the free limit bites on parsing, crypto and heavy loops, not on fetches.
- KV is eventually consistent. Changes can take 60 seconds or more to appear in other locations, and KV is a poor fit for keys written many times a second (how KV works). Counters, locks and anything read-after-write belong in Durable Objects or D1.
- Background work uses
ctx.waitUntil. It keeps a promise running for up to 30 seconds after the response is sent, shared across allwaitUntilcalls in the request (context API). Anything longer goes to a Queue. - Cron Triggers live in config. Declare
triggers.cronsin the Wrangler file and export ascheduled(controller, env, ctx)handler; schedules run in UTC (cron triggers). - Secrets stay out of
vars. Use.dev.varsor.envlocally (never committed) and Wrangler secrets in production.
## Cloudflare Workers
- Config: wrangler.jsonc; compatibility_date pinned; NEVER change it without instruction
- Bindings declared in wrangler.jsonc AND typed in the Env interface; redeclare per env
- Read secrets and bindings from the env parameter, not module scope
- Free plan: 10 ms CPU per request; heavy work goes to a Queue or Durable Object
- KV: cache and config only (eventually consistent); counters and locks in Durable Objects or D1
- After-response work: ctx.waitUntil(promise), max 30 s
- Cron: triggers.crons + scheduled() handler, UTC
- Local secrets: .dev.vars (gitignored); production: wrangler secret
- NEVER remove an existing binding when adding a new one
A config Claude can extend safely, because every binding is explicit:
{
"name": "api",
"main": "src/index.ts",
"compatibility_date": "2026-10-01",
"kv_namespaces": [{ "binding": "CACHE", "id": "<kv-id>" }],
"d1_databases": [{ "binding": "DB", "database_name": "app", "database_id": "<d1-id>" }],
"triggers": { "crons": ["0 3 * * *"] }
}
Common Claude Code deployment mistakes, and the rule that prevents each
Claude learns fastest from before-and-after pairs, so paste the rows that apply to your platform into CLAUDE.md.
| What Claude writes by default | Why it hurts | Rule to add |
|---|---|---|
app.listen(3000) |
No traffic reaches the container on Railway, Render or Fly.io | Bind 0.0.0.0 on process.env.PORT; match internal_port on Fly.io |
A literal DATABASE_URL in config |
Breaks when credentials change; leaks the secret into git | Railway ${{ Postgres.DATABASE_URL }}, Render fromDatabase |
| Migrations in the start command | Re-run on every restart and every instance | preDeployCommand (Railway, Render) or release_command (Fly.io) |
| One service running web and worker | Worker restarts with web traffic | One process type per service |
runtime = 'edge' on a new Vercel route |
Node-only packages fail; Vercel now recommends Node.js | Node.js runtime by default |
Function secrets in netlify.toml |
Functions cannot read them | Netlify UI or CLI for function variables |
| A catch-all redirect above a proxy rule | The proxy never matches | Review redirect order; first match wins |
env: node and autoDeploy: true in render.yaml |
Deprecated keys | runtime and autoDeployTrigger |
Comparing Fly-Region to PRIMARY_REGION |
Compares the edge region, not the Machine's region | Compare FLY_REGION |
| A KV counter in Workers | Eventual consistency loses increments | Durable Objects or D1 |
| An unauthenticated cron route | Anyone can trigger the job | Check CRON_SECRET or an equivalent token |
vercel --prod or fly deploy from the agent |
Unreviewed production release | Deny list plus git-based production deploys |
Frequently asked questions
Which hosting platform works best with Claude Code?
All six work well, because each is driven by a config file and a CLI that Claude can read and run once your CLAUDE.md pins the rules. Vercel suits Next.js and frontend-led apps, Netlify suits static sites with light functions, Railway suits multi-service projects with managed databases, Render suits services declared in one Blueprint, Fly.io suits multi-region containers, and Cloudflare Workers suits latency-critical code that fits an isolate.
Can Claude Code deploy my app by itself?
It can run the deploy commands, which is why it should not run the production ones unattended. Let Claude build, validate and create preview deployments, and put production deploy commands on the permission deny list so it has to ask. Route production releases through git pushes to your production branch or a CI pipeline.
Where should database migrations run when Claude Code deploys?
In the platform's release step: preDeployCommand on Railway and Render (paid services on Render), release_command on Fly.io. These run once per deploy, before the new version takes traffic, and a failure stops the deploy. Vercel, Netlify and Workers have no release phase, so run migrations from CI before the deploy step.
Should Claude Code use the Edge runtime on Vercel?
Not for new code. Vercel's own documentation recommends migrating from edge to Node.js for improved performance and reliability, and states that Next.js 16.3 no longer supports runtime = 'edge'. Keep new routes on Node.js and set maxDuration where a route needs longer.
What happened to the separate Vercel, Netlify, Railway, Render, Fly.io and Cloudflare Workers guides?
They are merged into this page. Each platform's distinctive rules are in its own section above, under one shared deployment skeleton, which is easier to keep current and easier to copy. Every platform fact on this page was checked against that platform's documentation on 21 October 2026.
Related guides
- Claude Code deployment: the pre-ship audit, rollback and verification steps that sit above any platform.
- Claude Code with Docker: writing the images that Fly.io, Railway and Render run.
- Claude Code for payments: webhooks, idempotency and secrets for Stripe, Paddle, Lemon Squeezy and Polar.
- Claude Code for transactional email: the same one-skeleton approach for Resend, Postmark, SendGrid, Mailgun and Mailtrap.
Ship to any host without surprises
The pattern in this guide is the one that makes Claude Code reliable anywhere: decide the non-negotiable rules once, write them where Claude reads them every session, and keep the platform-specific part short. For deployment that means a port read from the environment, secrets outside the repository, migrations in the release step, one process per service, idempotent scheduled jobs and a human in front of every production deploy.
Get Claudify. A complete Claude Code operating layer with 1,738 skills, 13 agents and 79 commands, plus persistent memory and quality hooks, ready to adapt to your stack.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify