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:

  1. wp-load.php sets ABSPATH and looks for wp-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 a wp-settings.php (that is, when it is not another WordPress install).

  2. wp-config.php runs. It define()`s constants, sets `$table_prefix and ends with require_once ABSPATH . 'wp-settings.php';.

  3. wp-settings.php calls the functions in wp-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 and FORCE_SSL_ADMIN use 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.

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

DB_NAME, DB_USER, DB_PASSWORD

(required)

Database name and credentials. The user needs the privileges listed in Installation; it should not be a MySQL administrative account.

DB_HOST

(required)

localhost, a host name, host:port (db.internal:3307) or a socket (localhost:/run/mysqld/mysqld.sock).

DB_CHARSET

utf8mb4 in the sample

Character set of the connection and of new tables. wp config create writes utf8 unless given --dbcharset=utf8mb4; WordPress still creates utf8mb4 tables when the server supports them. Details in Database Schema.

DB_COLLATE

''

Empty lets WordPress choose the best collation for the character set. Leave it empty unless a collation is required.

MYSQL_CLIENT_FLAGS

0

Flags passed to mysqli_real_connect(), for example MYSQLI_CLIENT_SSL for a TLS connection to a remote database.

$table_prefix (variable, not a constant)

wp_

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 lp_user_roles option and lp_capabilities user meta keys, must be renamed too.

CUSTOM_USER_TABLE, CUSTOM_USER_META_TABLE

not defined

Share the users tables of another install. Set them before installing; a usermeta table is still created, and roles must be managed per site.

WP_ALLOW_REPAIR

not defined

Enables /wp-admin/maint/repair.php, which repairs and optimizes the tables. The page works without logging in, so remove the constant as soon as the repair is done.

DO_NOT_UPGRADE_GLOBAL_TABLES

not defined

Skips the expensive ALTER TABLE on the users and Multisite global tables during a core database upgrade; for large networks whose tables are upgraded separately.

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

WP_HOME

home option

Site address (the public URL). Overrides the option without changing the database, and greys out the field in Settings > General.

WP_SITEURL

siteurl option

WordPress address (where core lives). Differs from WP_HOME only when core is in a subdirectory.

RELOCATE

not defined

When true, a visit to wp-login.php rewrites the siteurl option to the URL used to reach it. A one-off rescue tool after a move; remove it afterwards.

COOKIE_DOMAIN

''

Domain of the login cookies. Empty means the current host. Set it only to share cookies across subdomains on purpose.

COOKIEPATH, SITECOOKIEPATH, ADMIN_COOKIE_PATH, PLUGINS_COOKIE_PATH

derived from the URLs

Cookie paths. Rarely set by hand.

COOKIEHASH

md5() of the site URL

Suffix of every cookie name (wordpress_logged_in_<hash>). Changing the site URL therefore logs users out.

AUTH_COOKIE, SECURE_AUTH_COOKIE, LOGGED_IN_COOKIE, TEST_COOKIE, RECOVERY_MODE_COOKIE

wordpress_... names

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

ABSPATH

directory of wp-config.php + /

Path to core. Set by the last lines of the sample; never change it, even when wp-config.php moves one level up.

WP_CONTENT_DIR, WP_CONTENT_URL

ABSPATH . 'wp-content', siteurl/wp-content

Moves wp-content. Full path and full URL, no trailing slash. Set both.

WP_PLUGIN_DIR, WP_PLUGIN_URL

WP_CONTENT_DIR . '/plugins'

Moves the plugins directory. PLUGINDIR (relative to ABSPATH) exists only for old plugins.

WPMU_PLUGIN_DIR, WPMU_PLUGIN_URL

WP_CONTENT_DIR . '/mu-plugins'

Must-use plugins; MUPLUGINDIR is the relative legacy form.

UPLOADS

wp-content/uploads

Uploads directory relative to ABSPATH. Without the constant the Media settings decide.

WP_LANG_DIR

WP_CONTENT_DIR . '/languages'

Location of translation files.

WP_TEMP_DIR

system temp directory

Where downloads and unzips happen during updates; set it when /tmp is not writable or is too small.

WP_DEFAULT_THEME

twentytwentyfive

Theme used when installing and as the fallback when the active theme is missing.

TEMPLATEPATH, STYLESHEETPATH

from the active theme

Parent and child theme paths. Defined late; use get_template_directory() and get_stylesheet_directory().

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:

  1. the WP_ENVIRONMENT_TYPE environment variable, if set;

  2. the WP_ENVIRONMENT_TYPE constant, which overrides the variable;

  3. 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

WP_DEBUG

false (true in development or a development mode)

Sets error_reporting( E_ALL ) and enables the two constants below. Shows deprecation notices from _deprecated_*().

WP_DEBUG_DISPLAY

true

With WP_DEBUG, true sets display_errors to 1 and false sets it to 0; null leaves the php.ini value alone. REST, Ajax, XML-RPC and installer requests never display errors.

WP_DEBUG_LOG

false

true logs to wp-content/debug.log; a path string (5.1+) logs to that file. Only acts when WP_DEBUG is true.

SCRIPT_DEBUG

false

Loads the non-minified core JavaScript and CSS.

SAVEQUERIES

not defined

Records every query with its time and caller in $wpdb->queries. Costs memory and time on every request.

CONCATENATE_SCRIPTS

true in the admin

false stops load-scripts.php from concatenating admin scripts; useful when a broken script hides others.

WP_DISABLE_FATAL_ERROR_HANDLER

not defined

Disables the fatal error handler and recovery mode (5.2), so fatal errors show as plain PHP errors.

RECOVERY_MODE_EMAIL

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.ini value, so it changes nothing.

  • If the host forbids runtime changes (memory_limit set with php_admin_value or similar), both constants fall back to the current limit and setting them has no effect; the limit must be raised in php.ini or the pool configuration.

  • A php.ini value 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

DISABLE_WP_CRON

not defined

true stops page loads from spawning wp-cron.php. Scheduled events then only run if a system cron job calls wp-cron.php or wp cron event run --due-now.

ALTERNATE_WP_CRON

not defined

Runs cron through a redirect instead of a loopback request, for hosts where loopbacks fail. Visitors may see a ?doing_wp_cron URL; a system cron is the better fix.

WP_CRON_LOCK_TIMEOUT

60 (seconds)

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 auto_update_core_major site option, which is enabled on sites installed with 5.6 or later.

'minor'

Minor releases only, whatever the Dashboard setting says.

true

All core releases, major and minor. 'beta', 'rc', 'development' and 'branch-development' also mean "all", and also opt into pre-release builds of that channel.

false

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

edit_plugins, edit_themes (Theme and Plugin File Editors)

false

false

install_plugins, install_themes

true

false

update_plugins, update_core

true

false

delete_plugins

true

false

activate_plugins

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

FS_METHOD

direct, ssh2, ftpext or ftpsockets; chosen automatically when not set. Forcing direct on a shared host can leave files owned by the web server user.

FTP_HOST, FTP_USER, FTP_PASS, FTP_SSL

FTP or FTPS connection. FTP_SSL is FTP over TLS, not SFTP.

FTP_PUBKEY, FTP_PRIKEY

Key files for the ssh2 method; FTP_PASS may then be omitted.

FTP_BASE, FTP_CONTENT_DIR, FTP_PLUGIN_DIR

Paths as seen by the FTP/SSH account.

FS_CHMOD_DIR, FS_CHMOD_FILE

Modes for created directories and files. By default the site root’s mode OR 0755 and the mode of index.php OR 0644 (a 755/644 tree gives 755/644, a 775/664 tree 775/664); see File Permissions and Filesystem.

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

WP_CACHE

false

true makes core load the wp-content/advanced-cache.php drop-in early in the request. It caches nothing by itself; a page-cache plugin writes both the drop-in and the constant.

COMPRESS_SCRIPTS, COMPRESS_CSS, ENFORCE_GZIP

not defined

Legacy compression of the concatenated admin scripts by PHP. The web server should compress instead; do not define them on new sites.

WP_SPECULATIVE_LOADING_DEFAULT_MODE

prefetch

7.1: the mode that speculative loading’s auto setting resolves to (prefetch or prerender). Also read from an environment variable of the same name; the constant wins.

WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS

conservative

7.1: the default eagerness (conservative, moderate or eager; immediate is refused). Same constant-or-variable rule.

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

EMPTY_TRASH_DAYS

30

Days before trashed posts, pages and comments are deleted by a daily cron event. 0 disables the trash: "Trash" becomes "Delete Permanently", with no confirmation.

MEDIA_TRASH

false

true gives attachments a trash too; otherwise media are deleted permanently.

WP_POST_REVISIONS

true (unlimited)

false or 0 disables revisions; a positive integer keeps that many per post. Plugins and the wp_revisions_to_keep filter can override it per post type.

AUTOSAVE_INTERVAL

60 (seconds)

How often the editor autosaves.

IMAGE_EDIT_OVERWRITE

not defined

true keeps only one set of edited image files instead of a new set per edit.

DISALLOW_UNFILTERED_HTML

not defined

true removes the unfiltered_html capability from everyone, so even administrators' HTML goes through KSES.

ALLOW_UNFILTERED_UPLOADS

not defined

true grants administrators (super admins on Multisite) the unfiltered_upload capability, so any file type can be uploaded. Avoid it.

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/.

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

WP_ALLOW_MULTISITE

Shows Tools > Network Setup. Not needed once the network exists, but harmless.

MULTISITE

The site is a network.

SUBDOMAIN_INSTALL

true for kids.lumenpress.test, false for lumenpress.test/kids/.

DOMAIN_CURRENT_SITE, PATH_CURRENT_SITE

Domain and path of the main site.

SITE_ID_CURRENT_SITE, BLOG_ID_CURRENT_SITE

IDs of the network and of its main site.

NOBLOGREDIRECT

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. '%siteurl%' sends both to the network home URL. The ms_site_not_found action allows custom handling.

WP_MEMORY_LIMIT

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

WP_ADMIN_EMAIL

Does not exist. It is not a core constant, and nothing in core reads it. The administration e-mail is the admin_email option, changed in Settings > General (with a confirmation e-mail since 4.9) or with wp option update admin_email. One older book lists it as a configuration constant.

WP_USE_EXT_MYSQL

Obsolete. It once chose the old mysql PHP extension over mysqli. PHP 7.0 removed that extension, and WordPress 6.4 removed the remaining support from wpdb, which now always uses mysqli. One older book still recommends it.

FORCE_SSL_LOGIN

Deprecated in 4.0. Still forces HTTPS on the admin when true, but use FORCE_SSL_ADMIN.

WPLANG

Legacy (before 4.0). The site language is the WPLANG option, set in Settings > General or with wp site switch-language.

WP_ENVIRONMENT_TYPES

Removed in 5.5.1. The list of environment types is fixed; defining it only triggers a deprecation notice.

WP_ALLOW_REPAIR "to reset passwords"

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 evaluate wp-config.php more than once in a process; the official Docker image does the same for its getenv_docker() helper.

  • Environment variables are strings. 'false' is truthy in a plain define(), hence filter_var() with FILTER_VALIDATE_BOOLEAN for the debug switch; integers can be cast with (int).

  • DISALLOW_FILE_MODS is 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

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: and secrets: (mounted under /run/secrets/, read through a *_FILE variable). 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 reported staging 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 with env[LUMEN_DB_PASSWORD] = $LUMEN_DB_PASSWORD in the pool file (or set the value there), or set clear_env = no.

  • Apache with mod_php: SetEnv LUMEN_HOME https://lumenpress.test (or PassEnv) in the virtual host; the value was confirmed visible to getenv() in the official Apache image. Keep it in the server configuration, not in a .htaccess file inside the web root.

  • Kubernetes: env from a Secret or a mounted secret volume with *_FILE variables; 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

WP_ENVIRONMENT_TYPE

production (variable or constant)

Plugins disable test-only behaviour.

WP_DEBUG_DISPLAY

false

Errors never reach visitors.

WP_DEBUG / WP_DEBUG_LOG

off, or on with a log path outside the web root

wp-content/debug.log is downloadable.

SCRIPT_DEBUG, SAVEQUERIES

not defined

Slower pages, more memory.

WP_DEVELOPMENT_MODE

not defined

Disables caches meant to stay on.

DISALLOW_FILE_EDIT

true

No PHP editor for a stolen admin session.

DISALLOW_FILE_MODS

true only when code is deployed from a pipeline

Also stops automatic security updates.

WP_AUTO_UPDATE_CORE

'minor' or true; never false

Security releases must apply.

FORCE_SSL_ADMIN

true

Secure cookies even after an http:// import.

WP_HOME, WP_SITEURL

the production URL

A database import cannot redirect elsewhere.

EMPTY_TRASH_DAYS

7 to 30, not 0

Mistakes stay recoverable.

WP_POST_REVISIONS

10 to 30

Bounded wp_posts growth.

DISABLE_WP_CRON

true with a system cron job

Punctual cron on quiet sites, no cron on page views.

WP_ALLOW_REPAIR

not defined

The repair page needs no login.

ALLOW_UNFILTERED_UPLOADS

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.