Managing WordPress across multiple environments is one of those things that separates developers who ship clean code from those who push straight to production and pray. Get your environment config wrong and you end up with debug output leaking on a live site, or production database credentials checked into Git.
This guide covers every practical technique: conditional constants, the wp-config-local.php split pattern, WP_ENVIRONMENT_TYPE, and pulling settings from .env files. This is Article 6 of the wp-config Mastery series. If you want to go deeper on constants before environment splitting, the wp-config.php hidden settings guide covers 15 constants most developers overlook.
Why Environment-Specific Configuration Matters
A single wp-config.php that serves all three environments is a liability. Consider what each environment actually needs in practice.
Development needs verbose error output. You want full stack traces when something breaks, you want SCRIPT_DEBUG on so you load unminified JavaScript, and you want the database pointing at your local instance. You probably have a different domain too, something like myproject.local or localhost:8080.
Staging needs to behave like production but with guardrails. Debug output stays off so you do not accidentally expose server paths or query details in responses. The URL points to a staging subdomain. Outbound email needs to be suppressed so your test workflows do not spam real customers. Error logging should write to a log file rather than displaying to users.
Production needs everything locked down. WP_DEBUG is false. DISALLOW_FILE_EDIT blocks the theme and plugin editor. DISALLOW_FILE_MODS prevents plugin and theme installs from the admin. The database credentials are strong, the salts are unique, and nothing from development bleeds through.
Hardcoding any of those values without environment detection causes problems the moment you pull a database backup to your local or deploy to a new server.
The WP_ENVIRONMENT_TYPE Constant
WordPress 5.5 introduced WP_ENVIRONMENT_TYPE. Set it and core features adapt automatically. Valid values are local, development, staging, and production.
// wp-config.php
define( 'WP_ENVIRONMENT_TYPE', 'development' );
Read it back using the helper function:
if ( wp_get_environment_type() === 'production' ) {
// Only runs on production
}
When WP_ENVIRONMENT_TYPE is local or development, WordPress core disables fatal error protection (so you see full stack traces) and enables script debug mode automatically. You get useful behavior without extra constants.
Plugins can also read wp_get_environment_type() to conditionally activate features. A caching plugin might skip warming the cache in development. A logging plugin might enable verbose logging in staging. By setting WP_ENVIRONMENT_TYPE correctly, you unlock this plugin-level awareness without any custom detection logic.
Conditional Constants Approach
The oldest technique is detecting the current server hostname and branching your constants. It works without any extra tooling or dependencies.
// Detect environment by hostname
$http_host = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '';
if ( 'localhost' === $http_host || strpos( $http_host, '.local' ) !== false ) {
// --- DEVELOPMENT ---
define( 'WP_ENVIRONMENT_TYPE', 'development' );
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', true );
define( 'SCRIPT_DEBUG', true );
define( 'DB_NAME', 'wp_dev' );
define( 'DB_USER', 'root' );
define( 'DB_PASSWORD', '' );
define( 'DB_HOST', 'localhost' );
} elseif ( strpos( $http_host, 'staging.' ) === 0 ) {
// --- STAGING ---
define( 'WP_ENVIRONMENT_TYPE', 'staging' );
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_LOG', true ); // log errors silently
define( 'WP_DEBUG_DISPLAY', false );
define( 'DB_NAME', 'wp_staging' );
define( 'DB_USER', 'staging_user' );
define( 'DB_PASSWORD', 'staging_password' );
define( 'DB_HOST', '127.0.0.1' );
} else {
// --- PRODUCTION ---
define( 'WP_ENVIRONMENT_TYPE', 'production' );
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_LOG', false );
define( 'WP_DEBUG_DISPLAY', false );
define( 'DB_NAME', 'wp_production' );
define( 'DB_USER', 'prod_user' );
define( 'DB_PASSWORD', 'strong_password_here' );
define( 'DB_HOST', '127.0.0.1' );
}
This approach has a downside: credentials from all three environments live in the same file. If that file ever leaks via a misconfigured server, a Git accident, or a backup that gets too broadly shared, all three sets of credentials are exposed at once. For a solo developer on a low-stakes project it might be acceptable. For a team with multiple environments and real customer data, it is not.
The wp-config-local.php Pattern
The cleanest general-purpose solution: keep a shared wp-config.php committed to version control that contains no credentials, and load a machine-local file for everything environment-specific. The local file never gets committed.
This pattern has been around for years in the WordPress developer community under various names. Some teams call the local file wp-config-local.php, others call it local-config.php or wp-config.env.php. The mechanism is the same regardless of what you call it.
File Structure
.
├── wp-config.php # Committed to Git
├── wp-config-local.php # Gitignored, machine-specific
├── wp-config-local-example.php # Committed, template only
└── .gitignore # Contains wp-config-local.php
wp-config.php (shared)
wp-config-local.php (development example)
Add wp-config-local.php to your .gitignore immediately. Developers clone the repo, copy the example file, fill in their local values, and never touch the shared file. New developer onboarding becomes: clone, copy example, fill in database credentials, done.
Handling WP_SITEURL and WP_HOME Per Environment
Two constants that must change per environment are WP_SITEURL and WP_HOME. When defined in wp-config.php, they override the database values. This is exactly what you want: it means a database sync from production to local will not redirect you back to the live URL as soon as you boot the site.
Without this override, pulling production data to a local environment requires running a database search-replace on the URL. With these constants set in wp-config-local.php, the local machine just works immediately after a database pull.
// wp-config-local.php on development
define( 'WP_SITEURL', 'http://mysite.local' );
define( 'WP_HOME', 'http://mysite.local' );
// wp-config-local.php on staging
define( 'WP_SITEURL', 'https://staging.mysite.com' );
define( 'WP_HOME', 'https://staging.mysite.com' );
Verify they are loading from config rather than the database with WP-CLI:
wp option get siteurl
wp option get home
Integrating .env Files
Some teams prefer storing credentials in a .env file and loading them into PHP. This aligns with the Twelve-Factor App methodology and works well when your deployment pipeline already handles .env injection. CI/CD tools, container orchestration platforms, and many hosting providers inject environment variables at the OS level, which PHP can read through getenv() or $_ENV.
Option 1: Use php-dotenv (Composer)
composer require vlucas/phpdotenv
// wp-config.php, before any defines
require_once __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable( __DIR__ );
$dotenv->load();
$dotenv->required( [ 'DB_NAME', 'DB_USER', 'DB_PASSWORD' ] );
define( 'WP_ENVIRONMENT_TYPE', $_ENV['WP_ENVIRONMENT_TYPE'] ?? 'production' );
define( 'DB_NAME', $_ENV['DB_NAME'] );
define( 'DB_USER', $_ENV['DB_USER'] );
define( 'DB_PASSWORD', $_ENV['DB_PASSWORD'] );
define( 'DB_HOST', $_ENV['DB_HOST'] ?? '127.0.0.1' );
The required() call causes a hard exception if any listed variable is missing from the .env file. That is useful: it fails early and loudly on a misconfigured environment rather than silently connecting to the wrong database.
Option 2: Native getenv() Without Composer
If you need to avoid Composer (some hosting environments have restrictions, or you simply want zero dependencies), you can parse a .env file with a small loader function:
// Minimal .env loader, no Composer required
function load_env_file( string $path ): void {
if ( ! file_exists( $path ) ) {
return;
}
$lines = file( $path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
foreach ( $lines as $line ) {
if ( str_starts_with( trim( $line ), '#' ) ) {
continue; // skip comments
}
[ $name, $value ] = array_pad( explode( '=', $line, 2 ), 2, '' );
$name = trim( $name );
$value = trim( $value, " \t\n\r\0\x0B'\"" );
if ( '' !== $name ) {
putenv( "{$name}={$value}" );
$_ENV[ $name ] = $value;
$_SERVER[ $name ] = $value;
}
}
}
load_env_file( __DIR__ . '/.env' );
define( 'DB_NAME', getenv( 'DB_NAME' ) );
define( 'DB_USER', getenv( 'DB_USER' ) );
define( 'DB_PASSWORD', getenv( 'DB_PASSWORD' ) );
define( 'DB_HOST', getenv( 'DB_HOST' ) ?: '127.0.0.1' );
define( 'WP_ENVIRONMENT_TYPE', getenv( 'WP_ENVIRONMENT_TYPE' ) ?: 'production' );
Your .env file structure:
WP_ENVIRONMENT_TYPE=development
DB_NAME=wp_local
DB_USER=root
DB_PASSWORD=
DB_HOST=localhost
WP_SITEURL=http://mysite.local
WP_HOME=http://mysite.local
Always add .env to .gitignore. Keep a .env.example with blank or placeholder values committed. When someone joins the team, they copy the example and fill in their local values.
Using WP-CLI to Verify Config Values
Once your config is in place, verify constants are loaded correctly using WP-CLI without touching the browser. This is especially useful after a deployment to confirm the right values are in effect.
# Check all defined constants from wp-config.php
wp config list
# Check a specific constant
wp config get WP_ENVIRONMENT_TYPE
wp config get WP_DEBUG
# Get the environment type via WP function
wp eval 'echo wp_get_environment_type();'
The wp config list command outputs every constant defined in wp-config.php along with its value and whether it is a constant or a variable. Run it on each environment after deployment to confirm values loaded as expected. If a value is wrong, you catch it before users do.
Per-Environment Constants Reference
| Constant | Development | Staging | Production |
|---|---|---|---|
WP_DEBUG | true | false | false |
WP_DEBUG_LOG | true | true | false |
WP_DEBUG_DISPLAY | true | false | false |
SCRIPT_DEBUG | true | false | false |
SAVEQUERIES | true | false | false |
DISALLOW_FILE_EDIT | false | true | true |
DISALLOW_FILE_MODS | false | false | true |
WP_AUTO_UPDATE_CORE | false | 'minor' | 'minor' |
WP_CACHE | false | true | true |
Controlling File Upload and Memory Limits
PHP memory limits and upload sizes often differ across environments. Development machines typically have more generous limits; shared hosting production servers might be tighter. You can control the WordPress-level limits directly in wp-config.php without touching PHP ini files, as long as you stay at or below the PHP ceiling.
// wp-config-local.php on development
define( 'WP_MEMORY_LIMIT', '512M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' ); // admin processes
@ini_set( 'upload_max_filesize', '128M' );
@ini_set( 'post_max_size', '128M' );
Verify the actual limit with:
wp eval 'echo WP_MEMORY_LIMIT;'
wp eval 'echo ini_get("memory_limit");'
Disabling Automatic Updates Per Environment
Automatic core updates on staging should be off so you control what version you are testing against. On production, minor version auto-updates are generally safe and keep you current on security patches.
// Development and staging: no auto-updates
define( 'WP_AUTO_UPDATE_CORE', false );
define( 'AUTOMATIC_UPDATER_DISABLED', true );
// Production: allow minor version auto-updates only
define( 'WP_AUTO_UPDATE_CORE', 'minor' );
Cron and Scheduled Tasks Across Environments
WordPress uses pseudo-cron: scheduled tasks only fire when a page request comes in. On development, where traffic is minimal or non-existent, scheduled events may go unfired for long stretches. Two solutions work well depending on your setup.
First option: disable WordPress pseudo-cron and run a real server crontab. This gives you predictable task timing and avoids tasks firing on a slow-loading request from a real user.
// Disable WordPress pseudo-cron
define( 'DISABLE_WP_CRON', true );
// In your server crontab:
// */5 * * * * /usr/bin/php /path/to/wordpress/wp-cron.php
Second option: use WP-CLI to trigger events manually during development when you need to test a scheduled workflow:
# Run all due cron events
wp cron event run --due-now
# Run a specific hook manually
wp cron event run my_scheduled_hook
# List all scheduled events
wp cron event list
Staging-Specific Hardening
Staging is often forgotten when it comes to security and operational hygiene. It should mirror production as closely as possible while having a few important safeguards.
Suppress outbound email on staging. You do not want automated order confirmation emails or password reset links going to real users from a staging environment. Add a mail intercept plugin, or drop a small mu-plugin that intercepts all outbound mail and either discards it or redirects it to a development inbox.
Block search engine indexing. Even if your staging URL is not publicly known, it is good practice to keep staging out of search indexes. The Settings > Reading > Search Engine Visibility setting handles this for most cases.
Use different auth salts than production. If staging and production share auth salts, a session cookie generated on staging will authenticate on production. This creates a path for privilege escalation if staging is less controlled than production.
// wp-config-local.php on staging
define( 'WP_ENVIRONMENT_TYPE', 'staging' );
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'DISALLOW_FILE_EDIT', true );
// Staging-specific salts, different from production
define( 'AUTH_KEY', 'staging-unique-auth-key-here' );
define( 'SECURE_AUTH_KEY', 'staging-unique-secure-auth-key-here' );
Security Keys Per Environment
Security keys and salts sign session cookies and nonces. If dev, staging, and production all share the same keys, a session created on staging remains valid on production. That is a privilege escalation risk. The WordPress security hardening guide covers the full set of server-level and wp-config protections worth applying to production.
Generate a unique set for each environment:
# Generate fresh salts and output as PHP constants
wp config shuffle-salts
# Or fetch directly from the WordPress API
curl https://api.wordpress.org/secret-key/1.1/salt/
Store the production keys in your production wp-config-local.php or inject via .env on the server. They should never appear in a committed file. Regenerate them any time you suspect a breach or after a developer leaves the team.
CI/CD Integration
In automated pipelines (GitHub Actions, Bitbucket Pipelines, DeployHQ), you typically inject config via environment variables rather than committing any config file. The platform injects them into the process environment at deploy time, and your wp-config.php reads them via getenv() exactly like it would read from a .env file.
# GitHub Actions workflow snippet
env:
DB_NAME: ${{ secrets.STAGING_DB_NAME }}
DB_USER: ${{ secrets.STAGING_DB_USER }}
DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
WP_ENVIRONMENT_TYPE: staging
The deploy step pulls these from GitHub Secrets and injects them. Your wp-config.php does not change between deployments; only the environment variables change. This pattern works identically for Bitbucket Pipelines, GitLab CI, CircleCI, or any other pipeline tool that supports secret injection.
Multisite and Subdirectory Installations
For WordPress Multisite, the domain constants change per environment. With the wp-config-local.php pattern, each machine sets its own DOMAIN_CURRENT_SITE without touching the shared file, which prevents Multisite redirect loops when syncing databases across environments.
// Development multisite config in wp-config-local.php
define( 'DOMAIN_CURRENT_SITE', 'mynetwork.local' );
define( 'WP_ALLOW_MULTISITE', true );
define( 'MULTISITE', true );
define( 'SUBDOMAIN_INSTALL', false );
define( 'PATH_CURRENT_SITE', '/' );
define( 'SITE_ID_CURRENT_SITE', 1 );
define( 'BLOG_ID_CURRENT_SITE', 1 );
Common Mistakes and How to Fix Them
- WP_DEBUG left on in production: Move all debug constants to
wp-config-local.phpwhere they can never reach production. The shared file should not contain any debug constants at all. - Committing wp-config-local.php accidentally: Add a pre-commit hook that checks for the file. A one-line check in
.git/hooks/pre-commitwill catch it:git diff --cached --name-only | grep -i wp-config-local && exit 1 - Staging and production sharing auth salts: Run
wp config shuffle-saltson each new environment immediately after setup. Make it part of your environment provisioning script. - Missing wp-config-local-example.php: Every constant you add to the local config needs to appear in the example file with a placeholder value. Otherwise new developers have no idea what fields are expected.
- Forgetting to set WP_SITEURL on local after a database pull: If your
wp-config-local.phpdefinesWP_SITEURLandWP_HOME, this handles itself. If not, you need awp search-replaceafter every database sync.
Choosing the Right Approach for Your Team
For most WordPress projects, the wp-config-local.php split is the pragmatic choice. It keeps the shared file lean, plays nicely with Git, and requires no build tooling. The conditional hostname approach is fine for solo projects where you are the only developer. The .env approach pays off when you already have a pipeline that injects secrets and you want consistency with how other apps in your stack are configured.
| Approach | Best For | Downside |
|---|---|---|
| Conditional by hostname | Solo devs, simple setups | All credentials in one file |
| wp-config-local.php | Teams, most projects | Requires onboarding discipline |
| .env + loader | CI/CD-heavy pipelines | Adds a dependency or custom code |
A Production-Ready Shared Template
Here is a complete shared wp-config.php template that pulls together all the patterns from this guide. It is designed to be committed to version control with no credentials and to fail safely to production defaults when no local file is present:
The key pattern throughout this template is using if ( ! defined() ) guards around every constant. Since wp-config-local.php is loaded first, any constant defined there takes precedence. When the local file is absent (as on a fresh production server before provisioning), the shared file's values apply. This makes the default behavior safe rather than requiring you to explicitly handle the missing-local-file case.
Quick Checklist Before You Deploy
wp-config-local.phpand.envare in.gitignoreWP_DEBUGisfalseon staging and productionWP_DEBUG_DISPLAYisfalseon staging and production- Production uses unique salts not shared with staging or development
DISALLOW_FILE_EDITistrueon staging and production- Run
wp config liston each environment to verify values after deploy - Staging has outbound email suppressed
- Staging is excluded from search indexing
wp-config-local-example.phpis committed and up to date
Testing Your Configuration Before Going Live
Before deploying to a new environment, a quick smoke test catches misconfigured constants early. The goal is to confirm environment detection is working, the right database is connected, and no debug output would leak to visitors.
Run wp config list immediately after each deployment. Scan the output for WP_DEBUG being true on any non-development environment. Check that WP_ENVIRONMENT_TYPE matches the server you are on. If a value looks wrong, stop and fix it before touching anything else. A configuration bug caught before a site goes live costs minutes to fix. The same bug found after a breach or a support escalation costs significantly more.
Test the local file loading explicitly by temporarily adding a constant with a known value to wp-config-local.php, then running wp eval 'echo MY_TEST_CONSTANT;'. If it echoes the value, the loading mechanism is working. Remove the test constant and proceed with confidence.
Wrapping Up
Proper environment configuration is the foundation of a maintainable WordPress codebase. The options range from simple hostname detection to a full .env-driven setup depending on your team size and pipeline maturity. Whatever approach you pick, the principles stay the same: never commit credentials, never leave debug output on in production, and use WP_ENVIRONMENT_TYPE to let WordPress core adapt to your environment automatically.
Getting this right the first time on a new project takes maybe an hour. Fixing it later after a credential leak or after tracking down a production debug output issue takes much longer. The investment in a clean environment configuration pays dividends every time someone joins the team, every time you sync a database, and every time you deploy.
This wraps up the wp-config Mastery series. If you missed the earlier articles, start with Article 1 on understanding what wp-config.php actually does and work through each constant category from there. Each article builds on the previous so the full series gives you a complete picture of WordPress configuration management at the expert level.
admin-ajax.php WordPress Dev Workflow WordPress Staging WP Environment Config
Last modified: April 20, 2026