DeploymentAugust 7, 202613 min read

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.

RThe Runsite Team

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 HerokuWhat it really isWhat it becomes elsewhere
DynoA process running your code, sized by a plan nameA container sized by CPU and memory. The naming goes away and you pick numbers instead
BuildpackA build recipe chosen by inspecting your repositoryFramework auto-detection on platforms that offer it, or a Dockerfile you write once and own
`Procfile` and process typesA declaration of which commands to run and what to call themOne configured service per line: a web service, a background worker, a step in your deploy sequence
Add-on marketplaceA catalogue of third-party services, provisioned and billed through HerokuNothing. No other platform has this. Each add-on becomes its own decision
Heroku PostgresA managed PostgreSQL instance with backups and a connection stringManaged PostgreSQL from your new platform or a separate provider
Heroku RedisA managed Redis instance, usually a cache or a job queueManaged Redis, with a decision to make about persistence you may never have made before
Heroku SchedulerA cron-like runner for periodic commandsA scheduled job, with its own deploy lifecycle to think about
Review appsA running instance per pull requestPreview environments, which most modern platforms have and some charge for
Config varsEnvironment variables injected at start timeEnvironment variables injected at start time. This part travels unchanged
The migration map. Most rows are a straight swap; the add-on marketplace is the row that turns into a project.

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.

ShapeExamplesWhat you pay withWho fixes it at 3am
Managed EU PaaSClever Cloud, Scalingo, Upsun, RunsiteMoney, and some flexibilityThe provider. You get an incident page and a support ticket
Self-hosted PaaS on European infrastructureCoolify, Dokploy, Dokku or CapRover on Hetzner, Scaleway, OVHcloud, Exoscale or UpCloudYour time, on a schedule you don't controlYou. The PaaS layer is now yours to patch, back up and upgrade
Plain EU servers and your own pipelineA VPS and CI of your choosingYour time and your risk toleranceYou, with nothing between the incident and your terminal
Three ways to run an application in Europe. Named platforms are examples of each shape as of writing, not a shortlist or a ranking.

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.

bash
# 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 dashboard

The 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.

yaml
# 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 convention

Where 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.

FAQ

Frequently Asked Questions

Common questions about this service.

There isn't a single one, and the useful answer depends on how much you want to operate yourself. Managed European platforms in the same category include Clever Cloud, Scalingo, Upsun and Runsite, where the provider runs the platform and you deploy to it. A self-hosted PaaS such as Coolify, Dokploy, Dokku or CapRover on European infrastructure from Hetzner, Scaleway, OVHcloud, Exoscale or UpCloud gives the same push-to-deploy workflow for a lower bill, in exchange for you patching, backing up and upgrading the platform layer. Plain servers with your own pipeline is the third option. Before comparing features, check what each provider means by European: servers in an EU region is the easiest claim to satisfy and says nothing about which law the operating company is subject to.

Several platforms still offer a free tier, and the thing to read is what happens when the app is idle rather than the headline. Some suspend the container after a period of inactivity, which means the next visitor waits for it to start again; some cap monthly hours; some restrict the free tier to static output rather than a running server. Free managed databases are usually the tighter constraint, since they tend to carry size limits, time limits or both. Runsite's free tier covers one web service at 0.1 vCPU and 256 MB with auto-deploy from Git and free SSL, and it doesn't cold-start while the application is active, sleeping only after 14 days idle. For anything you'd be upset to lose, read the data retention terms before the performance ones.

Work through the bundle piece by piece. The dyno becomes a container sized by CPU and memory; the buildpack becomes framework auto-detection or a Dockerfile you own; each Procfile line becomes a service you configure explicitly, with the release line being the one that runs your migrations; Heroku Postgres and Heroku Redis become managed instances from your new platform or a separate provider; Heroku Scheduler becomes a scheduled job; review apps become preview environments; and config vars carry over unchanged. The row without an equivalent anywhere is the add-on marketplace, so every add-on you use becomes its own provider, contract and bill. The length of that add-on list, rather than the size of your codebase, is what determines how long the migration takes.

No. An EU region determines where bytes are written, which helps with latency and is a reasonable thing to require, but it says nothing about which government can compel the company that operates the service. A US-incorporated provider running servers in Frankfurt remains subject to US law. Two further checks separate a genuine European provider from a European region: which legal entity you are contracting with and where it is registered, which appears in the imprint, terms or DPA rather than on the pricing page, and who the subprocessors are and where they sit, which a provider taking GDPR seriously publishes as a list. Note also that Europe is wider than the EU: the UK and Switzerland sit outside it under separate adequacy arrangements whose status is worth checking on the day rather than assuming.

Your app deserves to be online

Free to start. Deploy in under a minute. No credit card needed.