Deploy to Render
Managed Postgres with real backups, and a free tier you can demo on.
Teams that want managed Postgres with point-in-time recovery without thinking about it, and a blueprint file checked into the repo.
Anything latency-sensitive on the free tier: free services spin down after inactivity and cold starts run to tens of seconds.
Quick setup
- 1
Describe the stack in render.yaml
Committing the blueprint means the environment is reviewable in a pull request instead of living in a dashboard nobody can diff.
services:- type: webname: my-app-apiruntime: dockerdockerfilePath: ./apps/api/DockerfilehealthCheckPath: /api/healthenvVars:- key: APP_ENVvalue: production- key: JWT_SECRETgenerateValue: true- key: DATABASE_URLfromDatabase:name: my-app-dbproperty: connectionStringdatabases:- name: my-app-dbplan: basic-256mb - 2
Run migrations before the first boot
Grit does not auto-migrate on boot in production: a process that rewrites the schema every time it restarts is a bad idea when the platform can restart it for its own reasons. Run migrations as an explicit step. On Render that is a pre-deploy command, which runs after the build and before traffic moves over.
Pre-Deploy Command: /app/migrate
What catches people out
Free Postgres instances expire after 90 days. Fine for a demo, quietly fatal for anything you forgot about.
Health check failures roll the deploy back rather than leaving it half-live: good behaviour, but it means a wrong healthCheckPath looks like "my deploy never finishes".
Free web services sleep. The first request wakes them, and the wake is slow enough that a health check can time out first.
Deploying Sentex: Render
The example application below is called sentex and its domains are on gritcms.com. Substitute your own project name and hostnames as you go — every other detail, including the service layout and the Compose translation, applies unchanged to any Grit project.
Render does not run docker-compose.yml files. Like Railway, it needs an explicit config — Render’s version is a render.yaml Blueprint committed to the repo, which becomes the single source of truth for every service, database, and env var. This part is closer to the Railway guide than to Dokploy/Coolify.
3.1 Write the Blueprint
Create render.yaml at the repo root:
databases:- name: sentex-postgresdatabaseName: sentexuser: sentexplan: starterregion: oregonservices:- type: keyvaluename: sentex-redisplan: starterregion: oregonipAllowList: [] # private — reachable only from services in this workspace- type: webname: sentex-apiruntime: dockerregion: oregonplan: starterdockerfilePath: apps/api/DockerfiledockerContext: apps/apidomains:- api.sentex.gritcms.compreDeployCommand: sh -c "./migrate && ./seed"envVars:- key: APP_ENVvalue: production- key: POSTGRES_HOSTfromDatabase: { name: sentex-postgres, property: host }- key: POSTGRES_PORTfromDatabase: { name: sentex-postgres, property: port }- key: POSTGRES_USERfromDatabase: { name: sentex-postgres, property: user }- key: POSTGRES_PASSWORDfromDatabase: { name: sentex-postgres, property: password }- key: POSTGRES_DBfromDatabase: { name: sentex-postgres, property: database }- key: REDIS_URLfromService: { type: keyvalue, name: sentex-redis, property: connectionString }- key: APP_URLvalue: https://api.sentex.gritcms.com- key: CORS_ORIGINSvalue: https://sentex.gritcms.com,https://admin.sentex.gritcms.com- key: R2_ACCOUNT_IDsync: false- key: R2_ACCESS_KEY_IDsync: false- key: R2_SECRET_ACCESS_KEYsync: false- key: R2_BUCKETsync: false- key: R2_ENDPOINTsync: false- type: webname: sentex-adminruntime: dockerregion: oregonplan: starterdockerfilePath: apps/admin/DockerfiledockerContext: .domains:- admin.sentex.gritcms.comenvVars:- key: NEXT_PUBLIC_API_URLvalue: https://api.sentex.gritcms.com- key: NEXT_PUBLIC_WEB_URLvalue: https://sentex.gritcms.com- key: NEXT_PUBLIC_ADMIN_URLvalue: https://admin.sentex.gritcms.com- key: THEMEvalue: atlas- key: SOCIAL_AUTH_ENABLEDvalue: "false"- key: NEXT_PUBLIC_DEMO_LOGINSvalue: "false"- type: webname: sentex-webruntime: dockerregion: oregonplan: starterdockerfilePath: apps/web/DockerfiledockerContext: .domains:- sentex.gritcms.comenvVars:- key: NEXT_PUBLIC_API_URLvalue: https://api.sentex.gritcms.com- key: NEXT_PUBLIC_ADMIN_URLvalue: https://admin.sentex.gritcms.com- key: THEMEvalue: atlas- key: SOCIAL_AUTH_ENABLEDvalue: "false"
Notes on the choices above, matching what’s in docker-compose.prod.yml:
dockerContextmirrors the Composebuild.contextfor each service:apps/apiforapi(matchescontext: ./apps/api), and.(repo root) foradmin/web(matchescontext: ., needed for the pnpm workspace).dockerfilePathis always relative to the repo root regardless ofdockerContext.preDeployCommandreplaces the standalonemigrateservice the same way Railway’s Pre-Deploy Command did — it runs in a fresh instance, before the newapiversion goes live, and aborts the deploy on non-zero exit. Wrap it insh -c "..."so&&is interpreted by a shell, same reasoning as the Railway guide.type: keyvalueis Render’s current Redis-compatible managed store (runs Valkey;redisis a deprecated alias for the same type). SettingipAllowList: []keeps it unreachable from the public internet — only other services in your Render workspace can reach it.- Build args: Render automatically forwards a Docker service’s
envVarsinto the build asARGs (as long as the Dockerfile declares matchingARGlines) — so no separate build-args block is needed foradmin/web’sNEXT_PUBLIC_*values, same requirement as in the Railway guide. sync: falseon the R2 credentials means Render will prompt you to type the actual values into the dashboard the first time the Blueprint syncs, rather than committing secrets intorender.yaml.
Commit and push render.yaml.
3.2 Deploy the Blueprint
- In the Render Dashboard, click New → Blueprint.
- Connect your GitHub account if you haven’t, then select the
sentexrepository. - Render detects
render.yamlat the repo root automatically. Give the Blueprint instance a name and pick the branch to track. - Render shows a preview of every resource it’s about to create (
sentex-postgres,sentex-redis,sentex-api,sentex-admin,sentex-web). Review it, then fill in the prompted values for thesync: falsevariables (your real R2 credentials). - Click Deploy Blueprint.
3.3 Domains
- Because
domains:is already set per service inrender.yaml, Render provisions those custom domains automatically as part of the sync — you don’t need a separate manual step to attach them. - Render shows you the CNAME target for each domain (under that service’s Settings → Custom Domains). Create the corresponding CNAME records at your DNS provider:api.sentex.gritcms.com → CNAME → <target shown by Render>admin.sentex.gritcms.com → CNAME → <target shown by Render>sentex.gritcms.com → CNAME → <target shown by Render>
- Wait for DNS to propagate and for Render to show each domain as verified with an active TLS certificate.
3.4 Verify and iterate
- Check each service’s Logs tab — confirm the
sentex-apipre-deploy log shows./migrate && ./seedexiting 0 before the service starts. - Visit all three domains, confirm no CORS errors, and confirm the seeded demo login works.
- Because Blueprints auto-redeploy affected services whenever
render.yamlchanges, and each service still auto-deploys on pushes to its watched paths, day-to-day pushes tomainbehave like the Railway and Dokploy/Coolify GitHub-connected flows — no manual redeploy step needed.
Platform dashboards and CLI flags change faster than these docs. For anything that looks different from what is written here, Render's own documentation is the authority: render.com/docs
