Skip to content
FrameworkPHP

Laravel .env file, APP_KEY and config caching

Laravel loads .env through phpdotenv, lets real environment variables win, and stops reading .env after config:cache. APP_KEY rotation and Crunu included.

LaravelAt a glance

Env file that wins
.env.[APP_ENV]

First of 2 .env files, highest priority first. See the load order

Public prefix
None

No names are copied into browser code by a prefix.

Values applied
At runtime

Values are read from the environment when the app runs.

Built-in secret store
Encrypted environment files: .env.encrypted from php artisan env:encrypt

laravel.com/framework/docs/13.x/configuration

Crunu command
crunu run --env laravel-app/development -- php artisan serve --no-reloadRecipeCrunu has no dedicated integration here: this is the general setup.

Common pitfalls

  1. After config:cache, Laravel stops loading .env, and env() outside config files returns only real system variables or null.
  2. config:cache writes the resolved configuration, secrets included, into one cached file that stays on disk until config:clear.
  3. Changing APP_KEY logs every user out and makes old encrypted data unreadable, unless the old key goes into APP_PREVIOUS_KEYS.
  4. When APP_ENV is set from outside, Laravel loads .env.[APP_ENV] instead of .env, not in addition to it.
  5. With a .env file present, php artisan serve removes $_ENV variables outside a short passthrough list from its server process. Use --no-reload.
On this page

Laravel reads its .env file with the phpdotenv library each time the application starts to handle a request or an Artisan command, and real environment variables set on the server or in the system take precedence over the file. Configuration files in config/ turn those variables into settings with env(), and the rest of the app reads settings with config(). In production, php artisan config:cache freezes that result into one cached file, after which Laravel doesn't read .env at all. If APP_ENV is provided from outside, Laravel loads .env.[APP_ENV] instead of .env when that file exists. This page follows the Laravel 13.x configuration documentation, checked October 30, 2026.

Which file Laravel loads

SituationWhat Laravel reads
APP_ENV set outside the app, or --env passed to Artisan, and .env.[APP_ENV] exists.env.[APP_ENV]
otherwise.env
configuration cached with config:cacheno file; the cached configuration plus real environment variables for env()

The first row surprises people who expect files to stack. .env.staging replaces .env, it doesn't override a few keys on top of it. Everything the staging app needs has to be in that one file.

Above any file sits the real environment. A variable set by the web server, the container runtime or a process manager overrides the same name in .env. That's also why a value exported by mistake on a server can shadow the file for months without anyone noticing.

What env() returns, and what it doesn't

All values in .env are strings, with a few reserved words. env() turns true and (true) into a boolean, false and (false) into false, empty into an empty string and null into PHP null. Quote values with spaces: APP_NAME="My Application".

env() has a second argument for a default, returned when the variable doesn't exist. Laravel's own config files use it everywhere, for example 'debug' => (bool) env('APP_DEBUG', false). That particular one deserves care: the documentation says APP_DEBUG must always be false in production, because true risks showing sensitive configuration values to your users.

config:cache changes the rules

Caching is recommended for every production deploy. php artisan config:cache combines all configuration files into one file that loads faster. From then on:

  • .env is not loaded, neither during requests nor during Artisan commands.
  • env() returns only external, system-level variables. A call to env('STRIPE_SECRET') in a controller or a job returns null in production unless the server's real environment has it.
  • Settings keep the values from the moment of caching. Changing an environment variable later does nothing until you cache again or run config:clear.

The rule that follows is short: call env() only inside files in config/, and use config() everywhere else. Then caching can't break anything.

There's a quieter consequence. The cache is a file with the resolved configuration, so every secret that went through env() in a config file is now written in it in plain text. Treat that file like a .env: keep it out of images you publish, out of backups that many people can read, and out of the repository.

APP_KEY and rotation

APP_KEY drives Laravel's encrypter. The encryption documentation says to generate it with php artisan key:generate. Every cookie Laravel sets is encrypted with it, including the session cookie, so changing the key logs out every user, and data you encrypted with the old key can no longer be decrypted.

Laravel supports graceful rotation. Put the new key in APP_KEY and the previous ones in APP_PREVIOUS_KEYS, comma-separated. Laravel always encrypts with the current key and, when decrypting, tries the current key first and then each previous key. Keep old keys there until the data and cookies that used them have been re-encrypted or expired.

Laravel's own encrypted .env

php artisan env:encrypt encrypts .env into .env.encrypted, which you can commit. The key is printed once, and you keep it in a password manager or pass it as LARAVEL_ENV_ENCRYPTION_KEY to php artisan env:decrypt, which writes the plain .env back to disk. The --readable option keeps variable names visible and encrypts only values, which makes pull requests easier to review. The default cipher is AES-256-CBC.

That's a reasonable setup for a small team. Its limits are the usual ones for a shared file key: everyone with LARAVEL_ENV_ENCRYPTION_KEY can decrypt every committed version, there is one key for the whole file, and the decrypted .env ends up on disk on every machine that runs the app.

Run Laravel with values from Crunu

crunu run decrypts a Crunu environment in memory and starts your command with its variables in the process environment. Laravel treats them as external variables, which outrank .env, so no file has to exist. Each stage has its own Crunu environment and its own key, and a person with access to development never receives production values.

  1. Put APP_KEY, APP_ENV, APP_DEBUG, database credentials and API keys into Crunu environments such as laravel-app/development and laravel-app/production.
  2. Develop with crunu run --env laravel-app/development -- php artisan serve --no-reload, and run other Artisan commands the same way.
  3. On a Linux server with the agent, cache the configuration with crunu run --env laravel-app/production -- php artisan config:cache during the deploy.
  4. Run queue workers under crunu run --env laravel-app/production --restart on-change -- php artisan queue:work.

Why --no-reload in step 2? When a .env file exists, php artisan serve watches it and restarts its PHP server on changes. To let the restarted server read the file fresh, it starts that server with a filtered environment: every variable in $_ENV that isn't on a short passthrough list, such as APP_ENV and PATH, is removed, as the ServeCommand source shows. Depending on the variables_order setting in your php.ini, that can include the values crunu run passed in. --no-reload keeps the whole environment, and so does deleting .env once its values live in Crunu.

Step 3 is a choice with a cost. Caching under crunu run gives you the speed of a cached configuration, but the cached file then holds the production values on the server's disk, as described above. If that matters more than the speed gain, skip config:cache and let PHP read the environment on each boot. Either way, re-run the cache step after any value changes, or the app keeps the old ones.

For long-running processes such as queue workers, --restart on-change restarts the worker when a value changes, so a rotated API key reaches it without a deploy.

The web side needs more thought. Under PHP-FPM, the clear_env directive defaults to yes: FPM clears the environment of its worker processes and adds back only the variables listed with env[...] in the pool configuration. Starting FPM under crunu run therefore doesn't hand your variables to the PHP code by itself. You either list each variable in the pool with env[NAME] = $NAME, set clear_env = no for that pool, or use config:cache from step 3 so the web workers read the cached configuration instead of the environment. Laravel Octane and php artisan serve run as one long-lived command and inherit the environment of crunu run directly.

Frequently asked questions

Something else? Write to support@crunu.com

Does a server environment variable override the value in .env?

Yes. Laravel's documentation says any variable in .env can be overridden by external environment variables, such as server-level or system-level ones.

Why does env() return null in production?

Because the configuration is cached. After php artisan config:cache, Laravel doesn't load .env during requests or Artisan commands. Call env() only inside config files and read values with config() everywhere else.

Is .env.encrypted a good place for production secrets?

It keeps an encrypted copy in Git, decrypted with LARAVEL_ENV_ENCRYPTION_KEY. Everyone who has that key can read every committed version, so treat the key like the values themselves and rotate both when someone leaves.

How do I rotate APP_KEY without logging users out?

Set the new key in APP_KEY and list the previous keys, comma-separated, in APP_PREVIOUS_KEYS. Laravel encrypts with the new key and tries the old ones when decrypting.

Sources

Every source this page relies on, with the date we last checked it.

  1. Laravel 13.x: Configuration · checked October 30, 2026
  2. Laravel 13.x: Encryption · checked October 30, 2026
  3. Laravel framework source: ServeCommand (13.x) · checked October 30, 2026
  4. PHP manual: FPM configuration, clear_env · checked October 30, 2026

Keep your .env values in one place

Free during early access. Values are encrypted on your devices, each environment has its own key, and crunu run hands them to your app without writing a file.