As part of my volunteering with GDI Minneapolis, I’ve been getting back into WordPress development (child themes, custom themes, etc.), while TAing, teaching, and helping develop some classes. One of the key things we want to show students is how to develop their sites safely and learn the trade of software development in the WordPress environment.
There are several ways one can do this. There are some really excellent tools out there now that make this a snap for people not versed in setting things up themselves.
This is how I set up my local environment using two tools I use heavily in other areas of web development:
First off, this is going to be less of a tutorial and more a description of what I’m doing. I’m definitely not holding this out as a definitive way to set up your local WordPress development environment, but my way that works for me. If you’re brand new to all this, and don’t want to learn all about systems and devops, then I recommend using one of the first two options above. (I’ve played a bit with Local, and find it amazingly intuitive and simple, so that’s my latest recommendation.)
Secondly, the sandbox setup is available on GitHub at github.com/tamouse/sandbox.wp.local so feel free to fork it, and do what you want with it. I’ll happily take PRs if you find bugs, too.
My working system:
- Macbook Pro 13”
- 8 GiB RAM
- 4 CPU Cores
- about 50 GiB free disk space (I didn’t need anywhere near this, it’s just what was there when I started.)
This is stuff I already had on my system because of other development I do.
Steps to Get Things Set Up
Create a project folder and initialize it
I always start my projects the same way:
mkdir -p ~/Projects/wordpress-stuff/sandbox.wp.local cd ~/Projects/wordpress-stuff/sandbox.wp.local git init echo 'Local WordPress Development Sandbox running in Vagrant with Ansible Provisioning' | tee README.md > .git/description hub create -d "$(cat .git/description)" git add -Av git commit -m 'initial commit' git push -u origin master
(Truth be told, this is one of my bash functions, so it really looked like this:
new_proj sandbox.wp.local 'Local WordPress Development Sandbox running in Vagrant with Ansible Provisioning' 'initial commit'
I typically use one of the later Ubuntu server variants; mostly I’ve been using ‘trusty’:
vagrant init 'ubuntu/trusty64'
This writes out a default
Vagrantfile (which is written in Ruby).
Vagrantfile for my needs
I modify the file so it looks like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 BOX_NAME="sandbox.wp.local" DEFAULT_IP="192.168.33.35" require "resolv" def my_ip @my_ip ||= Resolv::Hosts.new.getaddress(BOX_NAME) || DEFAULT_IP rescue @my_ip ||= DEFAULT_IP end Vagrant.configure(2) do |config| config.ssh.forward_agent = true config.vm.define :sandbox_wp do |sb| sb.vm.box = "ubuntu/trusty64" sb.vm.network "private_network", ip: my_ip sb.vm.network "forwarded_port", guest: 80, host: 8088 sb.vm.hostname = BOX_NAME sb.vm.provider "virtualbox" do |vb| # Display the VirtualBox GUI when booting the machine # vb.gui = true # Customize the amount of memory on the VM: vb.customize ["modifyvm", :id, "--memory", "2048"] vb.customize ["modifyvm", :id, "--vram", "18"] vb.customize ["modifyvm", :id, "--cpus", "2"] vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] end end config.vm.provision :ansible do |a| a.playbook = 'ansible/sandbox.yml' # a.verbose = 'vvvv' end end
Some explanation about the various settings:
These define two constants that get used later in the
edited my machine’s
/etc/hosts file, which maps IP addresses to
hostnames locally. The line I added to
/etc/hosts looks like so:
192.168.33.35 sandbox.wp.local sandbox
This lets me type ‘http://sandbox/’ or ‘http://sandbox.wp.local’ in the browser address bar to access the web server that will be running in the Vagrant Virtual Machine (aka “VM”).
(Note: when I used Local, it did something similar.)
Then I’m bringing in ruby’s
resolv standard library, which gives the
ability to use that mapping given in the
/etc/hosts file; the
DEFAULT_IP constant provides a fallback in case it can’t find
my_ip method defined sets and returns the IP address to be used
for my WordPress sandbox.
With all that handled, vagrant can begin it’s configuration. Most everything from here out can be found in vagrant’s documentation, if you want.
config.ssh.forward_agent = true
I set this to true so when I’m logged into the vagrant box, it will use my ssh keys from my development machine; this is especially helpful when using git commands that talk to GitHub, etc.
sb.vm.network "private_network", ip: my_ip
Here is where that calculation for figuring out what IP address to use that matches the name ‘sandbox.wp.local’ I set up is made.
sb.vm.hostname = BOX_NAME
This sets the VM host name, so it will match ‘sandbox.wp.local’ when I’m logged in.
vb.customize ["modifyvm", :id, "--memory", "2048"]
This reserves 2GiB of RAM for the VM.
vb.customize ["modifyvm", :id, "--vram", "18"]
This reserves 18MB of RAM for the video buffer.
vb.customize ["modifyvm", :id, "--cpus", "2"]
This allows up to 2 CPU Cores to be used by the VM.
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
This does some magic to use the host machine’s DNS resolver to find IP address, which pulls in the magic of mapping ‘sandbox.wp.local’.
config.vm.provision :ansible do |a|
Tells vagrant I’m using Ansible provisioning.
a.playbook = 'ansible/sandbox.yml'
Specifies the Ansible ‘playbook’
# a.verbose = 'vvvv'
Leaving this commented out, but usually it’s uncommented for me to be able to debug things during provisioning.
Make a git savepoint
Committing the current changes at this point to create a “save point” to get back to if I end up mucking things up.
git add -Av && git commit -m 'Vagrantfile updated' && git push
(And this is also a bash function:
gacp 'Vagrantfile updated'
At this point, I decided I would make a branch to work on Ansible stuff, too:
git checkout -b ansible-playbook
Create the Anisble Playbook
Ansible playbooks are build as YAML files, which is just a way of specifying structured data. It’s akin to JSON and XML.
I made the ansible playbook in the
ansible subdirectory, the
ansible/ group_vars/ all.yml roles/ external/ .keep internal/ cleanup/ tasks/ main.yml common/ tasks/ install.yml main.yml requirements.yml sandbox.yml sudo_roles.yml
Top level playbook
the top-level playbook that sets the whole provisioning activity
off. It is simple and just contains:
--- - include: sudo_roles.yml
Sudo Roles Playbook
the playbook that runs all the roles that need to be performs as the
superuser. In this project, that’s all of them. My playbook goes in
the following order:
Defining External Requirements
In building this up, my starting point was to figure out what
pre-built roles I could use to install the software and
configurations I would need. These filled out
roles/requirements.yml. I’m using these roles:
geerlingguy.nodejs- installs the latest stable version of Node.js
geerlingguy.apache- the Apache 2.x web server
geerlingguy.mysql- MySQL database management system, 5.x
geerlingguy.php- PHP language, 5.x
darthwade.wordpress- WordPress installation
darthwade.wordpress-apache- Apache requirements for WordPress
calebwoods.brightbox_ruby- Ruby 2.x because I like working in Ruby, too
Each of these requires some configuration. The configuration settings
set things up as follows:
- enable apache
- MySQL root password
- MySQL WordPress database and user
- Ruby version 2.2 and 2.3
- Node.js version 6, npm user
Various PHP options and modules:
- memory limit: 128MiB
- execution time: 90s
- max file upload size: 256MiB
- disable apcu
- version 4.0
- install directory
- db user:
- alias: ‘sandbox’
- admin email: “email@example.com” (because I’m not sending any emails)
These are things I installed and configured myself without relying on pre-defined roles.
sudo_roles.yml file calls
roles/internal/common/tasks/main.yml gets run automatically during
provisioning. It calls
I’m using to install necessary software packages.
After updating the APT caches, I installed:
WordPress definitely doesn’t need all of these, but my development tools and workflow generally do, and that’s what this is all about.
Final configuration and cleanup
After installing all the internal and external roles, I still had some
things I wanted to configure and clean up. These went
- enabling the VHosts module for Apache2
- disable the default and vhosts sites
- reset user and group ownership of the WordPress site to
- use the
FS_METHODwhich lets the WordPress installation directly update from the codex without using FTP.
Another Git Save Point
Yep. This is how I roll.
gacp 'Ansible Playbook Created'
Gathering the External Requirements
Specifying the external requirements is not enough, I needed to tell ansible to fetch them.
ansible-galaxy install -r ansible/roles/requirements.yml --force --ignore-errors
Bringing up the VM and first provisioning
Now I was ready to pull together all the prior stuff and build the box.
The first time you run
up vagrant will start running the
provisioning after the box comes up. After this first time, however,
when you run the
up command, vagrant doesn’t try to
So, you know, the first time you try something, you mistype something, or you forget a configuration value, and so on. I know I did. I don’t recall the specifics, but it doesn’t really matter, trial and error, get things working, figure stuff out, and eventually I ended up with a clean provisioning.
You don’t need to keep running the
up command, you run the
provision command instead while the box stays up. Since the point of
ansible is to provide an “idempotent” (i.e. same result each time
it’s run) solution, it will check if it’s successfully run a step and
skip over it. So my provisioning actually looked something like:
vagrant up # something broke, fix it vagrant provision # something broke, fix it vagrant provision # something broke, fix it vagrant provision # something broke, fix it vagrant provision # and so on vagrant provision # yay it finally worked!
And another save point, and merge back to master
gacp 'Anisble provisioning works! Yay!` git checkout master # aliased to: gco master git merge ansible-playbook gacp 'Merging ansible-playbook to master'
Set up WordPress Installation
I was now ready to give the WordPress five-minute installation a go. I
fired up my browser at
http://sandbox.wp.local and there was the
installation page, just as I’d hoped.
After running through that, playing with appearance, plugins, making a couple posts and pages, I was feeling good.
I tried installing some themes and plugins from the WordPress codex, and they installed nicely. Updated the WordPress installation itself, and everything was great.
Creating a development environment
This is really the whole point of this exercise: I wanted a sandbox that would let me develop child and custom themes, plugins, other custom things as I wanted, using my local machine to edit things, and apply my favourite tools including Sass, gulp, ruby, and so on.
With the WordPress install running in
www-data, I still needed a way to be able to edit files locally and
have them show up under the WordPress site.
wp-content folder is used for a few things, but most important
to this task, I wanted to be able to have themes and plugins available
for local editing.
Logging into the VM, I created a folder
/vagrant/dev, which would
show up on the local machine in the project root as just
Then I recursively copied the contents of
created two folders
cp -r /var/www/sandbox_wp/wp-content/themes /vagrant/dev cp -r /var/www/sandbox_wp/wp-content/plugins /vagrant/dev
Creating a little demo custom theme
To test things out, I created a custom theme under
dev/ on the local
machine, and filled with some bare-bones content:
mkdir -p dev/themes/demo
The demo content was pretty complicated to set up, and I’m not going into it here. Easier would have been just making a child theme to try stuff out.
Telling WordPress about the Custom Theme
Back over on the VM, I needed to tell the WordPress installation about this new custom theme:
cd /var/www/sandbox_wp/wp-content/themes sudo ln -s /vagrant/dev/themes/demo .
Popping back over to the browser, and pulling up the Appearances -> Themes menu, lo and behold, the custom theme now showed up.
The development workflow
I now could edit files comfortably in my local editor of choice, saving files, and view the results by refreshing the browser pointing at the WordPress site running in the VM.
I created this set up initially during WordCamp MSP 2016 for the fundamentals day so I could have a local hacking spot without trying to roll up a remote server or build up a local server that I may not want to keep around.
(This is why the demo custom theme is as complex as it is.)
One of the excellent things about using Vagrant and Ansible is the ease of which you can spin something up again if you want to. However, for future WordPress development work, I will probably be going with Local because it is such a slick product, and that’s what I’m recommending to my WordPress students.