Skip to main content

Migrate from Heroku to Hostim: Step-by-Step Guide (2026)

This guide moves a Heroku app with a Heroku Postgres database to Hostim. Hostim runs in Germany (EU).

We tested every step on 2026-09-23. We deployed Heroku's official sample app, python-getting-started (Django + Postgres), to Heroku by following Heroku's own tutorial. Then we moved it to Hostim with the commands below. The data came across complete, and the app ran on Hostim after one code change.

The move takes about 15 minutes for a small app. Your app is offline while the database is copied.

Before you start: what changes

HerokuHostimWhat you do
Buildpacks build your appYou provide a DockerfileWrite a short Dockerfile (example below)
git push heroku mainHostim builds from your Git repositoryPush your code to GitHub, GitLab or similar
Procfile with web and workerOne process per appEach process type becomes its own app
release: phaseNo release phaseRun migrations when the container starts
Config varsEnvironment variablesExport once, import once
Heroku PostgresHostim PostgresDump and import
Heroku Key-Value Store (Redis)Hostim RedisCreate a new one. Cache data is not copied.
heroku run bashhostim exec webSame idea
heroku logs --tailhostim logs web -fSame idea
Heroku SchedulerNo built-in schedulerSee Scheduler
Review appsNot availableSee What does not carry over

All commands use the Hostim CLI. Install it and log in:

curl -fsSL https://raw.githubusercontent.com/hostimdev/cli/main/install.sh | sh
hostim login

1. Add a Dockerfile

Heroku detects your language and builds the app with a buildpack. Hostim builds your app from a Dockerfile in your repository. This is the biggest change.

Writing a Dockerfile is extra work once. In return, you control exactly what runs:

  • You choose the base image and the language version.
  • You install any system package with one line, for example ffmpeg or libvips. No custom buildpack needed.
  • The same image runs on your laptop with docker run. What you test is what you ship.
  • No forced stack upgrades. Heroku retires its stacks on its own schedule. Your image changes only when you change it.
  • Images can be up to 4 GB. Heroku limits a compiled slug to 500 MB.

This is the Dockerfile we used for Heroku's Python sample:

FROM python:3.14-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput

EXPOSE 5006
# Heroku's release phase has no Hostim equivalent: migrate on start instead.
CMD ["sh", "-c", "./manage.py migrate --no-input && exec gunicorn --config gunicorn.conf.py gettingstarted.wsgi"]

For Node.js, Ruby, Go or PHP, start from the official image for your language (node, ruby, golang, php) and follow the same shape: install dependencies, copy the code, set the start command.

Build it locally once to catch errors early:

docker build -t my-app .

Procfile

Take the web: line from your Procfile and put it in the Dockerfile's CMD. Every other process type, such as worker:, becomes a separate app. See Workers.

Release phase

Heroku runs the release: command before a new version goes live. Hostim has no release phase. Put the command in front of your start command, as in the Dockerfile above:

./manage.py migrate --no-input && exec gunicorn ...

The migration runs each time a container starts. Django, Rails and most other frameworks skip migrations that already ran, so this is safe.

One limit: if you run more than one replica, each replica runs the migration when it starts. Keep one replica, or make your migration tool take a lock.

Code that checks for Heroku

Search your code for DYNO and HEROKU:

grep -rnE 'DYNO|HEROKU' --include='*.py' --include='*.rb' --include='*.js' --include='*.ts' .

Heroku sets DYNO in every container. Hostim does not. Heroku's own Python sample uses it to decide whether it runs in production:

IS_HEROKU_APP = "DYNO" in os.environ and "CI" not in os.environ

Without DYNO, the sample app used its local settings on Hostim. It rejected every request with 400 Bad Request (Django's DisallowedHost error) and ignored DATABASE_URL. It fell back to a SQLite file inside the container, which is lost on every restart.

Fix it in one of two ways:

  • Change the check to a variable you control, for example ENVIRONMENT=production.
  • Or set DYNO on Hostim. We did this in the test: DYNO=web.1.

Port

Heroku gives your app a random PORT. On Hostim you choose the port when you create the app. The sample app listens on PORT and falls back to 5006, so we used --port 5006. If your app requires PORT, set it to the same number.

2. Push your code to a Git repository

Hostim builds from a Git URL, not from git push. If your code only lives in Heroku's Git remote, push it to GitHub or GitLab first.

For a private repository, create a token that can read the repository. On GitHub, use a fine-grained token with Contents: Read-only on that one repository.

3. Create a project and a database

hostim projects create my-app --region eu-center
hostim use my-app
hostim db postgres create main --plan sp-1

List the database plans with hostim regions pricing eu-center --for postgres. Pick one at least as big as your Heroku database. heroku pg:info shows the current size under Data Size.

4. Stop writes on Heroku

Turn on maintenance mode, so no new data is written while you copy the database:

heroku maintenance:on -a your-heroku-app

Your app is offline from here until you switch the domain in step 8.

5. Copy the database

Dump the Heroku database as plain SQL. You do not need Postgres installed locally, the postgres Docker image has pg_dump:

docker run --rm postgres:18 pg_dump --no-owner --no-acl --exclude-schema=_heroku \
"$(heroku config:get DATABASE_URL -a your-heroku-app)" > heroku.sql

Do not use heroku pg:backups:download. It gives you a file in Postgres's binary backup format, and the import below needs plain SQL.

Import it into Hostim:

hostim db postgres import main -f heroku.sql

The import runs through your project's SSH bastion. The first time, it offers to add your SSH public key to the project. Add -y to skip the question.

Heroku adds its own objects to every database. They cannot be created outside Heroku, so the import prints a few errors like these:

ERROR:  permission denied to create event trigger "00_validate_before_start"
ERROR: must be owner of extension pg_stat_statements
ERROR: unrecognized configuration parameter "transaction_timeout"

The command ends with a summary:

Imported the dump into "main" with 7 SQL errors (shown above). Check them; the rest of the dump was applied.

These errors are safe to ignore. Your tables and data are imported. --exclude-schema=_heroku in the dump already leaves out Heroku's helper functions, but the event triggers that call them are always in the dump, so those errors stay.

Check the row counts of your biggest tables before you go on. The import command runs any SQL, so you can use it for a quick check:

echo 'SELECT count(*) FROM your_table;' | hostim db postgres import main

In our test, every row and the ID sequences came across unchanged.

If your app uses Postgres extensions, add them before the import, for example:

hostim db postgres extensions ls
hostim db postgres extensions add main pg_trgm

6. Copy config vars

Export Heroku's config vars to a file, without Heroku's DATABASE_URL:

heroku config -s -a your-heroku-app | grep -v '^DATABASE_URL=' > app.env

Add the Hostim database URL:

hostim db postgres credentials main -o json \
| jq -r '"DATABASE_URL=postgres://\(.username):\(.password)@\(.hostname):\(.port)/\(.database)"' \
>> app.env

Also add anything from Code that checks for Heroku, for example DYNO=web.1.

Other add-ons also set config vars, for example REDIS_URL or SENDGRID_API_KEY. Remove the ones for add-ons you replace. Keep the ones for services you still use from outside Heroku.

7. Deploy

hostim deploy web \
--git https://github.com/you/your-app --branch main \
--git-token "$GITHUB_TOKEN" \
--plan sa-1-1 --port 5006 \
--env-file app.env

The command waits for the build and for the app to start. If the app keeps crashing after the build, the command stops and says so. Then check the app:

hostim status web
hostim logs web

The logs show the migration output first, then your web server. Hostim gives every app a URL like https://xxxxxxxx.eu-center.hostim.dev. Open it and test the app before you move the domain. hostim apps get web shows the URL.

List the app plans with hostim regions pricing eu-center.

8. Move your domain

Add your domain to the Hostim app:

hostim domain add www.example.com -a web

The command prints a DNS record. At your DNS provider, replace the CNAME that points to Heroku with this record. Then check the status:

hostim domain status web

When the domain shows as active, Hostim has issued the SSL certificate. You do not need to set up SSL yourself. Heroku's Automated Certificate Management is not needed any more.

DNS changes can take a while to reach everyone. Keep the Heroku app in maintenance mode until the old record has expired, so nobody writes data to the old database.

9. Shut down Heroku

When traffic arrives at Hostim and you have checked your data, remove the Heroku app:

heroku apps:destroy -a your-heroku-app

This also deletes Heroku Postgres and all other add-ons. Keep your heroku.sql dump until you are sure you do not need it.

Other Heroku features

Workers

Every Procfile line other than web becomes its own app. Deploy it from the same repository with a different start command, and do not make it public:

hostim deploy worker \
--git https://github.com/you/your-app --branch main \
--git-token "$GITHUB_TOKEN" \
--plan sa-1-1 --public=false \
--command "celery -A myapp worker" \
--env-file app.env

Redis

Create a Redis database and set REDIS_URL:

hostim db redis create cache --plan sr-1
hostim db redis credentials cache -o json \
| jq -r '"REDIS_URL=redis://:\(.password)@\(.hostname):\(.port)"' \
| xargs hostim env set -a web

Heroku's Redis uses TLS (rediss://). Hostim's Redis is only reachable inside your project, over plain redis://. If your code forces TLS or turns off certificate checks for Heroku, remove that.

Redis usually holds cache and job queues. Let the queue empty on Heroku before you switch, and start with an empty Redis on Hostim.

One-off commands

heroku run becomes hostim exec:

hostim exec web -- ./manage.py createsuperuser
hostim exec web # interactive shell

Heroku Scheduler

Hostim has no built-in scheduler. Two options:

  • Run a small scheduler inside a worker app, for example supercronic with a crontab file.
  • Run the job from a scheduled GitHub Actions workflow with hostim exec web -- <command>.

Logs and metrics

Logs and basic metrics are included. You do not need a logging add-on such as Papertrail to see your logs. See Observability.

What does not carry over

  • Buildpacks. You need a Dockerfile. See step 1.
  • Release phase. Migrations run when the container starts.
  • Review apps and pipelines. Hostim has no review apps for pull requests and no staging-to-production promotion. You can create a second project for staging.
  • Heroku Scheduler. See above.
  • Dyno types and dyno hours. Hostim bills fixed monthly plans per app and database. There is no sleeping and no per-dyno metering. See pricing.
  • Add-on marketplace. Hostim has Postgres, MySQL, Redis and volumes built in. For other add-ons, sign up with the provider directly and set their config vars on Hostim.
  • Heroku Git remote. You deploy from GitHub, GitLab or another Git host.

Next steps

👉 Start your migration: create a project on Hostim