WordPress with Vagrant: a classic caravan

Here’s why you should consider deploying WordPress with Vagrant for development and testing – and how to do it! I throw in some Composer for WordPress and wp-cli configuration as a bonus.

If you are actively developing your platform, or develop plugins or themes then you can quickly run out of installs to try things out on. You might hear of a bug that only seems to show itself with a particular version of WordPress or peculiar combination of plugins. Or perhaps you’d like to compare three or four themes side by side. When you look around your server space, though, all of your available installs are spoken for.

You can do a lot on a good host with subdomains, but it’s still a pain clicking through all the install steps, and standing up new databases, generating the .htaccess file, fixing up various configuration options.

When I need a WordPress environment for development, I grab a skeleton setup – which is really a couple of directories and files, I tweak some names, and then run vagrant up. Then it’s time to make coffee while Vagrant sets up a Linux virtual machine, downloads and installs the packages I’ve specified, sets up the MySQL database, installs the version of WordPress I need, grabs and activates my chosen theme and plugins, handles configuration, and manages URL rewriting.

If you’ve worked with WordPress for a while, you’ll know that none of that is particularly hard work but can be a drag – especially if you have to do it often.


So what’s all this about Vagrant then? Vagrant is really just a management layer that works with your favourite virtualization package (probably VirtualBox). Because it uses virtualization, you don’t need to worry about hosting, but you can still work with an instance of WordPress on a platform that’s near-identical to your public server. Not only that, but Vagrant can mount directories from your local computer (the so-called host machine) on the virtual OS (the guest machine). That means that you can use your local development tools without the need for uploading or installing.

In order for all this easy stuff to work, you’ll need hardware that supports virtualization (most modern computers do) and Vagrant installed. The installation processes for both Virtualbox and Vagrant are pretty easy these days.

Once Vagrant is installed on a machine, I usually add a Vagrant plugin: vagrant-hostmanager. This configures hostnames for you so that you can use something more friendly than an IP address to access your website. Adding a plugin to vagrant is simple:

$ vagrant plugin install vagrant-hostmanager 

Setting up the environment

Made it through the installation phase? Great.

So here’s the skeleton I’m going to build. It consists of two mostly empty sibling directories dev and infrastructure.



Let’s begin with infrastructure. This is where we’ll put the files for managing and configuring the Vagrant build. Vagrantfile is the heart of this process – it tells vagrant where to get its virtual machine, which directories to mount, and all sorts of other stuff. Luckily, you can generate a vanilla Vagrantfile and only edit what you need to customise. Let’s do that. First, I’ll set aside my Vagrantfile so that I can generate a new one for you.

Now to generate the Vagrantfile:

$ cd infrastructure
$ vagrant init
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

As the output of the init command says – the generated file is full of useful comments. I’m going to stick to what I need to add or alter.

Now let’s edit Vagrantfile. There’s one line we’ll need to replace. I look for:

config.vm.box = "base"

and replace it with a reference to a virtual machine. There are plenty of directories available online. I want to use CentOS, which is a nice stable distro and part of the RedHat family. I look at Vagrant’s own resource for boxes – and find one that will work for me. I edit the config.vm.box line.

config.vm.box = "bento/centos-6.7"

If you chose a different distro and you’re following along be aware you’ll probably need to amend the setup script I describe below in order to use the right package management commands for your OS.

This is enough to get a working box but let’s add some more configuration before we proceed.

config.ssh.forward_agent = true

  config.vm.synced_folder "../dev", "/var/www/happydev"

  config.vm.hostname = "happydev.vagrant.internal" 
  config.hostmanager.enabled = true
  config.hostmanager.manage_host = true

  config.vm.network :private_network, ip: ""
  config.vm.provision "shell", path: "setup.sh" 

Let’s break that down. config.ssh.forward_agent (line 1) will magically import your ssh-agent settings into the box enviornment – that means that your keys, for example, will work from within the context of the box – very handy if you’re checking stuff out from git.

Then I use config.vm.synced_folder so that a directories on the local filesystem (relative to the infrastructure directory) will be mounted on the guest box.

In the next block of directives, I set up networking. I specify a more or less arbitrary hostname and IP address. If you haven’t installed hostmanager plugin, you’ll need to comment out the config.hostmanager.* lines – or your deployment will fail with an error – and you’ll need to configure your own `/etc/hosts</code file (or equivalent).

Finally, I use config.vm.provision` to tell vagrant to run my install script: `setup.sh` And that’s it for Vagrantfile.

Later on, I’ll have a full install script in setup.sh – but I’ll replace that for now with a placeholder script:

echo hello;

We can ignore happydev.conf for now – it contains basic apache configuration and we’ll copy it into place on the guest box later on.

Let’s kick things off then.

$ vagrant up

For the first time round, you’ll need to wait while Vagrant downloads the box. It will cache it, though, so future deployments will be faster.

Here’s an edited version of my up command’s output:

Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'bento/centos-6.7'...


==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Setting hostname...
==> default: Configuring and enabling network interfaces...
==> default: Mounting shared folders...
    default: /vagrant => /home/mattz/scrap/happydev/infrastructure
    default: /var/www/happydev => /home/mattz/scrap/happydev/dev
==> default: Updating /etc/hosts file on active guest machines...
==> default: Updating /etc/hosts file on host machine (password may be required)...
==> default: Running provisioner: shell...
    default: Running: /tmp/vagrant-shell20170404-12521-1cxro40.sh
==> default: hello

So, we can see it has followed my instructions. It has mounted two directories (infrastructure and dev), configured my host, and run my set up script (that’s the hello at the end).

Let’s log on to the box and take a quick look.

$ vagrant ssh

I am logged in to the server. And I can see my WordPress development directory:

[vagrant@happydev ~]$ ls -al /var/www/happydev/
total 12
drwxrwxr-x. 1 vagrant vagrant 4096 Apr  4 16:58 .
drwxr-xr-x. 8 root    root    4096 Apr  4 16:57 ..
-rw-rw-r--. 1 vagrant vagrant 1266 Apr  4 16:15 composer.json


In fact, Vagrant thinks we have already provisioned this box – that’s because I specified the script setup.sh which it duly ran. In order to get Vagrant to run a more involved script I could destroy and recreate the box.

$ vagrant destroy
$ vagrant up

This is a time consuming way of going about things – though I do it occasionally to check that everything still works from scratch. More often though, it’s enough to tell Vagrant to re-provision the box.

$ vagrant provision

So what should I have setup.sh do? The answer is – everything else. Here is my setup script broken up into chunks and annotated.

First, some configuration:





These should be pretty self-explanatory. Note that the URL matches the hostname I configured in Vagrantfile and that SERVERDIR matches the mounted directory on the guest box. There is nothing magic about any of these variables – they are not related to Vagrant at all. They are internal to the script and allow me to configure new boxes in a relatively central location without too much find/changing.

Next a bit of housekeeping:

die() {
    echo $1;
    exit 1;

Just a convenience function for killing the script if necessary. Next, I install various core packages.

sudo rpm -Uvh https://mirror.webtatic.com/yum/el6/latest.rpm
yum install -y php70w php70w-opcache

sudo yum -y remove mysql-libs
sudo yum -y install mysql-server
sudo yum -y install httpd;
sudo yum -y install php70w
sudo yum -y install php70w-mysql
sudo yum -y install php70w-xml

CentOS uses Yum as its package manager. By default, it isn’t very adventurous with PHP versions – so the webtatic repo is a must. After installing that I’m able to install PHP 7, and some associated extensions.

Next: Composer:

cd /tmp
curl -sS https://getcomposer.org/installer | php 
sudo mv composer.phar /usr/bin/composer
composer update

I get Composer – then I change directory to $SERVERDIR – in this case /var/www/happydev and run composer update. Of course that’s where the magic of Composer and and wpackagist.org come into play. This stage will get me the WordPress core files and any plugins and themes I need. You can read more about installing and setting up WordPress with Composer in an earlier post.

I prevent irritating warnings about date and time by adding a timezone to php.ini:

sudo sed -i 's^;[ ]*date.timezone =[ ]*.*^date.timezone = Etc/UTC^' /etc/php.ini

and move on to the database:

sudo service mysqld start

/usr/bin/mysqladmin -u root password 'vagrant' || echo " -- unable to create pass - probably already done"
/usr/bin/mysqladmin -u root -h $URL password 'vagrant' -pvagrant || echo " -- unable to create pass - probably already done";

echo "CREATE DATABASE IF NOT EXISTS $DBNAME" | mysql -u root -pvagrant
echo "GRANT ALL PRIVILEGES  ON $DBNAME.* TO '$DBUSER'@'localhost' IDENTIFIED BY '$DBPASS' WITH GRANT OPTION" | mysql -u root -pvagrant
echo "FLUSH PRIVILEGES" | mysql -u root -pvagrant

I start mysql, and create the database and user specified in the configuration section.

Time for Apache:

sudo cp $VAGRANTDIR/$APACHECONF /etc/httpd/conf.d/

I copy across the apache configuration file. This is a very basic apache set up which establishes the host and a web-facing directory at /var/www/happydev/site.

Next I move on to WordPress at last:

mkdir -p ~/.wp-cli
echo "apache_modules:
  - mod_rewrite" > ~/.wp-cli/config.yml

./vendor/bin/wp core config \
    --dbname=$DBNAME \
    --dbuser=$DBUSER \
    --dbpass=$DBPASS \
    --path=site \
    --force || die "wp-cli core install failed"

# install the database tables
./vendor/bin/wp core install \
    --url=$URL \
    --title=$TITLE \
    --admin_user=$ADMINUSER \
    --admin_email=$ADMINMAIL \
    --admin_password=$ADMINPASS \
    --path=site || die "wp-cli core install failed"

# set rewrite structure
./vendor/bin/wp rewrite structure /%postname%/ --hard --path=site
./vendor/bin/wp theme activate onepress --path=site

Thanks to Composer I have access to wp-cli. You may have read a few posts about this amazing tool already on this blog – one about basic setup with wp-cli, another about using it for migrating the database, and a third about importing and exporting posts. I use it here for performing the install and configuration and then for generating the .htaccess file to handle URL rewriting.

The Composer file

I skirted a little over that composer update stage earlier. Remember that Composer is a dependency management system. It installs what you tell it to, as well as anything needed by the packages that you specify. wpackagist.org track and manage WordPress and its plugins and themes for us, so we can set up much of WordPress via a single composer.json file (though we still need to rely on tools like wp-cli to manage local configuration and the database).

Here is the composer.json file I am using for this example.

    "name": "getinstance/happyfun",
    "description": "core for http://getinstance.com",
    "authors": [
            "name": "Matt Zandstra",
            "email": "matt@getinstance.com"
    "bin": ["bin/"],
    "repositories": [
            "type": "package",
            "package": {
                "name": "wordpress/wordpress",
                "type": "webroot",
                "version": "4.7.3",
                "dist": {
                    "type": "zip",
                    "url": "https://github.com/WordPress/WordPress/archive/4.7.3.zip"
    "require": {
        "wordpress/wordpress": "4.7.3",
        "fancyguy/webroot-installer": "1.0.0",
        "wp-cli/wp-cli": "1.1.0",
        "wpackagist-theme/onepress": "1.3.0",
    "extra": {
        "webroot-dir": "site",
        "webroot-package": "wordpress/wordpress",
        "installer-paths": {
            "site/wp-content/plugins/{$name}/": ["type:wordpress-plugin"], 
            "site/wp-content/themes/{$name}/": ["type:wordpress-theme"]

I specify WordPress 4.7.3 along with a theme and a plugin as proof of concept.

And now.. WordPress with Vagrant – I hope

Well, here goes nothing!

vagrant provision

Vagrant generates a LOT of verbosity at this point – passing through the output from all the commands in setup.sh. If I’m lucky the process ends without error. You should design your setup script so that it can run harmlessly multiple times – that way missteps can be corrected by running provision again (and again). Vagrant supports tools for provisioning such as ansible that you might find better suited to this job. I have always found a quick and dirty shell script adequate for my purposes.

The moment of truth

It may seem that I’ve put in a lot of work here – but remember, this is a script – I can use it over and over again with very little alteration.

Let’s see whether I have a running WordPress install. I point my browser at http://happydev.vagrant.internal.

And there it is! I am ready to start coding. Next time I want to launch this environment I just need to run

vagrant up

and thirty seconds later the site will be good to go.