Migrating from Render
This guide is for teams running services on Render that want to move to a Convox V3 rack on their own cloud account (AWS, GCP, Azure, DigitalOcean, or bare metal). The practical reasons to migrate are running in your own cloud account with your own networking and IAM, defining the whole app (services, datastores, scheduled jobs) in one convox.yml, and avoiding per-service plan tiers in favor of direct CPU and memory allocation. If you already deploy Render services from a Dockerfile, most of the work is translating render.yaml into convox.yml.
Prerequisites
- A running Convox Rack on your cloud provider.
- The
convoxCLI installed and logged in. - A Dockerfile for each service. Render can build native (non-Docker) runtimes such as Node, Python, Ruby, and Go directly from source using a
buildCommandandstartCommand. Convox builds from a Dockerfile, so if your Render service used a native runtime rather thanruntime: docker, you will need to add a Dockerfile that reproduces the same build and start steps. If your Render service already setruntime: dockerwith adockerfilePath, you can reuse that Dockerfile as-is.
Concept Mapping
| Render | Convox |
|---|---|
render.yaml blueprint |
convox.yml manifest |
services: entry with type: web |
Service with a port |
services: entry with type: worker |
Service with no port |
services: entry with type: pserv (private service) |
Service with internal: true |
services: entry with type: cron |
Timer |
services: entry with type: keyvalue (managed Redis) |
Resource of type: redis |
databases: (managed Postgres) |
Resource of type: postgres |
runtime: docker + dockerfilePath |
service build pointing at a Dockerfile |
Native runtime + buildCommand / startCommand |
a Dockerfile plus the service command |
plan (instance tier) |
scale.cpu and scale.memory |
scaling (min/max instances) |
scale.count range plus scale.targets or scale.autoscale |
healthCheckPath |
service health |
envVars (key/value) |
service or app-level environment |
envVarGroups + fromGroup |
top-level environment shared across services |
envVars with fromDatabase / fromService |
resource linking (auto-injected connection variables) |
| Custom domain on a service | service domain |
disk (persistent disk) |
Volume |
Render occasionally renames blueprint keys between releases (for example, its managed Redis service
typeis nowkeyvalue). If yourrender.yamluses older names, check the current Render Blueprint reference as you map fields.
convox.yml
Before (render.yaml)
services:
- type: web
name: web
runtime: docker
dockerfilePath: ./Dockerfile
plan: standard
healthCheckPath: /healthz
envVars:
- key: RAILS_ENV
value: production
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString
- key: REDIS_URL
fromService:
type: keyvalue
name: app-cache
property: connectionString
- fromGroup: app-secrets
scaling:
minInstances: 2
maxInstances: 6
targetCPUPercent: 70
- type: worker
name: worker
runtime: docker
dockerfilePath: ./Dockerfile
dockerCommand: bin/worker
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString
- fromGroup: app-secrets
- type: cron
name: nightly-cleanup
runtime: docker
dockerfilePath: ./Dockerfile
schedule: "0 3 * * *"
dockerCommand: bin/cleanup
envVars:
- fromGroup: app-secrets
- type: keyvalue
name: app-cache
ipAllowList: []
databases:
- name: app-db
plan: standard
postgresMajorVersion: "16"
envVarGroups:
- name: app-secrets
envVars:
- key: SECRET_KEY_BASE
generateValue: true
After (convox.yml)
environment:
- SECRET_KEY_BASE
- RAILS_ENV=production
resources:
database:
type: postgres
cache:
type: redis
services:
web:
build: .
port: 3000
health: /healthz
resources:
- database
- cache
scale:
count: 2-6
targets:
cpu: 70
worker:
build: .
command: bin/worker
resources:
- database
timers:
nightly-cleanup:
schedule: "0 3 * * *"
command: bin/cleanup
service: worker
Notes on the translation:
- The Render
webservice becomes a service with aport. Setportto the port your app listens on. Render injects aPORTvariable; Convox also setsPORTto the value of your serviceport, so an app that already readsPORTkeeps working. - The Render
worker(no inbound traffic) becomes a service with noportand an explicitcommand. - The Render
cronbecomes a Timer. A timer runs against an existing service; here it reuses theworkerservice image to runbin/cleanup. Render'sdockerCommandfor the cron maps to the timercommand. plan: standardhas no direct equivalent. Convox reserves resources per service withscale.cpu(1000 units is one full CPU) andscale.memory(MB). Pick values that match your previous plan's CPU and RAM. See Autoscaling.
Environment and Secrets
Render distinguishes plain envVars (key/value), secret values (sync: false or generateValue: true), and shared envVarGroups attached with fromGroup. Convox handles all of these through environment variables:
- A variable with a fixed value becomes a defaulted entry, for example
- RAILS_ENV=production. - A secret becomes a bare name in the manifest (
- SECRET_KEY_BASE) whose value you set at deploy time and never commit:
$ convox env set SECRET_KEY_BASE=$(openssl rand -hex 64) -a myapp
Setting SECRET_KEY_BASE... OK
Release: RABCDEFGHI
- A Render
envVarGroupshared across services maps to the top-levelenvironmentblock, which is available to every service. Per-serviceenvVarsmap to a service-levelenvironmentblock. See Application Level vs Service Level.
Setting or changing an environment variable creates a new Release; promote it to apply the change. There is no separate "environment group" object to manage in Convox; the manifest and convox env set are the only two places values live.
Datastores
Render-managed Postgres (databases:) and managed key-value/Redis (type: keyvalue) become Convox Resources. Declare the resource and link it to the services that need it:
resources:
database:
type: postgres
cache:
type: redis
services:
web:
build: .
port: 3000
resources:
- database
- cache
Linking a resource injects connection environment variables named after the resource. A resource named database injects DATABASE_URL (plus DATABASE_HOST, DATABASE_USER, DATABASE_PASS, DATABASE_PORT, DATABASE_NAME). This replaces Render's fromDatabase / fromService references: you no longer wire DATABASE_URL by hand because linking produces it. If your code reads DATABASE_URL and REDIS_URL, name the resources so the injected variable matches, or set an explicit env var to the injected value.
Production options:
- The example above starts containerized Postgres and Redis inside the rack. That is the fastest path and good for staging.
- For production durability on AWS, switch to managed services with a Resource Overlay: change
type: postgrestotype: rds-postgresandtype: redistotype: elasticache-redis. The injectedDATABASE_URLkeeps the same format, so application code does not change. See PostgreSQL and Redis. - On Convox Cloud you can use managed databases with simplified pricing.
To migrate existing data, export from Render (for Postgres, pg_dump against the Render external connection string) and import into the Convox resource with convox resources import. You can also keep an external managed database by setting its connection string directly with convox env set DATABASE_URL=..., which stops Convox from starting a containerized resource of that name (see Overlays).
Scheduled Jobs and Workers
| Render | Convox |
|---|---|
type: cron with schedule |
Timer with schedule and command |
type: worker (long-running background process) |
Service with no port |
A Render cron job runs a command on a schedule. A Convox timer does the same, but it runs against an existing service rather than being its own deployable unit. You can point a timer at any service, including one scaled to zero, which is a clean way to provide a job runner image:
services:
jobs:
build: ./jobs
scale:
count: 0
timers:
nightly-cleanup:
schedule: "0 3 * * *"
command: bin/cleanup
service: jobs
concurrency: Forbid
Render's schedule uses standard cron syntax and so does Convox. All Convox timer schedules run in UTC. Set concurrency: Forbid if a run must never overlap a previous run. See Timer for the full attribute list.
Render background workers (type: worker) map directly to a Convox service with no port. Give it an explicit command and link any resources it needs.
Deploy and Cutover
- Add a Dockerfile for each service if you used a native Render runtime, and write the
convox.ymlabove. - Create the app on your rack:
$ convox apps create myapp
- Set secrets that the manifest declares as bare names:
$ convox env set SECRET_KEY_BASE=... -a myapp
- Migrate datastore data (see Datastores) before sending traffic.
- Deploy:
$ convox deploy -a myapp
This builds, creates a Release, and promotes it as a rolling deployment. See Deploying Changes.
- Verify the new deployment is healthy before changing DNS:
$ convox ps -a myapp
$ convox services -a myapp
Test against the Convox-assigned service hostname shown by convox services.
- Cut over DNS last. Keep the Render service running, attach your custom domain to the Convox service with the
domainattribute, then move your DNS record to the Convox load balancer. Once traffic has drained from Render and you have confirmed the Convox app is serving production traffic, decommission the Render service. Doing the DNS change last keeps a working rollback target during the cutover.
Gotchas
- No native buildpacks. Render builds native runtimes from source with
buildCommandandstartCommand. Convox always builds from a Dockerfile. Reproduce the build steps in a Dockerfile and put the start command in the servicecommand(or the DockerfileCMD). - Plans become explicit resources. There is no
plan: standard. You set CPU and memory directly withscale.cpuandscale.memory. Translate your old plan's CPU/RAM into those numbers. - Connection strings come from linking, not references. Render's
fromDatabase/fromServicewiring is replaced by linking a resource to a service, which auto-injects the connection variables. Do not also hand-set those variables unless you are intentionally pointing at an external datastore. - The
PORTcontract. Convox setsPORTto your service'sportvalue. If your app binds to a hardcoded port, set the serviceportto match it. - Static sites. Render's
type: statichas no direct Convox primitive. Serve static assets from a small web service (for example annginxDockerfile) behind aport, or host them on object storage and a CDN outside Convox. - Private services. A Render
pserv(private service) maps to a service withinternal: true, reachable only inside the rack. See Service Discovery. - Health checks. Render's single
healthCheckPathmaps to the readinesshealthpath. Convox also supports separate liveness and startup probes; see Health Checks.
See Also
- convox.yml for the full manifest reference
- Service for ports, scaling, and health checks
- Resource for databases and caches, including managed overlays
- Timer for scheduled jobs
- Environment Variables for configuration and secrets
- Deploying Changes for the deploy and promote flow
- Dockerfile for building services from source