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.
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
| Heroku | Bahriya |
|---|---|
Web process (web: in the Procfile) | An HTTP container |
| Worker / background process | A worker container |
| Scheduler job | A cron job |
| Buildpack build | A Dockerfile producing an image |
| Config Vars | Environment variables and secrets |
$PORT | The port you declare with --port |
| Dyno formation / scaling | Replicas and autoscaling |
| Heroku Postgres add-on | An external database, connection string held as a secret |
| Heroku Data for Redis / Memcached | Bahriya Memcached, or an external cache via a secret |
| App domains + ACM | Custom 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.02. 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.
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-base7. 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.