Migrate to Zenifra

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 HerokuOn Zenifra
AppHTTP project
web process in the ProcfileThe 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
BuildpackRuntime and Version (Node.js, Python, Bun, Go, or Java) or OCI image
Config VarsEnvironment Variables
Number of web dynosDesired Instances or auto-scaling
Heroku SchedulerScheduled jobs
Heroku PostgresManaged PostgreSQL
Heroku Key-Value StoreManaged cache or Key-Value
Review AppsPreview Environments
heroku maintenance:onUse 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.env

Use --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_URL and REDIS_URL were 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:

Procfile
release: bundle exec rails db:migrate
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq

Write 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-app

Buildpacks and language version

heroku buildpacks -a my-app

The main language buildpack defines the Runtime on Zenifra. The version comes from the repository itself:

LanguageWhere Heroku reads the version
Node.jsengines.node in package.json
Python.python-version (or runtime.txt, which Heroku considers deprecated)
Gogo line in go.mod
Javajava.runtime.version in system.properties
Rubyruby entry in the Gemfile

Domains, deploys, and Scheduler

  • Domains: heroku domains -a my-app lists 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-app opens 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 HerokuIn the Zenifra console
Language buildpackRuntime
Version in the repositoryVersion, the same major version
App folder in the repositoryRoot directory. Use . for the root
Automatic buildpack buildBuild Command; see the table below
web: line in the ProcfileStart Command
$PORTPort and a PORT variable with the same value; see Port
Automatic deploys onAutomatic by branch deployment mode
Automatic deploys offManual deployment mode
Config VarsEnvironment Variables
Number of web dynosDesired Instances

Commands by language

Heroku runs some build steps on its own. On Zenifra, each step is explicit in the project fields:

LanguageBuild CommandStart Command
Node.jsnpm run build if package.json has a build script; empty otherwiseweb: line in the Procfile or npm start
Python (Django)python manage.py collectstatic --noinputgunicorn project.wsgi --bind 0.0.0.0:8000
Python (Flask, FastAPI)emptyweb: line with --bind 0.0.0.0:8000 or --host 0.0.0.0 --port 8000
Gogo build -o app ./cmd/server (folder of the main package)./app
Java (Maven)./mvnw -B -DskipTests package or mvn -B -DskipTests packagejava -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, and heroku-postbuild scripts do not run on Zenifra. Put what they did in the Pre-build Command or the Build Command.
  • Zenifra installs with npm ci and needs a committed package-lock.json. If the app uses Yarn or pnpm, generate and commit a package-lock.json before migrating. See Node.js dependencies.
  • NPM_CONFIG_PRODUCTION is not needed: devDependencies are 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.txt

Ruby, 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 start or python 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:

  1. Create another HTTP project with the same repository and branch.
  2. Under Exposure, choose Private - no public route.
  3. Use the worker command in the Start Command, such as bundle exec sidekiq or celery -A app worker.
  4. 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 frequencyCron expression on Zenifra
Every 10 minutes*/10 * * * *
Every hour, at minute 3030 * * * *
Every day at 06:00 UTC0 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.dump

Heroku 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.pgdump

Use 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

  1. Open the project's *.clients.zenifra.com URL and test login, main flows, e-mails, workers, and webhooks.
  2. Check Build logs and the application logs for errors.
  3. At switch time, turn on Heroku maintenance mode to freeze writes: heroku maintenance:on -a my-app.
  4. Run the final database copy, add the domain on Zenifra, and update DNS. Follow the switch-day checklist.
  5. 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

SymptomLikely causeFix
npm ci failsApp uses Yarn or pnpm, without package-lock.jsonGenerate and commit a package-lock.json
Python build does not install dependenciesProject uses Poetry, uv, or PipenvExport a requirements.txt
Public URL does not respondApp listens on localhost or on another portListen on 0.0.0.0, on the Port field value, and set PORT
Tables do not existThe release phase does not run on ZenifraRun the migrations; see Release phase
Background tasks stoppedWorker was not createdCreate a private project for each process other than web
Missing assets or staticfilesThe buildpack ran that step on its ownPut 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

On this page