# No application encryption key has been specified

> Laravel throws this when APP_KEY is empty in the config it loaded: no .env, a config cache built without the key, or a container that never got it.

`Illuminate\Encryption\MissingAppKeyException: No application encryption key has been specified.` means `config('app.key')` is empty at the moment Laravel needs the encrypter. Every web request needs it (cookies and sessions are encrypted), so the whole site returns 500. The value comes from `APP_KEY` in `.env` or the environment, unless a cached config overrides both.

This is what the log says on Laravel 13.40 with `APP_KEY=` in `.env`:

```text
[2026-11-10 16:38:59] production.ERROR: No application encryption key has been specified. {"exception":"[object] (Illuminate\\Encryption\\MissingAppKeyException(code: 0): No application encryption key has been specified. at .../vendor/laravel/framework/src/Illuminate/Encryption/EncryptionServiceProvider.php:83)
```

## The key was never generated

A fresh clone has `.env.example` but no `.env`, or a `.env` with an empty `APP_KEY=`. Generate one:

```bash
cp .env.example .env
php artisan key:generate
```

`key:generate` writes `APP_KEY=base64:…` into `.env`. In production it asks for confirmation; add `--force` in scripts. Without a `.env` file it fails with `file_get_contents(.../.env): Failed to open stream`, so in a container where the environment comes from elsewhere, generate the key once and store it as a secret instead:

```bash
php artisan key:generate --show
```

`--show` prints a new key without writing it anywhere. Put that value in your host's secret store or environment settings (Forge, Laravel Cloud, Kubernetes secrets, the Docker Compose `environment:` block), not in the image.

Generate the key once per environment and keep it. A new key on every deploy logs every user out and makes encrypted data unreadable.

## The config cache was built without the key

`php artisan config:cache` writes `bootstrap/cache/config.php` with every value resolved at that moment. From then on Laravel reads only that file and ignores `.env`. Two ways this goes wrong:

- The cache was built in a CI or Docker build step where `APP_KEY` wasn't set yet. The empty key is now baked into the image.
- You added the key to `.env` on the server, but an old cache is still there.

Confirm it:

```bash
ls bootstrap/cache/config.php
php artisan tinker --execute="var_dump(filled(config('app.key')));"
```

`bool(false)` with a cache file present means the cache holds an empty key. Rebuild it on the server, after the environment is in place:

```bash
php artisan config:clear
php artisan config:cache
```

Then restart PHP-FPM, Octane, queue workers and anything else that keeps the old config in memory (`php artisan queue:restart` for workers). In a container, run `config:cache` in the entrypoint at start, not in the `Dockerfile`.

## The variable doesn't reach PHP

The key is set somewhere, but not where the PHP process looks:

- **PHP-FPM clears the environment by default.** With `clear_env = yes` in the pool config, variables set for the container or the systemd unit never reach your app. Use a `.env` file, or set `clear_env = no` or `env[APP_KEY] = $APP_KEY` in the pool.
- **The wrong `.env` file.** When `APP_ENV=staging` is set in the real environment (or you pass `--env=staging`), Laravel loads `.env.staging` if it exists, not `.env`.
- **A deploy directory without it.** Zero-downtime deploys (Forge, Envoyer, Deployer) create a new release directory and symlink a shared `.env` into it. A broken symlink means no `.env`.

Check what PHP actually sees, from the same user and directory as the web server:

```bash
php artisan about --only=environment
```

## The key exists but has the wrong format

A key without the `base64:` prefix, or one that was cut off when it was copied, fails differently:

```text
Unsupported cipher or incorrect key length. Supported ciphers are: aes-128-cbc, aes-256-cbc, aes-128-gcm, aes-256-gcm.
```

The default cipher `AES-256-CBC` needs 32 bytes; `key:generate` produces `base64:` plus 44 characters. Copy the whole value, including `base64:`.

## Rotate APP_KEY without logging everyone out {#rotate-app-key}

Changing `APP_KEY` breaks every session cookie and everything encrypted with `Crypt` or the `encrypted` cast. Users get logged out, open forms fail with [419 Page Expired](/errors/419-page-expired/), and decrypting old data throws `DecryptException`. Since Laravel 11, list the old key in `APP_PREVIOUS_KEYS` while you switch:

```dotenv
APP_KEY="base64:NEW_KEY"
APP_PREVIOUS_KEYS="base64:OLD_KEY"
```

`NEW_KEY` is the output of `php artisan key:generate --show`; `OLD_KEY` is the value you're replacing. Separate several old keys with commas. Laravel encrypts with the new key and tries the previous keys when decrypting. Re-encrypt stored data with the new key, then remove the old one. Every server needs the same `APP_KEY` and `APP_PREVIOUS_KEYS` at the same time.

## Catch it in production

A missing key fails every page that uses sessions or cookies, so you notice it from outside first. Laravel's `/up` health route doesn't: it runs without the `web` middleware, and in our test it kept answering `200` while the home page returned `500`. Point a Lampwick [uptime monitor](/docs/uptime/) at a real page, such as your home page or `/login`. It alerts the team after two failed checks in a row, which with a 30-second interval is about a minute after the deploy. If your app sends exceptions with the [Laravel package](/docs/laravel/), the `MissingAppKeyException` also opens an issue under **Issues**, linked to the deploy that introduced it.

Other errors that tend to appear right after a deploy: [SQLSTATE[HY000] [2002] Connection refused](/errors/sqlstate-hy000-2002/) and [419 Page Expired](/errors/419-page-expired/).
