Migrate from Heroku to Dokku

A practical path for moving Heroku apps to Dokku on your own server: what carries over, what needs rework, how Postgres data moves, and how to cut over DNS without losing data.

On this page

Migrate from Heroku to Dokku: what the move involves

Most standard web apps move to Dokku with a git push and a buildpack, but the data, add-ons and day-to-day operations move to your team. If your renewal, a pricing change, a residency requirement or lock-in has started this review, expect the application code to be the easy part. Dokku accepts Heroku-style deploys: it creates an app on the first push to its dokku remote, builds with the herokuish builder (Heroku's v2a buildpack specification), reads the Procfile and supports a release process type that works like the Heroku Release Phase. Apps that rely on Heroku-specific behavior, or on add-ons with no Dokku plugin, need more work. Dokku's own documentation notes that not all Heroku functionality is supported, so check your app.json and every add-on against it before you commit a date.

Platform or DevOps engineers usually do the work, sometimes with an outside implementation partner for the data migration and cutover. The effort is mostly in the datastores and configuration. Each Postgres database must be exported with heroku pg:backups, restored into a Dokku Postgres service with the dokku-postgres plugin, and linked so the app receives DATABASE_URL. Config vars must be re-entered with dokku config:set, and any scheduled jobs, workers or third-party webhooks need to be repointed. Risk is highest at the database cutover, where writes must stop long enough to take a final dump. Plan that window with the application owners, and confirm with your own environment how long the final export and restore take for your database size.

Dokku runs on a single server by default, and that changes who carries operational responsibility. The default docker-local scheduler runs containers directly on the Dokku host, so the host is both your capacity limit and a single point of failure unless you add redundancy yourself. You also take on operating system patching, Dokku and plugin upgrades, TLS renewal behind the proxy, log retention and database backups, which on Heroku were handled by the platform. Before you cut over, set up backups for each Dokku Postgres service, test a restore into a scratch host, and write down a rollback path. Ask any implementation partner for a written scope that names who owns each of these tasks after go-live.

Which Heroku features map cleanly to Dokku and which need rework

This table maps each Heroku feature your app relies on to its Dokku equivalent and labels it clean, needs rework or gap. Read the status column first, then the notes, and check every row your app depends on before you set a cutover date. Most standard web apps map cleanly, while add-ons, scheduled jobs and review-style workflows need a plan.

Heroku featureDokku equivalentStatus and what to check
Git push deploysgit push to a dokku remote, with apps created on the first pushClean. Change the remote URL and confirm your deploy key and SSH access on the Dokku host.
Buildpacksherokuish builder using the v2a buildpack specification, or a BUILDPACK_URL variable or .buildpacks fileClean for standard stacks. Pin any custom buildpack and test it, since Heroku's current stack may differ from herokuish.
Build options beyond buildpacksCloud Native Buildpacks via the pack CLI, Nixpacks, Railpack or a DockerfileNeeds rework when you move to a newer builder. Choose one, install the pack CLI if used, and rebuild staging before cutover.
Procfile process typesProcfile read under procfile-util strict mode, with ps:scale for each process typeClean for web and worker types. Fix any Procfile lines that strict mode rejects before the first deploy.
Release phaseA release process type that works like the Heroku Release PhaseClean. Confirm migrations run in the release process and that a failed release stops the deploy.
app.jsonDokku app.json for deploy scripts, cron tasks, process formation and healthchecksNeeds rework. Dokku documents that not all Heroku app.json functionality is supported, so compare each key you use.
Config varsEnvironment variables set with config:set on the appClean for values. Export them with the Heroku CLI, review secrets and avoid copying them into a repository.
Heroku Postgres and DATABASE_URLdokku-postgres plugin with postgres:import and postgres:link, which sets DATABASE_URLNeeds rework. Restore with pg_restore and check extensions, roles and PostgreSQL version against your source database.
Custom domains and HTTPSDomains set on the app, with nginx by default or Caddy, HAProxy, OpenResty or Traefik as alternativesNeeds rework. Point DNS in stages and confirm certificate issuance on the proxy you choose.
Scaling with ps:scaleps:scale for process counts on the single Dokku hostClean for vertical and per-process counts. Horizontal scaling across servers needs the k3s scheduler and a registry, so treat it as a separate project.
Heroku Redis, Scheduler and Review AppsRedis plugin, cron tasks in app.json and no direct review app equivalentGap to confirm. Check each with its own sibling page before deciding how you will replace it.

What data and integrations you must move

A Heroku app is rarely just code. The database, config vars, files, webhooks and DNS records each move through a different path, and each can break production if it is missed. Work through these cards as an inventory, and confirm each item in your own environment before you schedule the cutover.

Heroku Postgres export with pg:backups

Take a fresh backup with heroku pg:backups:capture, then download it with heroku pg:backups:download. Heroku PGBackups produces a pg_dump custom format file. Heroku's signed download URL expires after 60 minutes, so download the file in one sitting and verify its size before you start the restore. Heroku advises PGBackups only for moderately loaded databases up to 20 GB. Above that size, plan a logical export instead and confirm the approach with your database owner.

Restore and import into dokku-postgres

Create the target service with dokku postgres:create, then import the dump with dokku postgres:import, reading from stdin or a file on the Dokku host. If you restore the dump with pg_restore into another PostgreSQL database first, use the --no-acl and --no-owner options, because Heroku role names will not exist on your server. Run the restore against a staging copy and compare row counts for the largest tables before you continue.

Link services and DATABASE_URL

Running dokku postgres:link sets the app's DATABASE_URL, the same variable Heroku uses to point an app at its primary Heroku Postgres database, so most application code needs no change. Check for any code that reads a hard-coded host, a Heroku-specific HEROKU_* variable or a second database URL. Confirm that every add-on the app uses, including Redis and search, has an equivalent Dokku plugin or an external service with a plan.

Config vars and secrets

Export the current values with heroku config, then set them on the Dokku app with dokku config:set. Treat this export as sensitive, because it holds every credential the app uses. Rotate third-party API keys, session secrets and database passwords after cutover, since the old values have passed through a laptop and a chat or ticket. Confirm which values differ between staging and production so that a copied value does not point a live app at test services.

File storage and uploaded assets

Heroku's filesystem is ephemeral, so uploads written to the dyno disk are already lost on every restart and should be in an external store. Identify where user uploads, generated reports and exports live, usually an S3 bucket or a similar service. Dokku's local storage is persistent on the host, so decide whether files stay in that bucket, move to a self-hosted S3-compatible store or sit on a mounted volume, and back that location up separately from the database.

Third-party webhooks and scheduled jobs

Payment providers, identity providers, email services and Git hooks often call a fixed Heroku hostname. Update each callback URL and secret in the provider console, and plan a short window where both old and new endpoints accept events. Check the Heroku Scheduler jobs and any app.json cron entries, because a job that runs on both platforms at once can send duplicate emails or charges. Confirm each provider's retry behavior so that events sent during cutover are not lost.

DNS records and TLS certificates

List every hostname that points at the Heroku app, including apex, www, custom subdomains and any records used for domain verification. Lower the TTL well before the change, then point each record at the Dokku host. Dokku's default proxy is nginx, and it can use Caddy, HAProxy, OpenResty or Traefik instead. Confirm that the certificate issuance method works for every hostname before you remove the Heroku record, and keep the Heroku app running until traffic has moved.

Migration sequence from inventory to decommission

This sequence moves one Heroku app and its Postgres data to Dokku in stages, so each step produces a checkable result and the Heroku app stays live until the new host has proven itself.

  1. Inventory apps, add-ons, config vars and domains

    The app owner lists every Heroku app in scope, its process types from the Procfile, its buildpacks, its add-ons, its config vars and every hostname that points at it. The inventory also records which add-on backs DATABASE_URL and whether app.json defines deploy scripts, cron tasks or healthchecks. Confirm each item against the Heroku CLI and the Dashboard rather than memory. The output is a signed-off checklist with one row per app and per dependency, including any add-on that has no Dokku plugin equivalent, which then needs a replacement decision before go-live.

  2. Deploy a pilot app with git push

    Choose a low-risk app that is representative of the estate and create it on the Dokku host. Add a dokku git remote and push the code, then confirm that the builder selected by Dokku matches what Heroku used. For a custom buildpack, set BUILDPACK_URL or a .buildpacks file in the repository root. Scale the web process, check that the release process type runs, and load config vars with the config commands. The output is a running pilot on a Dokku hostname with a passing health check and a recorded list of any Heroku-specific behavior that did not carry over.

  3. Rehearse the database restore

    Create a dump with heroku pg:backups:capture and download it with heroku pg:backups:download before the signed URL expires, which Heroku documents as 60 minutes. Heroku recommends PGBackups for databases up to 20 GB, so larger databases need a logical export instead. Create a PostgreSQL service with the dokku-postgres plugin, import the dump with postgres:import, and restore with pg_restore using --no-acl and --no-owner. Link the service to the pilot with postgres:link so DATABASE_URL is set. Run row counts and a few application queries against both databases. The output is a restored database whose counts match the source, with the rehearsal timings recorded for the cutover plan.

  4. Run a parallel staging environment

    Point a staging hostname at the Dokku pilot and run the full application against the restored database, including background processes, scheduled jobs and any outbound webhooks. Keep the Heroku app as the only production target. Ask the application owner to test sign-in, file uploads, email delivery and payment callbacks, because these often depend on hostnames or secrets that differ between platforms. Check certificate issuance for the staging hostname through the proxy Dokku uses, which is nginx by default. The output is a written test result for each user path, with defects logged and closed before cutover.

  5. Schedule and rehearse the DNS cutover

    Lower the TTL on the production record well ahead of the change window so that resolvers pick up the new value quickly, and confirm the current TTL with the DNS provider. Write a cutover runbook that lists the final data sync from Heroku Postgres, the write freeze on the Heroku app, the record change, the health checks and the owner for each action. Include a rollback path that points the record back to Heroku and states who makes that call and on what signal. Walk through the runbook with the on-call team. The output is a signed-off cutover runbook with named approvers and a tested rollback procedure.

  6. Execute cutover and monitor

    Freeze writes on the Heroku app, take a final pg:backups capture, restore it into the Dokku database, and then change the DNS record. Watch error rates, response times, logs and database connections against the baseline recorded in the inventory. Keep the Heroku app running and its database intact so the rollback path remains available. Verify the public hostname, the TLS certificate and the scheduled jobs. The output is a cutover log with timestamps, the post-change health check results and an explicit go or rollback decision made by the named approver.

  7. Decommission the Heroku app after a stable period

    Keep the Heroku app and its database available until the owner agrees that the new environment has run stably through its normal cycle of traffic and jobs. Then export a final backup for retention, remove the domains from Heroku, scale dynos to zero, and delete the add-ons and the app. Confirm that backup copies are stored where your retention policy requires. Record the decommission date and the retained backup location in the migration record. The output is a closed migration record showing that no production traffic, data or scheduled work still depends on Heroku.

How to run Heroku and Dokku in parallel and cut over safely

Keep Heroku live and serving production while Dokku runs the same app on a staging hostname, such as a subdomain you control that does not yet appear in public DNS. Deploy to the Dokku remote with git push dokku main, attach the same domain list you will use in production, and test the paths that matter: sign-in, webhooks, file uploads, background jobs and any outbound calls that depend on source IP allowlists. Because Dokku's proxy and TLS setup are separate from Heroku's, the staging hostname is also the place to confirm that certificates issue and renew before any traffic moves. Dokku ships nginx as its default proxy, with Caddy, HAProxy, OpenResty and Traefik documented as alternatives in the Dokku proxy management guide. Decide which one you run before cutover, then check how it terminates HTTPS for each domain, since a certificate that works on the staging name can still fail on the production name if the domain was never added to the app.

The hard part is data. Heroku keeps accepting writes until you stop it, so you need a decision on which system is the source of truth during the window. For most apps the safest pattern is a short write freeze: put Heroku into maintenance mode or stop its web and worker processes, take a final pg:backups capture or a direct pg_dump of the database, load it into the Dokku PostgreSQL service with dokku postgres:import, link it so DATABASE_URL is set, and then verify row counts and key tables before reopening writes. Ask the team which tables change continuously, such as sessions, queues and event logs, and whether replaying a delta is worth the risk. Plan the freeze length from your own measured restore time on a copy of the data, not from any general estimate, and rehearse the whole sequence at least once against a recent backup.

DNS is the last step and the one you do not fully control. Lower the record TTL on the production hostname well ahead of the cutover so resolvers pick up the new target quickly, then point the record at the Dokku server once the freeze is complete and verification has passed. Recursive resolvers, client caches and corporate DNS may keep returning the old address for longer than the TTL you set, so keep Heroku running with a rollback path until traffic has clearly stopped arriving there. Confirm the rollback steps in writing: which DNS record reverts, who can make the change and what happens to writes accepted on Dokku during the interval. Also confirm with your registrar and certificate process that the change does not break validation for the domain.

What drives the cost and duration of the move

This table lists the factors that change how much work and recurring effort a Heroku to Dokku move takes, why each one matters and how to keep it under control. Read it before requesting a quote, because the same app count can produce very different effort depending on database size, add-ons and staff availability. The main takeaway is that the one-time migration work is only part of the cost, since the server, patching and backup time continue after cutover.

DriverWhy it changes cost or effortHow to keep it down
Number of apps and process typesEach app needs its own Dokku app, domains, TLS, scaling settings and deploy test, and each extra process type (web, worker, clock) adds configuration to verify.Group apps with identical buildpacks and process layouts, and migrate low-risk apps first to build a repeatable runbook.
Database size against the 20 GB PGBackups guidanceHeroku advises PGBackups only for moderately loaded databases up to 20 GB, so larger databases need logical backups and a longer import window (Heroku Dev Center).Check database size and load before scheduling, and plan a logical dump with a rehearsed restore for anything above that guidance.
Add-ons beyond PostgresRedis, search, queues or email add-ons each need a matching Dokku plugin or a self-hosted service, plus new credentials and client configuration.Inventory every add-on and config var first, and map each to an official Dokku plugin or a service you already run.
Custom buildpacksDokku uses herokuish for Heroku v2a buildpacks, while a custom buildpack needs a BUILDPACK_URL or .buildpacks file and may need a rebuild or a move to Cloud Native Buildpacks, Nixpacks or a Dockerfile.Pin buildpack versions, confirm they build with herokuish, and test a single build before planning the rest.
app.json hooks and unsupported Heroku featuresDokku supports part of the app.json format, and deploy scripts, cron tasks and healthchecks must be checked one by one because not all Heroku functionality is supported.Compare app.json against Dokku's documented support, and rewrite unsupported hooks as explicit Dokku commands or scheduled jobs.
Compliance and audit evidenceRegulated workloads need documented access control, backup testing, logging and change records on the Dokku host, which adds design and evidence work that Heroku's managed tier partly covers.Agree the control list and evidence format in writing before work starts, so the scope includes what an assessor will request.
Staff availability and on-call coverageSomeone must own patching, certificate renewal, backup monitoring and restores after cutover, and a small team may not have that time without planning.Name an owner and a backup owner before cutover, and ask whether the scope covers handover or an ongoing support retainer.
Server or servers you rentYou pay the provider for the Dokku host, storage and bandwidth every month, and a single server means one host to size for the combined app load.Size memory and disk from measured Heroku usage, and ask the provider for the monthly line items and snapshot pricing before deciding.
Optional paid Dokku ProDokku Pro is an optional paid product, so it adds a recurring line item only if you choose it, and its terms apply on top of the free MIT-licensed core.Confirm which features you need with the Dokku project, and compare that against the free core before adding a recurring fee.

What usually goes wrong and how to prevent it

Most failed Heroku to Dokku moves fail on data, configuration or capacity, not on the build. The cards below cover the risks that recur in this migration and the check that prevents each one. Work through them before the DNS cutover, not after.

Restore that was never rehearsed

A dump that restores cleanly on paper can still fail on a real Dokku host because of extensions, roles or ownership. Restore into a scratch dokku-postgres service first, using the --no-acl and --no-owner options that Heroku documents for pg_restore, and then run the application's read paths against it. Time the restore so you know how long the production window needs to be.

Stale dump taken before cutover

A pg:backups capture taken days before the switch will miss every write made since. Plan a final capture after you stop writes to the Heroku app, then import that dump into Dokku and compare row counts on the critical tables. Name the owner of the freeze window in the runbook so nobody keeps writing to Heroku after the dump starts.

Expired signed download URLs

The signed URL that heroku pg:backups:download returns expires after 60 minutes. Teams that copy the link into a ticket and fetch it the next morning end up with a 403 and a second capture. Download the file in the same session, store it on the target host with a checksum, and keep a copy in storage you control before you start the import.

Config vars that never made it across

Heroku config vars do not move on their own. Export them with heroku config, set them with dokku config:set, and diff the two lists before cutover. Check DATABASE_URL after dokku postgres:link, and check any third-party keys, webhook secrets and feature flags that the app reads only at boot, because a missing value often shows up only on a code path nobody tests.

Buildpack behavior that differs from herokuish

Dokku's default herokuish builder follows Heroku's v2a buildpack specification, but the image contents, stack and cache behavior are not identical to Heroku's. Pin the buildpack or set BUILDPACK_URL explicitly, then compare the built app's runtime version, system packages and start command with production. If the build relies on Heroku's Fir or Cloud Native Buildpacks stack, test the Cloud Native Buildpacks builder instead of assuming parity.

app.json features Dokku does not support

Dokku reads an app.json based on Heroku's format for deploy scripts, cron tasks, process formation and healthchecks, but documents that not all Heroku functionality is supported. Inventory every key in your app.json against the Dokku docs before the first deploy. Anything unsupported, such as review app or addon directives, needs a replacement you write and test, such as a Dokku plugin or a scheduled job.

One host with no backup plan

Dokku's default docker-local scheduler runs containers directly on one Dokku host, so a disk failure takes the apps and their data down together. Set up scheduled backups for the Postgres service, for example to S3-compatible storage, and test a restore onto a second server. The k3s scheduler can spread work across several servers, but it requires a Docker registry and only the initial Dokku server accepts git push, so size the first host for peak load and plan the second host before you need it.

How the move changes by app count and starting point

A single web app with a modest Postgres database is the simplest case. Dokku runs on one server, so the work is mostly a one-time rebuild: install Dokku, create the app, push the code to the dokku remote, and import the data. The application code usually needs no structural change if its Procfile and buildpack already work on Heroku. The main decisions are whether the app can tolerate a short write freeze during the final data copy and whether you want the server to hold backups off-host.

  • Build path: Heroku's buildpacks are usually reproduced by Dokku's herokuish builder, so a standard Ruby, Node or Python app often deploys with little change.
  • Database move: A single pg:backups dump restored into the dokku-postgres plugin is a manageable task at this size.
  • Config vars: Only a handful of variables need to be re-entered, which makes a manual copy practical.
  • Operations: You take on patching, TLS renewal and backups for one host, which is the main ongoing change.
  • Rollback: Keeping the Heroku app running until DNS has moved gives you a clean fallback.

Confirm first: the buildpack the app uses today, the Postgres version, and whether any add-on other than Postgres is attached.

Questions teams ask before moving from Heroku to Dokku

How much effort and cost should we expect to move from Heroku to Dokku?

The cost is mostly labor and a server, not a license, because Dokku is MIT licensed and has no per-dyno fee. The effort depends on how many apps you run, how many use add-ons, and how much of your build relies on Heroku-specific behavior. Recurring costs are the rented server, backups storage, and the time someone spends on patching, upgrades and on-call. One-time costs are the migration itself, the database cutover and testing. Ask any quote to list each app with its buildpack or Dockerfile, each add-on to be replaced, the backup and restore plan, the staging environment, and who patches the host after handover.

How long does a Heroku to Dokku migration take?

The duration depends on your inventory more than on Dokku itself. Count the apps, their process types, the number of databases and their size, the config vars, and any app.json features that Dokku documents as partially supported. A small app with a standard buildpack and no add-ons is a short exercise. An app with a large Postgres database, scheduled jobs and a custom buildpack needs more testing and a planned restore window. Dokku does not publish a migration duration, so ask for a timeline in the written scope that is tied to a named inventory and a dated cutover window.

Can our team migrate from Heroku to Dokku without outside help?

Yes, a team with Linux, Docker and PostgreSQL experience can do it, and Dokku's documentation covers git push deploys, buildpacks, Procfiles and the dokku-postgres plugin. The harder parts are operational: hardening the host, configuring the proxy and TLS, setting up backups you have tested, and handling Postgres upgrades yourself. Heroku's managed platform hides those tasks, so they move onto your team's calendar. If your team has little time for on-call work, consider a fixed-scope engagement with DeployOpen for planning, implementation and handover, and agree the support model in writing.

What access, test environment and restore window does a Dokku migration need?

You will need administrative access to the target server or cloud account, DNS control for the cutover, and access to the Heroku apps, config vars and Postgres backups. A staging Dokku host is strongly advisable so you can deploy each app with its real config and run its tests before any traffic moves. Plan a restore window for the database: the dump is taken, transferred and loaded, and the app is paused or set to read-only while the final data is copied. Confirm the window length with your own data size and change-freeze rules rather than assuming one.

Does Dokku handle backups and patching for us?

No. Dokku is a platform you run on your own server, so backups, patching and recovery are your responsibility or your provider's. The dokku-postgres plugin can run one-off or scheduled backups to S3-compatible storage, and it can export and import databases, but you must configure storage, test restores and monitor the jobs. Operating system and Dokku updates are also your work. Coolify's guide for teams coming from managed platforms makes the same point: a rollback can redeploy an application image but does not restore databases or persistent files. Ask the quote to include a backup schedule, a tested restore procedure and a patching cadence.

Do we have to keep Heroku data in a specific region after moving to Dokku?

Dokku runs on whatever server you choose, so the region is set by your hosting account and not by Dokku. If your data residency rules require an EU or national region, pick a server and database location there and confirm that backups and off-site copies stay in the same jurisdiction. Heroku's pg:backups snapshots are stored in the United States regardless of database region, so that step needs a separate plan during the exit. Confirm with your compliance owner which regions, storage locations and transfer paths are allowed before you export any data.

Does Dokku support every Heroku feature our apps depend on?

Not every feature. Dokku supports Heroku-style git push deploys, herokuish buildpacks, Procfiles, a release process type and an app.json format based on Heroku's, but Dokku documents that not all Heroku functionality is supported. Dokku also provides no datastores by default, so each add-on needs a plugin or a self-managed service. Review each app's Heroku features, including add-ons, scheduler jobs, pipelines and Private Space networking, against Dokku's documentation and list the gaps in your scope document with a decision for each one.

Which parts of the Heroku setup need the most checking before cutover?

Focus on four areas: the build, the database, configuration and the traffic switch. Confirm each app builds with the same buildpack or with a Dockerfile, and that the Procfile process types and the release step behave as before. Restore the Postgres dump with the no-acl and no-owner options, then link the database so DATABASE_URL is set. Re-create config vars as Dokku config values and check secrets are not copied into logs. Finally, lower the DNS TTL in advance and keep the Heroku app running until the new host passes checks.

How DeployOpen scopes and runs a Heroku to Dokku migration

DeployOpen plans, implements, hardens, backs up and documents self-hosted open-source platforms inside your own cloud accounts, data centers or air-gapped networks. For a Heroku to Dokku move, we start with a written scope that maps each app, datastore and cutover step to a deliverable before any work begins.

How we scope

  • Inventory every Heroku app, process type, add-on and custom buildpack before agreeing the scope in writing.
  • Confirm which Heroku Postgres databases move, their sizes and whether pg:backups suits each one.
  • List the domains, certificates, config vars and external services that must change during DNS cutover.

How we build

  • Install Dokku on servers you control, configure the reverse proxy and wire each app to its builder.
  • Test Procfile, release process and app.json behavior against each app, recording any Heroku features that do not carry over.
  • Restore databases into the dokku-postgres plugin, link each app to its DATABASE_URL, and verify data and access rules.

How we hand over

  • Deliver runbooks for deploys, rollbacks, database restores, scaling with ps:scale and config var changes.
  • Document backup schedules, retention, restore tests and the controls your compliance team needs to review.
  • Walk your engineers through the cutover checklist and the rollback steps agreed before the DNS change.

How we support

  • Offer ongoing support, upgrades and continuous testing as a retainer after the fixed-scope engagement ends.
  • Plan Dokku and PostgreSQL upgrades in writing, including the major version support dates you must track.
  • Confirm with your team which operational tasks stay in-house and which the retainer covers before handover.

Sources and further reading

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.