All deployment targets
Managed platform

Deploy to Render

Managed Postgres with real backups, and a free tier you can demo on.

From
$0 (free tier) / ~$7 production
Ops effort
Lowest
Managed Postgres
Persistent disk
Best for

Teams that want managed Postgres with point-in-time recovery without thinking about it, and a blueprint file checked into the repo.

Not for

Anything latency-sensitive on the free tier: free services spin down after inactivity and cold starts run to tens of seconds.

Quick setup

  1. 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: web
    name: my-app-api
    runtime: docker
    dockerfilePath: ./apps/api/Dockerfile
    healthCheckPath: /api/health
    envVars:
    - key: APP_ENV
    value: production
    - key: JWT_SECRET
    generateValue: true
    - key: DATABASE_URL
    fromDatabase:
    name: my-app-db
    property: connectionString
    databases:
    - name: my-app-db
    plan: basic-256mb
  2. 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.

Full walkthrough

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-postgres
databaseName: sentex
user: sentex
plan: starter
region: oregon
services:
- type: keyvalue
name: sentex-redis
plan: starter
region: oregon
ipAllowList: [] # private — reachable only from services in this workspace
- type: web
name: sentex-api
runtime: docker
region: oregon
plan: starter
dockerfilePath: apps/api/Dockerfile
dockerContext: apps/api
domains:
- api.sentex.gritcms.com
preDeployCommand: sh -c "./migrate && ./seed"
envVars:
- key: APP_ENV
value: production
- key: POSTGRES_HOST
fromDatabase: { name: sentex-postgres, property: host }
- key: POSTGRES_PORT
fromDatabase: { name: sentex-postgres, property: port }
- key: POSTGRES_USER
fromDatabase: { name: sentex-postgres, property: user }
- key: POSTGRES_PASSWORD
fromDatabase: { name: sentex-postgres, property: password }
- key: POSTGRES_DB
fromDatabase: { name: sentex-postgres, property: database }
- key: REDIS_URL
fromService: { type: keyvalue, name: sentex-redis, property: connectionString }
- key: APP_URL
value: https://api.sentex.gritcms.com
- key: CORS_ORIGINS
value: https://sentex.gritcms.com,https://admin.sentex.gritcms.com
- key: R2_ACCOUNT_ID
sync: false
- key: R2_ACCESS_KEY_ID
sync: false
- key: R2_SECRET_ACCESS_KEY
sync: false
- key: R2_BUCKET
sync: false
- key: R2_ENDPOINT
sync: false
- type: web
name: sentex-admin
runtime: docker
region: oregon
plan: starter
dockerfilePath: apps/admin/Dockerfile
dockerContext: .
domains:
- admin.sentex.gritcms.com
envVars:
- key: NEXT_PUBLIC_API_URL
value: https://api.sentex.gritcms.com
- key: NEXT_PUBLIC_WEB_URL
value: https://sentex.gritcms.com
- key: NEXT_PUBLIC_ADMIN_URL
value: https://admin.sentex.gritcms.com
- key: THEME
value: atlas
- key: SOCIAL_AUTH_ENABLED
value: "false"
- key: NEXT_PUBLIC_DEMO_LOGINS
value: "false"
- type: web
name: sentex-web
runtime: docker
region: oregon
plan: starter
dockerfilePath: apps/web/Dockerfile
dockerContext: .
domains:
- sentex.gritcms.com
envVars:
- key: NEXT_PUBLIC_API_URL
value: https://api.sentex.gritcms.com
- key: NEXT_PUBLIC_ADMIN_URL
value: https://admin.sentex.gritcms.com
- key: THEME
value: atlas
- key: SOCIAL_AUTH_ENABLED
value: "false"

Notes on the choices above, matching what’s in docker-compose.prod.yml:

  • dockerContext mirrors the Compose build.context for each service: apps/api for api (matches context: ./apps/api), and . (repo root) for admin/web (matches context: ., needed for the pnpm workspace). dockerfilePath is always relative to the repo root regardless of dockerContext.
  • preDeployCommand replaces the standalone migrate service the same way Railway’s Pre-Deploy Command did — it runs in a fresh instance, before the new api version goes live, and aborts the deploy on non-zero exit. Wrap it in sh -c "..." so && is interpreted by a shell, same reasoning as the Railway guide.
  • type: keyvalue is Render’s current Redis-compatible managed store (runs Valkey; redis is a deprecated alias for the same type). Setting ipAllowList: [] 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 envVars into the build as ARGs (as long as the Dockerfile declares matching ARG lines) — so no separate build-args block is needed for admin/web’s NEXT_PUBLIC_* values, same requirement as in the Railway guide.
  • sync: false on 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 into render.yaml.

Commit and push render.yaml.

3.2 Deploy the Blueprint

  1. In the Render Dashboard, click New → Blueprint.
  2. Connect your GitHub account if you haven’t, then select the sentex repository.
  3. Render detects render.yaml at the repo root automatically. Give the Blueprint instance a name and pick the branch to track.
  4. 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 the sync: false variables (your real R2 credentials).
  5. Click Deploy Blueprint.

3.3 Domains

  1. Because domains: is already set per service in render.yaml, Render provisions those custom domains automatically as part of the sync — you don’t need a separate manual step to attach them.
  2. 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>
  3. Wait for DNS to propagate and for Render to show each domain as verified with an active TLS certificate.

3.4 Verify and iterate

  1. Check each service’s Logs tab — confirm the sentex-api pre-deploy log shows ./migrate && ./seed exiting 0 before the service starts.
  2. Visit all three domains, confirm no CORS errors, and confirm the seeded demo login works.
  3. Because Blueprints auto-redeploy affected services whenever render.yaml changes, and each service still auto-deploys on pushes to its watched paths, day-to-day pushes to main behave 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