The European Heroku Alternative: What to Look for When You Migrate
What you're really replacing when you leave Heroku, why "European" has three different meanings, and which parts of a migration actually break.
Either the bill went up, or somebody filling in a security questionnaire asked where the data physically sits. Whichever it was, you have ended up reading a list of Heroku alternatives, and the lists all have the same shape: ten platforms, numbered, with the author's own product at number one.
Those lists answer a question you don't have. You aren't picking your first platform. You have a running application with a database attached to it, a Redis add-on, a nightly job, and a review-app flow your team has got used to, and what you need is to know what happens to each of those pieces. If the reason you're moving is European, there's a second thing you need, because that word means at least three different things and only one of them is easy to verify.
It helps to know why there are so many of these lists in the first place. Heroku retired its free plans in November 2022, which pushed a generation of side projects and student apps off the platform within a few weeks and produced most of the comparison posts still ranking today. If your application has been on a paid plan the whole time, that history doesn't apply to you, but it does explain why so much of the available advice is written for hobby projects. Free tiers elsewhere carry their own conditions, and the condition to read first is what happens when the app goes idle, which is its own subject in what cold starts are and why free-tier apps fall asleep.
So this is the other half of the comparison: what you are actually replacing, how to test a claim of being European, and which parts of the move genuinely break.
What you're actually replacing when you leave Heroku
Heroku's product was never the dyno. It was the bundle: a runtime, a build system that worked out what your application was without being told, a marketplace where a database was one click and one line on the same invoice, a scheduler, and a review-app flow. Leaving replaces one bundle with a set of separate decisions, and it's the number of decisions rather than the difficulty of any one of them that people underestimate.
| What you have on Heroku | What it really is | What it becomes elsewhere |
|---|---|---|
| Dyno | A process running your code, sized by a plan name | A container sized by CPU and memory. The naming goes away and you pick numbers instead |
| Buildpack | A build recipe chosen by inspecting your repository | Framework auto-detection on platforms that offer it, or a Dockerfile you write once and own |
| `Procfile` and process types | A declaration of which commands to run and what to call them | One configured service per line: a web service, a background worker, a step in your deploy sequence |
| Add-on marketplace | A catalogue of third-party services, provisioned and billed through Heroku | Nothing. No other platform has this. Each add-on becomes its own decision |
| Heroku Postgres | A managed PostgreSQL instance with backups and a connection string | Managed PostgreSQL from your new platform or a separate provider |
| Heroku Redis | A managed Redis instance, usually a cache or a job queue | Managed Redis, with a decision to make about persistence you may never have made before |
| Heroku Scheduler | A cron-like runner for periodic commands | A scheduled job, with its own deploy lifecycle to think about |
| Review apps | A running instance per pull request | Preview environments, which most modern platforms have and some charge for |
| Config vars | Environment variables injected at start time | Environment variables injected at start time. This part travels unchanged |
Read the table as one claim: everything except the marketplace is a swap, and the marketplace is the work. A dyno is a container, a buildpack is a build recipe you now name explicitly, a `Procfile` line is a service you configure, and `DATABASE_URL` is still `DATABASE_URL`. What has no equivalent anywhere is the catalogue: the mail sender, the log drain, the error tracker, the search index and the metrics dashboard that were each one click and one line on a single invoice become separate providers, each with its own contract and its own bill. Count them before you start, because that count, rather than the size of your codebase, is what determines how long the migration takes.
The upside nobody mentions is that most teams find add-ons in that list they stopped using two years ago and kept paying for, because cancelling one was never urgent enough to schedule.
"European" means three different things
Every platform selling to European developers now says it is European, and the claim is doing three separate jobs at once. They are not equally hard to satisfy, and vendors tend to satisfy the easy one and let you assume the others.
1. The servers are in the EU
The weakest of the three, because any provider on earth can open a Frankfurt region. Heroku has offered an EU region in its Common Runtime for years, as of writing, and so do AWS, Google Cloud and Azure. If a region were sufficient, nobody would need this article. What a region buys you is latency and a defensible answer about where bytes are written; what it does not touch is who the operating company answers to, which is the subject of the next two.
It's still worth confirming rather than assuming, and confirming means the whole stack rather than the application container. Build artifacts, logs, backups and metrics are data too, and they are the parts most likely to sit somewhere else by default. The reasoning behind treating them the same way as the database is set out in EU hosting for developers.
2. The operating company is incorporated in the EU
This is the one that changes the legal picture, because it decides which government can compel the company and under which law. A US-incorporated provider running EU servers is subject to US law regardless of where the disks are, and the instability that follows from that is the subject of Schrems II and data transfers. Heroku is a Salesforce product, and Salesforce is a US company, which is a plain fact about corporate structure rather than a judgement about the platform.
Europe is also not the same shape as the EU, and this catches out several lists. The United Kingdom left the EU in 2020 and now sits under a separate adequacy arrangement that has been adopted and extended rather than made permanent, so a UK-incorporated platform is European in the everyday sense and third-country in the regulatory one. Switzerland is outside the EU too, with its own adequacy decision and its own data protection act. Neither of those is a reason to rule a provider out, and both are a reason to know which column you are in before an auditor asks. Check the company registration on the vendor's own imprint or terms rather than inferring it from the region list, and check the adequacy status on the day you read this rather than trusting a blog post, including this one.
3. The control plane, the support desk and the subprocessors are in the EU
The quietest of the three and the one that most often fails. Your containers can run in Frankfurt while the dashboard that manages them, the support engineer who can read your logs during a ticket, the error tracker, the analytics on the marketing site and the payment processor all sit elsewhere. Each of those is a subprocessor, and a provider that takes GDPR seriously publishes the list, which is why the list is the fastest way to test the claim. A short list with EU names is a good sign. A long list of US SaaS tools with a Frankfurt region at the bottom tells you what kind of European you are dealing with.
Three questions, and where the answers actually live
Which legal entity am I contracting with, and where is it registered? Look at the imprint, the terms of service or the DPA, not the pricing page. Where do build artifacts, logs and backups live, not just the application? Look at the documentation, or open a ticket and keep the reply. Who are your subprocessors and where are they? Look for a published list; if there isn't one, that is itself the answer.
Latency, incidentally, is the axis where the answer is simplest and the least contested: distance is physics, and a European application serving European users is faster from a European region whoever owns it. That argument, along with the cost of crossing a border, is worked through in EU vs US hosting, so it isn't repeated here.
The European options, sorted by shape rather than by rank
Ranking platforms is what the lists already do, and it ages badly. Sorting them by shape ages better, because the shape decides what you're signing up to operate. There are three, and the axis that separates them is who gets paged when something breaks at three in the morning.
| Shape | Examples | What you pay with | Who fixes it at 3am |
|---|---|---|---|
| Managed EU PaaS | Clever Cloud, Scalingo, Upsun, Runsite | Money, and some flexibility | The provider. You get an incident page and a support ticket |
| Self-hosted PaaS on European infrastructure | Coolify, Dokploy, Dokku or CapRover on Hetzner, Scaleway, OVHcloud, Exoscale or UpCloud | Your time, on a schedule you don't control | You. The PaaS layer is now yours to patch, back up and upgrade |
| Plain EU servers and your own pipeline | A VPS and CI of your choosing | Your time and your risk tolerance | You, with nothing between the incident and your terminal |
The middle row is the one that tempts developers who have just read a bill, because a self-hosted PaaS on a €5 server does look like a fully featured platform for the price of a coffee. It is a genuinely good option, and the cost it moves rather than removes is operational: the machine, the PaaS layer running on it, the database on it and the backups of that database are all now on your maintenance list. That trade is worked through in full in what you take on with Coolify and a self-hosted PaaS. The bottom row removes even the PaaS layer, which is a different calculation again, and mostly a question of how much of a deploy pipeline you want to maintain by hand after reading what a deploy pipeline actually does.
One thing all three rows share: none of them is Heroku, so none of them will feel like Heroku on the first day. The bundle is the thing you're giving up, and no single replacement rebuilds it.
What actually breaks in the migration
The code usually moves without complaint. What breaks is the set of things Heroku was quietly doing on your behalf, and each of them fails in a way that looks like a bug in your application.
The variables you never set yourself
Heroku populates a handful of environment variables for you, and the first deploy elsewhere is where you find out which ones your code assumed. `PORT` is the usual first failure: the platform assigns it, your application binds to it, and an application that binds a hardcoded port will start, look healthy in its own logs and never receive a request. `DATABASE_URL` and `REDIS_URL` appeared when you attached the add-ons, so on the new platform they are yours to set and yours to keep in sync when the database moves.
# Heroku set these. Nothing on the other side will.
PORT=... # assigned by the platform; read it, never hardcode it
DATABASE_URL=... # appeared when you attached Heroku Postgres
REDIS_URL=... # appeared when you attached Heroku Redis
# Worth grepping for before you move:
# any literal port number in your server bootstrap
# any add-on variable you didn't set by hand
# anything reading a *_URL you can't point at in your own dashboardThe Procfile becomes configuration
A `Procfile` is a short file with a lot of convention behind it: Heroku knows that the process type named `web` is the one that gets HTTP traffic, that `release` runs before the new version goes live, and that everything else is a worker. Elsewhere, each line turns into something you configure explicitly, and the `release` line is the one to look at hardest, because that is usually where your migrations run.
# Heroku: three lines, and the platform knows what they mean.
web: gunicorn app.wsgi
worker: celery -A app worker
release: python manage.py migrate
# Elsewhere: three separate things you set up.
# web -> a web service with a health check path
# worker -> a background service with its own deploy lifecycle
# release -> a step you place deliberately in the deploy sequence,
# because nothing runs it for you by conventionWhere that `release` step ends up matters more than it looks, for reasons that have nothing to do with Heroku: during a rolling release two versions of your application talk to the same database, so a migration has to be workable for the outgoing code as well as the incoming code. That constraint, and the expand-and-contract pattern that satisfies it, is covered in git push to deploy. It's the same on both sides of the migration, which is exactly why moving is a good moment to check you were getting it right.
The build stops guessing
Buildpacks inspect a repository and infer the rest, which is pleasant right up to the point where you don't know what was inferred. Some platforms detect frameworks the same way and you carry on as before. Some hand you a Dockerfile, which means every implicit choice becomes an explicit line: the runtime version, the system packages someone added through a custom buildpack in 2021, the asset compilation step that ran because a file happened to exist. This is more control and more homework, and the homework is front-loaded. It's also worth knowing that the buildpack idea outlived Heroku's implementation of it as Cloud Native Buildpacks, a CNCF project, so "no buildpacks" and "a Dockerfile forever" are not the same statement.
The bill changes shape, which makes price comparison misleading
Heroku bills dyno hours against a plan name. Most replacements bill a container by the CPU and memory you allocate it. Those are different units, so putting two monthly figures side by side compares nothing useful until you know what your application actually consumes, and most teams find they were paying for a plan tier chosen years ago rather than for the resources in use. Get the memory and CPU numbers off your current metrics first, then price against those.
Price the workload, not the plan name
A dyno plan and a container size are different units of account, so comparing the two monthly figures tells you very little. Take the actual CPU and memory your application uses from your existing metrics, price a container that size on each candidate, then add every add-on you're replacing as its own line. That total is the comparable number, and it is often lower than expected on both sides for the same reason: the old plan was sized by guesswork.
And the parts that don't break
The container filesystem behaves identically, which is to say anything your application wrote next to itself still disappears when the release retires the old container. That was already true on Heroku, so nothing gets worse. Rolling releases and health-gated traffic work the way they always did, and a rollback still depends on whether the platform kept the previous artifact rather than on which platform you're using. Your `DATABASE_URL` convention survives, and so does every twelve-factor habit the Heroku era taught you. That's the pleasant irony of this migration: the platform that popularised those habits left your application portable.
The order to move things in
The sequence that goes wrong is the one that starts with the database, because the database is the piece that can't be run twice. Start with the stateless things instead. Get the application building and running on the new platform against the old database, if your network rules allow it, or against a copy, and leave production traffic exactly where it is. That first deploy is where you find the hardcoded port, the missing environment variable and the buildpack that was doing something you didn't know about, and finding them costs nothing while nobody is watching.
Add-ons come next, one at a time, because each is an independent decision and independent decisions are safest made in isolation. Background jobs and scheduled tasks follow, with one thing to watch: a scheduler running on both platforms at once will send two of every email. Turn the old one off in the same change that turns the new one on. The database goes last and is the only step with a real cutover, which means a brief window where writes stop, a final replication or dump, and a switch of the connection string. Everything before that point is reversible by leaving the old platform running, and the day you delete the Heroku app should be at least a week after the day you stop using it.
That's the shape of it rather than the commands; the step-by-step for moving a specific application onto Runsite lives in the Runsite docs.
When staying on Heroku is the right answer
A migration is real work, and there are cases where the honest recommendation is not to do it:
- You're deep in the Salesforce ecosystem. If Heroku Connect is synchronising objects into your database, the platform has become part of your integration architecture rather than a place your application happens to run, and replacing it is a different project with a different budget.
- Your add-on list is long and load-bearing. Ten add-ons is ten migrations. If they're all in daily use, price the total effort honestly before comparing monthly bills.
- The bill is small. A hobby project on a low tier is not where infrastructure time pays for itself. Move it when it becomes something you'd be upset to lose.
- Nobody owns the move. A half-finished migration, with a database on one platform and workers on another, is worse than either endpoint. If there's no week for it in the next quarter, wait for the week.
The one case where waiting is expensive is a compliance requirement with a date attached, because the DPA and the subprocessor answers are needed before the deal closes rather than after, and no amount of engineering speed compresses a procurement review.
How Runsite reads on these axes
For a concrete answer against the three tests above: Runsite runs in Frankfurt, Germany, and the containers, environment variables, build artifacts and logs stay inside the EU rather than the application alone. A signed GDPR Data Processing Agreement comes with every plan, including the free one, instead of being an enterprise upgrade. Those are the first and third tests; the second is a question about the operating entity, and the right place to check it is the imprint and the DPA, exactly as recommended for any other vendor on this page.
On the migration map: connect a repository on GitHub, GitLab or Bitbucket and a webhook builds on every push to your branch. The framework is detected automatically for Next.js, Django, Rails, FastAPI and others, which is the closest thing to a buildpack, and your own Dockerfile is used instead when you supply one. Any language that runs in a container works, so Node.js, Python, Go, Ruby, Rust, Java, PHP and .NET all deploy the same way. Releases are rolling with a configurable health check gating traffic, and a failed check returns to the previous healthy version rather than putting a broken build in front of users. Every pull request gets its own environment on its own URL with no extra configuration, which is the review-app row. Rollbacks and deploy history are on the Pro plan (€25/mo as of writing). A deploy takes around 30 seconds, with the full clone, build and release cycle usually under a minute, and HTTPS is automatic on the runsite.app subdomain or your own domain.
For the add-on rows, the pieces are separate services on the same account and the same invoice: managed PostgreSQL hosted in the EU from €5/mo, managed Redis for caches and job queues from €5/mo, scheduled jobs with their own deploy lifecycle, object storage with no egress charges, and transactional email with 3,000 messages a month included. That is not a marketplace and doesn't pretend to be one; anything outside that list stays a decision you make yourself, per the table at the top. The free tier is one web service at 0.1 vCPU and 256 MB with auto-deploy and SSL, and it doesn't cold-start while the app is active, which makes it a reasonable place to run the first test deploy described above. Details are on the EU-hosted platform that builds and releases on every push.
The short version
You're replacing a bundle rather than a host. Every row of that bundle except the add-on marketplace is a straight swap, and the marketplace is where the work actually lives, so count your add-ons before you estimate anything. Then test the word European against three separate claims: servers in the EU, which any provider can satisfy; incorporation, which decides whose law applies; and the subprocessor list, which is where the claim usually comes apart. That test run against two named US platforms, both of which offer an EU region, is in Render versus Railway. Sort your candidates by shape rather than by rank, because managed, self-hosted and do-it-yourself differ mainly in who is awake at three in the morning. And expect the platform's invisible favours to break first. `PORT`, the add-on variables, the `Procfile` conventions. The container filesystem and your migrations behave exactly as they did before.
The cheapest way to find out what your own migration involves is to run it once, badly, with nothing at stake: point a copy of the application at a European platform that deploys straight from Git, deploy it, and read what fails. That list is your actual migration plan, and it takes an afternoon to produce.