wp-config.php is the one file in WordPress whose contents must differ per environment. Database credentials, authentication salts, debug switches and cache settings live there, and every one of them differs between local, staging and production. That is why the file cannot be in the repository: commit one version and you have made the others wrong.
Two workable places for environment values
Two arrangements hold up in practice, and both follow the same principle: the repository states what is required, never what the value is.
The first option has wp-config.php read an untracked file, with only a sample of that file committed. The second reads environment variables, which suits containers and, under PHP-FPM, is injected through env[] in the pool configuration.
<?php
// wp-config.php — never committed.
$local = __DIR__ . '/wp-config-local.php';
if ( is_readable( $local ) ) {
require $local; // DB_*, salts, other secrets
}
$env = getenv( 'WP_ENV' ) ?: 'production';
define( 'WP_ENVIRONMENT_TYPE', $env );
define( 'WP_DEBUG', 'production' !== $env );
define( 'WP_DEBUG_DISPLAY', false );
define( 'DISALLOW_FILE_EDIT', true );
// ⚠ every definition above must sit above this line.
require_once ABSPATH . 'wp-settings.php';
That last comment is the thing people trip over most. Constants must be defined before wp-settings.php is required. Put them after and WordPress has already booted, so they are silently ignored — no error, no warning, the setting simply does not apply.
WP_ENVIRONMENT_TYPE is a constant WordPress reads itself. In code you read it back through wp_get_environment_type(), and the permitted values are local, development, staging and production. The staging safeguards in part five are built on it.
Do not hand-edit the values
Editing the config by hand when standing up a new environment invites typos and omissions. WP-CLI does the same job as a command.
wp config create --dbname=wp --dbuser=wp --dbpass=... --skip-check
wp config set WP_ENVIRONMENT_TYPE staging
wp config set WP_DEBUG true --raw # write it unquoted
wp config get DB_NAME
wp config shuffle-salts # issue eight fresh auth keys
Without --raw, true is written as a string. The string "true" happens to be truthy, so WP_DEBUG works by accident — but a constant expecting a number will not. It is worth forming the habit correctly.
A committed secret is a leaked secret
Mistakes happen; what matters is what you do next. Start by finding out whether a secret is in the history at all.
git log -p -S 'DB_PASSWORD' --all -- wp-config.php
If that returns anything, the value has to be replaced. History-rewriting tools exist, but they only tidy the repository — the clones, forks, CI logs, backups and screenshots that already exist do not come back. When you cannot control where a value went, assuming it went everywhere is the only safe premise.
Rotate the database password, API keys, webhook keys and the authentication salts. Changing salts logs everybody out and forces a fresh login, and that is precisely the point — it invalidates any stolen cookie. Finally, check permissions: the web server only needs to read wp-config.php, so 640 is enough.
Secrets handling and server hardening continue in the Security archive, and if you would like a live site’s configuration, permissions and vulnerabilities reviewed in one pass, that diagnostic is part of our optimization program.
Next part
Code and configuration are sorted; data is what remains. The next part covers the most widely misunderstood topic in this series — database sync — and why replacing a domain with SQL makes settings vanish.