Ok, so from the sounds of it, you want to host a Bedrock multisite network where the domains are all unique (not subdomains or subdirectories). For multisite installs WordPress gives you two options, subdomains or subdirectories. In order to do domain mapping — i.e. using custom domains for each site — we need to go with the subdomain option. This means we just need one Bedrock install. When you initially create a site in the network admin, you will need to create the domain as: <domain-i-want>.<main-domain>.<tld>
, but after the site is created, WordPress lets you change that domain to <domain-i-want>.<tld>
.
Here’s an example Trellis config for a couple of environments:
trellis/group_vars/production/wordpress_sites.yml (production sites)
wordpress_sites:
example.com:
site_hosts:
- canonical: example.com
redirects:
- www.example.com
- canonical: client.com # ← Add your custom domains like this.
- www.client.com
local_path: ../site
repo: git@github.com:agency/wp-network.git
repo_subtree_path: site
branch: master
multisite: # ← Multisite config stuff for Trellis
enabled: true
subdomains: true
ssl:
enabled: true
provider: letsencrypt
# Disable HSTS – I’m doing this since I don’t want HSTS on all of the domains.
hsts_max_age: 0
cache:
enabled: false
domain_current_site: example.com # ← This is the domain for the network admin.
trellis/group_vars/development/wordpress_sites.yml (development sites)
wordpress_sites:
example.com:
site_hosts:
- canonical: example.test
redirects:
- www.example.test
- canonical: client.test
redirects:
- www.client.test
local_path: ../site
admin_email: admin@example.test
multisite:
enabled: true
subdomains: true
ssl:
enabled: false
provider: self-signed
cache:
enabled: false
In development, since you are using multisite, Trellis will using the Vagrant Landrush plugin instead of the host manager to set the development domains. You do need to ensure you have the plugin installed first. You can do so with: vagrant plugin install landrush
. Vagrant Landrush creates entries in your host machine’s DNS resolver for each domain you specify. You can double check it’s working by checking the contents of the /etc/resolver
directory. After running vagrant up
(wait to do this though, there is more important config below), you should see:
/etc/resolver
├── client.test
└── example.test
Ideally, the plugin should just work and you shouldn’t need to manually do this, but I found it helpful to know what magic was occurring and how I could debug it.
Now, the code that tells WordPress to be multisite needs to go into the Bedrock application config. I’ve included the actually snippet of code (for copy & pasting) and then a diff demonstrating where that code should (probably) be added:
site/config/application.php (snippet)
// Allow Multisite
Config::define('WP_ALLOW_MULTISITE', true);
/**
* Multisite
*/
define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', true);
define('DOMAIN_CURRENT_SITE', env('DOMAIN_CURRENT_SITE'));
define('PATH_CURRENT_SITE', env('PATH_CURRENT_SITE') ?: '/');
define('SITE_ID_CURRENT_SITE', env('SITE_ID_CURRENT_SITE') ?: 1);
define('BLOG_ID_CURRENT_SITE', env('BLOG_ID_CURRENT_SITE') ?: 1);
Config::define('COOKIE_DOMAIN', $_SERVER['HTTP_HOST']);
site/config/application.php (full)
<?php
/**
* Your base production configuration goes in this file. Environment-specific
* overrides go in their respective config/environments/{{WP_ENV}}.php file.
*
* A good default policy is to deviate from the production config as little as
* possible. Try to define as much of your configuration in this file as you
* can.
*/
use Roots\WPConfig\Config;
/** @var string Directory containing all of the site's files */
$root_dir = dirname(__DIR__);
/** @var string Document Root */
$webroot_dir = $root_dir . '/web';
/**
* Expose global env() function from oscarotero/env
*/
Env::init();
/**
* Use Dotenv to set required environment variables and load .env file in root
*/
$dotenv = Dotenv\Dotenv::create($root_dir);
if (file_exists($root_dir . '/.env')) {
$dotenv->load();
$dotenv->required(['DB_NAME', 'DB_USER', 'DB_PASSWORD', 'WP_HOME', 'WP_SITEURL']);
}
/**
* Set up our global environment constant and load its config first
* Default: production
*/
define('WP_ENV', env('WP_ENV') ?: 'production');
/**
* URLs
*/
Config::define('WP_HOME', env('WP_HOME'));
Config::define('WP_SITEURL', env('WP_SITEURL'));
/**
* Custom Content Directory
*/
Config::define('CONTENT_DIR', '/app');
Config::define('WP_CONTENT_DIR', $webroot_dir . Config::get('CONTENT_DIR'));
Config::define('WP_CONTENT_URL', Config::get('WP_HOME') . Config::get('CONTENT_DIR'));
/**
* DB settings
*/
Config::define('DB_NAME', env('DB_NAME'));
Config::define('DB_USER', env('DB_USER'));
Config::define('DB_PASSWORD', env('DB_PASSWORD'));
Config::define('DB_HOST', env('DB_HOST') ?: 'localhost');
Config::define('DB_CHARSET', 'utf8mb4');
Config::define('DB_COLLATE', '');
$table_prefix = env('DB_PREFIX') ?: 'wp_';
/**
* Authentication Unique Keys and Salts
*/
Config::define('AUTH_KEY', env('AUTH_KEY'));
Config::define('SECURE_AUTH_KEY', env('SECURE_AUTH_KEY'));
Config::define('LOGGED_IN_KEY', env('LOGGED_IN_KEY'));
Config::define('NONCE_KEY', env('NONCE_KEY'));
Config::define('AUTH_SALT', env('AUTH_SALT'));
Config::define('SECURE_AUTH_SALT', env('SECURE_AUTH_SALT'));
Config::define('LOGGED_IN_SALT', env('LOGGED_IN_SALT'));
Config::define('NONCE_SALT', env('NONCE_SALT'));
/**
* Custom Settings
*/
Config::define('AUTOMATIC_UPDATER_DISABLED', true);
Config::define('DISABLE_WP_CRON', env('DISABLE_WP_CRON') ?: false);
// Disable the plugin and theme file editor in the admin
Config::define('DISALLOW_FILE_EDIT', true);
// Disable plugin and theme updates and installation from the admin
Config::define('DISALLOW_FILE_MODS', true);
// Allow Multisite
+ Config::define('WP_ALLOW_MULTISITE', true);
+
+ /**
+ * Multisite
+ */
+ define('MULTISITE', true);
+ define('SUBDOMAIN_INSTALL', true);
+ define('DOMAIN_CURRENT_SITE', env('DOMAIN_CURRENT_SITE'));
+ define('PATH_CURRENT_SITE', env('PATH_CURRENT_SITE') ?: '/');
+ define('SITE_ID_CURRENT_SITE', env('SITE_ID_CURRENT_SITE') ?: 1);
+ define('BLOG_ID_CURRENT_SITE', env('BLOG_ID_CURRENT_SITE') ?: 1);
+
+ /**
+ * Use the current HTTP host as the cookie domain. This ensures cookies and
+ * nonces are using the correct domain for the corresponding site. Without
+ * this, logins, REST requests, Gutenberg AJAX requests, and other actions
+ * which require verification will not work.
+ */
+ Config::define('COOKIE_DOMAIN', $_SERVER['HTTP_HOST']);
/**
* Debugging Settings
*/
Config::define('WP_DEBUG_DISPLAY', false);
Config::define('SCRIPT_DEBUG', false);
ini_set('display_errors', 0);
$env_config = __DIR__ . '/environments/' . WP_ENV . '.php';
if (file_exists($env_config)) {
require_once $env_config;
}
Config::apply();
/**
* Bootstrap WordPress
*/
if (!defined('ABSPATH')) {
define('ABSPATH', $webroot_dir . '/wp/');
}
Note, I’m using define()
rather than \Roots\Config::define()
since Trellis will temporarily add these constants when it deploys. The latter will throw an error that stops the deploy while the former is silent (even though constants are being redefined).
Something else to note, is that I am setting the COOKIE_DOMAIN
to $_SERVER['HTTP_HOST']
. First of all, this is to avoid the login loop issue that happens in multisite installs. Often, guides to fix that issue will tell you to leave it as define('COOKIE_DOMAIN', '')
, however, this causes issues with REST requests, which use cookie nonces to verify requests. If the domain of the current sub site (unique from the network domain) is not found the verification will fail (and Gutenberg or whatever won’t save), so setting this $_SERVER['HTTP_HOST']
is the best way to remedy the issue.
As far as I know, that’s really all that’s required for a multisite install using custom domains. If you have issues, a good debugging step is to check what is set for the domains
column in the wp_blogs
table and the rows for the siteurl
and home
option_value
s in the wp_<#>_options
tables.
Let me know if there’s any issues you encounter as there may have been something that I’ve missed.
Update:
As the multisite docs note, I found adding the following to the Vagrantfile
(line 62 or so) was important, otherwise, the VM couldn’t download anything.
config.landrush.guest_redirect_dns = false