env.dev

Laravel Environment Variables: env(), config() & Caching

env() returns null after php artisan config:cache — the #1 Laravel env trap. env() vs config(), APP_KEY rotation, .env precedence, and deployment caching.

By env.dev Updated

Laravel loads .env through vlucas/phpdotenv at bootstrap and expects you to read configuration through config(), never env() — because the moment a deploy script runs php artisan config:cache, the .env file is no longer read and every env() call outside the config/ directory returns null. That single rule explains the most common "works locally, breaks in production" bug in the framework. Nothing about it changed in Laravel 13 (March 17, 2026) — the trap is as old as config caching and still catches teams every release.

TL;DR

  • env() belongs only inside config/*.php files; application code reads config('app.name').
  • After php artisan config:cache, .env is never parsed again — env() outside config files returns null. This is the #1 Laravel env trap.
  • Real environment variables (shell, Docker, Forge, Vapor) beat .env values — phpdotenv runs in immutable mode and never overwrites an existing variable.
  • APP_KEY encrypts sessions, cookies, and encrypted casts. Losing or casually rotating it logs out every user and bricks encrypted data; since Laravel 11, APP_PREVIOUS_KEYS enables graceful rotation.

How does Laravel load the .env file?

During bootstrap, Laravel loads phpdotenv in immutable mode against the project root. Immutable means first-wins: a variable already present in the real process environment — set by Docker, your web server, Forge, or CI — is never overwritten by the file. That is the twelve-factor-correct default, but it also means editing .env does nothing for a key your platform already exports. The same silent-precedence behavior exists in plain-PHP phpdotenv usage, where the mechanics of getenv() vs $_ENV are covered in depth.

If APP_ENV is set externally before boot and a matching .env.[APP_ENV] file exists, Laravel loads that file instead of .env — this is how .env.testing takes over during php artisan test and PHPUnit runs.

ini
# .env — the standard local-development skeleton
APP_NAME="My App"
APP_ENV=local
APP_KEY=base64:GENERATED_BY_ARTISAN_KEY_GENERATE
APP_DEBUG=true
APP_URL=http://localhost

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=secret

Quoting rules, inline comments, and multiline values follow phpdotenv's parser, which differs from the Node.js and Python parsers in subtle ways — the .env syntax guide compares them side by side.

What is the difference between env() and config()?

env('KEY', $default) reads the raw environment. config('file.key') reads the arrays returned by the files in config/. The intended data flow is one-directional:

php
<?php
// config/services.php — the ONLY place env() should appear
return [
    'stripe' => [
        'key' => env('STRIPE_KEY'),
        'secret' => env('STRIPE_SECRET'),
    ],
];

// app/Services/Billing.php — application code reads config()
$secret = config('services.stripe.secret');

// Anti-pattern: env() in application code.
// Works in dev, returns null after config:cache in production.
$secret = env('STRIPE_SECRET'); // DON'T

Custom values follow the same path: add your variable to .env, map it in a config file, read it via config(). The indirection looks like ceremony until the first time config caching saves you — and it gives every value a typed, documented home with a default.

Why does env() return null after config:cache?

php artisan config:cache executes every file in config/ once and serializes the merged result to bootstrap/cache/config.php. On subsequent requests Laravel loads that single file and skips loading .env entirely — the framework even documents that the env() function will only return external, system-level environment variables once config is cached. Any value that only existed in .env is gone, so env() calls sprinkled through controllers, Blade templates, or service classes silently become null.

The symptom pattern is distinctive: everything works in local dev (nobody caches config there), then mail credentials, API keys, or feature flags evaporate on the first production deploy that runs config:cache or optimize. Debug it in three commands:

bash
# Is config cached? (file exists = yes)
ls bootstrap/cache/config.php

# Drop the cache and re-test — if the value comes back, you found it
php artisan config:clear

# Grep for the anti-pattern outside config/
grep -rn "env(" app/ resources/ routes/

What does APP_KEY do, and can you rotate it?

APP_KEY is the symmetric key behind Laravel's encrypter — session cookies, the CSRF cookie, encrypted model casts, and anything you pass through Crypt::encrypt(). It is generated by php artisan key:generate (AES-256-CBC by default) and stored as a base64:-prefixed value. Deploy without one and Laravel throws MissingAppKeyException: No application encryption key has been specified.

Changing the key invalidates everything encrypted with the old one: all users are logged out and encrypted database columns become undecryptable. Since Laravel 11 you can rotate gracefully — move the old key(s) into a comma-delimited APP_PREVIOUS_KEYS variable, put the new key in APP_KEY, and Laravel encrypts with the new key while still decrypting values produced by the old ones. Treat the key like any other production secret: never commit it, and hand it to teammates through a secure one-time channel rather than chat.

How should you handle env variables in deployment?

The production recipe is short, and every line exists because of an incident somewhere:

  • Keep .env out of git and out of the web root. The 2017-era wave of mass scans for /.env never stopped; bots request it on every new domain within hours. Laravel's public/ docroot protects you unless a misconfigured vhost serves the project root.
  • Cache config in the deploy script. php artisan optimize (or config:cache + route:cache + view:cache) — both for speed and because it forces the env()-only discipline early.
  • Prefer platform-level variables for secrets. Forge, Vapor, Cloud, Docker, and Kubernetes all inject real environment variables; immutable loading means they automatically win over any stale .env on disk.
  • Restart workers after changing values. Queue workers and Octane hold the old config in memory — php artisan queue:restart (and an Octane reload) or the new values never arrive.
bash
# Typical zero-downtime deploy tail
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize          # caches config, routes, views, events
php artisan queue:restart     # workers re-read config on next boot

When should you not put a value in .env?

  • Values that never vary per environment — pagination sizes, format strings, business constants. Put them directly in a config file; an env var implies "this changes between dev and prod" to the next reader.
  • High-value secrets on shared or long-lived servers .env is a plaintext file readable by any process running as the same user, and env values leak into phpinfo() dumps and crash reporters. For payment keys and the like, a secrets manager (Vault, AWS Secrets Manager) injected at deploy time is the safer pattern — the security guide weighs the options.
  • Structured or per-tenant configuration — flattening a nested structure into TENANT_3_FEATURE_X=true keys is a smell. That data belongs in the database or a dedicated config store.

Frequently Asked Questions

Why does env() return null in production Laravel?

Because config is cached. php artisan config:cache serializes all config files into bootstrap/cache/config.php and stops loading .env entirely, so env() outside the config/ directory only sees real OS-level variables — for everything else it returns null. Read values through config() in application code and reserve env() for config files.

What is the difference between env() and config() in Laravel?

env() reads the raw environment (and .env before caching); config() reads the arrays defined in config/*.php, which survive config caching. The supported pattern is .env → config file via env() → application code via config(). env() in controllers, models, or Blade templates breaks under config:cache.

Do I need to run anything after editing .env in Laravel?

In local dev, no — values load on the next request (restart queue workers, which hold config in memory). In production with cached config, run php artisan config:cache again (or config:clear) or the edit is invisible. Octane and queue workers additionally need a restart/reload.

What happens if APP_KEY changes?

Everything encrypted with the old key becomes undecryptable: all sessions and signed cookies are invalidated (every user logged out) and encrypted model casts throw DecryptException. Since Laravel 11 you can rotate safely by listing old keys in APP_PREVIOUS_KEYS — Laravel encrypts with the new APP_KEY but still decrypts with previous ones.

How does Laravel pick .env.testing over .env?

When APP_ENV is set to "testing" externally — which php artisan test and PHPUnit do — Laravel looks for .env.testing and loads it instead of .env if present. The same rule generalizes: an externally set APP_ENV=staging makes Laravel prefer .env.staging.

Should I commit .env to git?

No. Commit .env.example with placeholder values instead; the real file holds credentials and belongs in .gitignore (Laravel ships it ignored by default). If a .env with real secrets was ever committed, rotate every credential in it — deleting the file in a later commit does not remove it from history.

References

Lint your .env for syntax mistakes with the env validator, or read the complete .env guide for the format's history and cross-language quirks.

Was this helpful?

Frequently Asked Questions

Why does env() return null in production Laravel?

Because config is cached. php artisan config:cache serializes all config files into bootstrap/cache/config.php and stops loading .env entirely, so env() outside the config/ directory only sees real OS-level variables — for everything else it returns null. Read values through config() in application code and reserve env() for config files.

What is the difference between env() and config() in Laravel?

env() reads the raw environment (and .env before caching); config() reads the arrays defined in config/*.php, which survive config caching. The supported pattern is .env → config file via env() → application code via config(). env() in controllers, models, or Blade templates breaks under config:cache.

Do I need to run anything after editing .env in Laravel?

In local dev, no — values load on the next request (restart queue workers, which hold config in memory). In production with cached config, run php artisan config:cache again (or config:clear) or the edit is invisible. Octane and queue workers additionally need a restart/reload.

What happens if APP_KEY changes?

Everything encrypted with the old key becomes undecryptable: all sessions and signed cookies are invalidated (every user logged out) and encrypted model casts throw DecryptException. Since Laravel 11 you can rotate safely by listing old keys in APP_PREVIOUS_KEYS — Laravel encrypts with the new APP_KEY but still decrypts with previous ones.

How does Laravel pick .env.testing over .env?

When APP_ENV is set to "testing" externally — which php artisan test and PHPUnit do — Laravel looks for .env.testing and loads it instead of .env if present. The same rule generalizes: an externally set APP_ENV=staging makes Laravel prefer .env.staging.

Should I commit .env to git?

No. Commit .env.example with placeholder values instead; the real file holds credentials and belongs in .gitignore (Laravel ships it ignored by default). If a .env with real secrets was ever committed, rotate every credential in it — deleting the file in a later commit does not remove it from history.

Stay up to date

Get notified about new guides, tools, and cheatsheets.