Learn How To Deploy Multiple WordPress Sites Using Virtualmin and Ansible on Ubuntu 16.04

January 26, 2019

Table of Contents

A common usage of An ITWeb.Services virtual server is to host WordPress websites. This guide shows you how to automate the configuration of a virtual server from scratch (using Ansible) and deploy multiple independent WordPress websites (using Webmin/Virtualmin). Virtualmin/Webmin is a graphical user interface that allows you to manage deployment of multiple virtual server accounts on the same machine (complete with LAMP/LEMP stack). Virtualmin is very similar to cPanel and Plesk, and in this tutorial we’ll be using the free GPL edition.
After initial setup of the IT Web Services server and installation of Virtualmin, you can very quickly setup multiple virtual servers from within the Virtualmin interface and directly install WordPress on that virtual server complete with its own domain name.

In this tutorial, instead of manually entering a long list of commands, we’ll instead use Ansible. Ansible is a python based automation tool which allows you to reliably and repeatedly automate server tasks. This means once you’ve followed this tutorial, you’ll be able to deploy another server in the same way with just a couple of commands.


  • At least one Fully-Qualified Domain Name and access to the DNS records
  • An ITWeb.Services account

Step 1 – Installing Ansible on your local machine

Install Ansible on your local machine or another server.

mkdir ansible
cd ansible
virtualenv env
source env/bin/activate
pip install ansible

Step 2 – Generate SSH keys and deploy server

Ansible works by logging into your server via SSH. SSH access is most secure if we use keys rather than a password. Let’s first generate a public and private key pair.

mkdir ssh_keys
ssh-keygen -t rsa -b 2048 -f ./ssh_keys

In the ssh_keys directory there will now be two files, ssh_keys and ssh_keys.pub. ssh_keys is your private key file and should be kept safe. You can now open the ssh_keys.pub, which contains the public key.

Login to the IT Web Services web dashboard and click Deploy New Server.

Select a region, Server type (Ubuntu 16.04), Server size, and then in part 6 (SSH keys), click Add New. On the next page paste your public key and give it a name, and click Add SSH key. Finally ensure that key is selected and click Deploy now.
Once the server has finished deploying you’ll be shown its ip address. You’ll need to login to your domain name’s DNS server and point it to this address.

Step 3 – Create a basic Ansible configuration

Ansible’s automation files are called roles. We’ll first setup the directory structure (inside the ansible directory you just created in step 1), and the basic files.

mkdir -p group_vars roles/common/tasks/ roles/common/handlers
touch hosts group_vars/all deploy.yml roles/common/handlers/main.yml

Edit the hosts file to contain the following, substituting the ip address for the server you just created. Ansible uses python 2, which Ubuntu 16.04 doesn’t have installed by default. In the hosts file we tell Ansible to use python 3.

[common] ansible_python_interpreter=/usr/bin/python3

Edit the deploy.yml file to contain the following. We are going to be using the root user.

- name: apply common configuration to server
  hosts: all
  user: root
    - common

Edit the /group_vars/all file to contain the following. These variables tell Ansible the location of your SSH keys, swap file parameters, your Fully Qualified Domain Name and the root password. Please remember not to include the file in source control as it contains your password in clear text.

ssh_dir: ./ssh_keys
swap_file_path: /swapfile
swap_file_size: 1G
swappiness: 1
hostname: example.com
new_password: YOUR_PASSWORD_HERE

Edit the common/handlers/main.yml file to contain the following.

- name: restart sshd
  service: name=ssh state=restarted

Step 4 – Create Ansible tasks for basic server setup

Ansible automation is easier to understand if we break it down into tasks. Let’s create files for each of our tasks in the process.

cd roles/common/tasks
touch hosts main.yml setup.yml users.yml ufw.yml swap.yml virtualmin.yml

main.yml should point to each file containing the Ansible commands, so edit it to contain the following.

- include: setup.yml
- include: users.yml
- include: ufw.yml
- include: swap.yml
- include: virtualmin.yml

The first step in setting up a new server is to update the repo cache and set the timezone. Edit the common/handlers/setup.yml file to contain the following.

- apt: update_cache=yes
  sudo: yes
- name: set timezone to Europe/London
    name: Europe/London

Now, we’ll give the root user a password (which we will need to access the virtualmin web interface), but disable password logins over SSH (since we are using the more secure keys method of authentication). Edit users.yml to contain the following.

- name: Change passwd
  user: name=root password={{ new_password | password_hash('sha512') }} update_password=always
- name: Disable SSH password login
  lineinfile: dest=/etc/ssh/sshd_config regexp="^#?PasswordAuthentication" line="PasswordAuthentication no"
  notify: restart sshd

For security, we need a firewall. We’ll use the Uncomplicated Firewall to allow SSH access on port 22, web access on port 80 and secure web access on port 443. Edit the ufw.yml file to contain the following.

- name: Set default firewall policy to deny all
  become: True
  ufw: state=enabled direction=incoming policy=deny
  tags: firewall
- name: enable SSH in firewall
  ufw: rule=allow port=22
  sudo: yes
- name: enable HTTP connections for web server
  ufw: rule=allow port=80
  sudo: yes
- name: enable HTTPS connections for web server
  ufw: rule=allow port=443
  sudo: yes
- name: enable firewall
  ufw: state=enabled
  sudo: yes

Optionally, you can include a swap file. This is essential if your server has less than 2GB RAM to avoid out of memory crashes. Edit swap.yml to contain the following.

- name: Set swap_file variable
    swap_file: "{{swap_file_path}}"
    - swap.set.file.path
- name: Check if swap file exists
    path: "{{swap_file}}"
  register: swap_file_check
    - swap.file.check
- name: Create swap file
  command: fallocate -l {{swap_file_size}} {{swap_file}}
  when: not swap_file_check.stat.exists
    - swap.file.create
- name: Change swap file permissions
  file: path="{{swap_file}}"
    - swap.file.permissions
- name: Format swap file
  sudo: yes
  command: "mkswap {{swap_file}}"
  when: not swap_file_check.stat.exists
    - swap.file.mkswap
- name: Write swap entry in fstab
  mount: name=none
    - swap.fstab
- name: Turn on swap
  sudo: yes
  command: swapon -a
  when: not swap_file_check.stat.exists
    - swap.turn.on
- name: Set swappiness
  sudo: yes
    name: vm.swappiness
    value: "{{swappiness}}"
    - swap.set.swappiness

Step 5 – Add Ansible task for virtualmin setup

Virtualmin has its own installer file which can be downloaded and run by Ansible. Here we are using the minimal install (LINK). The additional items are to configure the MySQL server password which is not set when installed by Virtualmin. We need to temporarily stop MySQL and add the authentication directory prior to changing the password. Edit virtualmin.yml to contain the following.

- name: download virtualmin install script
  get_url: >
- name: virtualmin install (takes around 10 mins) you can see progress using $ sudo tail -f /root/virtualmin-install.log
  tags: non-idem
  shell: ~/install.sh --force --hostname {{ hostname }} --minimal --yes
    chdir: /root
- name: temp stop mysql
    name: mysql
    state: stopped
- name: change owner (and group) of mysqld dir
    path: "/var/run/mysqld"
    state: directory
    owner: mysql
    group: mysql
- name: virtualmin set mysql password
  shell: virtualmin set-mysql-pass --user root --pass {{ new_password }}
- name: restart mysql
    name: mysql
    state: started

The Ansible role is now finished and we’re ready to deploy.

Step 6 – Perform install with Ansible

From the ansible folder, we can now simply run the following command, and Ansible will carry out all of the tasks we have created automatically. The first time you connect you’ll get an SSH key warning, just type “yes” at the prompt.

ansible-playbook deploy.yml --private-key=ssh_keys/ssh_keys -i hosts

If we wish to use another server, we can simply change the ip address in the hosts file and run that command again to complete exactly the same setup.

Step 7 – Virtualmin post-installation wizard

The installation is complete and we can now go to (use the ip address of your server). Your browser will issue a security warning because the certificate is self signed, so click advanced and add an exception. You will be presented with a login page. The username is root, and the password is the one you entered into group_vars/all file in step 3. The first time you enter Virtualmin you’ll be presented with the post-installation wizard. You can either go through these settings manually or click cancel to accept the defaults.

Step 8 – Create a server and install WordPress

To get your first WordPress server up and running, from the Virtualmin dashboard click Create Virtual Server. You’ll need to enter a domain name, description and an administrators password. The domain name should be different from the Virtualmin fully qualified domain name, and you’ll need to point the DNS record to the ip address of your server.

Click Create Server. Once Virtualmin has finished creating your server, click Install Scripts on the left hand menu. Select WordPress, click Show install options, and on the following page choose the location of the WordPress install. Just choose At top level and click Install Now.

That’s all you need to do – you can complete the WordPress install by visiting your http://example.net/wp-admin/install.php (where example.net is this virtual servers domain name). If your DNS records haven’t propagated yet you can go to Services > Preview Website from the Virtualmin menu.

You can repeat this step multiple times to create multiple WordPress sites all on the same IT Web Services server.

Need help?

Do you need help setting up this on your own service?
Please contact us and we’ll provide you the best possible quote!