Migrate from Heroku
Move a Heroku app with its database, config vars and domain.
This guide moves one Heroku app to Nodion: code, config vars, Heroku Postgres, Heroku Key-Value Store, Scheduler
jobs and the custom domain. Plan a short write freeze while the database is copied. There is no importer for
app.json or heroku.yml; each step below is a few clicks or commands.
What maps to what
| Heroku | Nodion |
|---|---|
| App | Application |
Process types and dynos (web, worker) | Processes of one app, each with its own replicas and package |
| Dyno size | Package (Discovery, General Purpose) |
| Config vars | Environment variables (Environment tab) |
| Procfile | Procfile (buildpack builds) |
| Release phase | Procfile release: entry or the app's release command |
| Heroku Postgres | Managed Postgres, attached as DATABASE_URL |
| Heroku Key-Value Store | Valkey, attached as REDIS_URL |
| Heroku Scheduler | Cron jobs on the app |
| Review apps | Preview environments |
| Automated Certificate Management | Automatic certificates |
1. Create the project and the database
Create a project and, in it, a PostgreSQL database in the region the app should run in (EU Central for Frankfurt). The app and its database must be in the same region; Nodion refuses to attach across regions. Pick a Postgres version at least as new as your Heroku database. More on Databases.
2. Copy the database
Stop all writes first. Heroku's maintenance mode
keeps worker dynos and Scheduler jobs running. So scale every process type that writes to zero (not only worker)
and remove the jobs in Heroku Scheduler for the duration of the copy. According to Heroku, maintenance mode is not available
for Fir apps; there, scale web to zero as well or stop writes in the app itself:
bashheroku maintenance:on --app example-app
heroku ps:scale worker=0 --app example-app # every process type that writes On Fir:
bashheroku ps:scale web=0 worker=0 --app example-app # Fir: no maintenance mode
Then capture and download a backup. Heroku writes it as latest.dump in pg_dump's custom
format (Heroku docs).
Heroku recommends PGBackups for databases up to 20 GB; for larger ones, run pg_dump directly against the URL from heroku pg:credentials:url (classic databases) or
heroku data:pg:credentials:url (Advanced databases, Heroku docs).
bashheroku pg:backups:capture --app example-app
heroku pg:backups:download --app example-app Advanced databases do not support PGBackups; for them, and for databases over 20 GB, dump directly in the same custom format, with the URL from the matching credentials command. According to Heroku, Private and Shield databases do not accept external connections (and Shield has no PGBackups); for those, follow the route Heroku describes for Private Spaces.
bashpg_dump -Fc --no-acl --no-owner -f latest.dump \
"postgres://USER:PASSWORD@HOST:5432/DATABASE?sslmode=require"
In Nodion, open the database's Overview tab and click Enable public access. Wait until the endpoint
no longer shows Provisioning, then copy the connection URL the dashboard shows (always TLS, sslmode=require).
Add your own IP under allowed sources so only you can connect. Then restore:
bashpg_restore --verbose --no-owner --no-acl \
--dbname "postgresql://nodion:PASSWORD@HOST:PORT/DATABASE?sslmode=require" \
latest.dump
Use a pg_restore at least as new as the newer of the two servers. If the restore reports errors about the
heroku_ext schema, create it first: CREATE SCHEMA heroku_ext; (Heroku's own advice). Check row
counts in the database's Browse tab, then turn public access off again; the database keeps running internally.
An empty Valkey is only enough if your app uses Key-Value Store as a cache, or as a queue you let drain first. If it holds data you need (sessions or persistent keys, for example), move that data before shutting down, or keep the source until it is no longer needed.
3. Create the application
Create an application from your Git repository with the Buildpacks strategy. The buildpacks read your
Procfile: web is the default process and the other lines become process types
(Buildpacks). For Node.js, a heroku-postbuild script runs after the build.
As on Heroku, the web process gets the PORT variable.
If the Procfile has no web: line, its first line becomes the default and runs in Nodion's web process.
Nodion checks that process over HTTP before it goes live, so a worker without an HTTP server does not pass that check. Keep a
web: line for your app's HTTP server.
Procfile lines do not create processes. For a worker: line, open Processes, New process, name it
worker and leave the command empty; it then runs the process type of the same name.
A release: line runs once per deploy before web starts. If it fails, the deploy fails and the
previous release keeps serving. If you built with heroku.yml on Heroku, use the Dockerfile strategy: there
is no Procfile there, so set the app's release command (Settings, in the Preview environments card, where the field shows once previews are enabled, or release_command over the API). Nodion does not set PORT on Dockerfile apps; the app listens on
the web process port (else the image's EXPOSE, else 8080).
4. Move the config vars
Export the config vars in shell format (Heroku CLI):
bashheroku config -s --app example-app > heroku.env
Paste the lines into Environment, Add many, and import them. Check the result: values with quotes or
backslashes may need fixing by hand, and values that span several lines (PEM keys, for example) go in through the CLI or
API. Imported variables are not secret; set secrets with nodion env set KEY=value --secret.
Then delete DATABASE_URL and REDIS_URL from the imported variables: a variable you set yourself
overrides the value an attached database injects. Use Attach database for the Postgres database
(url as DATABASE_URL) and for Valkey (url as REDIS_URL).
Inside Nodion the connection runs over the private network without TLS (sslmode=disable). If your code
forces SSL for the database, as Heroku requires, turn that off for this URL; the same goes for rediss:// to Valkey.
5. Scheduler jobs become cron jobs
Add every Heroku Scheduler job on the app's Cron tab: a five-field cron schedule in UTC, the same command, a timeout of up to 30 minutes. An app takes up to 8 cron jobs; while a run is still going, the next tick is skipped.
6. Deploy and check
Deploy, watch the Logs tab and open the app on its Nodion hostname. If / does not answer quickly with a 2xx
or 3xx, set your own health check path under Settings.
7. Move the domain
Add the domain on the Domains tab. Nodion shows two records: a TXT record _nodion-challenge.your-domain and a
CNAME (an ALIAS at the apex) to the app's default hostname. You can add the TXT record ahead of time. The certificate is
issued once the CNAME points at Nodion, so expect a few minutes without a valid certificate after the switch, and lower the
TTL beforehand.
8. Shut down Heroku
Once traffic and data run on Nodion, delete the Heroku app. According to Heroku this cannot be undone, so keep latest.dump.
bashheroku apps:destroy --app example-app What works differently
- There is no maintenance mode and no pipelines with promotion between apps.
- Instead of one-off dynos, the Shell tab opens a terminal in a running process.
- The CLI fetches logs but cannot follow them; watch them live on the Logs tab.
- Preview environments need no
app.json; each one gets an empty database. - A rollback redeploys an earlier image with the current environment variables.
Next
Nodion compared with Heroku, Pricing, Applications and Databases.