2026年11月12日

2026年11月12日

WordPress Staging Setup: Login, Basic Auth, noindex, Stopping Email

This article is a localized edition of the Japanese original. Read the Japanese original

A staging environment is a copy of your live site where you try out updates and design changes. Because it holds the same data as production, leaving it open can cause problems: duplicate pages show up in search results, and test orders or form submissions send email to real customers.

This article walks through the settings to make on a staging site, in order. Everything was checked with WordPress 6.9 and WP-CLI 2.12 as of November 2026.

1. Set the environment type in wp-config.php

Since WordPress 5.5 you can set the environment type with the WP_ENVIRONMENT_TYPE constant. The value is one of local, development, staging or production; if you do not set it, WordPress treats the site as production.

<?php
// wp-config.php (add above the "That's all, stop editing!" line)
define( 'WP_ENVIRONMENT_TYPE', 'staging' );

Theme and plugin code can read the current value with wp_get_environment_type(). The email-blocking code later in this article uses that value to tell whether it is running on production.

If you need debug output, also set WP_DEBUG and WP_DEBUG_LOG. Note that when WP_DEBUG is not defined, it is turned on automatically only for development, not for staging.

2. Protect the whole site with Basic authentication

Keep the staging site closed to anyone outside the project. The simplest way is HTTP Basic authentication on the server. Many hosting control panels can set it up for you; here is how to do it in the configuration files.

Apache (2.4)

Create the password file (keep it outside the public web directory).

htpasswd -c /home/username/.htpasswd staginguser

Add the following to the staging site's .htaccess.

AuthType Basic
AuthName "Staging"
AuthUserFile /home/username/.htpasswd
Require valid-user

To let your office network in without a password, group the Require lines with RequireAny.

AuthType Basic
AuthName "Staging"
AuthUserFile /home/username/.htpasswd
<RequireAny>
    Require ip 203.0.113.10
    Require valid-user
</RequireAny>

Order, Allow from and Satisfy are Apache 2.2 syntax. On 2.4, use Require as shown above.

Nginx

Put the directives inside the server block (outside any location). If you put them only inside location / { … }, the separate location that handles .php files is not protected, and pages such as wp-login.php open without a password.

server {
    # ...existing settings...
    auth_basic           "Staging";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

After changing the configuration, check it with nginx -t and then load it with systemctl reload nginx.

Once a browser has passed Basic authentication, it sends the credentials with every request to the same site, including admin-ajax.php. So you do not need to exclude admin-ajax.php from authentication for the admin to work.

Side effects of Basic authentication

Requests that WordPress sends to itself from the server do not carry the Basic authentication credentials. As a result:

  • "Tools" → "Site Health" reports a failed loopback request
  • WP-Cron, which runs scheduled posts and other tasks when the site gets a visit, may not run

If you need to test scheduled tasks on staging, add define( 'DISABLE_WP_CRON', true ); to wp-config.php and run cron through WP-CLI from the server's cron.

# Example crontab entry that runs every 5 minutes
*/5 * * * * cd /home/username/staging && wp cron event run --due-now --quiet

3. Keep search engines from indexing the site

Go to "Settings" → "Reading", and under "Search engine visibility" check "Discourage search engines from indexing this site". With WP-CLI, the command is:

wp option update blog_public 0

With this setting, WordPress outputs <meta name='robots' content='noindex, nofollow' /> on every page. You do not need to add code to functions.php that prints the same tag.

This value is stored in the database, so if you copy the production database to staging again, it goes back to the production value (indexing allowed). Set it again after every copy. Also, noindex is a request to search engines, not access control. Use it together with Basic authentication.

4. Stop outgoing email

When you test orders or contact forms on staging, email can reach the users copied from production. The pre_wp_mail filter, available since WordPress 5.7, lets you stop sending through wp_mail().

<?php
// wp-content/mu-plugins/stop-mail-on-staging.php
add_filter( 'pre_wp_mail', function ( $return, $atts ) {
    if ( 'production' === wp_get_environment_type() ) {
        return $return; // Do nothing on production
    }
    $to = is_array( $atts['to'] ) ? implode( ',', $atts['to'] ) : $atts['to'];
    error_log( '[staging] mail suppressed: ' . $to . ' / ' . $atts['subject'] );
    return true; // Report success without actually sending
}, 10, 2 );

If the filter returns anything other than null, wp_mail() skips sending and returns that value. The recipient and subject are logged to debug.log (when WP_DEBUG_LOG is on). Code in mu-plugins loads regardless of which theme or plugins are active.

If you want to see the messages, you can redirect them to a developer instead.

<?php
add_filter( 'wp_mail', function ( $args ) {
    if ( 'production' === wp_get_environment_type() ) {
        return $args;
    }
    $original      = is_array( $args['to'] ) ? implode( ',', $args['to'] ) : $args['to'];
    $args['to']      = 'dev@example.com';
    $args['subject'] = '[STAGING to ' . $original . '] ' . $args['subject'];
    return $args;
} );

Neither method stops mail that a plugin or external service sends without going through wp_mail(). If you use a plugin that sends through an email delivery service's API, deactivate it on staging or remove its API key.

5. Replace URLs after copying production data

A copied production database still has the production URL in siteurl and home. If you open the staging admin in that state, you are sent to the production login screen. Replace the URLs with WP-CLI.

# First check how many replacements will be made
wp search-replace 'https://example.com' 'https://staging.example.com' --skip-columns=guid --dry-run

# If that looks right, run it
wp search-replace 'https://example.com' 'https://staging.example.com' --skip-columns=guid

wp option get siteurl
wp option get home

wp search-replace also fixes the string lengths inside serialized data. Replacing directly with SQL REPLACE can break plugin and widget settings, so do not use it.

You can also change production users' email addresses to addresses that cannot receive mail. Without --skip-email, each change sends a notification to the original address.

for id in $(wp user list --field=ID); do
    wp user update "$id" --user_email="user${id}@example.com" --skip-email
done
wp option update admin_email 'dev@example.com'

example.com is a domain reserved for examples, so the mail never reaches a real person. Avoid rewrites that add a "+" such as user+staging@gmail.com, because some providers deliver those to the original inbox.

6. Log in to staging

A staging site copied from production has the same usernames and passwords as production. If you cannot log in, check the following.

  • Are siteurl and home set to the staging URL (step 5 above)?
  • Does production use a security plugin that changes the login URL, or a two-factor authentication plugin? If so, you need to log in with the same URL and method as on production.
  • Even if your browser has a logged-in cookie for production, it does not work on staging, which has a different domain. Log in again on staging.

You can create test users with WP-CLI. If you leave out --user_pass, a password is generated and shown on screen.

wp user create staging_admin dev@example.com --role=administrator
wp user create staging_editor editor@example.com --role=editor

To change an existing user's password, add --skip-email so no notification is sent.

wp user update admin --user_pass='new-password' --skip-email

Things to watch out for

  • Staging holds production customer data as is. When you are done, delete it, or keep the access restrictions in place.
  • Manage wp-config.php and mu-plugins separately for each environment so that the staging WP_ENVIRONMENT_TYPE and the email-blocking code are not carried over when you push to production.

Related articles

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲