Migrating from Heroku to Bunnyshell: Complete Guide
GuideSeptember 29, 202613 min read

Migrating from Heroku to Bunnyshell: Complete Guide

The Short Version

A Heroku app is described by a small, familiar set of files and concepts: a Procfile defining process types, an app.json describing add-ons and Review App behavior, and Config Vars holding runtime configuration. None of that logic disappears when moving to Bunnyshell, it translates directly into a bunnyshell.yaml, one component per process type, environment variables in place of Config Vars, and real Database or Service components in place of add-ons. What changes is the platform underneath: Kubernetes on infrastructure you actually control, rather than Heroku's proprietary dyno runtime, with per-minute pricing instead of Heroku's per-dyno rates that compound quickly once an app grows past a couple of services.

Why Teams Migrate Off Heroku

Heroku's appeal was always its simplicity: push code, get a running app, no infrastructure to think about. That same simplicity becomes a real constraint once a team's needs grow past what a managed dyno runtime is built for. Three specific pressures show up most often.

Cost compounds fast with more than one or two services. Heroku bills per dyno: Eco at $5/month, Basic at $7/dyno, Standard-1X at $25/dyno, Performance-M at $250/dyno, before add-on costs for Postgres, Redis, or anything else attached. A team running twenty microservices on Standard-1X dynos alone is looking at roughly $500/month in compute, with add-ons on top, a bill that grows linearly with every new service regardless of how lightly loaded it actually is.

No Kubernetes access, ever. Heroku's dyno runtime is proprietary. There is no path from Heroku to running a Helm chart, applying a raw Kubernetes manifest, or using any tooling built around the Kubernetes ecosystem, which becomes a real blocker once a team's infrastructure needs grow past what buildpacks and add-ons cover.

Review Apps are genuinely useful, and genuinely limited. Heroku's disposable per-PR environments are one of its best features and one of the clearest signs a team already understands the value of ephemeral environments. They are still bound to Heroku's dyno pricing and Heroku's buildpack constraints, with no path to running the same pattern against infrastructure the team already owns.

None of this means Heroku was the wrong choice early on. It usually means the app outgrew the platform it started on, which is a normal and common trajectory, not a sign anything was done wrong.

Concept Mapping: Heroku to Bunnyshell

Heroku ConceptBunnyshell EquivalentNotes
AppEnvironmentAn app's running instance maps to an environment; multiple environments (staging, PR previews, production) can come from the same underlying template.
Dyno / Process type (web, worker)Component (Application or GenericComponent)Each line in a Procfile becomes its own component in bunnyshell.yaml.
Procfilecomponents section of bunnyshell.yamlSee Procfile to bunnyshell.yaml for the direct translation.
app.jsonbunnyshell.yaml + template variablesFormation, add-ons, and env vars described in app.json map to components, Database/Service components, and environmentVariables respectively.
Config VarsenvironmentVariables / SECRET[]Plain config vars become environmentVariables; anything sensitive uses SECRET[], encrypted at rest as ENCRYPTED[] once saved.
BuildpacksDockerfile (or DockerImage component)Bunnyshell builds from a Dockerfile rather than auto-detecting a buildpack, which trades some initial convenience for full control over the runtime image.
Add-ons (Heroku Postgres, Redis, Kafka)Database / Service components, or a BYOC-connected managed cloud databaseThe connection string an add-on injected as a config var becomes an exported variable from the equivalent component, referenced the same way.
Release phase (release: in Procfile)dependsOn-ordered deploy stepA migration or setup step that must complete before the app serves traffic, achieved through component ordering rather than a dedicated release-phase concept.
Review AppsEphemeral, per-PR environmentsSee Heroku Review Apps to Preview Environments for the direct migration path.
Pipelines (staging → production promotion)Environment cloning / template-based promotionPromoting a build through stages maps to deploying the same template against a different environment, rather than Heroku's built-in pipeline promotion UI.
Heroku CLIbns CLIEquivalent day-to-day operations: creating, deploying, and inspecting environments from the command line.

Component Types: How a Heroku App Runs in Bunnyshell

A typical Heroku app with a web process, a worker process, and a Postgres add-on translates into a bunnyshell.yaml with one component per process type plus a database component:

YAML
1kind: Environment
2name: my-heroku-app
3type: primary
4
5environmentVariables:
6  RAILS_ENV: production
7  DATABASE_URL: 'postgres://{{ env.vars.DB_USER }}:{{ env.vars.DB_PASS }}@{{ components.postgres.exported.HOST }}:5432/{{ env.vars.DB_NAME }}'
8
9components:
10  - kind: Database
11    name: postgres
12    dockerCompose:
13      environment:
14        POSTGRES_PASSWORD: SECRET['db-pass']
15    volumes:
16      - name: db-data
17        size: 10Gi
18        type: disk
19
20  - kind: Application
21    name: web
22    dependsOn: [release]
23    gitRepo: 'https://github.com/org/my-app.git'
24    dockerCompose:
25      build:
26        context: .
27    hosts:
28      - hostname: 'web-{{ env.base_domain }}'
29        servicePort: 3000
30
31  - kind: Application
32    name: worker
33    dependsOn: [release]
34    gitRepo: 'https://github.com/org/my-app.git'
35    dockerCompose:
36      build:
37        context: .
38        target: worker
39
40  - kind: GenericComponent
41    name: release
42    dependsOn: [postgres]
43    runnerImage: '@self'
44    dockerCompose:
45      build:
46        context: .
47    deploy:
48      - 'bundle exec rake db:migrate'

The release component here does exactly what Heroku's release: Procfile line does: it runs once per deploy, before the app-serving components, and both web and worker declare dependsOn: [release] so neither starts serving (or processing jobs) against a database that hasn't been migrated yet. This is the same guarantee Heroku's release phase provides, expressed through ordinary component dependency ordering instead of a dedicated platform feature.

Handling Add-Ons

Heroku add-ons inject their connection details as config vars automatically the moment they're provisioned. The Bunnyshell equivalent is either a self-hosted component running the same open-source software, or a BYOC connection to an existing managed database in your own cloud account, with the connection details wired the same way any other cross-component reference works:

YAML
1components:
2  - kind: Service
3    name: redis
4    dockerCompose:
5      image: redis:7-alpine
6
7  - kind: Application
8    name: worker
9    dockerCompose:
10      environment:
11        REDIS_URL: 'redis://{{ components.redis.name }}:6379'

For a team that specifically wants to keep using a managed database service rather than self-hosting, connecting an existing RDS, Cloud SQL, or equivalent instance and referencing its connection string through a plain environment variable works identically, since nothing about this pattern requires the database itself to run inside the cluster.

Migration Paths

Two concrete, common paths cover most Heroku migrations, and most teams end up walking through both, since they address different halves of a typical Heroku setup:

Procfile to bunnyshell.yaml covers translating your app's actual process types, web, worker, release, and any custom process types, into their component equivalents, including how Heroku's buildpack-based build gets replaced by an explicit Dockerfile.

Heroku Review Apps to Preview Environments covers migrating the specific PR-preview workflow app.json's review app configuration drives, onto Bunnyshell's ephemeral, per-PR environment pattern.

Running Both Platforms in Parallel During Migration

Bunnyshell operates entirely independently of Heroku, deploying to your own Kubernetes cluster rather than anything Heroku-managed, which means both platforms can serve real traffic simultaneously during a transition. A common, low-risk sequencing: migrate one process type or one service at a time, validate it against real traffic (or a subset of it, behind a load balancer split) running on Bunnyshell, and only decommission the corresponding Heroku dyno once that validation is complete, rather than cutting over the entire app in one step.

Migration Checklist

  • Every Procfile process type has a corresponding component in bunnyshell.yaml
  • Config Vars are migrated to environmentVariables, with anything sensitive using SECRET[]
  • A Dockerfile replaces the buildpack-based build, tested to produce a working image locally before relying on it in a deploy pipeline
  • Add-ons are replaced by equivalent components or BYOC-connected managed services, with connection strings wired through the same interpolation pattern
  • The release phase's migration logic runs as a dependsOn-ordered step before app-serving components start
  • Review Apps' PR-preview workflow has a corresponding ephemeral environment pattern in place before decommissioning the Heroku-side equivalent

What You Gain

Beyond replicating what Heroku already did, the migration typically unlocks capabilities Heroku's platform never offered: full Kubernetes access for anything buildpacks and add-ons couldn't cover, per-minute pricing that doesn't compound linearly with every new service the way per-dyno billing does, and a single declarative template that describes every environment (production, staging, every PR preview) consistently, rather than a combination of app.json, Heroku Pipelines configuration, and manual add-on provisioning spread across the Heroku dashboard.

Handling Heroku Pipelines: Staging to Production Promotion

Heroku Pipelines let a build move through staging and production via heroku pipelines:promote, which takes the exact slug that passed staging and deploys it to production, guaranteeing what ships to production is bit-for-bit what was already validated, not a fresh rebuild that could differ subtly. The Bunnyshell equivalent achieves the same guarantee by deploying the same built image, referenced by its exact tag, to a different environment rather than rebuilding:

Bash
1# Confirm the exact image that passed staging
2$ bns environments show --id <STAGING_ENV_ID> -o json | \
3    python3 -c "import json,sys; d=json.load(sys.stdin); print(d['components'][0]['image'])"
4registry.example.com/web:a1b2c3d
5
6# Deploy that exact image to production, not a fresh build
7$ bns environments update --id <PRODUCTION_ENV_ID> \
8    --set-image web=registry.example.com/web:a1b2c3d

This is a meaningful improvement over relying on identical source and hoping a rebuild produces the same result: referencing the exact same image tag makes the promotion guarantee explicit and verifiable rather than assumed.

A Realistic Migration Timeline

For a single-app Heroku setup with a web process, a worker, and a Postgres add-on, a full migration typically spans one to two weeks: a few days translating the Procfile and building a working Dockerfile, a few days validating the translated stack against real traffic in parallel with the still-running Heroku app, and a final cutover once validation is complete. A team with several Heroku apps, or a more complex formation with multiple add-ons and custom buildpacks, should expect proportionally more time, and benefits from migrating one app at a time rather than attempting all of them simultaneously, for the same reason a phased rollout beats a big-bang cutover in any migration: isolating where something goes wrong is far easier when only one piece changed at a time.

Next Steps

Start with the process-type mapping in Procfile to bunnyshell.yaml, since it's the foundational piece every other part of the migration builds on. From there, Heroku Review Apps to Preview Environments covers the PR-preview workflow specifically, which is usually the second piece teams want working before considering the migration complete.

✦✦

Move your Heroku app to infrastructure you actually control.

14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.

Frequently Asked Questions

Do I need to rewrite my application code to move off Heroku?

No. The application code itself is unaffected by this migration. What changes is how the app is built and run: Heroku's buildpacks are replaced by a Dockerfile, and the Procfile's process types become components in a bunnyshell.yaml, but the actual business logic in the repository stays the same.

What happens to my Heroku add-ons like Heroku Postgres or Redis?

They get replaced by either a managed database service in your own cloud account (RDS, Cloud SQL) or a self-hosted Database or Service component running the equivalent open-source software (PostgreSQL, Redis) inside the cluster, referenced by other components the same way a Heroku add-on's connection string was referenced before.

Is there a direct equivalent to Heroku Review Apps?

Yes, and it is one of the more compelling reasons teams migrate: Bunnyshell's per-PR ephemeral environments do what Review Apps do, disposable environments per pull request, but on your own Kubernetes cluster with your own compute pricing, rather than Heroku's per-dyno rates.

Can I migrate one Heroku app at a time, or does it need to happen all at once?

One app (or one process type within an app) at a time is the recommended approach. Since Bunnyshell runs independently of Heroku, both can serve traffic during a transition period, letting a team validate each migrated service before fully cutting over and decommissioning the Heroku app.

Does Heroku's release phase (running migrations before new dynos start) have an equivalent?

Yes, through dependsOn ordering: a GenericComponent or the application component's own deploy script running the migration before the application's own deploy step completes, which achieves the same guarantee Heroku's release phase provides, code only serves traffic after migrations against the current release have succeeded.