Moving from Heroku
Move a Heroku app to Deplane with one command: deplane import heroku reads the app on Heroku, shows what moves and what stays, then creates the Deplane app with its secrets, workers and release command, copies the Postgres database, adds a Redis, and deploys your code. Heroku keeps serving your users until you switch.
The command runs on your own computer with a short-lived Heroku token that never leaves it. It only reads Heroku: nothing there is changed. The command and its flags are in the CLI reference, and every error has a fix in Troubleshooting and error codes.
Before you start
- Install the Deplane CLI and run
deplane login. - Have the app's code in a folder: a checkout of the repository you deploy to Heroku, at the commit Heroku runs. The report shows that commit and whether your folder is at it.
- Log in to the Heroku CLI (
heroku login).
The Heroku token. Create a token that can only read, and only for an hour:
heroku authorizations:create --scope read-protected --expires-in 3600 --short
read-protected lets it read config vars, including DATABASE_URL, which the import needs; it cannot change anything on Heroku. Don't use heroku auth:token: that one can do everything your account can, for a year.
Give the token to Deplane in one of three ways, never as a flag:
- pipe it in:
heroku authorizations:create ... --short | deplane import heroku my-app --check; - set it for one command:
HEROKU_API_KEY=$(heroku authorizations:create ... --short) deplane import heroku my-app; - or run
deplane import heroku my-appin a terminal and paste it at the hidden prompt.
A token piped on stdin wins over HEROKU_API_KEY: the variable is used when nothing is piped in (when it is set, the CLI waits up to 20 seconds for a piped token, then uses it). The CLI says which one it used ("Using the Heroku token piped on stdin."), never the token itself. It sends the token only to Heroku's API (api.heroku.com). It is never written to disk, never printed, left out of --debug, and never sent to Deplane.
Check first
--check reads the app and prints the report without creating anything. When you are logged in to Deplane, it also reads your plan and what your team uses (read-only) and checks that the import fits; without a Deplane login it says the plan was not checked yet, and the import checks it.
$ heroku authorizations:create --scope read-protected --expires-in 3600 --short | deplane import heroku shop --check
Heroku app shop (region us, stack heroku-24, commit 1a2b3c4) → Deplane app shop
Code
✓ Deploys /home/me/shop with Railpack: Detected Node 22, port 3000.
✓ Your folder is at commit 1a2b3c4, the one Heroku runs.
Processes
✓ web: 2 × standard-2x on Heroku → 2 medium instance(s).
✓ worker: 1 × standard-1x on Heroku → worker worker, 1 small instance(s): `node worker.js`.
✓ release phase → release command `npm run migrate`.
Data
✓ Postgres (heroku-postgresql:essential-0): copied into a new Deplane database shop-db ...
⚠ Redis (heroku-redis:mini): a new Deplane Redis shop-redis ... Its data is not copied ...
Config vars
✓ 3 copied as secrets: APP_URL, SECRET_KEY_BASE, SENDGRID_API_KEY.
✓ Not copied, Deplane sets them: DATABASE_URL, PORT, REDIS_URL.
✓ Not copied, Heroku-managed: HEROKU_APP_NAME, REDIS_TLS_URL.
Add-ons
⚠ Heroku Scheduler: its jobs cannot be read through Heroku's API ...
⚠ sendgrid (sendgrid:starter): email. Its variables are kept ...
Domains
⚠ www.shop.example points at Heroku ... It moves when you switch ...
Plan
✓ Fits your Pro plan: after the import the team uses 6 of 10 instances and 3 GB of 3 GB memory.
Nothing was changed (--check). Run the command again without --check to import.
✓ is what moves as it is, ⚠ is what you should know or do yourself, and ✗ is what stops the import, with what to do about it. The report names variables but never shows their values. --check --json prints the same report as one JSON document.
Import
Run the command again without --check. In a terminal it asks before it creates anything; from a script, or with the token piped in, add --yes.
heroku authorizations:create --scope read-protected --expires-in 3600 --short | deplane import heroku shop --yes
| Flag | What it does |
|---|---|
--source DIR | The folder to deploy. Default: the current folder. |
--name NAME | The Deplane app's name. Default: the Heroku app's name. The database and Redis are named after it. |
--check | Print the report and stop. |
--yes | Import without asking. |
In order, the import:
- checks your plan (the report's Plan section) and has Deplane check the Postgres database (version, size, what it holds). If the plan cannot hold the app, or Deplane cannot copy the database, the import stops here and nothing is created;
- creates the app and sets the copied config vars as secrets, the release command, the number and size of web instances, and the workers;
- copies the database into a new Deplane database with the Postgres import in copy mode, and attaches it as
DATABASE_URL; - creates a Redis and attaches it as
REDIS_URL; - deploys your folder as
deplane launchdoes (it writesdeplane.tomlthere), runs the release command against the copied database, and prints the app's URL.
Then it prints what to do next: test the app, the switch-over steps below, and how to revoke the token.
What moves
| On Heroku | On Deplane |
|---|---|
The web process | The app. Instances as on Heroku (at least 1). Railpack starts your Procfile's web line, or your Dockerfile's CMD; the report warns when they differ from what Heroku runs. |
Other process types that run dynos (worker, clock, ...) | Workers, with the same command and number of dynos (at most 20 each, 10 per app). A name Deplane does not accept is changed, for example clock_tick becomes clock-tick. Types at 0 dynos on Heroku (such as the console and rake a Rails app lists) are not created; run one-off commands with deplane run. |
The release process type | The release command. It may run 15 minutes (Heroku allows an hour). |
| Dyno sizes | eco, basic, standard-1x: small (0.5 CPU, 512 MB). standard-2x: medium (1 CPU, 1 GB). performance-* and larger: large (2 CPU, 2 GB), with a warning when that is less memory than on Heroku. See Limits. |
| Config vars | Secrets, except the ones Heroku or Deplane manages: Heroku's dyno metadata (HEROKU_APP_*, HEROKU_RELEASE_*, HEROKU_SLUG_*, HEROKU_DYNO_*, HEROKU_BUILD_*), PORT, and the variables of the Postgres and Redis that Deplane replaces. The report lists every dropped name. Variables of a follower or second database that stays on Heroku are copied, and the report names them. Deplane's Redis sets REDIS_URL only, not REDIS_TLS_URL. |
Heroku Postgres (DATABASE_URL) | A new Deplane database, copied once (copy mode), attached as DATABASE_URL. |
Heroku Key-Value Store (REDIS_URL) | A new, empty Deplane Redis (Valkey 8, 256 MB), attached as REDIS_URL. |
| The code | Your folder, built by Railpack, or by your Dockerfile if it has one. |
What stays on Heroku
- Redis data is not copied. Nothing to do for a cache; let job queues (Sidekiq, RQ, BullMQ) drain before you switch. A Heroku plan larger than 256 MB gets no Deplane Redis, and
REDIS_URLkeeps pointing at Heroku. - Scheduler jobs cannot be read through Heroku's API. Open them with
heroku addons:open scheduler -a APPand add each as a cron job. Both run in UTC:
| Heroku Scheduler | Deplane schedule |
|---|---|
| Every 10 minutes | */10 * * * * |
| Every hour at :MM | MM * * * * |
| Every day at HH:MM | MM HH * * * |
``sh
deplane processes add nightly-report --schedule "30 2 * * *" --command "rake report" --app shop
``
- Other add-ons stay with Heroku, and their variables are copied so your app keeps using them. They end when the Heroku app or the add-on is deleted, so keep the Heroku app (at 0 dynos) until each has its own account with the vendor:
| Add-on | What happens |
|---|---|
heroku-postgresql | Copied into a Deplane database (copy mode) |
heroku-redis | Recreated empty on Deplane; data not copied |
scheduler | Recreate the jobs as cron jobs yourself |
papertrail, logdna (Mezmo), coralogix, sumologic, logentries | Logging stays on Heroku. Deplane keeps your logs itself (deplane logs) and has no log drains yet; to keep the service, open an account with the vendor |
sendgrid, mailgun, postmark, mailtrap | Variables kept; move to a direct account with the vendor before you delete the Heroku app |
bucketeer, cloudcube (S3) | Variables kept; files are not moved; same as above |
cloudamqp (RabbitMQ) | Variables kept; same as above |
heroku-kafka | Ends with the Heroku app, and Deplane has no Kafka |
memcachier | Variables kept; move to a direct account, or use a Deplane Redis |
newrelic, scout, sentry, rollbar, honeybadger, bugsnag | Variables kept; most agents work anywhere; the vendor account must become a direct one |
jawsdb, cleardb (MySQL) | Variables kept. Deplane has managed MySQL, but importing MySQL comes later |
heroku-inference, heroku-connect, heroku-applink | Work only on Heroku or with Salesforce |
| Anything else | Variables kept; move to a direct account before you delete the Heroku app |
- Custom domains keep pointing at Heroku until you switch them.
- Also not carried over: binaries that came from third-party buildpacks (add a Dockerfile if the build needs them),
WEB_CONCURRENCYthat Heroku's buildpacks set for each dyno size (set it yourself if you relied on it), other Postgres databases and followers (HEROKU_POSTGRESQL_<COLOR>_URL), and pipelines and review apps. Deplane runs in one region, in Europe.
Switch over
The first import is a rehearsal: Heroku keeps serving, and writes made there after the copy are not in the Deplane database. When the Deplane app works:
- Before the day: for each custom domain, run
deplane domains add HOSTNAME --app APPand add the TXT record it shows. The domain keeps pointing at Heroku. See Custom domains. - Make room for one more database, before you stop Heroku. The switch copies the data into a second database, and the rehearsal's copy is deleted only after the switch, so for a while your team has both. The report's Plan section says if your plan has no room for it: free a database's worth (a database, one instance, 512 MB), or email support@deplane.io to change your plan, first.
- Stop writes on Heroku:
heroku maintenance:on -a APP, and scale every worker to 0 (heroku ps:scale worker=0 -a APP): maintenance mode stops web traffic only. - Copy the data again into a fresh database, now that nothing writes, and move the app to it in one release (
deplane db switchasks you to confirm with your passkey):
``sh
heroku config:get DATABASE_URL -a APP | deplane db import --name APP-db-final --mode copy --yes
deplane db switch APP-db-final --from APP-db
``
- Switch DNS: change each custom domain's CNAME (or A records, for a root domain) to the target
deplane domains addshowed. - Wait for the certificates:
deplane domains check HOSTNAME --app APPuntil it is ready. Expect a short gap while the certificate is issued. - Keep the Heroku app at 0 dynos (
heroku ps:scale web=0 worker=0 -a APP) until every add-on has moved. Scaling to 0 stops dyno billing, not add-on billing. - Delete the rehearsal's database when the app works on the new copy:
deplane db destroy APP-db. - Revoke the token:
heroku authorizationslists its ID;heroku authorizations:revoke ID. It expires by itself after an hour anyway.
The downtime is from step 3 to the end of step 4: a few minutes per GB of data. Classic Heroku Postgres gives no replication rights, so a live import (switch in under a minute) is not possible from it.
Plan limits
Before it creates anything, the import adds up what it needs (the app, its web instances and workers with their sizes, the database and the Redis) and compares it with your plan and what your team already uses. Each limit it would cross is a ✗ line in the Plan section, with the fix:
- Workers need a plan whose apps do not sleep (Hobby and larger). On Free, a Heroku app with a process type other than
webthat runs dynos is refused; types at 0 dynos are not created, so a web-only app (a Rails app with itsconsoleandraketypes, for example) is fine. - Sizes your plan does not include (Free has only small, so a
standard-2xweb or aperformanceworker is refused). - Apps, Postgres databases and Redis: one more of each than your team has must fit the plan.
- Instances and memory: the team's total after the import, counted as the plan does (a database is one instance of its size, a Redis 256 MB).
- The switch-over's second database (see Switch over): when the plan has no room for it, a ⚠ line says what is missing, so you can make room before you stop Heroku.
To import anyway, email support@deplane.io to change your plan, or free room by deleting what you no longer need. The API still has the last word: if something changed in the meantime, the step it refuses stops the import as below.
If the import stops
The command stops at the first step that fails (a plan limit, a build that fails, a database that cannot be copied) and shows the error as it came. Before the error it lists what it created so far, the commands that finish the import by hand from that step, and the commands that delete what it created so you can start again. A Postgres check that is still waiting is cancelled, so your team can start another import. Plan limits are in Limits.
Limits
| Limit | Value |
|---|---|
| Postgres versions | 9.2 to 17. Heroku creates new databases on Postgres 18, which Deplane cannot import yet: the check says so before anything is created. Postgres 18 support is coming. |
| Database size | 5 GB, as for every Postgres import |
| Databases | DATABASE_URL only; other databases later, one at a time with deplane db import |
| Redis | A new 256 MB Redis; data is not copied |
| Workers | 10 per app, 20 instances each |
| Sizes | small, medium, large (2 GB at most) |
| Private Spaces | An app whose database is in a Private Space or Shield space is refused: the database only accepts connections from inside the space. Email support@deplane.io |
| Container stack | Deplane builds your Dockerfile; a release: in heroku.yml is not read (set it with deplane config --release-command), and a second image is imported as its own app |
| Who can import | Copying the database needs a person logged in to the CLI: CI tokens and AI tools cannot start a database import |
Not in this version yet: an Import from Heroku page in the dashboard, a one-click re-copy and switch, copying Redis data, and recreating Scheduler jobs for you.
This page is also available as Markdown. AI agents can start at /llms.txt.