Heroku pioneered platform-as-a-service but has become increasingly expensive since Salesforce's acquisition. Basic dynos start at $7/month with 512 MB RAM, and a production stack with PostgreSQL, Redis, and a worker dyno easily exceeds $200/month. Arxentra provides equivalent functionality at 40-60% lower cost with better performance (dedicated vCPUs instead of shared), more storage, and included monitoring.
This guide walks you through migrating a Heroku application to Arxentra with zero downtime. The migration typically takes 2-4 hours for a standard web application with a PostgreSQL database.
Migration overview
| Step | Duration | Downtime |
| 1. Convert Procfile to Dockerfile | 15-30 min | None |
| 2. Replace Heroku add-ons | 10-20 min | None |
| 3. Deploy to Arxentra | 5-10 min | None |
| 4. Migrate PostgreSQL data | 30-60 min | None (read-only) |
| 5. Configure environment variables | 10-15 min | None |
| 6. DNS cutover | 5-10 min | 0-5 min (DNS propagation) |
| 7. Verify and decommission Heroku | 15-30 min | None |
| Total | 1.5-3 hours | 0-5 min |
Step 1: Convert Procfile to Dockerfile
Heroku uses Procfile to define process types. Arxentra uses Dockerfiles. Here are conversions for common stacks:
Node.js / Express / Next.js
Heroku Procfile:
web: npm start
worker: npm run worker
Arxentra Dockerfile:
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
USER node
EXPOSE 3000
CMD ["npm", "start"]
Python / Django / FastAPI
Heroku Procfile:
web: gunicorn myapp.wsgi --bind 0.0.0.0:$PORT
worker: celery -A myapp worker
Arxentra Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "myapp.wsgi", "--bind", "0.0.0.0:8000", "--workers", "4"]
Ruby / Rails
Heroku Procfile:
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq
Arxentra Dockerfile:
FROM ruby:3.3-slim
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install --without development test
COPY . .
RUN bundle exec rake assets:precompile
USER nobody
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Key differences from Heroku:
- Heroku automatically detects the port from `$PORT` environment variable. In Docker, you explicitly set the port in the `CMD` and `EXPOSE` directives.
- Heroku buildpacks install system dependencies automatically. In Docker, you install them explicitly with `apt-get`.
- Workers are deployed as separate services on Arxentra, each with their own Dockerfile (or a shared Dockerfile with different CMD).
Step 2: Replace Heroku add-ons
Heroku add-ons are replaced with Arxentra managed services or compatible alternatives:
| Heroku add-on | Arxentra replacement | Monthly cost comparison |
| Heroku Postgres (Mini) | Managed PostgreSQL Starter | $5 vs $7 |
| Heroku Postgres (Basic) | Managed PostgreSQL Starter | $9 vs $7 |
| Heroku Postgres (Standard 0) | Managed PostgreSQL Pro | $50 vs $15 |
| Heroku Postgres (Standard 2) | Managed PostgreSQL Pro | $200 vs $30 |
| Heroku Data for Redis (Mini) | Managed Redis Starter | $3 vs $5 |
| Heroku Data for Redis (Premium 0) | Managed Redis Pro | $15 vs $10 |
| Heroku Scheduler | Arxentra Cron Jobs | Free vs Included |
| Papertrail | Built-in log viewer | $7+ vs Included |
| New Relic APM | Built-in monitoring | $0 (limited) vs Included |
| Mailgun | Mailgun (direct) | Same pricing |
| Cloudinary | Cloudinary (direct) | Same pricing |
| Heroku SSL | Let's Encrypt (auto) | $20 vs Free |
For add-ons that are external SaaS services (Mailgun, Cloudinary, SendGrid), keep using them directly. You only need to update the connection details in your environment variables.
Step 3: Deploy to Arxentra
Create separate services for workers:
<h1>arxentra.yaml</h1>
services:
web:
build: ./Dockerfile
port: 3000
worker:
build: ./Dockerfile
command: npm run worker
databases:
postgres:
type: postgresql
plan: pro
redis:
type: redis
plan: starter
Step 4: Migrate PostgreSQL data
This is the most critical step. Follow this process for zero-downtime data migration:
4a. Export data from Heroku
<h1>Get Heroku database URL</h1>
heroku config:get DATABASE_URL -a your-app-name
<h1>Export database (schema + data)</h1>
pg_dump -Fc --no-owner --no-acl \
"$(heroku config:get DATABASE_URL -a your-app-name)" \
> heroku_backup.dump
<h1>Verify the dump file</h1>
pg_restore --list heroku_backup.dump | head -20
Important flags:
- `-Fc`: Custom format (compressed, supports parallel restore)
- `--no-owner`: Don't set ownership (Arxentra uses a different user)
- `--no-acl`: Don't restore access privileges (Arxentra manages permissions)
4b. Import data to Arxentra
<h1>Get Arxentra database connection string from dashboard</h1>
<h1>Or use: arx db connection-string postgres</h1>
<h1>Restore to Arxentra database</h1>
pg_restore --no-owner --no-acl --clean --if-exists \
-d "postgresql://user:password@host:5432/dbname" \
heroku_backup.dump
<h1>Verify row counts match</h1>
psql "postgresql://user:password@host:5432/dbname" \
-c "SELECT schemaname, relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC;"
4c. Handle sequences
After importing, PostgreSQL sequences may be out of sync. Reset them:
-- Reset all sequences to match current max values
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT
pg_get_serial_sequence(t.table_name, c.column_name) AS seq,
t.table_name,
c.column_name
FROM information_schema.tables t
JOIN information_schema.columns c ON t.table_name = c.table_name
WHERE t.table_schema = 'public'
AND c.column_default LIKE 'nextval%'
LOOP
EXECUTE format(
'SELECT setval(%L, COALESCE(MAX(%I), 1)) FROM %I',
r.seq, r.column_name, r.table_name
);
END LOOP;
END $$;
4d. For large databases (10 GB+)
For databases larger than 10 GB, use parallel dump and restore:
<h1>Parallel dump (4 jobs)</h1>
pg_dump -Fd --jobs=4 --no-owner --no-acl \
"$(heroku config:get DATABASE_URL)" \
-f heroku_backup_dir
<h1>Parallel restore (4 jobs)</h1>
pg_restore --no-owner --no-acl --clean --if-exists --jobs=4 \
-d "postgresql://user:password@host:5432/dbname" \
heroku_backup_dir
For databases over 50 GB, consider using `pg_dump` with `--section` to migrate schema and data separately, allowing you to create indexes after the data import completes.
Step 5: Configure environment variables
Export your Heroku environment variables and import them to Arxentra:
<h1>Export Heroku config vars</h1>
heroku config -a your-app-name --json > heroku_config.json
<h1>Or export as .env format</h1>
heroku config -a your-app-name --shell > .env.heroku
<h1>Import to Arxentra</h1>
arx env import .env.heroku
Variables that need updating:
| Variable | Heroku value | Arxentra value |
| DATABASE_URL | Heroku Postgres URL | Arxentra Postgres URL (auto-injected) |
| REDIS_URL | Heroku Redis URL | Arxentra Redis URL (auto-injected) |
| PORT | Set by Heroku | Set in Dockerfile (EXPOSE) |
| HOST | 0.0.0.0 | 0.0.0.0 (same) |
| APP_URL | your-app.herokuapp.com | yourdomain.com |
Remove Heroku-specific variables: `HEROKU_APP_NAME`, `HEROKU_DYNO_ID`, `HEROKU_SLUG_COMMIT`. These are injected by Heroku and are not needed on Arxentra.
Step 6: DNS cutover
This is the only step that can cause brief downtime (0-5 minutes of DNS propagation).
Pre-cutover checklist
Cutover process
<h1>1. Put Heroku app in maintenance mode</h1>
heroku maintenance:on -a your-app-name
<h1>2. Final database sync (capture any writes since initial migration)</h1>
pg_dump -Fc --no-owner --no-acl --data-only \
"$(heroku config:get DATABASE_URL -a your-app-name)" \
> final_sync.dump
pg_restore --no-owner --no-acl --data-only \
-d "postgresql://user:password@host:5432/dbname" \
final_sync.dump
<h1>3. Update DNS records</h1>
<h1>Remove: CNAME your-app.herokuapp.com</h1>
<h1>Add: CNAME your-service.arxentra.app (or A record)</h1>
<h1>4. Verify DNS propagation</h1>
dig yourdomain.com +short
<h1>5. Verify application works on new domain</h1>
curl -I https://yourdomain.com
<h1>6. Disable Heroku maintenance mode (as fallback)</h1>
<h1>heroku maintenance:off -a your-app-name</h1>
Zero-downtime cutover (advanced)
For zero-downtime cutover, run both platforms simultaneously:
This approach requires running two databases briefly but eliminates any possible downtime.
Step 7: Verify and decommission
After cutover, monitor the Arxentra deployment for 24-48 hours before decommissioning Heroku:
Once verified, decommission Heroku:
<h1>Scale down dynos (stop charges immediately)</h1>
heroku ps:scale web=0 worker=0 -a your-app-name
<h1>Remove add-ons (after confirming data is migrated)</h1>
heroku addons:destroy heroku-postgresql -a your-app-name --confirm your-app-name
heroku addons:destroy heroku-redis -a your-app-name --confirm your-app-name
<h1>Delete app (after 30-day observation period)</h1>
heroku apps:destroy your-app-name --confirm your-app-name
Cost comparison
A typical Heroku-to-Arxentra migration for a production SaaS application:
| Component | Heroku | Arxentra | Savings |
| Web dyno (Standard-1X, 512 MB) | $25/mo | $5/mo (1 GB) | 80% |
| Web dyno (Standard-2X, 1 GB) | $50/mo | $10/mo (2 GB) | 80% |
| Worker dyno (Standard-1X) | $25/mo | $5/mo | 80% |
| PostgreSQL (Standard 0) | $50/mo | $15/mo | 70% |
| Redis (Premium 0) | $15/mo | $10/mo | 33% |
| SSL | $0 (included) | $0 (included) | -- |
| Logging (Papertrail) | $7/mo | Included | 100% |
| Monitoring | $0 (limited) | Included | -- |
| Production stack total | $172/mo | $45/mo | 74% |
| Annual total | $2,064 | $540 | $1,524 saved |
The most significant savings come from compute: Heroku's Standard-1X dyno ($25/month for 512 MB RAM on shared CPU) is replaced by an Arxentra container ($5/month for 1 GB RAM on dedicated vCPU). You get 2x the memory and dedicated CPU for 80% less.
Frequently asked questions
Q: Can I migrate a Heroku pipeline (staging + production)?
Yes. Create two services on Arxentra: one connected to your `staging` branch and one connected to your `main` branch. Configure separate environment variables for each. Arxentra does not have a "pipeline" concept, but branch-based deployments achieve the same result.
Q: What about Heroku review apps?
Arxentra's preview deployments replace Heroku review apps. Every pull request gets a unique URL for testing. Preview deployments share the same database as staging by default, or you can configure isolated databases for each preview.
Q: My app uses Heroku's ephemeral filesystem. Will it work?
Arxentra containers also have ephemeral filesystems -- files written to disk are lost on restart. If your app writes temporary files (uploads, generated PDFs), this works the same way. If your app relies on persistent local storage, migrate those files to object storage.
Q: How do I handle Heroku buildpack-specific features?
Heroku buildpacks handle language-specific setup automatically. In Docker, you handle this explicitly. Common buildpack features and their Docker equivalents: (1) Node.js buildpack: use `node:20-slim` base image + `npm ci`. (2) Python buildpack: use `python:3.12-slim` base image + `pip install`. (3) Multi-buildpack: use multi-stage Docker builds.
Q: What about Heroku Postgres follower databases?
Arxentra managed PostgreSQL supports read replicas on Pro plans and above. Create a read replica from the database dashboard. Your application connects to the replica using a separate `DATABASE_REPLICA_URL` environment variable.
Q: Can I keep using the Heroku CLI during migration?
Yes. During the migration period, you can use both `heroku` and `arx` CLIs simultaneously. The Heroku CLI continues to manage your Heroku resources, and the Arxentra CLI manages your new deployment. After decommissioning Heroku, uninstall the Heroku CLI: `npm uninstall -g heroku`.
For more platform comparisons, see Arxentra vs AWS or Best AWS Alternatives for Startups.