Not Sure How To Get Multisite Functional in Dev Environment

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_values 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
16 Likes