deplaneDocs Dashboard

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

  1. Install the Deplane CLI and run deplane login.
  2. 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.
  3. 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:

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
FlagWhat it does
--source DIRThe folder to deploy. Default: the current folder.
--name NAMEThe Deplane app's name. Default: the Heroku app's name. The database and Redis are named after it.
--checkPrint the report and stop.
--yesImport without asking.

In order, the import:

  1. 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;
  2. creates the app and sets the copied config vars as secrets, the release command, the number and size of web instances, and the workers;
  3. copies the database into a new Deplane database with the Postgres import in copy mode, and attaches it as DATABASE_URL;
  4. creates a Redis and attaches it as REDIS_URL;
  5. deploys your folder as deplane launch does (it writes deplane.toml there), 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 HerokuOn Deplane
The web processThe 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 typeThe release command. It may run 15 minutes (Heroku allows an hour).
Dyno sizeseco, 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 varsSecrets, 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 codeYour folder, built by Railpack, or by your Dockerfile if it has one.

What stays on Heroku

Heroku SchedulerDeplane schedule
Every 10 minutes*/10 * * * *
Every hour at :MMMM * * * *
Every day at HH:MMMM HH * * *

``sh deplane processes add nightly-report --schedule "30 2 * * *" --command "rake report" --app shop ``

Add-onWhat happens
heroku-postgresqlCopied into a Deplane database (copy mode)
heroku-redisRecreated empty on Deplane; data not copied
schedulerRecreate the jobs as cron jobs yourself
papertrail, logdna (Mezmo), coralogix, sumologic, logentriesLogging 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, mailtrapVariables 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-kafkaEnds with the Heroku app, and Deplane has no Kafka
memcachierVariables kept; move to a direct account, or use a Deplane Redis
newrelic, scout, sentry, rollbar, honeybadger, bugsnagVariables 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-applinkWork only on Heroku or with Salesforce
Anything elseVariables kept; move to a direct account before you delete the Heroku app

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:

  1. Before the day: for each custom domain, run deplane domains add HOSTNAME --app APP and add the TXT record it shows. The domain keeps pointing at Heroku. See Custom domains.
  2. 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.
  3. 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.
  4. Copy the data again into a fresh database, now that nothing writes, and move the app to it in one release (deplane db switch asks 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 ``

  1. Switch DNS: change each custom domain's CNAME (or A records, for a root domain) to the target deplane domains add showed.
  2. Wait for the certificates: deplane domains check HOSTNAME --app APP until it is ready. Expect a short gap while the certificate is issued.
  3. 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.
  4. Delete the rehearsal's database when the app works on the new copy: deplane db destroy APP-db.
  5. Revoke the token: heroku authorizations lists 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:

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

LimitValue
Postgres versions9.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 size5 GB, as for every Postgres import
DatabasesDATABASE_URL only; other databases later, one at a time with deplane db import
RedisA new 256 MB Redis; data is not copied
Workers10 per app, 20 instances each
Sizessmall, medium, large (2 GB at most)
Private SpacesAn 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 stackDeplane 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 importCopying 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.