Migrating from Heroku to Magic Containers

Posted by:

I’ve been a huge fan of Heroku since the early days. They were true pioneers of platform as a service, git push heroku master was magic when it first appeared, and they made building scalable web apps and services genuinely easy at a time when the alternative was wrestling with EC2 instances and shell scripts.

A lot of us built our first production apps on Heroku, and the developer experience they created shaped how an entire generation thinks about deployment.

On February 6, 2026, Heroku announced that it is entering a sustaining engineering model. No new features, no new enterprise contracts. If you’re starting to think about what comes next, Magic Containers offers a straightforward migration path.

If you’ve been building twelve-factor apps on Heroku, environment-based config, stateless processes, backing services as attached resources, you’ll find that most of those principles translate directly to containers. The deployment model is different, but the thinking is the same.

How Heroku concepts map to Magic Containers

If you're familiar with Heroku, here's how the terminology translates:

Heroku Magic Containers
App Application
Dyno (web/worker) Container
Buildpack Docker image
Add-on (e.g. Heroku Postgres) Additional container in the same app
Procfile Container image entrypoint
Config Vars Environment variables
heroku.yml / Dockerfile deploy Docker image from Docker Hub or GitHub
Dyno scaling Autoscaling (min/max instances per region)
Pipeline (staging/production) Separate applications per environment
Region (US/EU) 40+ regions worldwide

Key differences

  • No buildpacks, just Docker images: Heroku uses buildpacks to detect your language and build your app automatically. Magic Containers runs standard Docker images, giving you full control over your runtime, dependencies, and build process. You can deploy any public or private image from Docker Hub or GitHub Container Registry in any language or framework.
  • Multi-container composition with persistent storage: Heroku apps typically run as a single dyno, with databases provided as separate add-ons connected over the network. Magic Containers allows multiple containers within the same application that communicate over localhost. This lets you run your app alongside its database without an external hosted database service. Persistent volumes provide durable storage so database files, uploads, and application state survive redeployments and restarts.
  • Low-level networking: Heroku primarily provides HTTP routing in the US or the EU. Magic Containers supports TCP and UDP via global Anycast in addition to HTTP, enabling workloads such as DNS servers, game servers, VPN endpoints, or custom protocols.
  • No git push deploys: Instead of pushing code directly, you build a Docker image locally or in CI, push it to a registry, and select it in the Magic Containers dashboard. This fits naturally into GitHub Actions or any CI/CD pipeline.
  • Flexible autoscaling and provisioning: Heroku restricts autoscaling mainly to web dynos and higher-tier plans. Magic Containers autoscales by default and allows customization of scaling behavior and replica counts.

Migration steps

1. Containerize your app

If you already have a Dockerfile, you're ready. If not, create one for your app. Most frameworks have well-documented Docker setups.

Here's a minimal example for a Node.js app:

FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --production COPY . . EXPOSE 3000 CMD ["node", "server.js"]

On Heroku, your Procfile might define multiple process types like web and worker. With Docker, each process type becomes its own image (or the same image with a different command). For example, a worker that processes background jobs:

FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --production COPY . . CMD ["node", "worker.js"]

It’s also possible to use a single Dockerfile and override the command per container (common with Go), if that’s your thing. On Magic Containers, you'd add both as separate containers in the same application: the web container with a CDN endpoint, and the worker container with no endpoint. They share localhost, so your worker can connect to the same database and Redis instance as your web process.

2. Push your image to a registry

Build and push your image to Docker Hub or GitHub Container Registry:

docker build -t yourusername/myapp:latest . docker push yourusername/myapp:latest

Then connect your registry in the Magic Containers dashboard under Image Registries.

3. Create your application

In the Magic Containers dashboard, click Add App and choose your deployment strategy. For stateful apps with a database, choose Single Region. For stateless apps, Magic deployment will distribute your app globally.

4. Add your containers

Add your app container, selecting the image you just pushed. Set your environment variables. These are the same config vars you had in Heroku, such as DATABASE_URL, SECRET_KEY, and PORT.

If you were using Heroku Postgres, add a PostgreSQL container in the same application. Since containers in the same app share localhost, update your database connection to point to localhost instead of the Heroku Postgres hostname.

5. Expose your app

Add a CDN endpoint pointing to your app's container port. This gives you a public URL with automatic HTTPS, no need to configure SSL certificates. You can also use Anycast for non-HTTP protocols like TCP or WebSocket traffic.

6. Export and import your data

Export your Heroku Postgres database:

heroku pg:backups:capture --app your-app heroku pg:backups:download --app your-app

Then restore it into your new PostgreSQL container. If your new Postgres is accessible via an Anycast endpoint, you can connect directly with pg_restore or psql.

Example deployments

We have step-by-step guides for deploying popular languages, frameworks, and databases on Magic Containers. These include guides for building APIs with:

If you’re looking to get started with a popular web framework, you can host those too:

And databases, standalone or as sidecars to your container apps:

Each guide shows how to configure multi-container apps with databases, persistent volumes, and CDN endpoints.

You might not need a container

Not every Heroku app needs to become a container. bunny.net offers two other products that can replace parts of your stack with less overhead.

Edge Scripting is a serverless runtime for JavaScript and TypeScript that runs across bunny.net’s network. If your Heroku app is a lightweight API, a webhook handler, or middleware layer, Edge Scripting can replace it without a container at all. It also works as DNS middleware, letting you intercept and modify requests at the edge before they reach your origin. There’s no infrastructure to manage, or Dynos to scale.

Bunny Database is a globally distributed, SQLite-compatible database. If you were using Heroku Postgres through a third-party add-on, or your app doesn’t need the full power of PostgreSQL, Bunny Database is a simpler alternative that runs close to your code across bunny.net’s network.

Getting started

Magic Containers is designed to be the kind of platform Heroku was at its best: simple to deploy to, with none of the complexity you don’t need. Full flexibility of Docker and a global edge network.

You bring a container image, set your environment variables, attach storage where you need it, and you’re running. No buildpack debugging, no add-on marketplace, no dyno sleep.

We’d love to see what you’re building. If you’re mid-migration, just getting started, or want to swap notes with others making the same move, come join us on Discord.