Laravel Config: The env() Rule That Prevents Production Bugs

Laravel Config: The env() Rule That Prevents Production Bugs

Reading Time: 5 minutes

Series: Every Laravel Project Should Have These Building Blocks 
Part: 2 of 35 | Level: Beginner | Prerequisites: Folder Structure


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 of config/ 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() outside config/ files – this is non-negotiable. It will break after config: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.example is 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 (outside config/) breaks php artisan config:cache. When config is cached, env() always returns null. This is a silent production bug that works perfectly in development.

💡 Tip: Run php artisan config:cache locally occasionally to catch env() 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-in MailBroadcast, and Socialite integrations already follow this convention.

Further Reading


← Folder Structure | Next: AppServiceProvider — The Backbone →

Laravel Config: The env() Rule That Prevents Production Bugs

Oh hi there 👋
It’s nice to meet you.

Sign up to receive awesome content in your inbox.

We don’t spam! Read our privacy policy for more info.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments