# Laravel secrets without a .env file in production

Keep Laravel's production secrets in Scaleway Secret Manager instead of a .env file on a server, with revisions, env:pull and env:push.

## In short

- A production .env file is a plaintext copy of every secret your Laravel app has, and it tends to end up in backups, images and chat messages.
- Laravel reads real environment variables when there is no .env file, so production does not need one.
- With Brewless, an environment's variables live in Scaleway Secret Manager in your own project, and Brewless keeps only their names and revisions.
- A change to a variable reaches the containers with the next deploy, which makes every change part of a release you can roll back.
- Changing APP_KEY makes existing encrypted data and sessions unreadable, so carry it over when you move hosts and rotate it with APP_PREVIOUS_KEYS.

Most Laravel apps start production the same way: copy `.env.example` to the server, fill in the database password, the mail key and the payment key, and hope nobody copies the file anywhere else. They usually do. A `.env` file ends up in a server backup, in a Docker image, in a password manager note, and in a chat message to a colleague who "just needs the Mollie key for a second".

This post shows how to run Laravel in production without a `.env` file: the secrets live in Scaleway Secret Manager in your own account, you change them with two commands, and every change has a revision.

## Does Laravel need a .env file in production?

No. Laravel reads its configuration from environment variables, and the `.env` file is just a convenient way to set them on a development machine. When there is no `.env` file, Laravel uses the environment the process was started with, so a container that gets its variables from the platform needs no file at all.

That changes where the secrets live. Instead of a file on a disk, they sit in a secret store, and the platform hands them to the container when it starts.

## Why a .env file is a problem

| | `.env` file on a server | Secret store |
| --- | --- | --- |
| Where the values are | On the disk, readable by anything on it | In the store, given to the process at start |
| History | None, unless you keep copies (which is the problem) | A revision per change |
| Who changed what | Unknown | Written down |
| Two people editing | The last save wins | The second save is refused |
| Copies | In backups, images, laptops | Only where you pull them |

On serverless containers there is a second reason: there is no server to put the file on. The disk does not outlive the container, and a value baked into the image would be in every copy of it.

## Where Brewless keeps them

The variables of an environment are kept in Scaleway Secret Manager, in the Scaleway project its application runs in. That is your project, billed to you. Brewless keeps their names and their revisions, never a value. When you ask to see or change the values, Brewless reads or writes them in your Secret Manager and passes them on; they are not stored and not logged, and the fact that someone looked is written to the audit trail.

On each deploy, the release gets one revision of the variables, and the containers start with them as their environment. The default `docker/entrypoint.sh` runs `php artisan config:cache` when the container starts, not during the build, because that is the first moment the variables exist.

When Brewless sets an environment up, it writes the first variables itself: the database, the queue, the bucket, and a new application key.

## Pull, edit, push

Fetch the variables of an environment into a file:

```bash
brewless env:pull production
```

This writes `.env.production`, with a first line that notes the revision you pulled. Change what you need, then push it back:

```bash
brewless env:push production
```

`env:push` replaces all variables of the environment with the contents of the file. Two safeguards come with that:

- If someone else saved after your pull, the push is refused before it overwrites their revision. Pull again and make your change on top.
- Variables the push would remove are named, and the client asks before it removes them.

`--force` skips the question and pushes over a revision you did not pull. Leave it for when you mean exactly that.

```ini .env.production
# revision noted here by env:pull
APP_ENV=production
APP_KEY=base64:your-application-key
DB_PASSWORD=your-database-password
MOLLIE_KEY=your-mollie-key
```

> [!WARNING]
> `.env.production` holds real secrets. Check that your `.gitignore` covers it, and delete the file once the push is done.

Both commands go through Brewless, so your machine needs no Scaleway key. To keep the values away from Brewless altogether, add `--direct`: the client then reads or writes Secret Manager straight from your machine, with a Scaleway key from `SCW_SECRET_KEY` or the active profile of Scaleway's own client.

```bash
brewless env:push production --direct
```

The console offers the same under **Variables** on the page of an environment: the names, a form to set or remove a single variable, and **Show values** to edit the whole file as a new revision. See [Environment variables](https://docs.brewless.eu/running/variables) for every option.

## Plain values in brewless.yml

Not every variable is a secret. A locale or a log level can live in your repository, next to the code that reads it:

```yaml brewless.yml
environments:
  production:
    variables:
      APP_LOCALE: nl
      LOG_LEVEL: warning
```

A deploy writes these into a new revision before the build, next to the variables that are there. A variable the file gives is marked **Set in brewless.yml** in the console, and changing it anywhere else is refused, because the next deploy would put the file's value back.

Brewless refuses a name under `variables` that looks like a secret: one that contains `KEY`, `SECRET`, `PASSWORD`, `TOKEN`, `CREDENTIALS`, `PRIVATE`, `DSN`, `SALT` or `AUTH` as a word. The deploy stops with the line it is about, before anything is built. See [brewless.yml](https://docs.brewless.eu/reference/brewless-yml).

## When a change takes effect

Variables reach the containers with the next deploy, not before:

```bash
brewless env:push production
brewless deploy production
```

Until you deploy, the console shows that the newest revision is not live yet. That can feel slow when you are fixing a typo in a mail password. It is also what makes a variable change safe: it goes live as part of a release, the release records which revision it ran with, and a [rollback](/blog/laravel-rollbacks) takes that revision back along with the code.

Some variables have to exist before a deploy makes sense, such as `DB_USERNAME` and `DB_PASSWORD` when Brewless could not make the database user itself. Until the latest revision names each of them, a deploy is refused before the upload, with the missing names. Brewless checks the names only, never the values.

## APP_KEY

`APP_KEY` is the one variable you should think about before you touch it. Laravel encrypts cookies, sessions and anything you store with `Crypt` or the `encrypted` cast with it. Change the key, and Laravel can no longer read any of that: everyone is signed out, and encrypted columns become unreadable.

Three cases:

- **A new app.** Brewless writes a new application key when it sets the environment up. Leave it as it is.
- **Moving an existing app.** Brewless's new key would sign everyone out and lock your encrypted data. Pull the variables, replace `APP_KEY` with the key the app runs on today, and push before the first deploy. The [Vapor guide](https://docs.brewless.eu/getting-started/from-vapor) describes this for an app coming from Laravel Vapor.
- **Rotating the key.** Put the new key in `APP_KEY` and the old one in `APP_PREVIOUS_KEYS`. Laravel encrypts with the new key and still decrypts with the old ones.

```ini .env.production
APP_KEY=base64:your-new-application-key
APP_PREVIOUS_KEYS=base64:your-old-application-key
```

> [!TIP]
> Generate a key on your own machine with `php artisan key:generate --show`. It prints the key without writing it to a file.

## What stays outside Secret Manager

A few secrets are not variables of your app, and Brewless keeps them apart. The read key of a [bucket for backups](https://docs.brewless.eu/applications/applications-and-environments#a-bucket-for-backups) is kept in Secret Manager as a secret of its own and never goes into the variables, so the running app cannot read its own backups. A pipeline token for deploys lives in your CI's secret store, as described in [deploying Laravel from CI](/blog/deploy-laravel-from-ci).

## Summary

1. Production needs no `.env` file: Laravel reads the container's environment.
2. Keep the values in Scaleway Secret Manager in your own project; Brewless keeps names and revisions only.
3. `brewless env:pull`, edit, `brewless env:push`, then deploy. Delete the local file.
4. Plain values may go in `brewless.yml`; names that look like secrets are refused there.
5. Carry `APP_KEY` over when you move, and rotate it with `APP_PREVIOUS_KEYS`.

## Frequently asked questions

### Does Laravel need a .env file in production?

No. Laravel reads its configuration from environment variables, and the .env file is only one way to set them. When there is no .env file, Laravel uses the variables the process was started with.

### Where does Brewless keep my Laravel environment variables?

In Scaleway Secret Manager, in the Scaleway project your application runs in, which you own. Brewless stores the names and revisions of your variables, never their values, and logs that someone looked at them.

### Why did my changed variable not take effect?

Variables reach the containers with the next deploy. Push the change with brewless env:push, then run brewless deploy. Until then the console shows that the newest revision is not live yet.

### Can I put variables in brewless.yml?

Only plain values, such as APP_LOCALE or LOG_LEVEL, under variables. A name that looks like a secret, with KEY, SECRET, PASSWORD or TOKEN in it for example, is refused there. Secrets go to Secret Manager with brewless env:push.

### What happens if I change APP_KEY?

Laravel can no longer decrypt data, cookies and sessions that were encrypted with the old key. Visitors are signed out and encrypted columns become unreadable. Put the old key in APP_PREVIOUS_KEYS so Laravel can still read what it encrypted before.

Source: https://test.brewless.eu/blog/laravel-secrets-without-env-file
