Four commands install WordPress with WP-CLI: wp core download, wp config create, wp db create, wp core install. That is the whole install, and on a warm machine it finishes in a few seconds.
wp core download
wp config create --dbname=wp --dbuser=wp --dbpass=secret --dbhost=127.0.0.1
wp db create
wp core install --url=https://example.test --title="Example" \
--admin_user=admin --admin_password=secret [email protected] --skip-email
Every guide stops there. We run that sequence on every sandbox this platform creates, so this post is about the four things it does that the guides do not mention.
All four are read from wp-cli/core-command on main, fetched 2026-09-12.
1. It always turns search engine indexing on
wp core install has no flag for search visibility. The value is hardcoded in Core_Command::do_install():
$public = true;
$password = wp_slash( $args['admin_password'] );
That true is the fourth argument to wp_install(), which is the blog_public option. Every WordPress install created from the command line ships with "Discourage search engines from indexing this site" switched off.
For a production site that is what you want. For a staging copy, a client demo, or a throwaway install on a public hostname, it is the opposite of what you want, and nothing in the output tells you.
The fix is one more line, and it belongs in the same script:
wp option update blog_public 0
That line is in our own provisioner for exactly this reason. It runs immediately after wp core install, before the site is ever reachable.
2. --skip-email does not suppress every email
--skip-email is worth passing, and it is documented as "Don't send an email notification to the new admin user". What it actually does is narrower than that reads:
if ( true === Utils\get_flag_value( $assoc_args, 'skip-email' ) ) {
if ( ! function_exists( 'wp_new_blog_notification' ) ) {
function wp_new_blog_notification() {
// Silence is golden
}
}
add_filter( 'send_site_admin_email_change_email', '__return_false' );
}
It pre-defines a no-op wp_new_blog_notification() before core loads, and it filters off the admin-email-change notice. Two specific emails, suppressed by name.
Anything that sends mail later in your script is untouched. A plugin activated by wp plugin install --activate in the next line can still send whatever it likes, and it will go to a real inbox.
If your install script activates plugins, the flag is not enough. Capture the mail at the server instead of trusting each plugin, which is what our email log does per site.
3. Omit --admin_password and it prints the password
The flag is optional. Leave it out and WP-CLI generates a strong password, which is good practice, and then does this:
if ( empty( $args['admin_password'] ) ) {
WP_CLI::log( "Admin password: {$result['password']}" );
}
It goes to stdout. In an interactive shell that is fine and convenient. In a CI job, a provisioning log, or a set -x script, the admin password is now sitting in a log store that a much wider group of people can read than the group that should know it.
Either pass --admin_password from your secret store, or capture stdout and treat the whole log as a secret. Do not do neither.
4. A failed download can report the version you asked for
This one is ours, not from the source, and it cost us real time.
wp core download --version=6.8 unzips the archive in PHP. On a container with the common 128M memory_limit, that extraction can exhaust memory. The fatal does not always propagate to the exit code, so the command looks like it worked, and wp core version afterwards reports the version that was already bundled in the image rather than the one you asked for.
The result is a build that installs cleanly, passes every step, and is running the wrong WordPress. Two defences, both cheap:
php -d memory_limit=512M "$(command -v wp)" core download --version=6.8 --force
test "$(wp core version)" = "6.8"
Raise the limit for that one command, then assert the version actually landed. If you pin WordPress versions for compatibility testing, the assertion is the important half: without it, "we tested on 6.8" is a claim your pipeline never checked.
Note that a tagged pre-release reports a revision suffix (7.1-beta1-59120), so match on the prefix for those rather than on equality.
Re-running the script is safe, and does nothing
wp core install on an installed site is not an error:
if ( $this->do_install( $assoc_args ) ) {
WP_CLI::success( 'WordPress installed successfully.' );
} else {
WP_CLI::log( 'WordPress is already installed.' );
}
do_install() returns false early when is_blog_installed() is true. You get a log line and exit code 0.
That makes the command safe in an idempotent provisioning script, but it also means a re-run will not apply a changed title, URL, or admin password. Those need wp option update and wp user update, and a script that assumes otherwise will silently keep the old values.
wp core download behaves differently: it errors with "WordPress files seem to already be present here" unless you pass --force.
Running as root, and the bill that comes after
Most container install scripts run as root, so every WP-CLI call needs --allow-root. The flag suppresses the warning; it does not change the outcome.
Files created by that root process are owned by root. PHP-FPM runs as www-data (uid 33 in the Debian images), so uploads fail, the plugin installer fails, and the failure mode is usually a silent one rather than a clear error.
Chown afterwards, every time:
chown -R 33:33 /var/www/html
We hit this three separate times before moving the whole install path to run as uid 33 from the start. If you are writing your own script, the chown is the cheaper fix.
Multisite from the command line
wp core multisite-install replaces wp core install and takes the same arguments plus --subdomains and --base. One restriction is enforced in the source:
WP_CLI::error( "Multisite with subdomains cannot be configured when domain is 'localhost'." );
So a local subdomain network is off unless you give it a real hostname. Both multisite install commands also finish with a reminder that you still have to write the rewrite rules and the .htaccess yourself.
If you are choosing between subdomains and subdirectories, the choice is permanent and the usual advice about it is wrong in both directions. We took that apart in how to configure WordPress multisite.
When this is the wrong tool
WP-CLI is the right way to install WordPress on a server you control. It is a poor way to get a WordPress to look at for ten minutes.
If what you want is a copy of an existing site rather than a clean one, an install script is the wrong shape entirely. Clone it, and let search-replace fix the URLs.
And if you need the install to match production exactly, the four commands are the easy part. The database, PHP version, extensions, and web server config are what actually differ, and none of them are set by wp core install.
Skipping the script
A SandyWP sandbox runs this sequence for you, with the four fixes above already applied: blog_public set to 0, the version asserted after download, the file ownership correct, and multisite available as a checkbox at create time.
You still get a shell. SSH and SFTP are per sandbox, WP-CLI is on the path, and you can run a single command without an interactive session:
sandywp ssh my-site --cmd "wp plugin list"
That is the useful half of WP-CLI, without owning the install script. If you would rather own it, the four fixes above are the ones worth copying into yours.
