How to host a Laravel app EU-sovereign

A practical guide to running Laravel on European clouds with European owners, in accounts you own, from the first deploy to the services around your app.

In this post
  1. What does EU-sovereign hosting mean?
  2. Everything that touches your data
  3. The setup in this tutorial
  4. 1. Make the app ready for serverless
  5. 2. Open your own provider accounts
  6. 3. Connect the project
  7. 4. Pick the region
  8. 5. Keep the secrets in your own account
  9. 6. Deploy
  10. 7. Put bunny.net in front
  11. 8. Check the services around your app
  12. What you cannot promise
  13. Leaving again
  14. Summary

More Laravel teams are being asked the same question by their clients, their legal department or their own conscience: where does our app actually run, and who could be made to hand over its data? "We host in Frankfurt" used to be the answer. It no longer is.

This tutorial walks through what EU-sovereign hosting means for a Laravel app, which parts of your stack it touches, and how to set it up step by step: a serverless Laravel app on Scaleway, with bunny.net in front of it, in accounts you own, deployed with Brewless.

What does EU-sovereign hosting mean?

EU-sovereign hosting means your app and its data run in the EU, at a provider owned and headquartered in the EU, under a contract in your own name. Only then does no law from outside the EU reach the data.

That comes down to three questions, and a European data centre only answers the first, and a European data centre only answers the first:

  1. Where is the data? In a data centre in the EU.
  2. Who owns the provider? A company with its headquarters in the EU, under EU law.
  3. Who holds the contract? You, directly with that provider, so nobody in between decides where your app goes.

The second question is the one that matters most. The US CLOUD Act lets US authorities require a US company to hand over data it controls, wherever in the world that data is stored. An EU region of an American cloud is in the EU, but it is still run by a company that has to answer that request. A European provider does not.

So for a Laravel app to be EU-sovereign, every part that holds or sees its data needs a European owner, not just a European address.

Everything that touches your data

Go through your app and list where data lives or passes. For a typical Laravel app:

Part What it sees What to look for
Web runtime Every request and response A European cloud
Database Everything Same provider, same region
File storage Uploads, exports, invoices S3-compatible storage at a European provider
Queue Job payloads, often with personal data Same provider
Secrets APP_KEY, database passwords, API keys A secret store in your own account
Builds Your source code and .env during the build Built inside your own account, not on a third party's build server
CDN, DNS, firewall Every visitor's request A European edge provider
Your deploy tool Your code, your secrets, access to your cloud Ideally nothing: it should not sit in the request path or keep your secrets
Services your app calls Mail, payments, error tracking, analytics European alternatives, or a conscious choice

The last two rows are the ones people forget. A sovereign server that ships every exception, with a request body attached, to an American error tracker is not sovereign.

The setup in this tutorial

  • Scaleway (Paris, France) runs the app: Serverless Containers for the web, the queue worker and the scheduler, a database, Object Storage for files, Queues for jobs and Secret Manager for the environment variables.
  • bunny.net (Ljubljana, Slovenia) sits in front of it: DNS, CDN, TLS certificates and Shield, its firewall.
  • Brewless (Meppel, the Netherlands) builds and deploys the app into those two accounts. It is the control plane only: visitor traffic goes from bunny.net straight to your containers, the build runs inside your Scaleway project, and your secrets stay in your own Secret Manager.

Both providers bill you directly. If you stop using Brewless, the app keeps running where it is.

Note

The containers are serverless: they start when requests come in and scale back to zero when it is quiet. That is cheap, and it changes a few things in how Laravel should store sessions and files. The next section covers that.

1. Make the app ready for serverless

A serverless container keeps nothing on its own disk between requests, and two copies of your app do not share one. Three settings take care of that.

Sessions and cache in the database. That is Laravel's default since version 11. If your .env still says file, change it:

.env.example
SESSION_DRIVER=database
CACHE_STORE=database

Make sure the migrations for the sessions and cache tables exist (php artisan make:session-table and php artisan make:cache-table on an older app).

Files on the s3 disk. Scaleway Object Storage speaks the S3 API, so Laravel's own s3 disk works as it is. Anything you store through Storage:: goes there once FILESYSTEM_DISK=s3. Brewless sets that variable and the AWS_* ones when it creates the bucket.

composer require league/flysystem-aws-s3-v3 "^3.0"

Never rely on a file written in an earlier request. Temporary files within one request are fine. Anything that has to outlive it goes to the bucket, the database or the cache.

Then add the package that lets the containers take release commands, the queue and the scheduler:

composer require brewless/laravel

2. Open your own provider accounts

Create a Scaleway account and a project for the app. Brewless needs an API key that can reach that one project and nothing else:

  1. In the Scaleway console, under IAM, create an application, for example brewless.
  2. Give it a policy limited to your project, with access to the services in the Scaleway permissions list.
  3. Create an API key for that application.

For bunny.net, create a separate account for what Brewless manages: bunny.net has one API key per account with full access, so a dedicated account keeps your other zones out of reach.

Paste both keys on the Providers page of your Brewless organisation. Each service is checked with a read-only call before anything is saved.

3. Connect the project

Install the command line client and sign in. acme is the name of your organisation, as in acme.brewless.eu:

composer global require brewless/cli
brewless login acme

In the root of your Laravel project:

brewless init

init reads your composer.json, creates the application at Brewless, and asks which environments you want, in which region, with which database, where jobs should wait and whether you need a bucket. After one yes, it creates all of that in your Scaleway project. It writes a brewless.yml that you commit; it holds no secrets.

Coming from Laravel Vapor? With a vapor.yml in the project, brewless init --from-vapor writes the brewless.yml that matches it.

4. Pick the region

The region is one line per environment. Your containers, database, queue and bucket all go there:

brewless.yml
organisation: acme
application: shop
framework: laravel
php: "8.4"

environments:
  production:
    kind: production
    region: fr-par
    database: shop-main
    queue: sqs
    storage: shop-files
    web:
      min: 0
      max: 10
    domains:
      - shop.example.eu

The regions are Paris (fr-par), Amsterdam (nl-ams), Warsaw (pl-waw) and Milan (it-mil). Paris has the most: a serverless database, which sleeps when nobody asks, is only available there so far. In the other regions you use Managed PostgreSQL, and Queues are not in Warsaw or Milan yet.

Run brewless plan production to see what the file asks for next to what stands. It changes nothing.

5. Keep the secrets in your own account

Put the environment's variables in .env.production and push them:

brewless env:push production

They go into Scaleway Secret Manager in your own project. Brewless keeps the names and revisions of your variables, never their values.

Warning

Never commit .env.production, and never put a secret under variables in brewless.yml. Brewless refuses a name there that looks like one, such as anything with KEY, SECRET, PASSWORD or TOKEN in it.

6. Deploy

brewless deploy production

Only what is committed is deployed. The code goes from your machine to your own Scaleway project and is built there, by a job in that project. The migrations run as their own step; if one fails, the previous release stays live. The client ends with the address of the release.

7. Put bunny.net in front

The domain under domains in brewless.yml is added with the deploy, after a yes. You can also add one by hand:

brewless domain:add production shop.example.eu

With bunny.net connected and your DNS at Bunny DNS, the record is made for you. Your domain then points at a bunny.net pull zone, which caches assets close to visitors, takes care of the certificate and runs Shield in front of your containers. In bunny.net you can also limit which edge locations serve your zone to Europe.

8. Check the services around your app

Your hosting is now European. What your app talks to may not be. Go through your .env and config/services.php and list every outside service:

  • Mail. Transactional mail contains names, addresses and often more. There are EU-based providers; Brewless itself sends its mail through Lettermint.
  • Payments. Mollie (the Netherlands) is the obvious choice for Laravel apps in Europe, and has a well-maintained PHP client.
  • Error tracking and logs. Exceptions carry request data. Use an EU-hosted service from a European company, or a self-hosted one inside your Scaleway project.
  • Analytics. Pick a cookieless, EU-hosted tool, or none.
  • Fonts and scripts from CDNs. Every visitor's IP address goes to whoever serves them. Self-host them through Vite.

What you cannot promise

Be honest with yourself, and with whoever asked for this:

  • Sovereignty is as strong as the weakest link. One American service in the list above, and that data leaves the EU.
  • bunny.net answers DNS lookups from its worldwide network, and uses a few US-based tools for its own support and email. Those see your account details, not your visitors' traffic.
  • Your team's own tools (source hosting, CI, chat, the laptop of a developer abroad) are a separate question from where the app runs.

Written down like this, it is a clear answer to the question you were asked: where the app runs, who owns each provider, and the exceptions you know about.

Leaving again

Sovereignty also means you can walk away. Everything above runs in your own accounts and is billed to you by Scaleway and bunny.net. Cancel Brewless and the containers, database, files, queues, secrets and DNS keep running; you stop paying for deploys, rollbacks and the dashboard. Leaving Brewless describes the export and how to deploy without it.

Summary

  1. Store sessions, cache and files outside the container: database, cache and the s3 disk.
  2. Open your own Scaleway project and a separate bunny.net account; hand Brewless a key limited to that project.
  3. brewless init, pick a region, brewless env:push, brewless deploy.
  4. Put bunny.net in front, and limit its edge to Europe if you want.
  5. Replace or consciously accept every outside service your app calls.

The quickstart has every step in more detail.

Frequently asked questions

Is hosting Laravel in an EU region of AWS, Azure or Google Cloud EU-sovereign?

No. The data is in the EU, but the provider is an American company, and the US CLOUD Act lets US authorities require it to hand over data it controls, wherever that data is stored. Sovereign hosting needs a provider with European owners as well.

Which European clouds can run a Laravel app?

Scaleway (France) runs containers, databases, object storage, queues and a secret store in Paris, Amsterdam, Warsaw and Milan. bunny.net (Slovenia) provides DNS, a CDN, TLS and a firewall in front of it. Brewless deploys Laravel to both, in your own accounts.

Does Laravel need changes to run serverless?

Only in where it keeps state. Store sessions and cache in the database or Redis, put files on the s3 disk, and never rely on a file written in an earlier request. Queues, the scheduler and migrations keep working as they are.

Does Brewless see my visitors' traffic or my secrets?

No. Visitor traffic goes from bunny.net straight to your containers, your code is built inside your own Scaleway project, and your environment variables live in your own Secret Manager. Brewless keeps their names and revisions, not their values.

What happens to my app if I stop using Brewless?

It keeps running. The containers, database, files, queues, secrets and DNS are in your own Scaleway and bunny.net accounts and are billed to you directly. You lose the deploys, rollbacks and dashboard.

All posts Read this post as markdown

Try it on your own app

Brewless deploys your Laravel app to Scaleway and bunny.net, in your own accounts. We open access to Laravel teams in small batches.