Roots Discourse

Multisite (subdomains: true) + landrush = "This site can’t be reached - ERR_NAME_NOT_RESOLVED"

First off, the goal is to develop and maintain domain structure as follows by using the subdomains: true approach for multisite install:

clientsite.com as main site
clientsite.ru as sub-site
clientsite.pl as sub-site
clientsite.be as sub-site

This is how the some important files are configured:

/home/user/Site/clientsite.com/trellis/group_vars/production/wordpress_sites.yml
wordpress_sites:
  clientsite.com:
    site_hosts:
      - canonical: clientsite.com
        redirects:
          - www.clientsite.com
      - canonical: clientsite.ru
        redirects:
          - www.clientsite.ru
      - canonical: clientsite.pl
        redirects:
          - www.clientsite.pl
      - canonical: clientsite.be
        redirects:
          - www.clientsite.be
    local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root)
    repo: git@github.com:company/clientsite.com.git # replace with your Git repo URL
    repo_subtree_path: site # relative path to your Bedrock/WP directory in your repo
    branch: master
    admin_user: adminuser
    admin_email: email@company.ee
    multisite:
      enabled: true
      subdomains: true
    ssl:
      enabled: true
      provider: letsencrypt
    cache:
      enabled: true
    env:
      db_prefix: custom_
      domain_current_site: clientsite.com
/home/user/Site/clientsite.com/trellis/group_vars/development/wordpress_sites.yml
wordpress_sites:
  clientsite.com:
    site_hosts:
      - canonical: clientsite-com.test
        redirects:
          - www.clientsite-com.test
      - canonical: clientsite-ru.test
        redirects:
          - www.clientsite-ru.test
      - canonical: clientsite-pl.test
        redirects:
          - www.clientsite-pl.test
      - canonical: clientsite-be.test
        redirects:
          - www.clientsite-be.test
    local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root)
    admin_user: adminuser
    admin_email: email@company.ee
    multisite:
      enabled: true
      subdomains: true
    ssl:
      enabled: false
      provider: self-signed
    cache:
      enabled: false
    env:
      db_prefix: custom_
/home/user/Site/clientsite.com/trellis/Vagrantfile
ANSIBLE_PATH = __dir__ # absolute path to Ansible directory on host machine
ANSIBLE_PATH_ON_VM = '/home/vagrant/trellis'.freeze # absolute path to Ansible directory on virtual machine

require File.join(ANSIBLE_PATH, 'lib', 'trellis', 'vagrant')
require File.join(ANSIBLE_PATH, 'lib', 'trellis', 'config')
require 'yaml'

vconfig = YAML.load_file("#{ANSIBLE_PATH}/vagrant.default.yml")

if File.exist?("#{ANSIBLE_PATH}/vagrant.local.yml")
  local_config = YAML.load_file("#{ANSIBLE_PATH}/vagrant.local.yml")
  vconfig.merge!(local_config) if local_config
end

ensure_plugins(vconfig.fetch('vagrant_plugins')) if vconfig.fetch('vagrant_install_plugins')

trellis_config = Trellis::Config.new(root_path: ANSIBLE_PATH)

Vagrant.require_version '>= 2.1.0'

Vagrant.configure('2') do |config|
  config.vm.box = vconfig.fetch('vagrant_box')
  config.vm.box_version = vconfig.fetch('vagrant_box_version')
  config.ssh.forward_agent = true
  config.vm.post_up_message = post_up_message

  # Fix for: "stdin: is not a tty"
  # https://github.com/mitchellh/vagrant/issues/1673#issuecomment-28288042
  config.ssh.shell = %(bash -c 'BASH_ENV=/etc/profile exec bash')

  # Required for NFS to work
  if vconfig.fetch('vagrant_ip') == 'dhcp'
    config.vm.network :private_network, type: 'dhcp', hostsupdater: 'skip'

    cached_addresses = {}
    config.hostmanager.ip_resolver = proc do |vm, _resolving_vm|
      if cached_addresses[vm.name].nil?
        if vm.communicate.ready?
          vm.communicate.execute("hostname -I | cut -d ' ' -f 2") do |_type, contents|
            cached_addresses[vm.name] = contents.split("\n").first[/(\d+\.\d+\.\d+\.\d+)/, 1]
          end
        end
      end
      cached_addresses[vm.name]
    end
  else
    config.vm.network :private_network, ip: vconfig.fetch('vagrant_ip'), hostsupdater: 'skip'
  end

  main_hostname, *hostnames = trellis_config.site_hosts_canonical
  config.vm.hostname = main_hostname

  if Vagrant.has_plugin?('vagrant-hostmanager') && !trellis_config.multisite_subdomains?
    redirects = trellis_config.site_hosts_redirects

    config.hostmanager.enabled = true
    config.hostmanager.manage_host = true
    config.hostmanager.aliases = hostnames + redirects
  elsif Vagrant.has_plugin?('landrush') && trellis_config.multisite_subdomains?
    config.landrush.enabled = true
    config.landrush.tld = trellis_config.site_hosts_canonical.reject { |host| host.end_with?(".#{main_hostname}") }
    config.landrush.guest_redirect_dns = false
    hostnames.each { |host| config.landrush.host host, vconfig.fetch('vagrant_ip') }
  else
    fail_with_message "vagrant-hostmanager missing, please install the plugin with this command:\nvagrant plugin install vagrant-hostmanager\n\nOr install landrush for multisite subdomains:\nvagrant plugin install landrush"
  end

  bin_path = File.join(ANSIBLE_PATH_ON_VM, 'bin')

  vagrant_mount_type = vconfig.fetch('vagrant_mount_type')

  extra_options = if vagrant_mount_type == 'smb'
    {
      smb_username: vconfig.fetch('vagrant_smb_username', 'vagrant'),
      smb_password: vconfig.fetch('vagrant_smb_password', 'vagrant'),
    }
  else
    {}
  end

  if vagrant_mount_type != 'nfs' || Vagrant::Util::Platform.wsl? || (Vagrant::Util::Platform.windows? && !Vagrant.has_plugin?('vagrant-winnfsd'))
    vagrant_mount_type = nil if vagrant_mount_type == 'nfs'
    trellis_config.wordpress_sites.each_pair do |name, site|
      config.vm.synced_folder local_site_path(site), remote_site_path(name, site), owner: 'vagrant', group: 'www-data', mount_options: mount_options(vagrant_mount_type, dmode: 776, fmode: 775), type: vagrant_mount_type, **extra_options
    end

    config.vm.synced_folder ANSIBLE_PATH, ANSIBLE_PATH_ON_VM, mount_options: mount_options(vagrant_mount_type, dmode: 755, fmode: 644), type: vagrant_mount_type, **extra_options
    config.vm.synced_folder File.join(ANSIBLE_PATH, 'bin'), bin_path, mount_options: mount_options(vagrant_mount_type, dmode: 755, fmode: 755), type: vagrant_mount_type, **extra_options
  elsif !Vagrant.has_plugin?('vagrant-bindfs')
    fail_with_message "vagrant-bindfs missing, please install the plugin with this command:\nvagrant plugin install vagrant-bindfs"
  else
    trellis_config.wordpress_sites.each_pair do |name, site|
      config.vm.synced_folder local_site_path(site), nfs_path(name), type: 'nfs'
      config.bindfs.bind_folder nfs_path(name), remote_site_path(name, site), u: 'vagrant', g: 'www-data', o: 'nonempty'
    end

    config.vm.synced_folder ANSIBLE_PATH, '/ansible-nfs', type: 'nfs'
    config.bindfs.bind_folder '/ansible-nfs', ANSIBLE_PATH_ON_VM, o: 'nonempty', p: '0644,a+D'
    config.bindfs.bind_folder bin_path, bin_path, perms: '0755'
  end

  vconfig.fetch('vagrant_synced_folders', []).each do |folder|
    options = {
      type: folder.fetch('type', 'nfs'),
      create: folder.fetch('create', false),
      mount_options: folder.fetch('mount_options', [])
    }

    destination_folder = folder.fetch('bindfs', true) ? nfs_path(folder['destination']) : folder['destination']

    config.vm.synced_folder folder['local_path'], destination_folder, options

    if folder.fetch('bindfs', true)
      config.bindfs.bind_folder destination_folder, folder['destination'], folder.fetch('bindfs_options', {})
    end
  end

  provisioner = local_provisioning? ? :ansible_local : :ansible
  provisioning_path = local_provisioning? ? ANSIBLE_PATH_ON_VM : ANSIBLE_PATH

  # Fix for https://github.com/hashicorp/vagrant/issues/10914
  if local_provisioning?
    config.vm.provision 'shell', inline: <<~SHELL
      sudo apt-get update -y -qq &&
      sudo dpkg-reconfigure libc6 &&
      export DEBIAN_FRONTEND=noninteractive &&
      sudo -E apt-get -q --option \"Dpkg::Options::=--force-confold\" --assume-yes install libssl1.1
    SHELL
  end

  config.vm.provision provisioner do |ansible|
    if local_provisioning?
      ansible.install_mode = 'pip'
      if Vagrant::VERSION >= '2.2.5'
        # Fix for https://github.com/hashicorp/vagrant/issues/10950
        ansible.pip_install_cmd = 'curl https://bootstrap.pypa.io/get-pip.py | sudo python'
      end
      ansible.provisioning_path = provisioning_path
      ansible.version = vconfig.fetch('vagrant_ansible_version')
    end

    ansible.compatibility_mode = '2.0'
    ansible.playbook = File.join(provisioning_path, 'dev.yml')
    ansible.galaxy_role_file = File.join(provisioning_path, 'galaxy.yml') unless vconfig.fetch('vagrant_skip_galaxy') || ENV['SKIP_GALAXY']
    ansible.galaxy_roles_path = File.join(provisioning_path, 'vendor/roles')

    ansible.groups = {
      'web' => ['default'],
      'development' => ['default']
    }

    ansible.tags = ENV['ANSIBLE_TAGS']
    ansible.extra_vars = { 'vagrant_version' => Vagrant::VERSION }

    if (vars = ENV['ANSIBLE_VARS'])
      extra_vars = Hash[vars.split(',').map { |pair| pair.split('=') }]
      ansible.extra_vars.merge!(extra_vars)
    end

    if !Vagrant::Util::Platform.windows?
      config.trigger.after :up do |trigger|
        # Add Vagrant ssh-config to ~/.ssh/config
        trigger.info = "Adding vagrant ssh-config for #{main_hostname } to ~/.ssh/config"
        trigger.ruby do
          update_ssh_config(main_hostname)
        end
      end
    end
  end

  # VirtualBox settings
  config.vm.provider 'virtualbox' do |vb|
    vb.name = config.vm.hostname
    vb.customize ['modifyvm', :id, '--cpus', vconfig.fetch('vagrant_cpus')]
    vb.customize ['modifyvm', :id, '--memory', vconfig.fetch('vagrant_memory')]
    vb.customize ['modifyvm', :id, '--ioapic', vconfig.fetch('vagrant_ioapic', 'on')]

    # Fix for slow external network connections
    vb.customize ['modifyvm', :id, '--natdnshostresolver1', vconfig.fetch('vagrant_natdnshostresolver', 'on')]
    vb.customize ['modifyvm', :id, '--natdnsproxy1', vconfig.fetch('vagrant_natdnsproxy', 'on')]
  end

  # VMware Workstation/Fusion settings
  %w(vmware_fusion vmware_workstation).each do |provider|
    config.vm.provider provider do |vmw, _override|
      vmw.vmx['displayName'] = config.vm.hostname
      vmw.vmx['numvcpus'] = vconfig.fetch('vagrant_cpus')
      vmw.vmx['memsize'] = vconfig.fetch('vagrant_memory')
    end
  end

  # Parallels settings
  config.vm.provider 'parallels' do |prl, _override|
    prl.name = config.vm.hostname
    prl.cpus = vconfig.fetch('vagrant_cpus')
    prl.memory = vconfig.fetch('vagrant_memory')
    prl.update_guest_tools = true
  end

  # Hyper-V settings
  config.vm.provider 'hyperv' do |h|
    h.vmname = config.vm.hostname
    h.cpus = vconfig.fetch('vagrant_cpus')
    h.memory = vconfig.fetch('vagrant_memory')
    h.enable_virtualization_extensions = true
    h.linked_clone = true
  end
end
/home/user/Site/clientsite.com/site/config/application.php
<?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(['WP_HOME', 'WP_SITEURL']);
    if (!env('DATABASE_URL')) {
        $dotenv->required(['DB_NAME', 'DB_USER', 'DB_PASSWORD']);
    }
}

/**
 * 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_';
// 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']);

if (env('DATABASE_URL')) {
    $dsn = (object) parse_url(env('DATABASE_URL'));

    Config::define('DB_NAME', substr($dsn->path, 1));
    Config::define('DB_USER', $dsn->user);
    Config::define('DB_PASSWORD', isset($dsn->pass) ? $dsn->pass : null);
    Config::define('DB_HOST', isset($dsn->port) ? "{$dsn->host}:{$dsn->port}" : $dsn->host);
}

/**
 * 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);

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');

/**
 * Allow WordPress to detect HTTPS when used behind a reverse proxy or a load balancer
 * See https://codex.wordpress.org/Function_Reference/is_ssl#Notes
 */
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    $_SERVER['HTTPS'] = 'on';
}

$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/');
}
/home/user/.vagrant.d/data/landrush/hosts.json
{
  "clientsite-ee.test": "192.168.50.5",
  "clientsite-ua.test": "192.168.50.5",
  "clientsite-ru.test": "192.168.50.5",
  "clientsite-pl.test": "192.168.50.5",
  "clientsite-be.test": "192.168.50.5",
  "clientsite-com.test": "192.168.50.5",
  "5.50.168.192.in-addr.arpa": "clientsite-com.test"
}

Right now, in development, I can reach http://clientsite.test but can’t reach the sub-sites, for example by trying to reach http://clientsite-ru.test

The result is as follows:

# This site can’t be reached

**clientsite-ru.test** ’s server IP address could not be found.

ERR_NAME_NOT_RESOLVED

To elaborate even more, when I vagrant up from project trellis folder, then vagrant ssh and then cd /etc I don’t see the /etc/resolver directori, mantioned by @knowler here:

At this point I’ve Googled for days and gone trough many posts in this forum but I’m out of ideas and can’t put together the big picture nor how to properly debug

As a side note when provisioned & deployed to production - everything works fine. I’m able to add new site, configure DNS and rename the site to the new domain address I set, for example having https://clientsite.com as the main site and https://clientsite.ru as a sub-site.

Btw, many thanks to @knowler for a great response for helping me to get the multisite running after my initial issues:

Update 1

I found out that for the subsites the host machine was simply not pointing to VM-s ip address. Doing
ping <sub-site-name>.test returned ping: <sub-site-name>.test: Name or service not known

Right now it seems to me that vagrant up doesn’t update the host machine hosts file properly since after initial vagrant up && vagrant hostmanager the /etc/hosts file contents are as follows:

127.0.0.1	localhost
127.0.1.1	pc

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

## vagrant-hostmanager-start id: 74be4c14-a29c-4a62-a776-d3cdc4c3011f
192.168.50.5	<main-site-name>.test
## vagrant-hostmanager-end

It makes me wonder why the hosts file isn’t updated properly. I eventually added the enty 192.168.50.5 <sub-site-name>.test for the sub-site manually and gained access to the sub-site.

Now trying to figure out why is this happening…