Migrating from Heroku
This guide is for teams running an application on Heroku that want to move it to Convox without rewriting it. If you already deploy containers, the move is mostly translating your Procfile, config vars, and add-ons into a single convox.yml. If you deploy with buildpacks, you will add a Dockerfile first. You can cut over with minimal downtime by deploying to Convox, migrating data, and switching DNS last.
Prerequisites
- A Convox Rack and the
convoxCLI installed and pointed at it (convox switch <rack>). - A
Dockerfileif your app currently builds with Heroku buildpacks. Heroku buildpacks do not run on Convox; you containerize the app instead. See Dockerfile. If you already push prebuilt images to Heroku's container registry, you can reuse that image with the serviceimage:field instead ofbuild:.
Concept Mapping
| Heroku | Convox |
|---|---|
| App | App |
| Buildpack | Dockerfile |
web process type (Procfile) |
A Service with a port |
worker / other process types (Procfile) |
A Service with no port |
| Procfile command | The service command in convox.yml |
Dyno (heroku ps:scale web=3) |
Service scale plus convox scale web --count 3 |
| Config vars | Environment Variables |
| Heroku Postgres add-on | A postgres (or rds-postgres) Resource |
| Heroku Redis / Key-Value add-on | A redis (or elasticache-redis) Resource |
| Heroku Scheduler | A Timer |
heroku run <cmd> |
convox run |
| Custom domain + ACM/automatic certs | Service domain plus a CNAME to the Rack router. See Custom Domains. |
convox.yml
Before: Heroku
A typical Heroku Rails app is described by a Procfile and config vars set with heroku config:set:
# Procfile
web: bundle exec rails server -b 0.0.0.0 -p $PORT
worker: bundle exec sidekiq
release: bundle exec rails db:migrate
Add-ons are attached separately (heroku-postgresql, heroku-redis), and the connection strings arrive as the DATABASE_URL and REDIS_URL config vars. Scheduled work is configured in the Heroku Scheduler dashboard.
After: convox.yml
The same app expressed for Convox. Each Procfile process type becomes a Service, the release command becomes an init container that runs migrations before the web container starts, add-ons become Resources, and the scheduled job becomes a Timer:
environment:
- SECRET_KEY_BASE
- RAILS_ENV=production
resources:
database:
type: postgres
options:
version: "16"
cache:
type: redis
services:
web:
build: .
command: bundle exec rails server -b 0.0.0.0 -p 3000
port: 3000
health: /healthz
scale:
count: 2
cpu: 250
memory: 512
resources:
- database
- cache
initContainer:
command: bundle exec rails db:migrate
worker:
build: .
command: bundle exec sidekiq
scale:
count: 1
resources:
- database
- cache
timers:
daily-report:
schedule: "0 6 * * *"
command: bin/rake reports:daily
service: worker
Notes on the translation:
- Heroku injects a dynamic
$PORT; on Convox you bind to a fixed port and setport:to the same value. The web service's load balancer routes to that port. - There is no Procfile
releasephase on Convox. Run release-time work such as migrations with aninitContainer, which runs to completion before the main container starts and receives the same environment and resource connections. - A process type with no
port(likeworker) becomes a service with noportand gets no load balancer.
Environment and Secrets
Heroku config vars map directly to Convox Environment Variables.
Declare the variable names your app needs in convox.yml (top level for every service, or under a single service to scope it). Values can carry a default (RAILS_ENV=production) or be left bare (SECRET_KEY_BASE) to require they be set before deploy:
environment:
- SECRET_KEY_BASE
- RAILS_ENV=production
Export your current Heroku config and set the values on Convox:
$ heroku config -s | tee heroku.env
$ convox env set SECRET_KEY_BASE=... -a myapp
Setting SECRET_KEY_BASE... OK
Release: RABCDEFGHI
Setting environment variables creates a new Release but does not deploy it. Promote it (or include the change in your next convox deploy) to make the values live. You do not need to copy DATABASE_URL or REDIS_URL; Convox injects those from the linked resources (see below).
Datastores
Heroku add-ons that provide a database or cache become Convox Resources. Linking a resource to a service injects connection environment variables named after the resource. For a postgres resource named database, Convox sets:
DATABASE_URL=postgres://username:password@host.name:port/database
DATABASE_USER=username
DATABASE_PASS=password
DATABASE_HOST=host.name
DATABASE_PORT=port
DATABASE_NAME=database
so a Rails app that reads DATABASE_URL works without changes. Map the common Heroku add-ons like this:
| Heroku add-on | Convox resource type |
|---|---|
| Heroku Postgres | postgres (in-cluster) or rds-postgres (AWS managed) |
| Heroku Redis / Key-Value Store | redis (in-cluster) or elasticache-redis (AWS managed) |
| ClearDB / JawsDB MySQL | mysql (in-cluster) or rds-mysql (AWS managed) |
The in-cluster types (postgres, redis, mysql) are quick to start and good for development and staging. For production durability on AWS, use the managed rds- and elasticache- types, which add backups, encryption, and Multi-AZ options. The injected variable names are identical for both, so you can use an overlay to run containerized in one environment and managed in another without code changes. On Convox Cloud, see Convox Cloud Databases for managed options. See Resource for the full list of types and options.
Scheduled Jobs and Workers
Heroku Scheduler jobs become Convox Timers. A timer runs a command against an existing service on a cron schedule (UTC):
timers:
daily-report:
schedule: "0 6 * * *"
command: bin/rake reports:daily
service: worker
A timer can target a service that is scaled to zero, which mirrors the Heroku one-off-dyno model: define a jobs service with scale: { count: 0 } and point timers at it so it only spins up on schedule. See Using a Template Service.
Long-running background workers (Sidekiq, Resque, Celery) map to a Service with no port, scaled by replica count, exactly like a Heroku worker dyno. Use convox scale worker --count N or, if traffic is variable, autoscaling.
Deploy and Cutover
Deploy to Convox first and verify it before moving any traffic. DNS is the last step.
- Create the app on the Rack:
$ convox apps create myapp - Set the environment variables you exported from Heroku:
$ convox env set SECRET_KEY_BASE=... -a myapp - Build and promote in one step. Convox creates a Release and starts a rolling deployment:
The$ convox deploy -a myappinitContainerruns your migrations before the web container starts. See Deploying Changes. - Find the load balancer hostname and smoke-test the app before touching DNS:
$ convox services -a myapp SERVICE DOMAIN PORTS web web.convox.0a1b2c3d4e5f.convox.cloud 443:3000 - Migrate your data. Heroku writes are still live at this point, so do a final sync during the cutover window. Capture from Heroku and restore into the Convox resource over a proxy:
Get the connection credentials with$ heroku pg:backups:capture -a heroku-app $ heroku pg:backups:download -a heroku-app # writes latest.dump $ convox resources proxy database -a myapp # proxies localhost:5432 $ pg_restore --verbose --clean --no-acl --no-owner \ -h localhost -U <user> -d <name> latest.dumpconvox resources url database -a myapp. If port 5432 is in use locally, stop your local Postgres or proxy on a different port. - Cut over DNS last to minimize downtime:
- A day ahead, lower the TTL on the DNS record you are migrating (for example to 60 seconds) so the change propagates quickly.
- Add the production hostname to the web service
domainand redeploy:
Convox provisions a certificate automatically for the domain. See Custom Domains and SSL.services: web: domain: www.example.com - At the cutover moment, disable Heroku Scheduler jobs, put the Heroku app in maintenance mode, let any worker dynos drain, and scale them to zero so nothing else writes to the Heroku database.
- Run a final data sync (step 5) to catch writes since the first restore.
- Point the production CNAME at the Rack router. Find it with
convox rack:
Then set$ convox rack Name convox Provider aws Router router.0a1b2c3d4e5f.convox.cloud Status runningwww.example.com CNAME router.0a1b2c3d4e5f.convox.cloud. - Wait for DNS to propagate and verify the site serves from Convox.
Gotchas and Differences
- No
$PORTinjection. Heroku assigns a port at runtime; on Convox you choose a fixed port and declare it asport:. Convox sets aPORTenvironment variable, but the value is your declared port, not a dynamic one. - No Procfile
releasephase. Migrations and other release-time tasks run in aninitContainer(runs before the service starts) or as a one-off withconvox run. - No buildpacks. Convox builds from a
Dockerfile. Pin your runtime, install system packages explicitly, and setCMD(or override it with the servicecommand). - Ephemeral filesystem, like Heroku. Containers have an ephemeral disk. If you need persistence, use a Resource (object storage, database) or a Volume; do not write durable data to the local filesystem.
convox.ymlis the source of truth. Scale, schedules, domains, and env var names live inconvox.ymland are versioned with your code, rather than being configured imperatively in a dashboard. The staticscale.countapplies only on first deploy; afterward, change running count withconvox scale.- Logs and one-off commands. Use
convox logs -a myappin place ofheroku logs --tail, andconvox run <service> <cmd>in place ofheroku run.