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

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

The Most Common .env Mistakes

Mistake 1: Committing .env to Version Control

Mistake 2: Using env() Outside of Config Files

Mistake 3: Identical Secrets Across Environments

Mistake 4: Leaving Debug Mode Enabled in Production

Mistake 5: Storing .env Backups on the Server

The Multi-Environment Strategy

What Should Differ Between Environments

The .env.example Contract

How Deploynix Handles Environment Variables

Encrypted Storage

Per-Site Configuration

Edit and Deploy Workflow

API Access

Advanced Strategies

Environment-Specific Config Files

Feature Flags via Environment Variables

Secret Rotation Without Downtime

Validation at Boot Time

The .env Audit Checklist

Conclusion 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. 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: The .env.example file — containing variable names without secret values — should be committed. It serves as documentation for what environment variables your application needs. 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. 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_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. 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. 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. 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. https://staging.example.com 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. 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. 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. Deploynix provides a dedicated interface for managing environment variables on each site. Here is how it addresses the common challenges: 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. 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. 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. 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: 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: This keeps environment-specific logic in version-controlled config files rather than requiring different .env values. Environment variables are a simple way to control feature flags: 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. When rotating secrets (database passwords, API keys), the concern is always downtime during the transition. Here is a safe rotation pattern: Add validation to your application that checks for required environment variables at boot time rather than failing at the point of use: 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. Before every production deployment, verify: 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. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

.env .env.* !.env.example .env .env.* !.env.example .env .env.* !.env.example // 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'); // 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'); // 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'); APP_DEBUG=false APP_ENV=production APP_DEBUG=false APP_ENV=production APP_DEBUG=false APP_ENV=production # 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= # 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= # 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= // config/logging.php 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => app()->environment('production') ? ['daily', 'slack'] : ['daily'], ], ], // config/logging.php 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => app()->environment('production') ? ['daily', 'slack'] : ['daily'], ], ], // config/logging.php 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => app()->environment('production') ? ['daily', 'slack'] : ['daily'], ], ], FEATURE_NEW_DASHBOARD=true FEATURE_BETA_API=false FEATURE_NEW_DASHBOARD=true FEATURE_BETA_API=false FEATURE_NEW_DASHBOARD=true FEATURE_BETA_API=false // 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 } // 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 } // 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 } // 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}"); } } // 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}"); } } // 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}"); } } - 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 - Setting environment variables as part of a CI/CD pipeline - Rotating secrets automatically - Syncing non-sensitive configuration across environments - 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. - For API keys: Many services allow multiple active API keys. Generate a new key, update .env, deploy, then revoke the old key. - 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. - 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