Setting up WordPress on HHVM on Digital Ocean

Note: This is a long tutorial (Over 4500 words) on how to set up WordPress on HHVM deployed with Capistrano and Composer. The actual process took me about an hour from start to finish. I’ve tried to be as complete as I could, but please leave comments if you see anything wrong or things that can be improved.

Also, I have absolutely no idea who my audience is for this post, so I apologise in advance if the level of verbosity isn’t quite for you. That being said, you will need to be at least relatively comfortable on the command line and have a basic understanding of how WordPress works – especially the wp-config.php file.

First up, let’s start with a few cautionary notes. HHVM is not finished. It still doesn’t support absolutely everything that native PHP does (it supports pretty much most of the stuff you should be using, though). If you use some crazy-ass WordPress plugins or some bonkers theme, it may well not be supported. It’s unlikely, but possible.

Second, let me make this absolutely clear; I am not an expert at this sort of stuff. I’m not a sysadmin. I’m not a devops overlord. I’m not a low-level engineer of any kind. I am, however, a guy who enjoys playing with the latest stuff and, after playing with this, I was so blown away by how fast it was, I thought I’d share my experience.

Third, this website – – doesn’t run on this stack just yet. I will be migrating to it fairly soon. Update: This site now runs on this stack. You can read the post about how I migrated from PHP to HHVM. I’ve set up several sites on this stack however, just not this one. I’ll probably write up a separate tutorial for switching from PHP-FPM and nginx to hhvm and nginx in the future.

What in the name of all that is holy is HHVM?

HHVM is an acronym for Hip Hop Virtual Machine. To quote the main HHVM site;

HHVM is an open-source virtual machine designed for executing programs written in Hack and PHP. HHVM uses a just-in-time (JIT) compilation approach to achieve superior performance while maintaining the development flexibility that PHP provides.

Yes, but what does all of that mean?

Fine question! For the most part I, honestly, have absolutely no idea. What I think it means is that HHVM reads, interprets and compiles PHP that you write at a much lower level then delivers it as fast as physically possible when it’s needed. (Just in time) In terms that I understand; this beauty is faaaaaaaast! And, beautifully, you don’t have to do anything you’re not already doing in your coding. (Unless you’re doing something crazy in which case you need to stop doing something crazy).

Why even bother with this?

Having seen just how fast this beast is, I believe, as I know Mark Jaquith does (he mentioned it in a talk he did recently), that HHVM and nginx is the future stack for WordPress platforms. I also believe that Composer (I’ll get onto what composer is later) is a rock solid way to help maintain a stable platform. Finally, Capistrano is a reliable way to deploy your application which is accessible, easy-to-use, easy-to-manipulate and importantly, stable.

HHVM, nginx, WordPress, Composer and Capistrano (oh, and MySQL) are the building blocks for this tutorial.

Step 0: A host

In order to start with all this, you’ll need somewhere to eventually put it all. You can almost certainly do this locally, on your own computer using something like Vagrant. And, in fact, you absolutely should be using something like Vagrant to run a setup locally so you know that when you deploy this to your production environment, that it will work. I’ll likely talk about that procedure in a different tutorial.

Back to the matter at hand; a host. I’ve fallen head over heels for DigitalOcean. These folks are fast (They use SSDs for everything), reliable (they have crazy-good uptime) and cheap (starts at $5 a month). They also have a great referral program. So any links to their site in this tutorial will include my referral link. I hope you don’t mind. (Hey, you get 2 months free, you win too 😉 )

So, once you’ve signed up to Digital Ocean (DO), click ‘Create Droplet‘ and select your size. This runs crazy-fast on even the $5 droplet. So if you’re just trying this out, or don’t expect a lot of traffic, the $5 option will probably be good for you.

Select your region. This is entirely up to you. Much of my traffic comes from the US/Canada, so I chose one of those regions.

Check the IPv6 box (it’s the future, go with it). Select Ubuntu 14.04 x64 and then add your SSH Key if you have one already on DO.

Choose a hostname and you’re done. Give it 45 seconds ish and you’ll have a new ‘server’ up and running. You’ll get an email from Digital Ocean with your root password (if you didn’t add your SSH key).

Step 1: Basic setup

When you get your email from DO with your root password, or after you’ve added your SSH key, you can SSH in. I’m a big fan of using TotalTerminal on a mac, but use whatever it is with which you are comfortable. Let’s imagine the IP Address of your new box is 123.456.789.012, then, in terminal;

ssh root@123.456.789.012

You’ll possibly be asked to enter your password. Go ahead and enter the one from the email you received and press enter.

OK, you’re in. At this point, there are some very basic things you can do to harden your new box. If you’re going to use this thing properly, and by ‘properly’ I mean for a production website, and not just as a test, then you may well want to follow this tutorial on a basic setup of your new Ubuntu 14.04 box. Go do that, I’ll be right here waiting. Probably drinking tea. Because I’m English. That’s what we do.

Step 2: Install prerequisites

Before we go ahead and install nginx and the rest of our stack, we need a few things, namely; git, curl, and a few other essentials. Before all that, we’ll ensure our package manager is up-to-date.

sudo apt-get update

sudo apt-get install -y unzip vim git-core curl wget build-essential python-software-properties software-properties-common

This will install a few bits and pieces. The -y flag means it’ll answer ‘yes’ to everything for you. You can remove this if you want to see what it’s doing.

Step 3: Install nginx

We install nginx first simply for ease later on. Brilliantly, the HHVM folks have built some magic into their installer which detects if nginx is installed and goes ahead and automagically adjusts the config files for you.

The first thing we’ll do in this step looks a bit weird. But it’s basically just ensuring we get the absolutely latest stable version of nginx.

sudo add-apt-repository -y ppa:nginx/stable

sudo apt-get update

sudo apt-get install -y nginx

Again, in two of the lines above, the -y flag means you don’t get asked to confirm a lot. Remove that if you wish.

OK, nice. You now have nginx installed.

Step 4: Install HHVM

Time for the nitty gritty.

wget -O - | sudo apt-key add -

echo deb trusty main | sudo tee /etc/apt/sources.list.d/hhvm.list

sudo apt-get update

sudo apt-get install -y hhvm

In order to do this we need to be able to add hhvm to our package manager. And in order to do that, we need to add the gpg key to your new box. (That’s what the first couple of lines above does).

Then we’ll update again and, finally, install hhvm.

Step 5: Configure HHVM

If you were paying attention to the output of the previous step you’ll have noticed that the fine hhvm folks have told you what to do next. Some of what they note is for apache. We don’t need that. So;

sudo /usr/share/hhvm/

sudo update-rc.d hhvm defaults

sudo service hhvm restart

Line 2 above ensures that, on reboot, hhvm runs without us having to do anything. If you don’t want that, you can skip that.

And…you’re done.

Yes, really. It really was that easy. Pretty cool, huh?

Step 5.1: Ensure HHVM runs instead of PHP

If you’ve ever run PHP from the command line or used anything like composer (which we’ll set up shortly), then you’ll know that you need to have php-cli available. i.e. so you can do something like

php -v

and you’ll get the version of PHP that you’ve installed. Now, the observant amongst you will notice that, well, we haven’t installed PHP. So that isn’t going to work is it? Fortunately, HHVM has it’s own command line tool and we can run a simple one-liner to ensure that hhvm is used whenever something asks for PHP.

sudo /usr/bin/update-alternatives --install /usr/bin/php php /usr/bin/hhvm 60

If you’re familiar with aliases, you’ll be able to liken this to aliasing ‘php’ to ‘hhvm’.

Now, if you actually go ahead and run php -v like we mentioned earlier, you’ll see that HHVM talks back to you. Awesome! The good news is… you’ve already done all of the hard work.

If you don’t want WordPress or MySQL or Composer or any of that stuff, then you have already got a working web server. You can run PHP to your heart’s content.

However, if you do want WordPress, then we’ll need to do a bit more leg work. Namely;

Step 6: Install MySQL

WordPress needs a database. And for the vast majority of the time, you need MySQL. It’s pretty easy to install:

sudo apt-get install mysql-server

You’ll be asked to provide a root password. Supply one, and don’t forget it. Make it a nice secure one, too. Now all that’s left to do is set up a few bits and pieces and run MySQL’s own setup procedure.

sudo mysql_install_db

sudo mysql_secure_installation

You won’t need to do much other than provide your root password. I told you not to forget it, didn’t I? It’ll ask you if you want to change your root password. You probably don’t need to do that. So press ‘n’ and enter. Then, it’ll ask you a series of questions about removing all of the default stuff. Simply press enter through these. It’s a nice added bit of extra security.

OK MySQL is ready to rock. Well played!

Step 7: Create a user and database ready for WordPress

We need to interact directly with MySQL for this part. That’s called an ‘interactive session’.

mysql -u root -p

You didn’t forget your root password, right? Jolly good as you’ll be asked for it.

OK, we’re in. Let’s make ourselves a database. For example’s sake, we’ll call our new database ‘wordpress‘. You probably don’t want to do that. Also, don’t forget that all commands in the interactive session need to end with a semi colon.


We now need to add a user which WordPress can use. It’s a security issue if we use the root user, so again, for example, we’ll use username as our wonderfully imaginative username. I know, I should write poetry or something.

CREATE USER username@localhost IDENTIFIED BY 'yourpasswordhere';

You probably don’t want to use that password. I recommend you try something a little more secure. OK, nearly there. We need to associate our user with our database next;

GRANT ALL PRIVILEGES ON wordpress.* TO username@localhost;

That says the username user can access all tables that we create on the wordpress database.

Finally, we need to do what’s called flush the privileges (it’s kind of like clearing your cache in your browser) and then we’re done;



Sweet deal. That’s MySQL set up. Time for a cup of tea, probably. English breakfast for me please. Strong but milky. 1 sugar. Thanks for asking.

Step 8: Install Composer

OK, as I promised earlier I’m going to talk a little about Composer. For some of you, composer may – for the moment – be overkill. The vast majority of WordPress plugins and themes do not support it natively. And, it definitely can add complexity to what could be a relatively simple setup (which, of course, WordPress is).

Composer is a dependency manager for PHP Applications. From the main Composer website, here’s what it does;

The problem that Composer solves is this:
a) You have a project that depends on a number of libraries.
b) Some of those libraries depend on other libraries.
c) You declare the things you depend on.
d) Composer finds out which versions of which packages need to be installed, and installs them (meaning it downloads them into your project).

If, at some point in the future, all WordPress plugins began supporting composer, I honestly believe an awful lot of websites would become much, much faster.

Here’s an in absurdum example; Imagine a site that uses 5 plugins. Each of those 5 plugins uses the same javascript charting library. They all do the right thing and enqueue the js properly, however, each one uses a different slug. WordPress doesn’t know they’re all actually the same thing, so you get 5 copies of the same javascript library loaded.

That happens more than you’d think.

The same can be said with PHP. Many plugin developers will use the same PHP library in their codebase. If they were to supply a composer.json file with their plugin, which lists the dependencies, only one copy of the library would be needed in total. Additionally, when WordPress’s minimum PHP version goalposts get moved and we can start supporting auto-loading, those PHP libraries would only be loaded when absolutely necessary.

The beauty of what we have to do to install Composer is that we’ve already done most of it. We just need to run;

curl -sS | php

We then, for ease, make it available by simply typing ‘composer’ anywhere on our filesystem. We do that by;

sudo mv composer.phar /usr/local/bin/composer

And that’s pretty much it for composer. It’s really that simple. We’re not going to use composer directly, our deploy script will do all that for us, and I’ll explain exactly what that does in the next section. But first, we have some setup to do.

Step 9: Prerequisites for capistrano deployment

If you are unfamiliar with Capistrano, you’re not alone. Until I started work at #Briteweb in 2013 I was blissfully unaware of deployment tools. I didn’t know how powerful they were or how easy they could make my life. On top of all of that I didn’t understand how solid, reliable and, well, awesome they are. There are several online services which offer the same sort of thing as Capistrano. However, I genuinely like having control over my entire codebase. On top of that, if I spot a feature that is missing, I can add it immediately. Oh, and it’s free.

Capistrano is an open-source (just like everything in this tutorial) server management and deployment tool written in Ruby. If you don’t speak Ruby, fear not – neither do I. The additional bits and pieces I’ve tacked on to my build script are incredibly rudimentary and, honestly, could probably be 1000 times easier and better written. However, they work. Every. Damn. Time.

This section is probably the longest in this whole tutorial, but it’s actually pretty simple. Promise.

First, we need to add a user for capistrano. I like the name ‘deployer’ but you can choose whatever you like.

adduser deployer

Next, we need to ensure we have a couple of groups available on our box into which we then place our new user.

groupadd www-data

groupadd www

sudo usermod -a -G www-data deployer

sudo usermod -a -G www deployer

This ensures that our deployer is able to modify its own files/directories as well as the ones that WordPress creates/uses.

Next up, we need to ensure that our local machine can SSH into our remote machine as the deployer user without passwords. We do this by adding an SSH key to our remote machine. The easiest way to do this is to use the ssh-copy-id command.

Chances are you don’t have ssh-copy-id installed on your local machine. If you’re on a mac, you can install it using brew thus (this is on your own local machine, don’t forget)

brew install ssh-copy-id

That’s if you have brew installed, of course. If you don’t have brew installed, then I seriously recommend you change that – it makes installing bits and bobs like this incredibly easy.

Now you can send your local machine’s SSH key to your remote server as a particular user in one line like so (again, this is on your local machine);

ssh-copy-id deployer@123.456.789.012

Now that capistrano can SSH into your server, once it does so it needs to be able to read from a git repository. In order to do this we need to generate SSH keys for the deployer user on the remote box and then add the public key to your github/bitbucket/other service. So, open up a new terminal window and ssh to the remote box as the deployer user (i.e. ssh deployer@123.456.789.012). You shouldn’t need to enter a password because you’ve already added an SSH Key for your local machine to your remote box as this user.

Now, generate the remote box’s SSH keys for this deployer user. To do that, the easiest method is to follow github’s tutorial on generating SSH Keys. One thing of note, you won’t be able to use pbcopy from your remote server. Try something like cat ~/.ssh/ and then manually copy the SSH key.

But to where?


Step 10: Set up the deployment script

I’ve set up a github repo for my deploy script which you are free to fork. You’ll want to change several things in there;

Change the config/deploy/production.rb and config/deploy/staging.rb files to match your server credentials (IP Address, user, roles, paths). It’s fairly well commented, and you’ll be able to see where you need to adjust things fairly simply, I hope.

Step 11: Set up the server ready for first deploy

We’re very nearly there. (10, 9, ignition sequence start).  In my github repo for my deploy script you will notice there’s a “shared” directory. Those files are the ones which aren’t part of our deployment process. They are shared across deploys.

This is where I need to ensure you understand what happens when we deploy.

Overview of a composer/capistrano deploy file structure
The directory and file structure on the server

Capistrano ssh’s into your remote machine, creates a new directory which is called a new release. This release directory is where everything is installed. Yes, everything. The whole project. Every time you deploy, it deploys the whole project – that includes WordPress core, all plugins, themes etc. This means that, should the deploy go wrong for whatever reason, or what you have decided to deploy is ‘broken’ in some way (say you’ve updated a plugin and it has a fatal error in it and your whole site is a white screen, for example), then we can simply revert to the previous release (this is called a rollback and it happens very, very quickly). It’s like an ‘undo’ functionality for your whole site. It’s incredibly powerful. (For reference: cap production deploy:rollback)

The way in which capistrano deploys the whole ‘app’ is to run ‘composer install’. (Hence the composer set up earlier in this tutorial and the composer.json file in the github repo). If you take a look at that file you’ll see that WordPress itself is a dependency. This means that we ‘install’ the whole of WordPress on every deploy.

However, the observant amongst you would notice that we don’t want to have our wp-config.php credentials being deployed each time and, what happens to our uploads directory? Fortunately, WordPress is pretty flexible. If we have our wp-config.php file and uploads directories in a place on our server which doesn’t change, then we can use symlinks to ensure that on each deploy, each release works independently of those pieces.

By default, in my deploy script, I set up my servers to have my websites run from /sites/ (or something along those lines anyway). If you follow the same idea, then, ssh into your server as deployer and create a ‘shared’ directory at /sites/ and place all of the files from the github repo into that directory.

You’ll then want to edit your wp-config.php file with the relevant database credentials that you set up earlier and keys/salts. You will notice that in the wp-config.php file in my github repo I manually define WP_CONTENT_DIR and WP_CONTENT_URL. This is to ensure WordPress understands where our plugins, themes and uploads are stored.

Now, create an ‘uploads‘ directory within your newly created shared directory. Ensure that the owner and group of this directory are the same as the WordPress user on your web server. This is, by default, www-data.

Step 12: The zeroth deploy – the ‘setup’ deploy

OK then, prepare for the magic. (6, 5, 4, 3) I hope.

On your local machine, in the directory where you cloned the github repo you forked earlier, run

cap deploy:setup

This will ask Capistrano to ssh into your server (As the deployer user) and set up all of the directories that it needs for deployment. It shouldn’t take long. If you get into trouble here asking for a stage, then try

cap production deploy:setup

Note: This may or may not work for you, depending on your version of Capistrano. You may need to actually manually create the directories (and chown them to your deployer user). The directories you may need to create are (if you follow my setup) all of these…


Now we’re really ready to rock

Step 14: The first real deploy

2, 1, 0, All Engines Running.  Liftoff. We have a liftoff.

cap production deploy

If you have left my capistrano script as-is, you will now be asked what ‘log level’ you want. The default is ‘error‘ (and will happen if you just press return at the prompt. But, for this first time, I recommend you type in ‘debug‘ (without the quotes) and then press return. This will let you see everything that capistrano is doing (including the composer stuff). This setting determines how much information is shown to you during the deployment process. ‘debug’ means pretty much everything. ‘error’ (the default) means only show stuff when there’s an error.

All being well, what will happen is composer will run, install WordPress, all the plugins and themes in the composer.json file, move files around as per the deployment scripts, create symlinks as necessary and end gracefully.

Note: This does not mean everything will be working just yet. Our web server doesn’t quite know what’s going on at the moment.

What we’ll want to check is that all of the directories were created, the new release included, and all symlinks have been made. So, ssh into your server and then go to your /sites/ directory. Run

ls -al

and you should see 5 things. One of which will be a symlink called ‘current’ which will be pointing to a newly made directory, something like, /sites/

“What’s this current symlink”? I hear you ask. Great question. You’re smart. This is where your ‘web root’ directory is. If you’re even remotely familiar with server setups you’ll know that you need to tell your web server (nginx in our case) where your web root is. Now, because capistrano builds a whole new release on every single deploy, this directory technically changes on every deploy. And, clearly, you don’t want to have to update your server config every time you deploy.

So, we use a symlink. This symlink gets updated on every deploy (right at the end). This means a couple of things; that, even though the deployment process may take 1 or 2 minutes (depends on how many items you have in your composer.json file and how much is stored in composer’s internal cache), the amount of downtime on your server will be practically zero (theoretically it’s the amount of time it takes your server to switch symlinks from the old release to the new one…that’s…not very long. Literally fractions of a second.) and we don’t need to update our config on every deploy.

The one remaining thing we need to do is update our server config to fit with our structure. That’s fairly straightforward;

sudo nano /etc/nginx/sites-available/default

Change (or add, if it doesn’t exist) your ‘root’ config

root /sites/;

You may also need to add index.php to the ‘index’ config, I personally prefer adding it after index.html and index.htm meaning that, should I need, I can put in an index.htm(l) file in my root and temporarily disable WP.

index index.html index.htm index.php;

Now, if you have your domain name set up (and the DNS pointing to your new box’s IP address) you can add that, too;

server_name *;

Note: This is a wildcard which means you can have a WordPress subdomain multisite setup work should you wish.

This should already be done for you, but just double check that this line is present

include hhvm.conf;

Now we have some custom WordPress-related doo-dads. First, ensure we have a ‘/’ at the end of wp-admin requests. It just makes things play nicely:

# Add trailing slash to */wp-admin requests.
rewrite /wp-admin$ $scheme://$host$uri/ permanent;

This next one needs a little explanation. Technically speaking WordPress is installed into /sites/ whereas the document root is /sites/ so this means that, again technically, if you were to visit WordPress would actually be in This almost certainly isn’t what you want, thus, let’s remove that extraneous /wp/

# Remove need for /wp/
rewrite ^(/[^/]+)?(/wp-.*) /wp$2 last;

If you want to install multisite, you may well need to point files to the wp-includes/ms-files.php file which will ensure WordPress knows how to handle any requests that come from /files/<something> (because technically that’s where WP plonks them on multisite). You’ll also need to un-comment the necessary lines in the wp-config.php file.

# Pass uploaded files to wp-includes/ms-files.php.
rewrite /files/$ /index.php last;

One final change, WordPress basically relies on an index.php file in your root install. And, even though you may not know it (because of lots and lots of url routing magic) most things gets passed through that file. We need to let our web server know this. This will be different from the default set up in this file.

location / {
    try_files $uri $uri/ /index.php?$args;

You may also notice that the index.php file is in my ‘shared’ folder. This is because this file has to be adjusted from the default one WordPress provides – again because of the /wp/ adjustment. This is all handled by a symlink which is created in the deployment scripts.

There’s definitely other bits and pieces you will want to put in here for a production site (and, in fact, you’ll probably want to move them into a separate file instead which is then called from the default file – this will make updating nginx and other modules easier). But for the purpose of this tutorial, that’s all you’ll need.

Just to be safe, let’s restart nginx and hhvm;

sudo /etc/init.d/nginx restart

sudo /etc/init.d/hhvm restart

Now, with a bit of luck, if you visit in a browser, you should be asked to install WordPress. Go ahead and do that. And then grab some scotch. Because you just shipped. And scotch is for shippers. Please drink responsibly. And, y’know if you’re not legal age to drink scotch then you should not do that.

Here’s a 2 minute-ish long anigif of the end game of this beast;


  1. Hi,

    What about firewall, maybe I have missed that part?
    Does this config fall back to PHP in case of problems?
    Is this config production ready? Can I host my website without problems with this?

    It was great reading by the way!

    1. Hi Casper,

      It’s production-ready insofar as it works 😉 However, like I said at the start, it’s down to your own code. If it’s compatible with HHVM then you should be good. There’s no “fallback to PHP” because ultimately this is a complete replacement, on your server, for PHP. I’m hosting this config on several sites (with additions such as firewalls etc. which I thought were out of scope for this tutorial). Glad you enjoyed the read and thanks for commenting.

  2. Wow. That was just an amazingly detailed, totally awesome bit of instructions, that on top of everything, was actually understandable. I’ve been wondering about HHVM (actually, wondering about all the other bits as well including nginx, etc.), but didn’t take the time to find the right tutorial. While this doesn’t answer every question I have, it is an excellent start, so thank you. (Next up…how to deal with not having an htaccess…one day, I have to find the right article on that). Anyway, you took the time to write this bunch of awesomeness, so I had to take the time to thank you for it.

    1. Hey Donna,

      I really appreciate you taking the time to write such a lovely comment. Thank you! And I’m really glad it made sense. As an aside, I found that is a great tool for converting .htaccess rules to nginx config for most things (it doesn’t work with everything, but it got me most of the way on a complex site). Hope that helps and thanks again.

  3. Thank you very much for the amazing tutorial Rich, it is the most clear and full step by step I could find about it.

    It would be deeply appreciated any further explanation about how to finish setting up ngix, firewalls and any other set up to get running a WordPress installation.

    There would be any problem installing first a wordpress multisite with with this set up and, afterwards, transfering another multisite with to the same droplet to run them at the same time?

    Cheers again!

  4. I am stuck. I am trying to run “cap deploy:setup” after installing a newer version of ruby with rbenv on my mac, and capistrano, but it gives me error “Stage not set, please call something such as `cap production deploy`, where production is a stage you have defined.”

    I started to get a bit lost in step 9.

  5. Great post, and I love your approach to writing. I too am totally in love with DigitalOcean. I’m having so much fun playing with my server that I forget to write code. Can’t wait to bump some hip hop!

    1. It isn’t! I love the ease with which composer and Capistrano allow me to deploy my site but it’s absolutely not necessary to set up WordPress with HHVM 🙂 Thanks for the comment, Kingsley.

  6. Dear Rich,

    Thanks for a great write up. As an intermediate PHP developer and turning into my own VPS admin using Digitalocean, it isn’t always easy to find a comprehensive tutorial – yours was just that – so thanks for taking me through many topics.

    After playing about with several tutorial/web server configs – I settled down into Nginx, HHVM and MariaDB instead of Mysql. My only addition to your great turial is that I also used digital ocean guide to setup nginx / HHVM and I didn’t have to add the ppa for nginx…

    I stopped short of Capistrano – just happy with wordpress and smaller PHP apps…

Leave a comment

Your email address will not be published. Required fields are marked *