A verbose build guide for a modern, high-performance production WordPress VPS.
This project aims to provide a straightforward, albeit lengthy and all-inclusive, build guide for a low-budget, high-performance WordPress hosting solution. For as little as $5/mo., one can develop a cutting edge hosting stack for his or her projects. The instructions are verbose so that developers with little server administration experience can track.
Component | Solution | Notes |
---|---|---|
Development Client | macOS | |
Production Host | DigitalOcean | |
Server | Ubuntu LTS x64 | |
WordPress Management Tools | WP-CLI | |
Database | MariaDB | |
Object Cache Store | Redis | in-RAM (persists) |
PHP Compiler | HHVM | |
Web Server | NGINX | w/FastCGI Caching in-RAM (persists) |
Connection | Modern TLS Ciphers HTTP/2 ipv4/ipv6 |
This stack is designed to host one or multiple WordPress sites with light to medium loads. It will scale well, but it is not designed for an ultra-heavy use case that requires load balancing across multiple servers, etc. Server configurations are not one-size-fits-all, for sure, but hopefully this guide serves as a "good-enough-for-most" solution. While configuration recommendations provided are a good starting point, it is no substitution for ongoing optimization. Both speed and security have been key values during the development of this guide. The instructions to follow are scoped to only cover a single self-contained VPS. No load-balancing or CDN configuration is described, while these are highly recommended.
- Items in curly brackets {} should be treated as variables and replaced with custom info.
- Recommended Snapshot points are annotated throughout, but feel free to take these more or less frequently.
- The developer has basic Linux terminal skills.
- The developer has access to a VPS host. DigitalOcean (DO) is used for the purposes of this guide, but competitors such as Linode work just fine with minor adaptations.
- The developer has a ssh key already created. The public key is stored with the host and the private .pem stored locally at {myPK}.
The best way to support this project is to submit issues and pull requests to assist in keeping the guide up-to-date. Clicking through the maintainer's DigitalOcean affiliate link when signing up is helpful as well, but by no means expected.
Feel free to use this guide to turbocharge projects! Please submit issues or pull requests for any problems discovered.
Please provide feedback. This guide should continue to receive ongoing optimizations and updates. In its current state, it will lead to a server that is higher-performing than most, but it is not perfect and the technologies powering it are constantly changing. Issues and pull requests are welcome.
This build guide is constructed from a compilation of sources from all over the web. Inline "via"s give credit to some of these authors, but apologies go out to any blogs that were forgotten. A special recognition goes out to Mark Jaquith and Carl Alexander whose talks played fundamental roles in this architecture.
- NGINX FastCGI Cache Tuning
- HHVM Tuning
- Redis Tuning
- MariaDB Tuning
- Verify Ubuntu Automatic Upgrades
- Verify WordPress Ownership and Permissions
- SSL Certificate Installation
- SSH Key Installation
- Automated Scheduled Backups
- Automated Realtime Backups
- Automated Build Script
- Dockerize
- Clusterize
- Create a new VPS running the latest Ubuntu LTS x64 in the DO control panel.
- Enable backups.
- Enable IPv6.
- Add your SSH key.
- Locally, configure a ssh config file to make ssh easy.
-
In Terminal,
sudo nano ~/.ssh/config
Host {myVpsName} HostName {myVpsIP} Port 22 User root IdentityFile {myPK}
- Press "ctrl + x" to save and exit.
- ssh into the new VPS.
ssh {myVpsName}
- Type "yes" to continue connecting.
- Create a new user and add it to the sudo group.
adduser {myUser}
- Provide {myUserPassword} twice when prompted.
- Press "return" repeatedly to accept the rest of the default options.
usermod -aG sudo {myUser}
- via DigitalOcean
- Copy the ssh key to the new user and configure ssh.
-
mkdir /home/{myUser}/.ssh
-
cp ~/.ssh/authorized_keys /home/{myUser}/.ssh/
-
chown -R {myUser}:{myUser} /home/{myUser}/.ssh
-
chmod 700 /home/{myUser}/.ssh
-
chmod 600 /home/{myUser}/.ssh/authorized_keys
-
nano /etc/ssh/sshd_config
- Modify
PermitRootLogin no
- Uncomment and modify
PasswordAuthentication no
- Modify
-
service ssh restart
-
Do not close the Terminal window yet. In a new Terminal window,
sudo nano ~/.ssh/config
Host {myVpsName} HostName {myVpsIP} Port 22 User {myUser} IdentityFile {myPK}
-
Test ssh into the VPS as {myUser} before closing the root Terminal window.
ssh {myVPSName}
-
Type
exit
in the root Terminal window and close it. -
via DigitalOcean
- Ensure all of the latest updates are installed.
sudo apt-get update
- Provide {myUserPassword} when prompted.
sudo apt-get upgrade
sudo apt-get dist-upgrade
- Snapshot 1
sudo poweroff
- Create a Snapshot in the DO control panel.
- Configure a basic firewall with ufw.
sudo ufw allow OpenSSH
sudo ufw enable
- Type "y" to proceed with the operation.
- via DigitalOcean
- Install fail2ban to protect SSH.
sudo apt-get install fail2ban
sudo service fail2ban restart
- via DigitalOcean
- Update the timezone and configure ntp sync.
sudo dpkg-reconfigure tzdata
- Select the local timezone.
sudo apt-get update
sudo apt-get install ntp
- via DigitalOcean
- Enable a swap file.
sudo fallocate -l {swapSizeInGb}G /swapfile
- For guidance on determining {swapSizeInGb}, see here.
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo sh -c 'echo "/swapfile none swap sw 0 0" >> /etc/fstab'
- via DigitalOcean
- Configure automatic updates, upgrades, and cleanup.
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
- Uncomment
"${distro_id}:${distro_codename}-updates";
- Uncomment
sudo nano /etc/apt/apt.conf.d/10periodic
- Modify
APT::Periodic::Download-Upgradeable-Packages "1";
- Modify
APT::Periodic::AutocleanInterval "7";
- Modify
- via Ubuntu
- Snapshot 2
- Install NGINX with ngx_cache_purge.
sudo apt-get install nginx
- Install MariaDB.
- Follow the 5 commands here based on the setup.
- Use the DO node that the VPS is hosted on as the mirror in both the 4th box and the 3rd command.
- Provide {myMariaDBRootPassword} twice when prompted.
mysql_secure_installation
- Provide {myMariaDBRootPassword}.
- Type
n
for do not change root password. - Press "return" repeatedly to accept the rest of the default options.
- Install HHVM.
sudo apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0x5a16e7281be7a449
sudo add-apt-repository "deb http://dl.hhvm.com/ubuntu $(lsb_release -sc) main"
sudo apt-get update
sudo apt-get install hhvm
sudo update-rc.d hhvm defaults
sudo /usr/share/hhvm/install_fastcgi.sh
sudo mkdir /var/cache/hhvm
(do only if RAM < 512mb)sudo chown www-data:www-data /var/cache/hhvm/
(do only if RAM < 512mb)sudo nano /etc/hhvm/server.ini
- Replace
hhvm.server.port = 9000
withhhvm.server.file_socket=/var/run/hhvm/hhvm.sock
- Modify
hhvm.repo.central.path = /var/cache/hhvm/hhvm.hhbc
(do only if RAM < 512mb)
- Replace
sudo service hhvm restart
- via DigitalOcean
- Snapshot 3
- Create a database for WordPress.
mysql -u root -p
- Provide {myMariaDBRootPassword}.
CREATE DATABASE {myWPDB} DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
GRANT ALL ON {myWPDB}.* TO '{myWPDBUser}'@'localhost' IDENTIFIED BY '{myWPDBPassword}';
FLUSH PRIVILEGES;
exit
- Repeat this step for each WordPress site to be installed with new values for {myWPDB}, {myWPDBUser}, and {myWPDBPassword}.
- via DigitalOcean
- Download and install WordPress.
wget http://wordpress.org/latest.tar.gz
tar -xzvf latest.tar.gz
rm latest.tar.gz
cd ~/wordpress
cp wp-config-sample.php wp-config.php
sudo nano wp-config.php
- Modify
define('DB_NAME', '{myWPDB}');
- Modify
define('DB_USER', '{myWPDBUser}');
- Modify
define('DB_PASSWORD', '{myWPDBPassword}');
- Replace
{myWPSecurityKeys}
Generate {myWPSecurityKeys} - Modify
$table_prefix = '{myRandomPrefix}_';
(Generate {myRandomPrefix}) - Add
define( 'WP_AUTO_UPDATE_CORE', true );
- Modify
mkdir wp-content/uploads
sudo mkdir -p /var/www/{myWPSiteName}
sudo rsync -avP ~/wordpress/ /var/www/{myWPSiteName}/
rm -rf ~/wordpress/
- Repeat this step for each WordPress site to be installed with new values for {myWPDB}, {myWPDBUser}, {myWPDBPassword}, {myWPSecurityKeys}, and {myRandomPrefix}.
- via DigitalOcean
- Configure permissions and ownership. (WARNING: still under review, however these should be appropriate)
sudo chown root:root /var/www/{myWPSiteName}/
sudo chown -R {myUser}:{myUser} /var/www/{myWPSiteName}/*
sudo chown {myUser}:www-data /var/www/{myWPSiteName}/wp-config.php
sudo find /var/www/{myWPSiteName}/ -type d -exec chmod 755 {} \;
sudo find /var/www/{myWPSiteName}/ -type f -exec chmod 644 {} \;
sudo chmod 440 /var/www/{myWPSiteName}/wp-config.php
- High Security Variant (does not allow plugins to be installed via Dashboard, recommended for use in conjunction with wp-cli)
sudo chown -R www-data:www-data /var/www/{myWPSiteName}/wp-content/uploads/
- Medium Security Variant
sudo chown -R www-data:www-data /var/www/{myWPSiteName}/wp-content/
-
- via StackOverflow
- Snapshot 4
- Configure FastCGI Cache RAM disk.
sudo mkdir /mnt/ramdisk
sudo nano /etc/fstab
- Add
tmpfs /mnt/ramdisk tmpfs defaults,size=32M 0 0
- Add
sudo mount /mnt/ramdisk
sudo mkdir /var/ramdisk-backup
sudo wget https://raw.githubusercontent.com/collinbarrett/wp-vps-build-guide/master/ramdisk -O /etc/init.d/ramdisk
sudo chmod +x /etc/init.d/ramdisk
sudo /etc/init.d/ramdisk sync
sudo crontab -e
- Add
@reboot /etc/init.d/ramdisk start >> /dev/null 2>&1
- Add
2 * * * * /etc/init.d/ramdisk sync >> /dev/null 2>&1
- Add
- via Observium
- Configure nginx.
sudo ufw allow 'Nginx Full'
sudo wget https://raw.githubusercontent.com/h5bp/server-configs-nginx/master/mime.types -O /etc/nginx/mime.types
sudo wget https://raw.githubusercontent.com/collinbarrett/wp-vps-build-guide/master/nginx.conf -O /etc/nginx/nginx.conf
sudo mkdir /etc/nginx/global
sudo wget https://raw.githubusercontent.com/collinbarrett/wp-vps-build-guide/master/global/common.conf -O /etc/nginx/global/common.conf
sudo wget https://raw.githubusercontent.com/collinbarrett/wp-vps-build-guide/master/global/wordpress.conf -O /etc/nginx/global/wordpress.conf
sudo wget https://raw.githubusercontent.com/collinbarrett/wp-vps-build-guide/master/global/hackrepair.conf -O /etc/nginx/global/hackrepair.conf
sudo rm /etc/nginx/sites-available/default
sudo rm /etc/nginx/sites-enabled/default
- Note: Before using the example conf in the next step, you may need to edit it if you are not installing SSL (see step 24 and this issue).
sudo wget https://raw.githubusercontent.com/collinbarrett/wp-vps-build-guide/master/sites-available/example -O /etc/nginx/sites-available/example
sudo mv /etc/nginx/sites-available/example /etc/nginx/sites-available/{myWPSiteName}
sudo nano /etc/nginx/sites-available/{myWPSiteName}
- Modify
root /var/www/{myWPSiteName};
- Replace
example.com
with{myWPSiteUrl}
- If site should not be the default for the server, toggle listen directives so the ones without
default_server
are active.
- Modify
sudo ln -s /etc/nginx/sites-available/{myWPSiteName} /etc/nginx/sites-enabled/{myWPSiteName}
- Repeat the last four top-level bullets for each WordPress site to be installed with new values for {myWPSiteName} and {myWPSiteUrl}.
- via DigitalOcean, DigitalOcean
- Configure TLS encryption.
sudo mkdir /etc/nginx/cert
sudo chmod 700 /etc/nginx/cert
sudo openssl dhparam 2048 -out /etc/nginx/cert/dhparam.pem
sudo chmod 600 /etc/nginx/cert/dhparam.pem
- Install certificate(s) and key(s) to
/etc/nginx/cert/
.- Outside the scope of this guide.
- Free Options:
- Snapshot 5
- Install and configure WP-CLI to auto-update WordPress.
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
sudo crontab -e
- Add
0 1 * * * /usr/local/bin/wp cli update --yes --allow-root
- Add
crontab -e
- Add
0 */6 * * * cd /var/www/{myWPSiteName}/ && /usr/local/bin/wp core update --quiet && /usr/local/bin/wp core update-db --quiet && /usr/local/bin/wp plugin update --all --quiet && /usr/local/bin/wp db optimize
- Add
- via WP-CLI
- Install and configure Redis.
-
sudo apt-get install redis-server
-
sudo nano /etc/redis/redis.conf
- Add
maxmemory 64mb
- Add
maxmemory-policy allkeys-lru
- Add
-
sudo nano /var/www/{myWPSiteName}/wp-config.php
define( 'WP_CACHE_KEY_SALT', '{myWPSiteName}_' ); $redis_server = array( 'host' => '127.0.0.1', 'port' => 6379, );
-
cd /var/www/{myWPSiteName}/
-
wp plugin install wp-redis
-
sudo ln -s /var/www/{myWPSiteName}/wp-content/plugins/wp-redis/object-cache.php /var/www/{myWPSiteName}/wp-content
-
Verify Redis is working by
redis-cli monitor
and watching Terminal as you load {myWPSiteUrl} in a browser. -
Repeat all but the first bullet for each WordPress site to be installed.
-
via Codeable
- Install and configure NGINX Helper.
cd /var/www/{myWPSiteName}/
sudo nano wp-config.php
- Add
define('RT_WP_NGINX_HELPER_CACHE_PATH','/mnt/ramdisk/nginx-cache');
- Add
wp plugin install nginx-helper --activate
- Log into WordPress and navigate to "Settings -> Nginx Helper".
- Configure settings as follows. Some settings do not appear until after you click "Save All Changes" the first time.
- Check
Enable Purge
- Check
nginx Fastcgi cache
- Check
Delete local server cache files
- Check all
Purging Conditions
- Check
- Repeat all for each WordPress site to be installed.
- Snapshot 6
- If the VPS is ever resized, the swap file size should be re-evaluated.
- The size of
/mnt/ramdisk
should be tuned on occasion. - MariaDB should be tuned on occasion.