Migrate from Heroku to Zenifra
This guide shows where to find each setting of a Heroku app and where to enter the same value in the Zenifra console. It covers the Procfile, Config Vars, buildpacks, the release phase, Heroku Scheduler, Heroku Postgres, and Heroku Key-Value Store. For the general plan, with data copy and DNS switch, see Migrate to Zenifra.
Keep the Heroku app running while you deploy and validate the application on Zenifra. Only turn the dynos off after switching DNS.
How the concepts map
| On Heroku | On Zenifra |
|---|---|
| App | HTTP project |
web process in the Procfile | The project's Start Command |
worker process (or any other name) | Another project with the same repository, Exposure Private, and the worker command |
Release phase (release:) | Beginning of the Start Command or a manual step; see Release phase |
| Buildpack | Runtime and Version (Node.js, Python, Bun, Go, or Java) or OCI image |
| Config Vars | Environment Variables |
Number of web dynos | Desired Instances or auto-scaling |
| Heroku Scheduler | Scheduled jobs |
| Heroku Postgres | Managed PostgreSQL |
| Heroku Key-Value Store | Managed cache or Key-Value |
| Review Apps | Preview Environments |
heroku maintenance:on | Use it on Heroku during the final data copy |
1. Copy the settings from Heroku
The steps below use the Heroku CLI. Replace my-app with your app name.
Config Vars
In the Heroku dashboard, Config Vars are in the app's Settings tab. To export all of them at once:
heroku config --shell -a my-app > heroku.envUse --json instead of --shell if you prefer JSON. The file contains secrets: do not commit it, and delete it after the migration.
When reviewing the file:
DATABASE_URLandREDIS_URLwere created by add-ons and point to Heroku databases. On Zenifra, replace them with the URLs of the new databases.- Variables prefixed with
HEROKU_belong to the platform. Only copy them if your code uses one, with an equivalent value. - Variables from other add-ons, such as e-mail or monitoring services, keep working if you keep your account with the add-on provider.
Procfile
Open the Procfile at the repository root. Each line is type: command:
release: bundle exec rails db:migrate
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiqWrite down the command for each type. Without a Procfile, Heroku uses the buildpack default, such as the start script in package.json for Node.js.
Also check how many dynos of each type are running:
heroku ps -a my-appBuildpacks and language version
heroku buildpacks -a my-appThe main language buildpack defines the Runtime on Zenifra. The version comes from the repository itself:
| Language | Where Heroku reads the version |
|---|---|
| Node.js | engines.node in package.json |
| Python | .python-version (or runtime.txt, which Heroku considers deprecated) |
| Go | go line in go.mod |
| Java | java.runtime.version in system.properties |
| Ruby | ruby entry in the Gemfile |
Domains, deploys, and Scheduler
- Domains:
heroku domains -a my-applists the domains and the DNS target of each one. - Deploys: in the app's Deploy tab, check whether the GitHub integration has Automatic deploys on and which branch is deployed.
- Scheduler:
heroku addons:open scheduler -a my-appopens the list of scheduled tasks. Write down each task's command and frequency. - app.json: if the repository has an
app.json, it lists variables, add-ons, and scripts used by Review Apps.
2. Create the project on Zenifra
In the console, click + Create project, choose HTTP Application and the plan. Under Advanced Settings, choose Git repository as the Project Source, the Git provider GitHub (or Forgejo), the same repository, and the branch deployed on Heroku.
| On Heroku | In the Zenifra console |
|---|---|
| Language buildpack | Runtime |
| Version in the repository | Version, the same major version |
| App folder in the repository | Root directory. Use . for the root |
| Automatic buildpack build | Build Command; see the table below |
web: line in the Procfile | Start Command |
$PORT | Port and a PORT variable with the same value; see Port |
| Automatic deploys on | Automatic by branch deployment mode |
| Automatic deploys off | Manual deployment mode |
| Config Vars | Environment Variables |
Number of web dynos | Desired Instances |
Commands by language
Heroku runs some build steps on its own. On Zenifra, each step is explicit in the project fields:
| Language | Build Command | Start Command |
|---|---|---|
| Node.js | npm run build if package.json has a build script; empty otherwise | web: line in the Procfile or npm start |
| Python (Django) | python manage.py collectstatic --noinput | gunicorn project.wsgi --bind 0.0.0.0:8000 |
| Python (Flask, FastAPI) | empty | web: line with --bind 0.0.0.0:8000 or --host 0.0.0.0 --port 8000 |
| Go | go build -o app ./cmd/server (folder of the main package) | ./app |
| Java (Maven) | ./mvnw -B -DskipTests package or mvn -B -DskipTests package | java -jar target/*.jar |
Details for each runtime are in Runtimes, Python, Django, and Node.js APIs.
Node.js: Heroku scripts
- The
heroku-prebuild,heroku-build, andheroku-postbuildscripts do not run on Zenifra. Put what they did in the Pre-build Command or the Build Command. - Zenifra installs with
npm ciand needs a committedpackage-lock.json. If the app uses Yarn or pnpm, generate and commit apackage-lock.jsonbefore migrating. See Node.js dependencies. NPM_CONFIG_PRODUCTIONis not needed:devDependenciesare available during the build.
Python: Poetry, uv, and Pipenv
Zenifra installs Python dependencies from requirements.txt. If Heroku used poetry.lock, uv.lock, or Pipfile.lock, export and commit a requirements.txt:
poetry export -f requirements.txt --output requirements.txt
uv export --format requirements-txt > requirements.txtRuby, PHP, and other languages
Ruby, PHP, .NET, and other languages outside the runtimes above are deployed as a Docker Image: build the image from a Dockerfile, push it to a registry, and enter the image in the project. See Ready-made Dockerfiles and Deploy via OCI image. The GitHub Action automates the build and deploy on every push.
3. Adjust what differs between the platforms
Port
On Heroku, the web process listens on the port from the PORT variable, chosen by the platform. On Zenifra, the port is the project's Port field. Since your code already reads PORT, set both to the same value, for example Port 3000 and the variable PORT=3000. The application must listen on 0.0.0.0.
Release phase
On Heroku, the release: line runs once per release, before the dynos start. Zenifra has no equivalent step. Choose one of these options:
- With the start command:
npx prisma migrate deploy && npm startorpython manage.py migrate && gunicorn .... The command runs every time an instance starts, so it must be idempotent, like most frameworks' migrations. With multiple instances, all of them run the command: prefer tools that lock the database during the migration. - Manual: run the migration from your machine against the Zenifra database before deploying the version that depends on it. This is the safest option for long migrations.
Workers
Each Procfile process type other than web becomes a project on Zenifra:
- Create another HTTP project with the same repository and branch.
- Under Exposure, choose Private - no public route.
- Use the worker command in the Start Command, such as
bundle exec sidekiqorcelery -A app worker. - Add the same variables as the web project.
The clock process, which schedules tasks inside a dyno, can become a private project the same way, or be replaced with scheduled jobs.
Heroku Scheduler
Each Scheduler task becomes a scheduled job. Create the job with Task source GitHub Repository, the same Runtime, the task command in Task command, and the variables it needs.
| Scheduler frequency | Cron expression on Zenifra |
|---|---|
| Every 10 minutes | */10 * * * * |
| Every hour, at minute 30 | 30 * * * * |
| Every day at 06:00 UTC | 0 6 * * * |
Both platforms use UTC. On Zenifra, the cron expression accepts any schedule, such as 0 9 * * 1-5 for weekdays at 09:00 UTC.
User uploads
On Heroku, files written to the dyno disk disappear when it restarts. On Zenifra, you can store files in persistent storage or in Armazém, which is S3-compatible. If the app already uses an S3 bucket, it keeps working with the same variables.
HTTPS and proxy
As on Heroku, Zenifra terminates HTTPS before forwarding the request. Keep your framework's trusted proxy settings, such as app.set('trust proxy', 1) in Express, config.force_ssl in Rails, or --proxy-headers in uvicorn.
4. Migrate the databases
Heroku Postgres
Create the managed PostgreSQL on Zenifra before the application. To copy the data, you can use a Heroku backup:
heroku pg:backups:capture -a my-app
heroku pg:backups:download -a my-app # creates latest.dump
pg_restore --list latest.dump > /dev/null && echo "dump ok"Then restore it on Zenifra with the console URI:
pg_restore --no-owner --no-acl --jobs=4 \
--dbname="postgresql://user:password@host:port/database?sslmode=verify-full&sslrootcert=system" \
latest.dumpHeroku recommends backups for databases up to 20 GB. For larger databases, or to copy directly, get the Heroku database URL and use pg_dump:
heroku pg:credentials:url DATABASE -a my-app
pg_dump --format=custom --no-owner --no-acl "HEROKU_URL?sslmode=require" > dump.pgdumpUse a pg_dump with the same major version as the database or newer. See Migrate the database to validate the copy.
Heroku Key-Value Store
Create a managed cache or a Key-Value database on Zenifra and replace REDIS_URL. Cache and session data are usually rebuilt by the application. If the Key-Value Store holds queues, such as Sidekiq or Celery queues, let the Heroku workers drain them before the switch.
5. Validate and switch DNS
- Open the project's
*.clients.zenifra.comURL and test login, main flows, e-mails, workers, and webhooks. - Check Build logs and the application logs for errors.
- At switch time, turn on Heroku maintenance mode to freeze writes:
heroku maintenance:on -a my-app. - Run the final database copy, add the domain on Zenifra, and update DNS. Follow the switch-day checklist.
- Keep the Heroku app for a few days for rollback. Then turn the dynos off and remove the add-ons to stop billing.
Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
npm ci fails | App uses Yarn or pnpm, without package-lock.json | Generate and commit a package-lock.json |
| Python build does not install dependencies | Project uses Poetry, uv, or Pipenv | Export a requirements.txt |
| Public URL does not respond | App listens on localhost or on another port | Listen on 0.0.0.0, on the Port field value, and set PORT |
| Tables do not exist | The release phase does not run on Zenifra | Run the migrations; see Release phase |
| Background tasks stopped | Worker was not created | Create a private project for each process other than web |
Missing assets or staticfiles | The buildpack ran that step on its own | Put the command, such as collectstatic or assets:precompile, in the Build Command |
Need help?
Write to [email protected] with your Procfile and the list of add-ons, or reach us on WhatsApp at +55 (11) 5199-3013.
Last updated on
Migrate from Vercel to Zenifra
Step-by-step guide to bring a Vercel project to Zenifra: where to copy the Build Command, Output Directory, Root Directory, and variables in Vercel, how to translate vercel.json, Functions, and Cron Jobs, and where to enter each value in the console.
Migrate from Netlify to Zenifra
Step-by-step guide to bring a Netlify site to Zenifra: where to copy the Build command, Publish directory, and variables, how to translate netlify.toml, _redirects and _headers, Functions, Forms, and DNS, and where to enter each value in the console.