Laravel Config: The env() Rule That Prevents Production Bugs
Series: Every Laravel Project Should Have These Building Blocks
Part: 2 of 35 | Level: Beginner | Prerequisites: Folder Structure
Table of Contents
What You’ll Learn
- The single rule that prevents environment-related bugs
- How to organize
config/as your application grows - When to create new config files vs. extending
services.php - How to properly expose third-party service config
- How to validate that required config values are present
The One Rule
Never call
env()outside ofconfig/files.
That’s it. That’s the entire configuration strategy.
If you follow this one rule consistently, you eliminate an entire category of bugs. If you break it even once, you’ll spend an afternoon wondering why your config is returning null after running php artisan config:cache.
Here’s why: when you run config:cache, Laravel compiles all your config files into a single cached file and stops reading .env entirely. If your code calls env('STRIPE_KEY') directly, it will return null in a cached environment – which is every production server.
// ❌ Wrong — breaks after config:cache
class StripePaymentService
{
public function __construct()
{
$this->apiKey = env('STRIPE_SECRET_KEY'); // null in production!
}
}
// ✅ Correct — always works
class StripePaymentService
{
public function __construct()
{
$this->apiKey = config('services.stripe.secret'); // reads from cache
}
}
The config/ Folder Structure
Laravel ships with a set of standard config files. The ones you’ll interact with most are:
config/
├── app.php # Application name, timezone, locale, debug flag
├── auth.php # Guards, providers, password brokers
├── cache.php # Default cache store, TTLs
├── database.php # Database connections
├── filesystems.php # Disk definitions (local, s3, etc.)
├── logging.php # Log channels — covered in a dedicated article
├── mail.php # Mailer config, default from address
├── queue.php # Queue connections and default queue
├── services.php # ← Third-party service credentials
└── session.php # Session driver and TTL
The most important one to understand as your app grows is services.php.
config/services.php – Your Third-Party Service Registry
services.php is Laravel’s designated place for external service credentials. It ships mostly empty:
// config/services.php (default)
return [
'mailgun' => [...],
'postmark' => [...],
'ses' => [...],
];
Every third-party service your application uses – Stripe, Twilio, AWS, a custom API – should have its credentials registered here:
// config/services.php
return [
'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
'whm' => [
'host' => env('WHM_HOST'),
'username' => env('WHM_USERNAME'),
'api_token' => env('WHM_API_TOKEN'),
'verify_ssl' => env('WHM_VERIFY_SSL', true),
],
'name_api' => [
'api_key' => env('NAME_API_KEY'),
'base_url' => env('NAME_API_URL', 'https://api.name.com/v4/'),
],
'ollama' => [
'api_url' => env('OLLAMA_API_URL', 'http://localhost:11434/api/chat'),
'model' => env('OLLAMA_MODEL', 'llama3.2'),
'timeout' => env('OLLAMA_TIMEOUT', 90),
'context_char_limit' => env('OLLAMA_CONTEXT_CHAR_LIMIT', 3000),
'temperature' => env('OLLAMA_TEMPERATURE', 0.1),
],
'admin' => [
'email' => env('ADMIN_EMAIL'),
],
];
Then access it in code:
config('services.stripe.secret')
config('services.whm.host')
config('services.ollama.model')
When to Create a New Config File
The rule is simple: create a new config file when the configuration for a feature is complex enough that it deserves its own namespace.
Don’t create a new file for simple key/value pairs. These belong in services.php:
// ❌ Overkill — this doesn't need its own file
// config/stripe.php
return ['key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET')];
// ✅ Just add it to services.php
'stripe' => ['key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET')],
Do create a new file when you have a domain with many settings that would clutter services.php. Good examples from real projects:
// config/logging.php — Laravel ships this by default; customize it for named channels
// config/error_mail.php — configuration for the ErrorReporter service
// config/knowledge.php — configuration for AI knowledge base feature
// config/error_mail.php
return [
'sensitive_fields' => [
'password',
'password_confirmation',
'token',
'card_number',
'cvv',
],
];
Validating Required Config at Boot
One underused feature: you can validate that required config values are present when the application boots, rather than getting cryptic errors at runtime.
In AppServiceProvider::boot():
public function boot(): void
{
$this->validateConfig();
}
private function validateConfig(): void
{
$required = [
'services.stripe.secret' => 'STRIPE_SECRET',
'services.whm.api_token' => 'WHM_API_TOKEN',
'mail.support.address' => 'MAIL_SUPPORT_ADDRESS',
];
foreach ($required as $configKey => $envKey) {
if (blank(config($configKey))) {
throw new \RuntimeException(
"Required configuration [{$configKey}] is missing. Set {$envKey} in your .env file."
);
}
}
}
This is especially useful for services that your app simply cannot function without. You’ll get a clear error immediately instead of a null-pointer exception buried in a stack trace.
The .env.example as Documentation
Your .env.example file is documentation. Every variable your application can use should be in it, with a comment explaining what it does:
# Application
APP_NAME="My App"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=
# Stripe Payment Gateway
# Get your keys from https://dashboard.stripe.com/apikeys
STRIPE_KEY=pk_test_
STRIPE_SECRET=sk_test_
STRIPE_WEBHOOK_SECRET=whsec_
# Error Reporting
# Comma-separated list of developer emails to receive exception alerts
MAIL_SUPPORT_ADDRESS=dev@yourcompany.com
A new developer should be able to read .env.example and understand every setting without asking anyone.
Environment-Specific Config Overrides
Sometimes you need a different configuration in testing vs. production. Use the config() helper within AppServiceProvider to override:
// In config/mail.php
'support' => [
'address' => env('MAIL_SUPPORT_ADDRESS', ''),
],
// In .env
MAIL_SUPPORT_ADDRESS=dev@company.com,senior@company.com
// In ErrorReporter — reads it as a comma-separated list
$emails = config('mail.support.address', []);
if (is_string($emails)) {
$emails = explode(',', $emails);
}
For test environments, use .env.testing:
# .env.testing
APP_ENV=testing
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
MAIL_SUPPORT_ADDRESS= # Empty — don't send real emails during tests
Frequently Asked Questions
Why does env() return null in Laravel production?
Because env() only reads from your .env file, and production servers almost always run php artisan config:cache. Once config is cached, Laravel stops reading .env entirely and serves values from the compiled cache file instead. Any env() call outside a config/ file then returns null.
The fix is the core rule of this article: never call env() outside config/ files. Reference your values through config('services.stripe.secret') instead, which always reads from the cache. This is a silent bug – it works perfectly in local development and only breaks in production.
What does php artisan config:cache do?
It compiles every file in your config/ directory into a single cached PHP file that Laravel loads on each request, skipping the work of parsing multiple config files and the .env file at runtime.
This is a meaningful performance win in production. The trade-off: once config is cached, .env is no longer read, so any direct env() call in your application code returns null. Run php artisan config:clear to remove the cache during local debugging, and run config:cache locally once in a while to catch env() misuse before it reaches production.
Where should I store API keys in Laravel?
Store the raw keys as variables in your .env file (which is never committed to version control), then expose them through config/services.php so your code reads them via the config() helper rather than env().
For example, put STRIPE_SECRET=sk_... in .env, map it in services.php as 'stripe' => ['secret' => env('STRIPE_SECRET')], and access it with config('services.stripe.secret'). Document every key in .env.example (without real values) so new developers know what to set. Never hardcode keys in code or call env() for them outside config files.
How do I fix config returning null after config:cache?
Find any place in your application code – services, controllers, middleware — that calls env() directly, and replace it with a config() call backed by a config file entry.
Move the variable into the appropriate config file (usually config/services.php), then run php artisan config:clear followed by php artisan config:cache to rebuild. To prevent it happening again, validate required config at boot in AppServiceProvider so a missing value throws a clear error immediately instead of surfacing as a null deep in your code.
Key Takeaways
- Never call
env()outsideconfig/files – this is non-negotiable. It will break afterconfig:cache. - All third-party credentials go in
config/services.php– don’t create a new file unless you have 5+ related settings. - Access config via
config('key.subkey')everywhere in your application code. - Your
.env.exampleis documentation – keep it up to date and well-commented. - Validate required config at boot for services your app can’t live without.
Tips and Gotchas
⚠️ Warning:
env()called directly in application code (outsideconfig/) breaksphp artisan config:cache. When config is cached,env()always returnsnull. This is a silent production bug that works perfectly in development.
💡 Tip: Run
php artisan config:cachelocally occasionally to catchenv()misuse before it reaches production.
🔥 Expert Note: Group all third-party service credentials in
config/services.php– not separate per-service config files. One file to open, one place to look. Laravel’s built-inBroadcast, andSocialiteintegrations already follow this convention.
Further Reading
← Folder Structure | Next: AppServiceProvider — The Backbone →
2 Comments