wp-config.php Reference
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages. |
This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the
version to run in production) and WP-CLI 2.12.0, and checked on 2026-10-28. WordPress 7.1 requires PHP 7.4 at minimum
and recommends PHP 8.3 or newer; WordPress 7.2 is in beta and due in
December 2026, and none of the constants below is expected to change with it. Every default and every behaviour
marked as verified was checked with wp eval against a WordPress 7.1.3 site running in the official
wordpress:7.1.3-php8.4-apache image with MariaDB 11.8, using the wordpress:cli-php8.4 image (WP-CLI 2.12.0) for the
commands. The output blocks are real output, with secrets replaced by placeholders.
wp-config.php is the one configuration file of a WordPress install: the database credentials, the authentication keys
and the table prefix are mandatory, and everything else is an optional constant that changes a core default. Creating
the file (by the browser installer, by hand from wp-config-sample.php or with wp config create) and generating the
keys and salts is covered in Installation; this page is the reference for what
can go into it. The examples configure the Lumen Press site used throughout this sub-section: database lumen_press,
table prefix lp_, host https://lumenpress.test/.
How WordPress reads wp-config.php
Every request enters through wp-load.php (directly, or through index.php, wp-admin/admin.php, wp-cron.php and
the other entry points). The order of events is what makes a constant work or not:
-
wp-load.phpsetsABSPATHand looks forwp-config.php, first in the WordPress directory, then in the directory above it, which it uses only when that parent directory does not itself contain awp-settings.php(that is, when it is not another WordPress install). -
wp-config.phpruns. Itdefine()`s constants, sets `$table_prefixand ends withrequire_once ABSPATH . 'wp-settings.php';. -
wp-settings.phpcalls the functions inwp-includes/default-constants.php, which define every constant that is still undefined with its core default (WP_MEMORY_LIMIT,WP_DEBUG,EMPTY_TRASH_DAYS, the cookie names, the content and plugin paths, and so on). Some defaults depend on the database (the cookie hash andFORCE_SSL_ADMINuse the site URL), so those are defined later in the load.
Three consequences follow. A constant must be defined above the require_once line, which is why the sample file
marks the place with "Add any custom values between this line and the "stop editing" line". A PHP constant cannot be
redefined, so the first definition wins and a plugin cannot override a value set in wp-config.php (redefining one
raises a "Constant already defined" warning and keeps the old value). And nothing in wp-config.php can call
WordPress functions such as add_filter(), because they are not loaded yet; filters that tune updates, cron or HTTP
belong in a must-use plugin in wp-content/mu-plugins/.
The database settings, keys and prefix that every file contains are shown, with their explanation, in Installation; the full environment-driven file used by Lumen Press is at the end of this page.
Save the file as UTF-8 without a byte order mark, with nothing before <?php. It needs no closing ?>; any byte after
one is sent to the browser and causes "headers already sent" errors.
Reading the effective values
A constant may come from wp-config.php or from the core default, so the reliable way to know the value a site runs
with is to ask WordPress itself. On a fresh Lumen Press install (site URL https://lumenpress.test) whose
wp-config.php is the one generated by the official Docker image (database settings, keys, prefix and WP_DEBUG off):
wp eval 'foreach ( array( "WP_MEMORY_LIMIT", "WP_MAX_MEMORY_LIMIT", "WP_DEBUG", "WP_DEBUG_DISPLAY",
"WP_DEBUG_LOG", "WP_CACHE", "SCRIPT_DEBUG", "EMPTY_TRASH_DAYS", "MEDIA_TRASH", "WP_POST_REVISIONS",
"AUTOSAVE_INTERVAL", "WP_CRON_LOCK_TIMEOUT", "FORCE_SSL_ADMIN", "COOKIE_DOMAIN", "COOKIEPATH",
"WP_DEFAULT_THEME", "WP_DEVELOPMENT_MODE", "WP_CONTENT_DIR", "WP_PLUGIN_DIR", "WPMU_PLUGIN_DIR",
"DISALLOW_FILE_MODS", "WP_AUTO_UPDATE_CORE", "WP_ADMIN_EMAIL", "WP_USE_EXT_MYSQL", "DISABLE_WP_CRON" ) as $c ) {
echo str_pad( $c, 22 ), " ", defined( $c ) ? var_export( constant( $c ), true ) : "(not defined)", "\n"; }
echo "environment ", wp_get_environment_type(), "\n";'
WP_MEMORY_LIMIT '40M'
WP_MAX_MEMORY_LIMIT '256M'
WP_DEBUG false
WP_DEBUG_DISPLAY true
WP_DEBUG_LOG false
WP_CACHE false
SCRIPT_DEBUG false
EMPTY_TRASH_DAYS 30
MEDIA_TRASH false
WP_POST_REVISIONS true
AUTOSAVE_INTERVAL 60
WP_CRON_LOCK_TIMEOUT 60
FORCE_SSL_ADMIN true
COOKIE_DOMAIN ''
COOKIEPATH '/'
WP_DEFAULT_THEME 'twentytwentyfive'
WP_DEVELOPMENT_MODE ''
WP_CONTENT_DIR '/var/www/html/wp-content'
WP_PLUGIN_DIR '/var/www/html/wp-content/plugins'
WPMU_PLUGIN_DIR '/var/www/html/wp-content/mu-plugins'
DISALLOW_FILE_MODS (not defined)
WP_AUTO_UPDATE_CORE (not defined)
WP_ADMIN_EMAIL (not defined)
WP_USE_EXT_MYSQL (not defined)
DISABLE_WP_CRON (not defined)
environment production
Two values in that output surprise people. WP_DEBUG_DISPLAY defaults to true: as soon as WP_DEBUG is switched on
without also setting WP_DEBUG_DISPLAY to false, errors are printed into the pages. And FORCE_SSL_ADMIN is true
although wp-config.php does not define it: when the site URL starts with https://, core defaults it to true.
Changing values with WP-CLI
The wp config commands edit wp-config.php as text, without loading WordPress, which makes them usable when the site
is broken. They need the file to be writable by the user that runs WP-CLI ("wp-config.php is not writable" otherwise).
wp config set WP_MEMORY_LIMIT 128M # string value, quoted in the file
wp config set DISALLOW_FILE_EDIT true --raw # --raw writes true, not 'true'
wp config set WP_DEBUG_LOG /var/log/wordpress/lumen-debug.log
wp config get WP_MEMORY_LIMIT
wp config is-true DISALLOW_FILE_EDIT; echo "exit=$?"
wp config delete WP_DEBUG_LOG
wp config shuffle-salts
Success: Added the constant 'WP_MEMORY_LIMIT' to the 'wp-config.php' file with the value '128M'.
Success: Added the constant 'DISALLOW_FILE_EDIT' to the 'wp-config.php' file with the raw value 'true'.
Success: Added the constant 'WP_DEBUG_LOG' to the 'wp-config.php' file with the value '/var/log/wordpress/lumen-debug.log'.
128M
exit=0
Success: Deleted the constant 'WP_DEBUG_LOG' from the 'wp-config.php' file.
Success: Shuffled the salt keys.
Source: WP-CLI Commands: wp config
New constants are inserted just above the "stop editing" line. wp config list prints what the file defines (names,
types and values, so it shows secrets), not the core defaults. Without --raw, true, false and numbers are written
as quoted strings, and 'false' is a non-empty string, which PHP treats as true; booleans and integers always need
--raw. wp config get reads the file only, so it reports "not defined in the 'wp-config.php' file" for a constant
that runs with its core default; use wp eval (above) for the effective value. wp config shuffle-salts replaces each
key definition with a literal string: on a file that reads the keys from environment variables (see
Loading values from environment variables), it silently replaces the getenv() calls, which was confirmed on the
official Docker image’s wp-config.php. Rotate the variables instead.
Database
| Constant | Default | Meaning and notes |
|---|---|---|
|
(required) |
Database name and credentials. The user needs the privileges listed in Installation; it should not be a MySQL administrative account. |
|
(required) |
|
|
|
Character set of the connection and of new tables. |
|
|
Empty lets WordPress choose the best collation for the character set. Leave it empty unless a collation is required. |
|
|
Flags passed to |
|
|
Prefix of every table. Letters, digits and underscores only. Changing it after installation makes WordPress believe
it is not installed: the tables, and the prefixed |
|
not defined |
Share the users tables of another install. Set them before installing; a |
|
not defined |
Enables |
|
not defined |
Skips the expensive |
WP_ALLOW_REPAIR repairs table structures; it does not reset passwords. An older book recommends it for regaining
access to a locked-out account, which it cannot do; use wp user update lp_admin --user_pass=<new-password> (or the
"Lost your password?" e-mail) instead, as described in Security Hardening.
Authentication keys and salts
The eight constants AUTH_KEY, SECURE_AUTH_KEY, LOGGED_IN_KEY, NONCE_KEY, AUTH_SALT, SECURE_AUTH_SALT,
LOGGED_IN_SALT and NONCE_SALT sign the login cookies and nonces. They are generated once per site by the
WordPress.org secret-key service or wp config shuffle-salts; their generation and what happens when one is missing are
explained in Installation. Changing them logs every user out, which is the
standard response to a suspected compromise. They are not encryption keys: a database dump does not depend on them,
and nothing in wp-config.php is "encrypted" by WordPress. An older book suggests encrypting the values in
wp-config.php; PHP must read them in clear text on every request, so the real protections are file permissions, a
location outside the web root and, best, keeping the secrets out of the file altogether (environment variables or a
secret manager).
Site URLs and cookies
| Constant | Default | Meaning and notes |
|---|---|---|
|
|
Site address (the public URL). Overrides the option without changing the database, and greys out the field in Settings > General. |
|
|
WordPress address (where core lives). Differs from |
|
not defined |
When |
|
|
Domain of the login cookies. Empty means the current host. Set it only to share cookies across subdomains on purpose. |
|
derived from the URLs |
Cookie paths. Rarely set by hand. |
|
|
Suffix of every cookie name ( |
|
|
Cookie names; changing them only renames the cookies, it adds no security. |
Pinning WP_HOME and WP_SITEURL per environment is the usual way to stop a staging copy from redirecting to
production after a database import; the full move procedure, with wp search-replace, is in
Importing, Exporting and Migrating. The cookie flags
themselves (HttpOnly, Secure) are covered in the
Cookies article of the Advanced
Administration Handbook.
Directories and paths
| Constant | Default | Meaning and notes |
|---|---|---|
|
directory of |
Path to core. Set by the last lines of the sample; never change it, even when |
|
|
Moves |
|
|
Moves the plugins directory. |
|
|
Must-use plugins; |
|
|
Uploads directory relative to |
|
|
Location of translation files. |
|
system temp directory |
Where downloads and unzips happen during updates; set it when |
|
|
Theme used when installing and as the fallback when the active theme is missing. |
|
from the active theme |
Parent and child theme paths. Defined late; use |
There is no constant for the themes directory: a plugin can add one with register_theme_directory(). A layout that
keeps core in its own directory (/wp) and wp-content beside it, as Composer-based setups do, uses the URL and content
constants together: WP_SITEURL points to /wp, WP_HOME to the root, and WP_CONTENT_DIR/WP_CONTENT_URL to
the content directory beside it, as described in
Giving WordPress Its Own Directory.
Environment type and development mode
WP_ENVIRONMENT_TYPE (WordPress 5.5) tells core, plugins and hosts which kind of environment runs the code. The
accepted values are local (added in 5.5.1), development, staging and production. wp_get_environment_type()
resolves it in this order:
-
the
WP_ENVIRONMENT_TYPEenvironment variable, if set; -
the
WP_ENVIRONMENT_TYPEconstant, which overrides the variable; -
anything else, including an unknown value, becomes
production.
Core itself uses it in a few places: development turns WP_DEBUG on when WP_DEBUG is not defined, and
local relaxes the HTTPS requirement of application passwords. Plugins use it to disable e-mails, payment gateways or
search indexing outside production. The list of types cannot be extended; a WP_ENVIRONMENT_TYPES constant was removed
in 5.5.1 and now only triggers a deprecation notice. Each line below was run against a wp-config.php that does not
define WP_DEBUG:
E='printf( "%s WP_DEBUG=%s\n", wp_get_environment_type(), var_export( WP_DEBUG, true ) );'
WP_ENVIRONMENT_TYPE=development wp eval "$E"
WP_ENVIRONMENT_TYPE=staging wp eval "$E"
WP_ENVIRONMENT_TYPE=prod wp eval "$E" # not an accepted value
WP_ENVIRONMENT_TYPE=staging wp --exec="define( 'WP_ENVIRONMENT_TYPE', 'local' );" eval "$E"
development WP_DEBUG=true
staging WP_DEBUG=false
production WP_DEBUG=false
local WP_DEBUG=false
WP_DEVELOPMENT_MODE (WordPress 6.3) is a separate switch for people working on code: core, plugin, theme,
all, or '' (default). It does not change error output; it changes caching that gets in the way of development, for
example theme stops global styles and theme.json data from being cached. wp_is_development_mode( 'theme' ) checks it, and all
matches every type. A non-empty development mode also turns WP_DEBUG on when WP_DEBUG is not defined. Never set it
in production.
// lumen theme development on a laptop.
define( 'WP_ENVIRONMENT_TYPE', 'local' );
define( 'WP_DEVELOPMENT_MODE', 'theme' );
Debugging
| Constant | Default | Meaning and notes |
|---|---|---|
|
|
Sets |
|
|
With |
|
|
|
|
|
Loads the non-minified core JavaScript and CSS. |
|
not defined |
Records every query with its time and caller in |
|
|
|
|
not defined |
Disables the fatal error handler and recovery mode (5.2), so fatal errors show as plain PHP errors. |
|
site admin e-mail |
Address that receives the recovery-mode link after a fatal error. |
wp-content/debug.log is inside the web root, so on most servers anyone can download it, together with paths, query
fragments and sometimes personal data. Site Health reports both a displayed and a publicly reachable log. In production,
either keep WP_DEBUG off or log to a path outside the web root and never display:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'WP_DEBUG_LOG', '/var/log/wordpress/lumen-debug.log' ); // writable by the PHP user, outside the web root
Tools, Site Health and troubleshooting procedures are on Debugging, Site Health and Troubleshooting.
Memory
WP_MEMORY_LIMIT is the PHP memory limit WordPress asks for on every request, and WP_MAX_MEMORY_LIMIT the one it
asks for in heavier admin tasks (image processing, updates, imports). Core defaults are 40M on a single site, 64M on
Multisite, and 256M for the maximum. Both work by calling ini_set( 'memory_limit', ... ), with three rules that explain
most "the constant does nothing" reports:
-
WordPress only raises the limit. The 40M default is below the usual
php.inivalue, so it changes nothing. -
If the host forbids runtime changes (
memory_limitset withphp_admin_valueor similar), both constants fall back to the current limit and setting them has no effect; the limit must be raised inphp.inior the pool configuration. -
A
php.inivalue above 256M (or-1, as WP-CLI uses) is kept as the maximum.
Measured in a web request on the wordpress:7.1.3-php8.4-apache image, whose php.ini sets 128M, first with no
constants and then with WP_MEMORY_LIMIT at 256M and WP_MAX_MEMORY_LIMIT at 512M, using a throwaway probe script
in the web root (delete such a file right after use):
<?php
// probe.php: memory limits before and after WordPress loads.
$before = ini_get( 'memory_limit' );
require __DIR__ . '/wp-load.php';
header( 'Content-Type: text/plain' );
printf( "php.ini memory_limit=%s, WP_MEMORY_LIMIT=%s, WP_MAX_MEMORY_LIMIT=%s, after load=%s\n",
$before, WP_MEMORY_LIMIT, WP_MAX_MEMORY_LIMIT, ini_get( 'memory_limit' ) );
php.ini memory_limit=128M, WP_MEMORY_LIMIT=40M, WP_MAX_MEMORY_LIMIT=256M, after load=128M
php.ini memory_limit=128M, WP_MEMORY_LIMIT=256M, WP_MAX_MEMORY_LIMIT=512M, after load=256M
On the same site converted to Multisite, WP_MEMORY_LIMIT reported 64M. An older book gives different Multisite
memory defaults; the values above come from default-constants.php in 7.1.
WP-Cron
| Constant | Default | Meaning and notes |
|---|---|---|
|
not defined |
|
|
not defined |
Runs cron through a redirect instead of a loopback request, for hosts where loopbacks fail. Visitors may see a
|
|
|
Minimum time between two cron runs. |
Setting DISABLE_WP_CRON without adding the system job silently stops scheduled posts, update checks and the Lumen
Press stock synchronisation. The cron setup is on WP-Cron and
Scheduled Tasks.
Updates and file modifications
Automatic core updates
WP_AUTO_UPDATE_CORE |
Effect |
|---|---|
not defined |
Minor (security and maintenance) releases always. Major releases follow the "automatic updates for all new versions"
setting on Dashboard > Updates, stored in the |
|
Minor releases only, whatever the Dashboard setting says. |
|
All core releases, major and minor. |
|
No automatic core updates, not even security releases. Site Health warns about it. |
Asked whether it would update to an offered 7.1.4 (minor) and 7.2.0 (major), a fresh 7.1.3 site answered:
for v in "" "'minor'" "true" "false"; do
[ -n "$v" ] && X="define( 'WP_AUTO_UPDATE_CORE', $v );" || X=""
wp --exec="$X" eval 'require_once ABSPATH . "wp-admin/includes/class-wp-upgrader.php";
printf( "%-10s 7.1.4=%s 7.2.0=%s\n",
defined( "WP_AUTO_UPDATE_CORE" ) ? var_export( WP_AUTO_UPDATE_CORE, true ) : "(unset)",
var_export( Core_Upgrader::should_update_to_version( "7.1.4" ), true ),
var_export( Core_Upgrader::should_update_to_version( "7.2.0" ), true ) );'
done
(unset) 7.1.4=true 7.2.0=true
'minor' 7.1.4=true 7.2.0=false
true 7.1.4=true 7.2.0=true
false 7.1.4=false 7.2.0=false
AUTOMATIC_UPDATER_DISABLED set to true turns off every background update (core, plugins, themes, translations) and
the update e-mails. The official documentation discourages it; prefer WP_AUTO_UPDATE_CORE and the per-type filters,
which belong in a must-use plugin, not in wp-config.php. Plugin and theme auto-updates, rollback and the filters are
covered in Updates and Auto-Updates.
DISALLOW_FILE_EDIT and DISALLOW_FILE_MODS
The two constants are often confused, and an older book describes DISALLOW_FILE_MODS as only disabling updates.
Checked as the administrator lp_admin with each constant set to true in turn:
wp --user=lp_admin eval 'foreach ( array( "install_plugins", "update_plugins", "delete_plugins", "edit_plugins",
"edit_themes", "update_core", "install_themes", "activate_plugins" ) as $cap ) {
printf( "%-17s %s\n", $cap, var_export( current_user_can( $cap ), true ) ); }
require_once ABSPATH . "wp-admin/includes/class-wp-automatic-updater.php";
printf( "auto-updater disabled: %s\n", var_export( ( new WP_Automatic_Updater() )->is_disabled(), true ) );'
| Capability or behaviour | DISALLOW_FILE_EDIT |
DISALLOW_FILE_MODS |
|---|---|---|
|
false |
false |
|
true |
false |
|
true |
false |
|
true |
false |
|
true |
true |
Background (automatic) updates disabled |
no |
yes |
With DISALLOW_FILE_EDIT, the run queried install_plugins, update_plugins, edit_plugins, edit_themes and the
updater; the remaining cells of that column follow from map_meta_cap(), which consults DISALLOW_FILE_EDIT only for
the editing capabilities.
DISALLOW_FILE_EDIT only removes the built-in code editors (Appearance > Theme File Editor, Plugins > Plugin File
Editor), which an attacker with an administrator session would otherwise use to run PHP. DISALLOW_FILE_MODS makes the
whole code base read-only from WordPress: no installs, updates or deletions of plugins, themes, core or translations
from the dashboard, no editors, and no automatic updates, including security releases, because
WP_Automatic_Updater::is_disabled() checks wp_is_file_mod_allowed() first. It suits sites whose code is deployed from
version control or a container image, where updates happen in the build. On a classically maintained site it leaves
known vulnerabilities unpatched; use DISALLOW_FILE_EDIT there.
Filesystem method and permissions
When WordPress cannot write files directly as the owner of the files, it asks for FTP or SSH credentials. These constants answer that form in advance:
| Constant | Meaning |
|---|---|
|
|
|
FTP or FTPS connection. |
|
Key files for the |
|
Paths as seen by the FTP/SSH account. |
|
Modes for created directories and files. By default the site root’s mode OR 0755 and the mode of |
Storing an FTP password in wp-config.php puts a second secret in the file; key-based ssh2 or deploying code
outside WordPress is preferable. The correct modes are 644 for files and 755 for directories, never 666 or 777 as some
older books advise; ownership, FS_METHOD and the credentials prompt are on
File Permissions and Filesystem.
HTTPS
FORCE_SSL_ADMIN set to true redirects wp-login.php and every admin page to HTTPS, so passwords and the
authentication cookies never travel in clear text. Since the site URL decides its default, a site configured with
https:// URLs already has it; defining it explicitly protects against a database import that brings an http:// URL.
FORCE_SSL_LOGIN was deprecated in WordPress 4.0, when logins and the admin were unified under FORCE_SSL_ADMIN.
7.1 still honours it (a true value calls force_ssl_admin( true )), but it should be replaced. On an http:// site
with neither constant defined, then with only FORCE_SSL_LOGIN:
wp eval 'printf( "no constants: FORCE_SSL_ADMIN=%s force_ssl_admin()=%s\n",
var_export( FORCE_SSL_ADMIN, true ), var_export( force_ssl_admin(), true ) );'
wp --exec="define( 'FORCE_SSL_LOGIN', true );" eval 'printf( "FORCE_SSL_LOGIN: FORCE_SSL_ADMIN=%s force_ssl_admin()=%s\n",
var_export( FORCE_SSL_ADMIN, true ), var_export( force_ssl_admin(), true ) );'
no constants: FORCE_SSL_ADMIN=false force_ssl_admin()=false
FORCE_SSL_LOGIN: FORCE_SSL_ADMIN=false force_ssl_admin()=true
Behind a load balancer or reverse proxy that terminates TLS, PHP sees plain HTTP and WordPress produces redirect loops or mixed content. Tell it about the original scheme, trusting the header only because the proxy always sets it:
// The proxy terminates TLS and always sets X-Forwarded-Proto.
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
$_SERVER['HTTPS'] = 'on';
}
The official Docker image ships a similar block in its wp-config.php. On a server reachable without the proxy,
anyone can send the header, so restrict direct access first.
Caching and performance
| Constant | Default | Meaning and notes |
|---|---|---|
|
|
|
|
not defined |
Legacy compression of the concatenated admin scripts by PHP. The web server should compress instead; do not define them on new sites. |
|
|
7.1: the mode that speculative loading’s |
|
|
7.1: the default eagerness ( |
A persistent object cache (Redis, Memcached, APCu) is a separate wp-content/object-cache.php drop-in that does not
need WP_CACHE; its connection settings are constants defined by the drop-in’s plugin, not by core. Page caching,
object caching and OPcache are on Performance and Caching.
Content: trash, revisions, autosave, media and HTML
| Constant | Default | Meaning and notes |
|---|---|---|
|
|
Days before trashed posts, pages and comments are deleted by a daily cron event. |
|
|
|
|
|
|
|
|
How often the editor autosaves. |
|
not defined |
|
|
not defined |
|
|
not defined |
|
Checked with five updates of one post for each revision setting, and with a trashed draft under EMPTY_TRASH_DAYS 0:
# Revisions: no constant, then 3, then false (wp-config.php does not define WP_POST_REVISIONS).
for v in "" "define( 'WP_POST_REVISIONS', 3 );" "define( 'WP_POST_REVISIONS', false );"; do
wp --exec="$v" eval '$id = wp_insert_post( array( "post_title" => "Rev test", "post_status" => "publish",
"post_content" => "v0" ) );
for ( $i = 1; $i <= 5; $i++ ) { wp_update_post( array( "ID" => $id, "post_content" => "v$i" ) ); }
printf( "WP_POST_REVISIONS=%s keep=%s revisions stored=%d\n", var_export( WP_POST_REVISIONS, true ),
wp_revisions_to_keep( get_post( $id ) ), count( wp_get_post_revisions( $id ) ) );
wp_delete_post( $id, true );'
done
# Trash disabled.
wp --exec="define( 'EMPTY_TRASH_DAYS', 0 );" eval '$id = wp_insert_post( array( "post_title" => "Draft to discard",
"post_status" => "draft" ) ); $r = wp_trash_post( $id );
printf( "wp_trash_post returned %s; post now: %s\n", is_object( $r ) ? get_class( $r ) : var_export( $r, true ),
var_export( get_post( $id ) ? get_post_status( $id ) : null, true ) );'
WP_POST_REVISIONS=true keep=-1 revisions stored=5
WP_POST_REVISIONS=3 keep=3 revisions stored=3
WP_POST_REVISIONS=false keep=0 revisions stored=0
wp_trash_post returned WP_Post; post now: NULL
WP_POST_REVISIONS only limits revisions created from now on; existing extra revisions stay until the post is saved
again. With EMPTY_TRASH_DAYS at 0, wp_trash_post() deletes the post outright (the post no longer exists), while
with the default it moves the post to the trash status and records the time in _wp_trash_meta_time (also
checked). An accidental click is unrecoverable without a backup, so Lumen Press keeps a short trash (14 days) rather
than none.
Outbound HTTP requests
WP_HTTP_BLOCK_EXTERNAL set to true makes the HTTP API (wp_remote_get() and friends) refuse every request except
to the site itself and localhost. WP_ACCESSIBLE_HOSTS lists the exceptions, comma-separated, with * wildcards.
Blocking api.wordpress.org also blocks update checks, plugin installs and translation downloads, so list it unless
updates are deployed another way. For the Lumen Press stock synchronisation with its distributor:
define( 'WP_HTTP_BLOCK_EXTERNAL', true );
define( 'WP_ACCESSIBLE_HOSTS', 'api.wordpress.org,downloads.wordpress.org,*.lumen-distributor.example' );
wp eval 'foreach ( array( "https://api.wordpress.org/core/version-check/1.7/", "https://example.com/",
"https://stock.lumen-distributor.example/v1" ) as $u ) {
printf( "%-50s blocked=%s\n", $u, var_export( ( new WP_Http() )->block_request( $u ), true ) ); }
$r = wp_remote_get( "https://example.com/" );
echo is_wp_error( $r ) ? "wp_remote_get error: " . $r->get_error_message() . "\n" : "fetched\n";'
https://api.wordpress.org/core/version-check/1.7/ blocked=false
https://example.com/ blocked=true
https://stock.lumen-distributor.example/v1 blocked=false
wp_remote_get error: User has blocked requests through HTTP to the URL: https://example.com/.
Source: Common APIs Handbook: HTTP API
The block applies only to code that uses the HTTP API; a plugin calling cURL directly ignores it, so it is a policy, not
a firewall. A forward proxy is configured with WP_PROXY_HOST and WP_PROXY_PORT (both required),
WP_PROXY_USERNAME/WP_PROXY_PASSWORD, and WP_PROXY_BYPASS_HOSTS (comma-separated, wildcards allowed).
Multisite
A network is enabled in two steps: WP_ALLOW_MULTISITE adds Tools > Network Setup, and the setup screen (or
wp core multisite-convert) creates the network tables and prints the remaining constants. Converting the Lumen Press
test site produced:
wp core multisite-convert --title="Lumen Press Network"
define( 'WP_ALLOW_MULTISITE', true );
define( 'MULTISITE', true );
define( 'SUBDOMAIN_INSTALL', false );
$base = '/';
define( 'DOMAIN_CURRENT_SITE', 'lumenpress.test' );
define( 'PATH_CURRENT_SITE', '/' );
define( 'SITE_ID_CURRENT_SITE', 1 );
define( 'BLOG_ID_CURRENT_SITE', 1 );
| Constant | Meaning |
|---|---|
|
Shows Tools > Network Setup. Not needed once the network exists, but harmless. |
|
The site is a network. |
|
|
|
Domain and path of the main site. |
|
IDs of the network and of its main site. |
|
Where a request for a non-existent subdomain site goes, instead of the sign-up form; any value also redirects 404s on the main site there. |
|
Defaults to 64M instead of 40M (verified above). |
The web server rules and the tables a network adds are on Multisite Networks.
Constants that do not exist or no longer work
Several constants found in older books or old tutorials have no effect in WordPress 7.1. Defining them raises no error,
which is why they survive in copied configurations. The dump at the top of this page shows WP_ADMIN_EMAIL and
WP_USE_EXT_MYSQL as not defined, and a search of the 7.1.3 source finds no reference to WP_ADMIN_EMAIL at all.
| Constant | Status |
|---|---|
|
Does not exist. It is not a core constant, and nothing in core reads it. The administration e-mail is the |
|
Obsolete. It once chose the old |
|
Deprecated in 4.0. Still forces HTTPS on the admin when true, but use |
|
Legacy (before 4.0). The site language is the |
|
Removed in 5.5.1. The list of environment types is fixed; defining it only triggers a deprecation notice. |
|
The constant exists but only repairs tables (see Database). |
Loading values from environment variables
A wp-config.php with literal passwords and keys cannot be committed to version control, and must be edited per
server. Reading the values from environment variables makes the file identical in every environment and keeps the
secrets in the platform that already manages them (Docker or Kubernetes secrets, the host’s control panel, a secret
manager). Core helps in three places: WP_ENVIRONMENT_TYPE and, since 7.1, the two speculative loading defaults are
read from the environment directly; everything else goes through getenv() in wp-config.php.
The Lumen Press configuration below reads every per-environment value and every secret from the environment. A
NAME_FILE variable pointing to a file takes precedence, which is how Docker and Kubernetes secrets are mounted. A
missing secret stops the request with a 500 and a log line, rather than letting WordPress fall back to empty
credentials or database-stored salts:
<?php
// wp-config.php for Lumen Press: identical in every environment; values and secrets come from the environment.
if ( ! function_exists( 'lumen_env' ) ) {
// An environment variable, or the contents of the file named by NAME_FILE (Docker/Kubernetes secrets).
function lumen_env( $name, $default = null ) {
$file = getenv( $name . '_FILE' );
if ( false !== $file && is_readable( $file ) ) {
return rtrim( file_get_contents( $file ), "\r\n" );
}
$value = getenv( $name );
if ( false !== $value && '' !== $value ) {
return $value;
}
if ( null === $default ) {
error_log( 'wp-config.php: required environment variable ' . $name . ' is not set.' );
http_response_code( 500 );
exit( 1 );
}
return $default;
}
}
// ** Database ** //
define( 'DB_NAME', lumen_env( 'LUMEN_DB_NAME', 'lumen_press' ) );
define( 'DB_USER', lumen_env( 'LUMEN_DB_USER', 'lumen' ) );
define( 'DB_PASSWORD', lumen_env( 'LUMEN_DB_PASSWORD' ) );
define( 'DB_HOST', lumen_env( 'LUMEN_DB_HOST', 'localhost' ) );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );
// ** Keys and salts: one variable each, LUMEN_AUTH_KEY ... LUMEN_NONCE_SALT ** //
define( 'AUTH_KEY', lumen_env( 'LUMEN_AUTH_KEY' ) );
define( 'SECURE_AUTH_KEY', lumen_env( 'LUMEN_SECURE_AUTH_KEY' ) );
define( 'LOGGED_IN_KEY', lumen_env( 'LUMEN_LOGGED_IN_KEY' ) );
define( 'NONCE_KEY', lumen_env( 'LUMEN_NONCE_KEY' ) );
define( 'AUTH_SALT', lumen_env( 'LUMEN_AUTH_SALT' ) );
define( 'SECURE_AUTH_SALT', lumen_env( 'LUMEN_SECURE_AUTH_SALT' ) );
define( 'LOGGED_IN_SALT', lumen_env( 'LUMEN_LOGGED_IN_SALT' ) );
define( 'NONCE_SALT', lumen_env( 'LUMEN_NONCE_SALT' ) );
$table_prefix = 'lp_';
/* Add any custom values between this line and the "stop editing" line. */
// WP_ENVIRONMENT_TYPE is not defined here: core reads the environment variable of the
// same name itself, and falls back to 'production'.
// ** URLs: pinned per environment, so Settings > General cannot break them ** //
define( 'WP_HOME', lumen_env( 'LUMEN_HOME', 'https://lumenpress.test' ) );
define( 'WP_SITEURL', WP_HOME );
define( 'FORCE_SSL_ADMIN', true );
// ** Debugging: log to a file outside the web root, never display ** //
define( 'WP_DEBUG', filter_var( lumen_env( 'LUMEN_DEBUG', 'false' ), FILTER_VALIDATE_BOOLEAN ) );
define( 'WP_DEBUG_DISPLAY', false );
define( 'WP_DEBUG_LOG', lumen_env( 'LUMEN_DEBUG_LOG', '/var/log/wordpress/lumen-debug.log' ) );
// ** Production code is deployed, never changed from the dashboard; staging keeps background updates ** //
if ( 'production' === ( getenv( 'WP_ENVIRONMENT_TYPE' ) ?: 'production' ) ) {
define( 'DISALLOW_FILE_MODS', true );
}
// ** Content ** //
define( 'WP_POST_REVISIONS', 20 );
define( 'EMPTY_TRASH_DAYS', 14 );
define( 'WP_MEMORY_LIMIT', '128M' );
// ** WP-Cron is triggered by a system cron job (see the WP-Cron page) ** //
define( 'DISABLE_WP_CRON', true );
/* That's all, stop editing! Happy publishing. */
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}
require_once ABSPATH . 'wp-settings.php';
Points about this file:
-
The helper is wrapped in
function_exists()because WP-CLI may evaluatewp-config.phpmore than once in a process; the official Docker image does the same for itsgetenv_docker()helper. -
Environment variables are strings.
'false'is truthy in a plaindefine(), hencefilter_var()withFILTER_VALIDATE_BOOLEANfor the debug switch; integers can be cast with(int). -
DISALLOW_FILE_MODSis set only in production (the same fallback as core when the variable is unset), because production code comes from the deployment pipeline; it also turns off background updates, so core security releases must arrive through that pipeline (see Updates and file modifications). Staging leaves it undefined so the background updater can test releases first, as described in Updates and Auto-Updates.
With the variables in an env file, the effective values were checked with WP-CLI, then with a different environment type and debug switch, and finally with the database password missing:
wp eval 'foreach ( array( "WP_HOME", "WP_MEMORY_LIMIT", "WP_DEBUG", "WP_DEBUG_DISPLAY", "WP_DEBUG_LOG",
"DISALLOW_FILE_MODS", "EMPTY_TRASH_DAYS", "WP_POST_REVISIONS", "DISABLE_WP_CRON" ) as $c ) {
printf( "%-20s %s\n", $c, var_export( constant( $c ), true ) ); }
printf( "%-20s %s\n", "environment", wp_get_environment_type() );'
WP_ENVIRONMENT_TYPE=staging LUMEN_DEBUG=true wp eval \
'printf( "%s WP_DEBUG=%s\n", wp_get_environment_type(), var_export( WP_DEBUG, true ) );'
LUMEN_DB_PASSWORD= wp option get blogname; echo "exit=$?"
WP_HOME 'https://lumenpress.test'
WP_MEMORY_LIMIT '128M'
WP_DEBUG false
WP_DEBUG_DISPLAY false
WP_DEBUG_LOG '/var/log/wordpress/lumen-debug.log'
DISALLOW_FILE_MODS true
EMPTY_TRASH_DAYS 14
WP_POST_REVISIONS 20
DISABLE_WP_CRON true
environment production
staging WP_DEBUG=true
[28-Oct-2026 10:37:30 UTC] wp-config.php: required environment variable LUMEN_DB_PASSWORD is not set.
exit=1
Source: PHP Manual: getenv
Getting the variables to PHP
getenv() only sees what the PHP process was given, and each server passes variables differently:
-
Docker and Compose:
environment:,env_file:andsecrets:(mounted under/run/secrets/, read through a*_FILEvariable). The Compose file below was run with the configuration above (on host port 18089, because 8080 was taken); the site answered with HTTP 200, the web request reportedstaging WP_DEBUG=true display_errors='0', and a test notice landed in the log file. -
PHP-FPM: pools start with
clear_env = yes, which empties the environment of the workers. List each variable withenv[LUMEN_DB_PASSWORD] = $LUMEN_DB_PASSWORDin the pool file (or set the value there), or setclear_env = no. -
Apache with mod_php:
SetEnv LUMEN_HOME https://lumenpress.test(orPassEnv) in the virtual host; the value was confirmed visible togetenv()in the official Apache image. Keep it in the server configuration, not in a.htaccessfile inside the web root. -
Kubernetes:
envfrom aSecretor a mounted secret volume with*_FILEvariables; see Secrets.
# compose.yaml: Lumen Press with every wp-config.php value supplied by the environment.
name: lumen-config
services:
db:
image: mariadb:11.8
environment:
MARIADB_DATABASE: lumen_press
MARIADB_USER: lumen
MARIADB_PASSWORD_FILE: /run/secrets/db_password
MARIADB_RANDOM_ROOT_PASSWORD: "1"
secrets: [db_password]
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
retries: 20
wordpress:
image: wordpress:7.1.3-php8.4-apache
depends_on:
db:
condition: service_healthy
ports:
- "8080:80"
env_file: lumen.env # LUMEN_AUTH_KEY ... LUMEN_NONCE_SALT, never committed
environment:
WP_ENVIRONMENT_TYPE: staging
LUMEN_DB_HOST: db
LUMEN_DB_PASSWORD_FILE: /run/secrets/db_password
LUMEN_HOME: http://localhost:8080
LUMEN_DEBUG: "true"
LUMEN_DEBUG_LOG: /var/www/debug/lumen-debug.log
secrets: [db_password]
volumes:
- ./wp-config.php:/var/www/html/wp-config.php:ro
- debug:/var/www/debug
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
debug:
The debug volume must be writable by www-data (uid 33). Generate lumen.env and secrets/db_password.txt with
random values (for example openssl rand -hex 32 per key) and keep both out of version control. Without a custom
wp-config.php, the official image builds one from WORDPRESS_DB_*, WORDPRESS_AUTH_KEY and similar variables, and
accepts extra PHP in WORDPRESS_CONFIG_EXTRA; the full local setup is on
Local Development Environments, and Compose itself on
Compose in Practice.
Composer-based projects often load a .env file with the vlucas/phpdotenv package before the define() calls. That
works the same way, but the .env file then sits on the server and must be outside the web root with the same
permissions as wp-config.php (see Composer and Packages).
Keeping wp-config.php outside the web root
WordPress also finds wp-config.php one directory above its own directory, as long as that directory is not another
WordPress install (it must not contain wp-settings.php). With the document root at /srv/lumenpress/public and
WordPress installed there, /srv/lumenpress/wp-config.php is outside what the web server serves. Nothing else changes:
ABSPATH keeps its default, contrary to an older book that says it must be edited after the move. Moving the file
on the official image from /var/www/html/ to /var/www/ left the site working unchanged.
/srv/lumenpress/
├── wp-config.php # 440, owner deploy, group www-data
└── public/ # document root
├── index.php
├── wp-settings.php
├── wp-admin/
├── wp-includes/
└── wp-content/
The Hardening page notes that the security benefit of the move is debated: a correctly configured PHP server never
serves the file’s source, and the real exposure is a PHP handler failure or a backup copy (wp-config.php.bak,
wp-config.php~) left in the web root, which the server will serve as text. Whatever the location, restrict the
file to the owner and the web server group (chmod 440 or 400), block direct requests to it in the web server
(see Web Servers: Apache and nginx), and never leave edited
copies behind. With WordPress in a subdirectory of the document root (public/wp/), the "one level up" location is
still inside the web root, so the two layouts do not combine; there, keep the secrets in environment variables instead.
Safe production values
| Constant | Production value | Why |
|---|---|---|
|
|
Plugins disable test-only behaviour. |
|
|
Errors never reach visitors. |
|
off, or on with a log path outside the web root |
|
|
not defined |
Slower pages, more memory. |
|
not defined |
Disables caches meant to stay on. |
|
|
No PHP editor for a stolen admin session. |
|
|
Also stops automatic security updates. |
|
|
Security releases must apply. |
|
|
Secure cookies even after an |
|
the production URL |
A database import cannot redirect elsewhere. |
|
|
Mistakes stay recoverable. |
|
|
Bounded |
|
|
Punctual cron on quiet sites, no cron on page views. |
|
not defined |
The repair page needs no login. |
|
not defined |
No arbitrary file types. |
Keys and salts |
unique, from the environment or a secret manager |
Never in version control. |
The broader checklist (MFA, application passwords, XML-RPC, the database user) is on Security Hardening.