Setting up new servers is tedious and time-consuming. In this article, I’ll show you how to skip all that, automate the entire process, and provision new servers in a matter of minutes with little to no intervention on your end.

Don’t get me wrong. As a web developer, there’s no better way to understand how web servers work than building your own from scratch. It’s a great learning experience, one that I recommend all WordPress developers undertake. Doing so will give you a greater understanding of the various components required to serve a website, not just the code you write. It can also broaden your knowledge on security and performance topics, which often get overlooked when you’re deep into coding.

However, once you are familiar with the process, setting up new servers is a task that you’re better off automating. Thankfully, you can do this using a tool called Ansible.

Why Ansible?

Ansible is an open-source automation tool for provisioning, application deployment (WordPress deployment in this case), and configuration management. Gone are the days of SSHing into your server to run a command or hacking together bash scripts to semi-automate painful tasks. Whether you’re managing a single server or an entire fleet, Ansible can simplify the process and save you time. So what makes Ansible so great?

Like SpinupWP, Ansible is completely agentless, meaning you don’t have to install any software on your remote servers (aka managed hosts). All commands are run through Ansible via SSH. If Ansible needs updating, you only need to update your single control machine and not any remote hosts. The only prerequisite to running Ansible commands is to have Python installed on your control machine.

Commands you execute via Ansible are idempotent, meaning they can be applied multiple times and will always result in the same outcome. This allows you to safely run multiple hosts without anything being changed unless required. For example, let’s say you need to ensure Nginx is installed on all hosts. Just run one command and Ansible will ensure only those hosts that are missing Nginx will install it. All other hosts will remain untouched.

That’s enough of an introduction. Let’s see Ansible in action.

Installing Ansible

We need to set up a single control machine which we’ll use to execute our commands. I’m going to install Ansible locally on macOS, but any Unix-like platform with Python installed would also work (e.g., Ubuntu, Red Hat, CentOS, etc.). Currently, Ansible requires Python version 3.8 or newer. Windows is not supported at this time.

To install Ansible on macOS, first, install the Python package manager, pip.

You may see a Homebrew deprecation warning about “Configuring installation scheme with distutils config files,” which points to this Homebrew issue. The issue details the fact that this is a Python deprecation warning of something that will be removed in Python 3.12, but that a full solution will be implemented before then, so it’s safe to continue.

Then install Ansible using pip.

Once the installation has completed, you can verify that everything was installed correctly by issuing:

On Linux operating systems, it should be possible to install Ansible via the default package manager. In Ubuntu 21.10 and newer, you can install it using apt.

However, if you’re running Ubuntu 20.04 LTS, you need to first add the Ansible PPA, before you can install Ansible.

The Ansible docs have detailed instructions for other operating systems.

Now that Ansible is set up, we need a few servers to work with. For the purpose of this article, I’m going to fire up three small DigitalOcean droplets with Ubuntu 20.04 LTS x64 installed. I’ve also added my public key so that it will be copied to each host during the droplet creation. This will ensure we can SSH in via Ansible using the root user without providing a password later on.

Creating droplets in DigitalOcean.

Once they’ve finished provisioning, you’ll be presented with the IP addresses.

Droplets with IP addresses.

Make sure you have manually SSHed into each droplet as the root user to validate that the ECDSA key fingerprint is valid, and add it to the list of known hosts on your control machine.

Inventory Setup

Ansible uses a simple inventory system to manage your hosts. This allows you to organize hosts into logical groups and negates the need to remember individual IP addresses or domain names. Want to run a command only on your staging servers? No problem. Pass the group name to the CLI command and Ansible will handle the rest.

Next, let’s create our inventory. Before doing so, we need to create a new directory to house our Ansible logic. Anywhere is fine, but I use my home directory.

The default location for the inventory file is /etc/ansible/hosts. However, we’re going to configure Ansible to use a different hosts file. Create a new plain text file called hosts in the new directory, with the following contents:

The first line indicates the group name. The following lines are the servers we provisioned in DigitalOcean. Multiple groups can be created using the [group name] syntax and hosts can belong to multiple groups. For example:

Now we need to configure Ansible to tell it where our hosts file is located. Create a new file called ansible.cfg with the following contents.

Running Commands

With our inventory file populated we can start running basic commands on the hosts, but first let’s briefly look at modules. Modules are small plugins that are executed on the host and allow you to interact with the remote system as if you were logged in via SSH. Common modules include aptservicefile, and lineinfile. Ansible ships with hundreds of core modules, all of which are maintained by the core development team. Modules greatly simplify the process of running commands on your remote systems and cut down the need to manually write shell or bash scripts. Generally, most Unix commands have an associated module. If not, someone else has probably created one.

Let’s take a look at the ping module, which ensures we can connect to our hosts by returning a “pong” response if successful:

To build the syntax, we provide the group, followed by the module we wish to execute. We also need to provide the remote SSH user (by default, Ansible will attempt to connect using your local user). Assuming everything is set up correctly, you should receive three success responses.

You can also run any arbitrary command on the remote hosts using the a flag. For example, to view the available memory on each host:

This time I haven’t provided a group, but instead passed all which will run the command across every host in your inventory file.

Already you should start to see how much time Ansible can save you over manually SSHing to your server to run commands, but running single commands on your hosts will only get you so far. Often, you will want to perform a series of sequential actions to fully automate the process of provisioning, deploying, and maintaining your servers. Let’s take a look at playbooks.

Ansible Playbooks

Playbooks allow you to chain commands together, essentially creating a blueprint or set of procedural instructions. Ansible will execute the playbook in sequence and ensure the state of each command is as desired before moving onto the next. If you cancel the playbook execution partway through and restart it later, only the commands that haven’t completed previously will execute. The rest will be skipped.

Playbooks allow you to create truly complex instructions, but if you’re not careful they can quickly become unwieldy. This brings us to roles.

Roles add organization to Ansible playbooks. They allow you to split your complex build instructions into smaller reusable chunks, very much like a class function in OOP programming. This makes it possible to share your roles across different playbooks, without duplicating code. For example, you may have a role for installing Nginx and configuring sensible defaults which can be used across multiple hosting environments.

Provisioning a Modern Hosting Environment on Ubuntu 20.04

For the remainder of this article, I’m going to show you how to put together a playbook based on our How to Install WordPress on Ubuntu 20.04 guide. The provisioning process will take care of the following:

  • User setup
  • SSH hardening
  • Firewall setup

It will also install the following software:

  • Nginx
  • PHP 8.1
  • MySQL
  • Redis
  • WP-CLI

You can clone the completed playbook from GitHub and follow along, but I will explain how it works below.


Let’s take a look at how our playbook is organized.

The hosts and ansible.cfg files should be familiar, but let’s take a look at the provision.yml file.

We set the group of hosts from our inventory file, select the user to run the commands, specify a few variables used by our roles, and list the roles to execute. The variables instruct Ansible which user to create on the remote hosts. We provide the username, the hashed sudo password, and the path to our public key. You’ll notice that I’ve included the password here, but for a more secure solution you should look into Ansible Vault. Once each server has been provisioned you will need to SSH in with the specified user, as the root user will be disabled.

The roles mostly map to the tasks we need to perform and the software that needs to be installed. The common role performs simple actions that do not need additional configuration, for example installing Fail2Ban.

Let’s break down the Nginx role to see how roles are put together, as it contains the majority of modules used throughout the remainder of the playbook.


Handlers contain logic that should be performed after a module has finished executing, and they work very similarly to notifications or events. For example, when the Nginx configurations have changed, run service nginx reload. It’s important to note that these events are only fired when the module state has changed. If the configuration file didn’t require any updates, Nginx will not be reloaded. Let’s take a look at the Nginx handler file:

You will see we have two handlers: One to restart Nginx and one to reload the configuration files.


Tasks contain the actual instructions which are to be carried out by the role. Nginx consists of the following steps:

The first command adds the package repository maintained by Ondřej Surý that includes the latest Nginx stable packages (this is the equivalent of doing add-apt-repository in Ubuntu). Each command is formatted the same way: provide a name, the module we wish to execute, and any additional parameters. In the case of apt_repository, we just pass the repo we wish to add.

Next, we need to install Nginx.

The command is fairly self-explanatory, but state and update_cache are worth touching upon. The state parameter indicates the desired package state, in our case we want to ensure Nginx is installed, but you could pass latest to ensure that the most current version is installed. Due to adding a new repo in the prior command we also need to ensure we run apt-get update, which the update_cache parameter handles. This will ensure the repo caches are updated so that Nginx pulls from the correct branch.

You’ll definitely need to customize the Nginx configs for whatever you’re hosting, but that’s outside the scope of this article. If you’re hosting WordPress, or really any PHP-based app , I suggest taking a look at our Install WordPress on Ubuntu 20.04 guide and downloading the accompanying Nginx configs:

Download the Complete Nginx 
Configuration Kit

Enter your name and email below and we’ll email you a zip of the Nginx configuration files. I promise we will only use your email to send you the config files, notify you of updates to the config files & this guide in the future and share helpful tips for managing your own server.


Unsubscribe any time from the footer of any email we send you.
If you want news about SpinupWP, you’ll need to subscribe at the bottom of the page.

The file module allows us to symlink the default site into the sites-enabled directory, which configures a catch-all virtual host and ensures we only respond to enabled sites. You will also see that we notify the reload nginx handler for the changes to take effect.

Next, we use the lineinfile module to update our Nginx config. We search the /etc/nginx/nginx.conf file for a line beginning with user and replace it with user {{ username }};. The {{ username }} is an Ansible variable that refers to a value in our main provision.yml file.

Finally, we restart Nginx to ensure the new user is used for spawning processes.

That’s all there is to the Nginx role. Check out the other roles on the repo to get a feel for how they work.

Running the Playbook

To run the playbook run the following command:

Assuming your hosts file is populated and the hosts are accessible, your servers should begin to provision.

The process should take roughly 5 minutes to complete all three servers, which is extraordinary when compared to the time it would take to provision them manually. Not only that, but if you’ve configured any of the roles incorrectly, you can fix that run, run the playbook, and any roles that have already been completed will be skipped. Once complete, the servers are ready to house your individual sites and should provide a good level of performance and security out of the box.


Manually running dozens of commands every time you need to add a new site becomes old quickly. You could script it, but that’s a lot of work, and before you know it your scripts are out-of-date. That gets old fast too.

SpinupWP uses Ansible to manage your server, and its scripts are always up-to-date, so you might want to give it a try. It also handles backups.

As I’m sure you can appreciate, Ansible is a very powerful tool and one which can save you a considerable amount of time.

Do you use Ansible for provisioning? What about other tools such as PuppetChef or Salt? Let us know in the comments below.