DEV Community

Cover image for Environment Variables Done Right: Managing .env Across Staging and Production
Deploynix
Deploynix

Posted on • Originally published at deploynix.io

Environment Variables Done Right: Managing .env Across Staging and Production

The .env file is one of the first things every Laravel developer encounters. It is also one of the first things to cause problems in production. What starts as a convenient place to store your database password quickly becomes a tangled mess of secrets, feature flags, service URLs, and configuration values that differ between your local machine, staging server, and production environment.

The twelve-factor app methodology established the principle: store configuration in the environment, not in code. Laravel embraced this through its .env file and config() helper system. But the methodology says nothing about how to manage those environment variables across multiple environments, how to keep secrets secure, or how to avoid the common pitfalls that lead to production incidents.

This guide covers the mistakes we see most often, the strategies that work, and how Deploynix simplifies the entire process.

The Most Common .env Mistakes

Mistake 1: Committing .env to Version Control

This is the cardinal sin, and it still happens regularly. A developer initializes a new repository, forgets to check .gitignore, and pushes their .env file to GitHub. Even if you delete it in a subsequent commit, the file remains in the git history. Automated scrapers scan public repositories for committed secrets, and they are fast — credentials pushed to a public repo can be exploited within minutes.

Check your .gitignore right now. It should contain:

.env
.env.*
!.env.example
Enter fullscreen mode Exit fullscreen mode

The .env.example file — containing variable names without secret values — should be committed. It serves as documentation for what environment variables your application needs.

Mistake 2: Using env() Outside of Config Files

Laravel's documentation is clear on this: the env() function should only be used inside configuration files in the config/ directory. Everywhere else, use config(). The reason is caching. When you run php artisan config:cache, Laravel compiles all configuration files into a single cached file and stops reading .env entirely. Any env() call outside of a config file will return null when the config is cached.

This is a particularly insidious bug because it works perfectly in development (where you rarely cache config) and breaks silently in production.

// Wrong - will break with config caching
$apiKey = env('THIRD_PARTY_API_KEY');

// Right - works with config caching
// In config/services.php:
'third_party' => [
    'api_key' => env('THIRD_PARTY_API_KEY'),
],

// In your code:
$apiKey = config('services.third_party.api_key');
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Identical Secrets Across Environments

Using the same database password, API key, or encryption key across development, staging, and production is a security liability. If your staging environment is compromised (and staging environments are often less secured), the attacker now has production credentials.

Every environment should have unique secrets. Every single one. This includes:

  • APP_KEY — each environment needs its own encryption key
  • Database credentials
  • Third-party API keys (use sandbox/test keys for non-production)
  • Mail credentials
  • Cache and queue connection passwords

Mistake 4: Leaving Debug Mode Enabled in Production

APP_DEBUG=true in production exposes stack traces, environment variables, and database queries to anyone who triggers an error. This is not just a security issue — it is an information disclosure vulnerability that gives attackers a detailed map of your application's internals.

APP_DEBUG=false
APP_ENV=production
Enter fullscreen mode Exit fullscreen mode

These two lines should be verified on every production deployment. Deploynix sets these correctly by default, but if you manage environment variables manually, double-check them.

Mistake 5: Storing .env Backups on the Server

Developers sometimes create .env.backup or .env.old files on the server before making changes. If your web server is misconfigured (or if a vulnerability allows file reading), these backup files may be accessible via HTTP. Unlike .env, which Nginx is typically configured to block, .env.backup may not be covered by your deny rules.

Never create .env backup files on the server. Use your deployment platform to manage environment variable history instead.

The Multi-Environment Strategy

A typical Laravel project has at least three environments: local development, staging, and production. Each needs different configuration, and managing these differences is where most teams struggle.

What Should Differ Between Environments

Variable

Local

Staging

Production

APP_ENV

local

staging

production

APP_DEBUG

true

true

false

APP_URL

http://localhost

https://staging.example.com

https://example.com

DB_HOST

127.0.0.1

staging-db-host

production-db-host

DB_PASSWORD

secret

unique-staging-pw

unique-production-pw

MAIL_MAILER

log

smtp (Mailtrap)

smtp (SES/Postmark)

QUEUE_CONNECTION

sync

redis

redis

LOG_CHANNEL

stack

stack

stack

LOG_LEVEL

debug

debug

warning

Notice that staging mirrors production in many ways (same queue driver, similar database setup) but uses different credentials and may use test/sandbox versions of third-party services.

The .env.example Contract

Your .env.example file is a contract between your application and its operators. Every environment variable your application needs should be listed here, with sensible defaults where appropriate and clear comments for variables that require specific values.

# Application
APP_NAME="My Laravel 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=my_app
DB_USERNAME=root
DB_PASSWORD=

# Third-Party Services
# Get your API key from https://dashboard.service.com
THIRD_PARTY_API_KEY=
THIRD_PARTY_WEBHOOK_SECRET=
Enter fullscreen mode Exit fullscreen mode

When you add a new environment variable to your application, add it to .env.example in the same commit. This ensures that anyone deploying the updated code knows they need to set the new variable.

How Deploynix Handles Environment Variables

Deploynix provides a dedicated interface for managing environment variables on each site. Here is how it addresses the common challenges:

Encrypted Storage

Environment variables entered through the Deploynix dashboard or API are encrypted at rest. They are never stored in plain text in the platform's database, and they are transmitted to your servers over encrypted SSH connections.

Per-Site Configuration

Each site on a server has its own set of environment variables. This means you can run staging and production on different servers (as you should) with completely independent configurations, all managed from a single dashboard.

Edit and Deploy Workflow

When you update environment variables through Deploynix, the changes are synced immediately to the .env file on your server. For Laravel to pick up the changes, you can either trigger a full deployment (which clears and rebuilds the config cache) or use the "Reload Config" button in the site dashboard, which runs php artisan config:cache without a full redeploy.

API Access

For teams that prefer infrastructure-as-code, Deploynix's API (authenticated with Sanctum tokens) allows you to manage environment variables programmatically. This is useful for:

  • Setting environment variables as part of a CI/CD pipeline
  • Rotating secrets automatically
  • Syncing non-sensitive configuration across environments

Advanced Strategies

Environment-Specific Config Files

Laravel allows you to create environment-specific configuration. While .env handles the basics, some teams create config files that vary behavior based on APP_ENV:

// config/logging.php
'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => app()->environment('production')
            ? ['daily', 'slack']
            : ['daily'],
    ],
],
Enter fullscreen mode Exit fullscreen mode

This keeps environment-specific logic in version-controlled config files rather than requiring different .env values.

Feature Flags via Environment Variables

Environment variables are a simple way to control feature flags:

FEATURE_NEW_DASHBOARD=true
FEATURE_BETA_API=false
Enter fullscreen mode Exit fullscreen mode
// config/features.php
return [
    'new_dashboard' => env('FEATURE_NEW_DASHBOARD', false),
    'beta_api' => env('FEATURE_BETA_API', false),
];

// In your code
if (config('features.new_dashboard')) {
    // Show new dashboard
}
Enter fullscreen mode Exit fullscreen mode

This approach lets you enable features per-environment without code changes. Enable on staging first, verify it works, then enable on production by updating the environment variable and redeploying.

Secret Rotation Without Downtime

When rotating secrets (database passwords, API keys), the concern is always downtime during the transition. Here is a safe rotation pattern:

  1. For database passwords: Create a new database user with the new password. Update the .env on Deploynix. Deploy with zero-downtime deployment (the new process uses the new credentials while the old process finishes its requests with the old credentials). Once the deployment is complete, remove the old database user.
  2. For API keys: Many services allow multiple active API keys. Generate a new key, update .env, deploy, then revoke the old key.
  3. For APP_KEY: This is the most sensitive rotation because it affects encrypted data. Laravel's php artisan key:generate can be combined with the APP_PREVIOUS_KEYS environment variable to support graceful key rotation without immediately invalidating all encrypted data.

Validation at Boot Time

Add validation to your application that checks for required environment variables at boot time rather than failing at the point of use:

// In a service provider boot() method
$required = ['APP_KEY', 'DB_HOST', 'DB_DATABASE', 'MAIL_MAILER'];

foreach ($required as $var) {
    if (empty(config(strtolower(str_replace('_', '.', $var))))) {
        throw new RuntimeException("Required environment configuration is missing: {$var}");
    }
}
Enter fullscreen mode Exit fullscreen mode

This gives you an immediate, clear error message during deployment rather than a cryptic failure when a user hits the code path that needs the missing variable.

The .env Audit Checklist

Before every production deployment, verify:

  • APP_ENV is set to production
  • APP_DEBUG is set to false
  • APP_KEY is set and unique to this environment
  • APP_URL matches the actual production URL (including scheme)
  • Database credentials are unique to this environment
  • Mail is configured for a production mail service (not log or array)
  • Queue connection is set to a persistent driver (not sync)
  • Session driver is set appropriately (not file on load-balanced setups)
  • Cache driver is set to a persistent store
  • All third-party API keys are production keys (not sandbox/test)
  • No .env.backup or .env.old files exist on the server

Conclusion

Environment variables are deceptively simple. The mechanics of reading a value from a .env file are trivial, but managing those values securely and consistently across multiple environments is a discipline that requires thought and tooling.

The core principles are straightforward: never commit secrets to version control, never share credentials across environments, always use config() instead of env() in application code, and use a deployment platform that encrypts your secrets and provides a clear workflow for updates.

Deploynix handles the infrastructure side of environment variable management — encrypted storage, per-site configuration, and API access for automation. Your job is to maintain a clean .env.example, rotate secrets regularly, and verify your production configuration before every deployment. Get these habits right, and the .env file becomes what it was always meant to be: a clean separation between your code and its configuration.

Top comments (0)