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:
[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:
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:
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_KEYwasn't set yet. The empty key is now baked into the image. - You added the key to
.envon the server, but an old cache is still there.
Confirm it:
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:
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 = yesin the pool config, variables set for the container or the systemd unit never reach your app. Use a.envfile, or setclear_env = noorenv[APP_KEY] = $APP_KEYin the pool. - The wrong
.envfile. WhenAPP_ENV=stagingis set in the real environment (or you pass--env=staging), Laravel loads.env.stagingif it exists, not.env. - A deploy directory without it. Zero-downtime deploys (Forge, Envoyer, Deployer) create a new release directory and symlink a shared
.envinto it. A broken symlink means no.env.
Check what PHP actually sees, from the same user and directory as the web server:
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:
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
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, and decrypting old data throws DecryptException. Since Laravel 11, list the old key in APP_PREVIOUS_KEYS while you switch:
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 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, 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 and 419 Page Expired.