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

HerokuNodion
AppApplication
Process types and dynos (web, worker)Processes of one app, each with its own replicas and package
Dyno sizePackage (Discovery, General Purpose)
Config varsEnvironment variables (Environment tab)
ProcfileProcfile (buildpack builds)
Release phaseProcfile release: entry or the app's release command
Heroku PostgresManaged Postgres, attached as DATABASE_URL
Heroku Key-Value StoreValkey, attached as REDIS_URL
Heroku SchedulerCron jobs on the app
Review appsPreview environments
Automated Certificate ManagementAutomatic 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.

Give your agent a cloud.

Earn up to 15 € in credit when signing up.

Continue with GitHub

or with email

Stay in the loop.

Product news, no spam. Unsubscribe anytime.