# Moving from Laravel Vapor to European infrastructure

How to move a Laravel app from Vapor to Scaleway and bunny.net, from vapor.yml to brewless.yml, with your variables, database, files and DNS.

## In short

- `brewless init --from-vapor` reads vapor.yml once and writes the brewless.yml that says the same, wherever Brewless has a setting for it.
- Nothing at Vapor or AWS is read or changed by the move, so the app keeps running there until you point your domain somewhere else.
- Your database is copied with brewless db:import and your files with brewless storage:import, both from AWS straight into your own Scaleway project, and neither removes anything at the source.
- Keep your Vapor APP_KEY when your app has encrypted data or sessions, because Laravel can only read those with the key they were written with.
- Deploy and test on the environment's own address first, and move the DNS only when the new release does what it should.

Laravel Vapor made serverless Laravel normal: no servers, a container per request, and a `vapor.yml` that describes every environment. It also runs on AWS, an American company. When a client or your own legal team asks for European infrastructure, you need the same kind of setup somewhere else.

This guide moves a Vapor app to Scaleway (France) with bunny.net (Slovenia) in front, in your own accounts, deployed with [Brewless](/). It follows the steps of the [Vapor migration docs](https://docs.brewless.eu/getting-started/from-vapor) and explains the choices along the way.

## What does moving from Vapor to Brewless involve?

Moving from Vapor means translating `vapor.yml` into `brewless.yml`, setting up a new database, queue and bucket at Scaleway, and then moving your variables, data, files and DNS. Brewless writes the configuration for you with `brewless init --from-vapor`; the data, files and DNS are steps you take yourself, at a moment you choose.

Nothing at Vapor or AWS is read or changed by Brewless. Your app keeps running on Vapor the whole time, and visitors only move when you move the domain.

| Step | Done by | Command |
| --- | --- | --- |
| Configuration | Brewless | `brewless init --from-vapor` |
| Check it | Brewless | `brewless plan` |
| Variables and secrets | You | `brewless env:pull`, `brewless env:push` |
| Database data | You, with Brewless | `brewless db:import`, from AWS straight into your Scaleway project |
| Files | You | `brewless storage:import`, from AWS straight into your Scaleway project |
| First release | Brewless | `brewless deploy production` |
| DNS | You, with Brewless | `brewless domain:add` |

You need a Scaleway account connected to your organisation and the command line client signed in, as in the [quickstart](https://docs.brewless.eu/getting-started/quickstart).

## 1. Write brewless.yml from vapor.yml

In the root of the project, next to `vapor.yml`:

```bash
brewless init --from-vapor
```

`init` asks which application at Brewless this is, or makes a new one. Then it adds the environments of `vapor.yml` you pick, in the region you choose, and sets each one up the way it was at Vapor: a new database where it had one, a queue at Scaleway Queues unless it said `queues: false`, and a bucket where it had storage.

For a production environment with a database, storage, two domains and `warm: 10`, the result looks like this:

```yaml brewless.yml
organisation: acme
application: shop
framework: laravel
php: '8.4'

release:
  - 'migrate --force'

environments:
  production:
    kind: production
    region: fr-par
    web:
      memory: 1024
      min: 1
    release:
      memory: 512
    worker:
      timeout: 540
    database: shop-db
    queue: sqs
    storage: shop-files
    domains:
      - shop.example.eu
      - www.shop.example.eu
```

A few keys change on the way. These are the ones that surprise people:

| vapor.yml | brewless.yml | Why |
| --- | --- | --- |
| `warm: 10` | `web.min: 1` | A Lambda container takes one request at a time, a replica up to 50 |
| `concurrency: 120` | `web.max: 3` | Worked out the same way |
| `deploy` | `release` | Artisan commands without `php artisan` in front |
| `build` | `build` | Steps Brewless already runs, such as `composer install` and `npm run build`, are left out |
| `gateway-version` | – | There is no API Gateway: visitors reach the container through bunny.net |
| `octane` | – | Brewless serves the app with FrankenPHP in classic mode |

`init` prints every key it did not carry over as it was, once, in the terminal. The file itself stays clean. Commit `brewless.yml`: it holds no secrets.

> [!NOTE]
> A `cache` in `vapor.yml` is not carried over by `init --from-vapor` yet. Add it to `brewless.yml` yourself; it becomes a Scaleway Managed Database for Redis. See [brewless.yml](https://docs.brewless.eu/reference/brewless-yml).

## 2. Check the plan

```bash
brewless plan
```

This shows, per environment, what the file asks for next to what stands now, and changes nothing. A database or bucket that `init` made has a name of its own, after the application and the environment, so the plan shows both names side by side. Write the name that stands into `brewless.yml`.

## 3. Variables and APP_KEY

`vapor.yml` holds no variables, and Brewless never reads them from Vapor. The new environment already has the variables Brewless set: the database, the queue, the bucket and a new `APP_KEY`. Pull those into a file of their own, so they do not mix with anything from Vapor:

```bash
brewless env:pull production --file=.env.brewless-production
```

Then add what your app needs besides, copied from your Vapor environment: mail, payments, other services. Keep the `DB_*`, `AWS_*`, `SQS_*`, `QUEUE_CONNECTION` and `FILESYSTEM_DISK` lines Brewless wrote, because they point at the new database, queue and bucket.

> [!WARNING]
> Replace the new `APP_KEY` with the one from Vapor when your app has encrypted data, encrypted casts or sessions you want to keep. Laravel can only read those with the key they were written with.

Push the file and remove it:

```bash
brewless env:push production --file=.env.brewless-production
rm .env.brewless-production
```

The values go into Scaleway Secret Manager in your own project. Brewless keeps the names and revisions, never the values. Plain values such as `APP_LOCALE` can go under `variables` in `brewless.yml` instead; anything that looks like a secret is refused there.

## 4. Database data

The new database is empty, and `brewless db:import` copies your database at Vapor into it: you say where the data is now and give a user that may read it, and where it goes follows from the environment.

```bash
brewless db:import production mysql://brewless-import@shop-db.abc123.eu-west-1.rds.amazonaws.com/vapor
```

The copy runs as a job in your own Scaleway project, so the data goes from AWS straight to Scaleway without passing through Brewless or your machine. The `DB_*` values of your database at Vapor are not in `vapor env:pull`: they are on the database's page in the dashboard, or make a user for the move with `vapor database:user shop-db brewless-import`. The password is never an argument or an option; it is asked, or read from `SOURCE_DB_PASSWORD` or `--from-env`.

A MySQL database is converted into the PostgreSQL of a Serverless SQL Database with pgloader on the way, or copied as it is into a Managed Database for MySQL that you name when you set the environment up. `--dry-run` counts first what there is, which is worth knowing because AWS bills the data that leaves it. See [Moving your database](https://docs.brewless.eu/applications/moving-a-database).

> [!TIP]
> Copy before the first deploy, so the release commands find your tables, or afterwards with `--replace`. Do a first copy to test with, and one more just before you switch the domain, with the Vapor app in maintenance mode, so no writes get lost in between.

## 5. Files

The files in your S3 bucket at AWS are not moved by themselves either. `brewless storage:import` copies them into the new bucket with rclone, in a job of your own Scaleway project, so they go from AWS to Scaleway without passing through Brewless or your laptop. It removes nothing, and run again just before you switch the domain it only copies what changed. Give it a key at AWS that may only read the bucket:

```bash
export SOURCE_AWS_ACCESS_KEY_ID=… SOURCE_AWS_SECRET_ACCESS_KEY=…
brewless storage:import production s3://my-vapor-bucket
```

Your app reads the files through the same `s3` disk as before; only the `AWS_*` variables changed. See [Move your S3 files from AWS to Brewless](/blog/move-s3-files-to-brewless) for the details.

## 6. Deploy and test

```bash
brewless deploy production
```

This builds the current commit inside your own Scaleway project, runs the release commands and puts the release live on the environment's own address, such as `acme-shop-production.b-cdn.net`, and on its [test address](https://docs.brewless.eu/running/domains#the-test-address). Your domain still points at Vapor, so you can try everything here first.

The deploy names the domains from `brewless.yml` that the environment does not answer on yet, and asks whether to add them. Answer no until you are ready to move, because adding one points its DNS at Brewless when the zone is in your accounts.

## 7. Move the DNS

When the release on Scaleway does what it should, add each domain:

```bash
brewless domain:add production shop.example.eu
brewless domain:add production www.shop.example.eu
```

With the zone at Bunny DNS or Scaleway DNS in your connected accounts, Brewless makes the record. With DNS anywhere else, it prints the `CNAME` you make yourself. Adding a domain never overwrites a record somebody else made: when an old address record is in the way, it stops and names it. The certificate follows once the record points at the edge.

Then set `APP_URL` to the new domain, which `domain:add` offers to do, and deploy once more so the containers get it. [Moving your domain](https://docs.brewless.eu/getting-started/moving-your-domain) covers every case.

Once visitors arrive on Scaleway, what you had at Vapor can go. Brewless never removes anything there, so that is a step you take at AWS yourself, when you are sure.

## Summary

1. `brewless init --from-vapor`, then `brewless plan`.
2. Pull the new variables, add your own, keep your old `APP_KEY` if you have encrypted data, and push.
3. Copy the database with `brewless db:import` and the files with `brewless storage:import`.
4. Deploy and test on the environment's own address.
5. Add the domains, set `APP_URL`, deploy again.

What changes in the application itself, and what does not, is in [From Vapor to Brewless without touching your code](/blog/vapor-migration-without-code-changes). The whole key-by-key comparison is in the [Vapor guide](https://docs.brewless.eu/getting-started/from-vapor). Why you might want European infrastructure in the first place is in [How to host a Laravel app EU-sovereign](/blog/eu-sovereign-laravel-hosting).

## Frequently asked questions

### Does brewless init change anything at Vapor or AWS?

No. It reads vapor.yml from your project and writes brewless.yml next to it. Your Vapor environments keep running until you move your domain, and Brewless never removes anything there.

### Is my data moved from the Vapor database automatically?

Not by itself, but one command does it. The database Brewless sets up is empty, and brewless db:import copies your database at Vapor into it, in a job of your own Scaleway project. Copy before the first deploy, so the release commands find your tables, or afterwards with --replace.

### What happens to Vapor's warm setting?

It becomes web.min, the number of replicas that keep running. A Lambda container takes one request at a time and a Brewless replica up to 50, so warm 10 becomes a minimum of 1. A running replica is paid for all the time.

### Can I move a MySQL database from Vapor?

Yes. db:import converts a MySQL source into the PostgreSQL of a Serverless SQL Database with pgloader, or copies it as it is into a Managed Database for MySQL that you name when you set the environment up. Try your application on PostgreSQL first, because it compares text differently than MySQL does.

### Should I reuse my APP_KEY from Vapor?

Yes, when your app has encrypted data, encrypted casts or sessions you want to keep. Laravel reads those only with the key they were written with. Without such data, the new APP_KEY Brewless generated works as well.

Source: https://brewless.eu/blog/from-laravel-vapor-to-european-infrastructure
