Migrate from Heroku to Bahriya

Heroku builds your app from source using a buildpack and a Procfile, runs it on dynos, and bolts on managed add-ons for databases and caches. Bahriya keeps the same building blocks but makes each one explicit: you produce a container image, deploy it across the regions you choose, and wire configuration in as environment variables and secrets. This guide maps each Heroku concept to its Bahriya equivalent and walks the move end to end.

Updated 3 Aug 20264 min read

Heroku builds your app from source using a buildpack and a Procfile, runs it on dynos, and bolts on managed add-ons for databases and caches. Bahriya keeps the same building blocks but makes each one explicit: you produce a container image, deploy it across the regions you choose, and wire configuration in as environment variables and secrets. This guide maps each Heroku concept to its Bahriya equivalent and walks the move end to end.

What maps to what

HerokuBahriya
Web process (web: in the Procfile)An HTTP container
Worker / background processA worker container
Scheduler jobA cron job
Buildpack buildA Dockerfile producing an image
Config VarsEnvironment variables and secrets
$PORTThe port you declare with --port
Dyno formation / scalingReplicas and autoscaling
Heroku Postgres add-onAn external database, connection string held as a secret
Heroku Data for Redis / MemcachedBahriya Memcached, or an external cache via a secret
App domains + ACMCustom domains with automatic TLS

1. Containerise: Procfile and buildpack to Dockerfile

Heroku infers how to build and start your app. On Bahriya you make that explicit in a Dockerfile. Translate the buildpack's runtime and your Procfile web command into a CMD:

FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]

If your Procfile had a release: phase (for example database migrations), run that as a separate step — an init job or a one-off command in your pipeline — rather than folding it into the web start command.

Build the image and push it to a registry such as GitHub Container Registry:

docker build -t ghcr.io/myorg/api:v1.0.0 .
docker push ghcr.io/myorg/api:v1.0.0

2. Bind to the port

Heroku assigns a port through $PORT and expects your app to bind to it. Bahriya routes to a fixed port you declare. Keep reading the port from the environment so the same image runs anywhere, and set the value in your deploy:

const port = process.env.PORT || 8080;
app.listen(port, "0.0.0.0");

Then declare it with --port 8080 (the default). Always bind to 0.0.0.0, not localhost.

3. Config Vars to environment variables and secrets

Heroku keeps everything in Config Vars. On Bahriya, split them:

  • Non-sensitive values (NODE_ENV, LOG_LEVEL, public URLs) become plain environment variables, visible in the Console.
  • Sensitive values (DATABASE_URL, API keys, tokens) become secrets — encrypted at rest, injected as env vars at deploy time. Create the secret, attach it to the project, then wire it to the variable name your app already reads.

4. Add-ons: Postgres and Redis

Heroku add-ons provision a managed database or cache and inject a connection URL as a Config Var. On Bahriya:

  • Database. Run your database wherever you prefer — a managed Postgres provider, or your own instance reachable over the network. Store its connection string as a secret and wire it to DATABASE_URL. Your application code does not change.
  • Cache. For a Memcached-compatible cache, create a Bahriya Memcached instance and point your app at it. For Redis or another engine, connect to an external instance the same way you handle the database.
Note

Plan your data move separately from the app cutover. Provision the new database, replicate or dump-and-restore your data, and confirm connectivity from a Bahriya container before you switch production traffic.

5. Dynos to replicas and autoscaling

Heroku scales by dyno count and size. On Bahriya you set minimum CPU and memory, a replica count, and — if you want automatic scaling — a maximum replica count. Setting --max_replicas above --replicas enables autoscaling between the two.

6. Deploy

From the Console follow Deploy Your First Container, or use Reis:

reis container:create \
  --type http \
  --name "Web API" \
  --handle web-api \
  --image ghcr.io/myorg/api:v1.0.0 \
  --registry ghcr \
  --port 8080 \
  --healthcheck /healthz \
  --cpu 500 \
  --memory 512 \
  --replicas 2 \
  --max_replicas 8 \
  --regions falkenstein-1 \
  --regions virginia-1 \
  --env NODE_ENV=production \
  --secrets DATABASE_URL=database-url \
  --secrets SECRET_KEY_BASE=secret-key-base

7. Point your domain and cut over DNS

Your container is live immediately at https://web-api.<region>.x.on.bahriya.app. Test there first. When it checks out, add your production domain, verify it, and switch DNS across — see Custom Domain Setup. Keep the Heroku app running until traffic has drained to Bahriya, then scale its dynos to zero and decommission.