Claude Code with Render: Web Services, Workers, Cron, Postgres
Why Render without CLAUDE.md generates configs that work locally but fail in production
Render is a Heroku-style platform-as-a-service that provisions web services, background workers, cron jobs, Postgres databases, and Redis instances from a single render.yaml file in your repository root. The declarative model is clean. The deployment pipeline is automatic. The operational surface is intentionally small. None of that protects you from Claude Code generating configs that pass local testing and fail silently the moment they hit a Render environment.
The failure modes are predictable. Claude hardcodes PORT=3000 in the server file because that is the Express default, but Render assigns a dynamic port at runtime and passes it via process.env.PORT. A server bound to 3000 never receives traffic. Claude writes npm run dev as the startCommand because that is what developers type locally, but the dev server uses hot reload, slower startup, and development middleware. Claude generates a raw DATABASE_URL string in the envVars block instead of using Render's fromDatabase reference, which means the connection string breaks the moment Render rotates credentials. Claude omits autoDeploy: true on the service definition, so pushes to the connected branch do nothing without a manual trigger in the dashboard.
These are not obscure edge cases. They are the four most common Claude-generated Render mistakes, and each one produces a service that deploys without an error but does not behave correctly in production. A CLAUDE.md file that declares Render's actual model prevents all four before Claude writes a single line.
This guide covers the CLAUDE.md configuration that anchors Claude Code to Render's 2026 Blueprint system: render.yaml as the source of truth for all infrastructure, fromDatabase and fromService references for wired credentials, the correct start command for each service type, and the PORT binding rule that cannot be negotiated. If you are configuring Claude Code for the first time, the Claude Code setup guide covers installation. For the Railway comparison with a similar infrastructure-as-code model, Claude Code with Railway is a useful contrast.
The Render CLAUDE.md template
The CLAUDE.md at your project root is read at the start of every session. For a Render deployment it needs to declare: the render.yaml Blueprint as the only infrastructure source of truth, service type definitions with their constraints, the PORT binding rule, envVarGroup structure for shared secrets, the fromDatabase and fromService reference pattern for wired credentials, the start command rules for each runtime, the free-tier sleep behaviour and its implications for health checks, and the hard rules that block the patterns Claude generates most often without guidance.
# Render deployment rules
## Stack
- Render Blueprint (render.yaml at project root)
- Node 20.x or Python 3.12.x runtime
- Postgres 16 (managed by Render)
- Redis 7.x (managed by Render)
## Infrastructure source of truth
- ALL services, databases, and env config live in render.yaml
- NEVER hardcode infrastructure config in application code
- NEVER use the Render dashboard to create services manually; declare them in render.yaml
- render.yaml changes on the default branch trigger a Blueprint sync
## Service types
- web: HTTP service exposed on a public URL; Render health-checks the / route by default
- worker: background process; no HTTP exposure; no health check; runs continuously
- cron: scheduled task; specify schedule as a standard cron expression; runs to completion then exits
- pserv: private TCP service; reachable only by other services in the same Render account
## PORT binding (CRITICAL)
- ALWAYS bind to process.env.PORT, NEVER hardcode a port number
- Correct Node: app.listen(process.env.PORT || 3000)
- Correct Python: uvicorn.run(app, host='0.0.0.0', port=int(os.environ.get('PORT', 8000)))
- Hardcoded port 3000 / 8000 / 8080 will NOT receive traffic on Render
## render.yaml structure
services:
- type: web
name: api
runtime: node
plan: starter
autoDeploy: true
buildCommand: npm ci && npm run build
startCommand: node dist/server.js
healthCheckPath: /health
envVars:
- key: NODE_ENV
value: production
- key: DATABASE_URL
fromDatabase:
name: my-postgres
property: connectionString
- key: REDIS_URL
fromService:
name: my-redis
type: redis
property: connectionString
- fromGroup: shared-secrets
- type: worker
name: queue-processor
runtime: node
plan: starter
autoDeploy: true
buildCommand: npm ci && npm run build
startCommand: node dist/worker.js
envVars:
- key: DATABASE_URL
fromDatabase:
name: my-postgres
property: connectionString
- fromGroup: shared-secrets
- type: cron
name: daily-cleanup
runtime: node
plan: starter
schedule: '0 2 * * *'
buildCommand: npm ci && npm run build
startCommand: node dist/scripts/cleanup.js
envVars:
- key: DATABASE_URL
fromDatabase:
name: my-postgres
property: connectionString
databases:
- name: my-postgres
databaseName: production
plan: starter
postgresMajorVersion: 16
envVarGroups:
- name: shared-secrets
envVars:
- key: JWT_SECRET
generateValue: true
- key: STRIPE_SECRET_KEY
sync: false
## Start command rules by runtime
- Node web: node dist/server.js (compiled output, NOT ts-node, NOT npm run dev)
- Node worker: node dist/worker.js (same rule as web)
- Python web: gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker
- Python worker: python -m app.worker
- NEVER use npm run dev, nodemon, ts-node src/, or any hot-reload command as startCommand
## Credential wiring
- ALWAYS use fromDatabase for Postgres connection strings
- ALWAYS use fromService for Redis connection strings
- NEVER paste raw connection strings into envVars.value
- Raw strings break silently when Render rotates credentials on restore or failover
## Hard rules
- autoDeploy: true is REQUIRED on every service unless deliberately omitted with comment
- healthCheckPath MUST be set on web services; default / is fine only if it returns 2xx
- generateValue: true for auto-generated secrets (JWT, encryption keys)
- sync: false for secrets that must be pasted manually in the dashboard (API keys, webhook secrets)
- NEVER commit secrets to render.yaml; use sync: false entries only
Three rules here prevent the majority of production failures Claude generates without them.
The PORT binding rule is the single most impactful entry. Render's load balancer forwards traffic to the port reported by process.env.PORT at runtime. A server listening on 3000 while Render expects a different port is reachable by nobody. The health check fails, the service is marked unhealthy, and no traffic is routed. Claude generates app.listen(3000) as the default because that is what every Node tutorial uses. The rule removes that option and forces process.env.PORT.
The fromDatabase / fromService rule matters because Render manages Postgres and Redis connection strings internally. If you paste a raw string into envVars.value, it works until Render rotates the credential during a restore, a failover, or a maintenance event. The application then loses its database connection with no obvious cause. The fromDatabase reference tells Render to inject the current string automatically at deploy time. Claude cannot know that Render rotates credentials, so it generates the raw string pattern by default.
The autoDeploy rule prevents a common confusion where a push to the connected branch appears to do nothing. Without autoDeploy: true, Render waits for a manual deploy trigger in the dashboard. Claude omits it when the render.yaml examples it has seen do not include it. Declaring it explicitly in CLAUDE.md means every new service Claude adds to render.yaml includes the field.
render.yaml infrastructure as code
The render.yaml Blueprint is Render's equivalent of a Terraform plan or a Railway railway.toml. Every service in your Render account for this project should have a corresponding entry. If a service is not in render.yaml, it is not reproducible. If your production database was created by clicking through the dashboard, it cannot be recreated from the file, and any developer who joins the project cannot spin up a local-equivalent stack from the repository alone.
Add a Blueprint discipline section to CLAUDE.md:
## Blueprint discipline
### Service naming
- Service names in render.yaml must match the Render dashboard names exactly
- Use kebab-case: api, queue-processor, daily-cleanup, my-postgres
- Names are used in fromService references; a mismatch causes a silent binding failure
### Plan selection
- starter: paid plan, no sleep, suitable for production web services
- free: sleeps after 15 minutes of inactivity, cold start ~30 seconds, not suitable for production
- Use free for preview environment services only
- Set plan: starter in CLAUDE.md as the default; explicitly note free only for previews
### Preview environments
- Enable previews by adding a previews block to each service:
previews:
generation: automatic
- Every pull request gets a preview URL automatically
- Preview services inherit envVarGroups from the parent service
- Preview databases are ephemeral; they reset on each PR push
- Do NOT use preview DATABASE_URL in migration scripts that modify production data
### Disks (stateful storage)
- Web services on Render are STATELESS; file writes are lost on restart
- NEVER write uploads, generated files, or persistent data to the local filesystem
- Use Cloudflare R2 or AWS S3 for file storage (apply the same pattern as Railway)
- Mounted disks are only available on pserv (private service) type; they persist between restarts
- If the application writes to disk, add a comment in render.yaml flagging the anti-pattern
### Blueprint sync
- render.yaml changes are applied on the next deploy of the default branch
- Deleting a service from render.yaml does NOT delete it from Render automatically
- To decommission a service: remove from render.yaml, then delete manually in the dashboard
- Document decommissioned services in a ## Removed services comment block at the bottom
The stateless filesystem rule catches a category of bugs that only appear in production. Locally, a developer uploads an avatar, it saves to ./uploads/, and the next request reads it back correctly. On Render, the upload survives until the web service restarts, which happens on every deploy. The next deploy clears it. Users lose uploaded files on every code push. The rule forces uploads to object storage from the beginning, before any production data is lost.
Environment variables and envVarGroups
Render's environment variable system has three tiers. An envVars block on a service sets variables for that service only. An envVarGroup is a named collection of variables that multiple services can reference with a single fromGroup line. A sync: false entry creates a placeholder that must be filled manually in the dashboard and is never committed to the repository.
Add an environment variable section to CLAUDE.md:
## Environment variables
### Tier 1: service-specific, committed
envVars:
- key: NODE_ENV
value: production
- key: LOG_LEVEL
value: info
Use for non-secret config that varies by service type.
### Tier 2: shared secrets, envVarGroup
envVarGroups:
- name: shared-secrets
envVars:
- key: JWT_SECRET
generateValue: true # Render generates a random value on first deploy
- key: SESSION_SECRET
generateValue: true
- key: STRIPE_SECRET_KEY
sync: false # Must be pasted in the dashboard; never in the file
- key: SENDGRID_API_KEY
sync: false
Reference in services with:
- fromGroup: shared-secrets
Use for secrets shared across api + worker + cron services.
### Tier 3: database and service references
- key: DATABASE_URL
fromDatabase:
name: my-postgres
property: connectionString
- key: REDIS_URL
fromService:
name: my-redis
type: redis
property: connectionString
Use for Render-managed service credentials. NEVER replace with raw strings.
### generateValue vs sync: false
- generateValue: true: Render generates a cryptographically random value; suitable for JWT_SECRET, ENCRYPTION_KEY
- sync: false: placeholder; developer must paste the value in the dashboard; suitable for third-party API keys
### Hard rules
- NEVER set DATABASE_URL to a raw postgres:// string in envVars.value
- NEVER commit a .env file that contains production secrets
- NEVER put a sync: false key in envVars.value; leave it empty or Render will expose it in the Blueprint file
The generateValue: true pattern is worth using for any secret that does not need to match an external system. JWT signing keys, session secrets, and encryption keys can all be random values that Render generates. The application reads them from the environment and never needs to know what they are. Claude defaults to asking the developer to paste a secret when generateValue: true would work. Declaring the distinction in CLAUDE.md means Claude uses the correct mechanism for each category.
For the broader PostgreSQL connection and migration patterns that sit above the credential wiring layer, Claude Code with PostgreSQL covers the Drizzle and Prisma conventions that work cleanly with Render's fromDatabase references. For Redis queue and cache patterns, Claude Code with Redis applies the same approach to fromService wiring.
Service types: web, worker, cron, pserv
Render's four service types map to four operational patterns. Claude will default to type: web for everything unless the distinction is explicit in CLAUDE.md. A background job running as a web service consumes a port, passes health checks, and receives HTTP traffic it ignores. A cron job running as a worker runs continuously and never exits. Both are wrong, and both appear to work until they fail under load or produce unexpected behaviour.
Add a service type section to CLAUDE.md:
## Service type decision rules
### web
- Exposes an HTTP port (process.env.PORT, mandatory)
- Receives public traffic via Render's load balancer
- Has a healthCheckPath; Render removes from rotation if health check fails
- Use for: API servers, Next.js apps, Express servers, FastAPI apps
- startCommand must start a long-running HTTP server
### worker
- No port, no HTTP exposure, no health check
- Runs the startCommand continuously; Render restarts it if it exits
- Use for: queue consumers (BullMQ, Celery), event listeners, stream processors
- Do NOT use for scheduled tasks; that is cron
### cron
- Runs startCommand on the schedule defined by schedule: (standard cron syntax)
- The process must exit after completing its work; Render does not terminate it automatically
- A cron that never exits blocks the next scheduled run
- Use for: database cleanup, report generation, cache warming, batch processing
- ALWAYS ensure the script calls process.exit(0) on completion
### pserv (private service)
- TCP service reachable only by other services in the same Render account
- Has a persistent mounted disk (unlike web/worker which are stateless)
- Use for: internal gRPC services, database proxies, stateful sidecar processes
- Not accessible from the public internet
### Cron script template (Node)
async function main() {
await connectDatabase();
await runCleanup();
await disconnectDatabase();
process.exit(0); // REQUIRED; without this Render holds the slot open
}
main().catch((err) => { console.error(err); process.exit(1); });
### Hard rules
- NEVER use type: web for a process that does not expose an HTTP port
- NEVER omit process.exit(0) from a cron script
- NEVER use type: worker for a scheduled task; that is type: cron with a schedule field
- healthCheckPath is mandatory on all web services
The process.exit(0) rule for cron jobs is the one Claude misses most consistently. A Node script that does async database work and then simply falls off the end of the function does not terminate automatically. The event loop stays open waiting for outstanding timers or callbacks. Render sees a running process and does not schedule the next run. The next execution never fires. Declaring the explicit exit pattern in CLAUDE.md means Claude always adds it to generated cron scripts.
Build commands, start commands, and the free-tier sleep
Render runs buildCommand once during deployment and startCommand continuously for web and worker services. These two phases have different requirements and different failure modes, and Claude will blur them if the distinction is not declared.
Add a build and start section to CLAUDE.md:
## Build and start commands
### buildCommand rules
- Runs once at deploy time in a temporary build environment
- The build environment is discarded after build; nothing written here persists to runtime
- Install all dependencies including devDependencies (needed for TypeScript compilation)
- Run the TypeScript compiler or bundler here
- Correct Node: npm ci && npm run build
- Correct Python: pip install -r requirements.txt
### startCommand rules
- Runs in the runtime environment after build completes
- Must be a long-running process for web and worker types
- Must exit cleanly for cron type
- NEVER use npm run dev (uses hot reload, not production server)
- NEVER use ts-node src/server.ts (compiles at runtime, slow startup, memory heavy)
- NEVER use nodemon (restarts on file changes; no files change in production)
- Correct Node: node dist/server.js
- Correct Python web: gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:$PORT
- Correct Python worker: python -m app.worker
### NODE_ENV and production flags
- Set NODE_ENV=production in envVars
- This disables development middleware, enables production error handling, and activates production logging
### Free-tier sleep behaviour
- Free plan web services sleep after 15 minutes of inactivity
- Cold start on wake: approximately 30 seconds
- The first request after sleep times out or returns a 503 during startup
- Health check probes do NOT prevent sleep; only real HTTP traffic does
- If the application must respond immediately: upgrade to starter plan
- If sleep is acceptable: document it in CLAUDE.md so Claude does not add keep-alive hacks
- Do NOT generate keep-alive ping scripts; they prevent sleep but burn free credits faster
### Build caching
- Render caches node_modules between builds when package-lock.json has not changed
- Use npm ci (not npm install) to respect the lockfile and enable cache hits
- Clear cache from the dashboard if a dependency is misbehaving between deploys
### Hard rules
- startCommand MUST match the output of buildCommand (compiled JS, not source TypeScript)
- NEVER generate a keep-alive pinger for free-tier services; document the sleep instead
- NEVER use npm run start if start in package.json calls npm run dev
The keep-alive anti-pattern is worth flagging explicitly. Developers often ask Claude to add a cron job or a scheduled HTTP ping to prevent free-tier sleep. The ping works, but it means the service is never idle, which defeats the purpose of the free tier and eventually triggers Render's fair-use limits. The correct answer is to document the sleep behaviour and decide whether the application needs the starter plan. CLAUDE.md's explicit prohibition on keep-alive scripts stops Claude from generating one the moment a developer mentions the sleep.
Deploy hooks and preview environments
Render supports deploy hooks and automatic preview environments for pull requests. Both are configured in render.yaml and both require specific CLAUDE.md guidance to generate correctly.
Add a deploy hooks section to CLAUDE.md:
## Deploy hooks
### Deploy hook URL
- Available in the Render dashboard under a service's Settings tab
- Triggering the URL with an HTTP POST starts a new deploy
- Use for: triggering deploys from external CI, after a migration completes, from n8n workflows
- Store the URL in an envVarGroup entry with sync: false; never commit it to the repo
### Preview environments
- Enable on a per-service basis in render.yaml:
previews:
generation: automatic
- Every pull request to the default branch gets a preview service
- Preview URL format: https://{service-name}-pr-{number}.onrender.com
- Preview services share the parent's envVarGroups
- Preview databases are ephemeral (reset on each PR push, deleted when PR closes)
- NEVER run production migrations against a preview database
### Post-deploy scripts
- Render does not have a native post-deploy hook in render.yaml
- Run migrations as part of buildCommand, not startCommand
- Correct pattern: npm ci && npm run build && npm run db:migrate
- NEVER run migrations in startCommand; a failed migration kills every web server restart
- Alternatively, use a one-off job triggered by the deploy hook URL
### Database migrations on Render
- Run migrations in buildCommand after the build step
- Use a migration tool that exits with code 0 on success and non-zero on failure
- Render fails the deploy if buildCommand exits non-zero, which prevents a broken app from going live
- Drizzle: npm ci && npm run build && npx drizzle-kit migrate
- Prisma: npm ci && npm run build && npx prisma migrate deploy
- NEVER run prisma migrate dev in production (it generates new migrations; deploy applies them)
## Hard rules
- ALWAYS run database migrations in buildCommand, not startCommand
- NEVER commit deploy hook URLs to the repository
- Preview environment services use free plan by default; document this, do not upgrade them
- Migration failures in buildCommand prevent deploy; treat this as the safety gate, not an obstacle
The migration placement rule prevents a category of production incident. If a migration runs in startCommand, every restart of the web service re-runs the migration. Idempotent migrations survive this. Non-idempotent migrations corrupt data on the second run. Running migrations in buildCommand means they run once per deploy, before the new code is live, and a migration failure prevents the broken code from being deployed at all. Claude places migrations in startCommand by default because that is the pattern in many tutorials. The rule moves them to the correct phase.
Common gotchas and what to review manually
Claude Code generates correct Render configuration in several areas when the CLAUDE.md template is in place. The fromDatabase reference for Postgres, the fromService reference for Redis, the NODE_ENV=production env var, the autoDeploy: true flag, and the process.exit(0) in cron scripts are all consistently generated when the rules are declared.
Four areas warrant manual review after Claude generates or modifies render.yaml.
The port binding in the application code. CLAUDE.md declares the rule, but Claude must apply it to every server file it generates or modifies. Search for any literal port number (3000, 8000, 8080) in the application code before deploying. A single hardcoded listen(3000) in a file Claude wrote without reading CLAUDE.md breaks the entire web service.
The health check path. healthCheckPath: /health is the convention declared in CLAUDE.md, but the route must actually exist in the application. Claude will add the YAML field without verifying that the Express router or FastAPI app has a /health endpoint that returns 200. Check that the endpoint exists, returns a 200 with a valid body, and does not perform expensive operations on every call.
The cron schedule expression. Render uses standard cron syntax with five fields (minute, hour, day, month, weekday). Claude sometimes generates six-field cron expressions (with seconds) from libraries that use a different format. A six-field expression is silently invalid on Render and the job never fires. Verify the schedule field uses exactly five space-separated fields.
The service plan for production. CLAUDE.md declares plan: starter as the default, but Claude may generate plan: free for new services when the context is ambiguous. A web service on the free plan sleeps, which is correct for preview environments and incorrect for production. Review every service's plan field before merging a render.yaml change that adds a new service.
For the deployment pipeline patterns that sit above the platform layer, Claude Code deployment covers the pre-ship audit and the rollback procedures that apply regardless of whether you are deploying to Render, Railway, or Vercel. Claude Code with Vercel covers the equivalent CLAUDE.md model for the Vercel platform, which uses a different infrastructure-as-code format but shares several of the same production discipline rules.
Deploying to Render with confidence
The Render CLAUDE.md in this guide produces a deployment configuration where ports are always dynamic because process.env.PORT is the only binding target, credentials are always current because fromDatabase and fromService references track Render's managed values, deploys are automatic because autoDeploy: true is declared on every service, and migrations run safely because they execute in buildCommand before the new code is live.
The underlying principle is the same as any infrastructure integration with Claude Code. A render.yaml without a CLAUDE.md produces a configuration that works in the developer's local environment and fails in Render's production runtime, because Claude reaches for the familiar defaults: port 3000, npm run dev, raw connection strings. A configuration with the rules above has a single correct deployment path from repository push to live service.
For the mechanics of how CLAUDE.md is read at session start and how to version it alongside your infrastructure configuration, see CLAUDE.md explained. Claudify includes a Render-specific CLAUDE.md template, pre-configured for the Blueprint service type rules, PORT binding enforcement, envVarGroup wiring, migration placement, and preview environment discipline shown in this guide.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify