in Code, Tech Trends, Tutorials

Super-fast Secure WordPress Install on DigitalOcean with NGINX, PHP7, and Ubuntu 16.04 LTS

Updated: 2018-01-01

I’ve recently begun migrating websites from my old web host to DigitalOcean. Today, I’m documenting the steps I use to stand up a new server instance. Some of these technologies are still close to the bleeding edge, so if you’re really worried about stability, you may want to stick with some of the more battle tested (i.e. older) versions of these packages. However, I’ve had pretty solid results so far with these:

  1. Ubuntu 16.04 LTS
    “Xenial Xerus” was launched 21 April 2016, and is the most recent LTS version of Ubuntu.
  2. NGINX
    While Apache is still the predominant server of choice for WordPress installs, NGINX offers speed improvements, particularly related to not having to process .htaccess files in every directory–even wordpress.com uses NGINX.
  3. PHP7-FPM
    PHP released the latest major version of its server back in December 2015, and may more than double the speed of page loads, particularly when paired with NGINX.
  4. LetsEncrypt
    As I’ve blogged about before, setting up HTTPS by default for all pages is rapidly becoming the new norm, and I’ve got some updates to my earlier post on how to do this.

So let’s get started!

Step 1: Install and Configure your Droplet

While DigitalOcean already has a pre-configured droplet that comes with a LAMP stack and WordPress, I’m not going with that one for the following reasons:

  1. It won’t install on the smallest size droplet, meaning you have to spend at least $10/month on your site. While this may end up being necessary, I’d prefer to have the option to keep it to the smallest, cheapest size and scale up when I’m ready.
  2. Since we’re using NGINX, we don’t need Apache, and don’t have to worry about uninstalling it.
  3. We can install PHP7 instead of the version of PHP5 that comes on that droplet.

So, I would follow the instructions in this post. For a small droplet like this, you could go with either the 32-bit or 64-bit OS. DigitalOcean recommends 32-bit for smaller installs since some processes on 64-bit architecture require more RAM. I chose 64-bit for scalability.

IMPORTANT: when you name your droplet, be sure to name it using your domain name so that the hostname and reverse DNS will be set up properly

Next follow the Initial Server Setup with Ubuntu 16.04 tutorial.

Step 2: Point your Domain Name at your Server

At this point it’s also a good idea to set up your domain name to point to your server. Once you’ve set it up, it can take up to a couple of days for the change to take effect, but in practice it usually only takes a few minutes to a couple of hours. Log into the site where you registered your domain name and change the nameservers to point to ns1.digitalocean.com , ns2.digitalocean.com , and ns3.digitalocean.com . After you do that, head over to your DigitalOcean account and go to Networking > Domains. In the form to “Add a Domain” enter your domain name and then select the droplet you created in Step 1. I typically set up my domains to route email through Mailgun’s servers, but I’m also going to show how to do this with GMail as well, since that’s a popular use case. If you are using Mailgun, once you get done editing your domain, it should look something like this:

DNS Records for Morphatic.com

Alternatively, you can follow this tutorial to set up your DNS to use GMail’s servers.

Step 3: Install NGINX

In order to use a WordPress plugin for purging the NGINX cache that I talk about below, you have to install a custom version of NGINX. From the command line:

$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3050AC3CD2AE6F03
$ sudo sh -c "echo 'deb http://download.opensuse.org/repositories/home:/rtCamp:/EasyEngine/xUbuntu_16.04/ /' >> /etc/apt/sources.list.d/nginx.list"
$ sudo apt-get update
$ sudo apt-get install nginx-custom
$ sudo ufw allow 'Nginx Full'
$ sudo ufw enable

This will download and install NGINX and set up the firewall to allow both HTTP (port 80) and HTTPS (port 443) traffic.

Step 4: Install and Configure MariaDB

MariaDB is a drop-in replacement for MySQL. You can read about why people think it’s better, but I’m mostly convinced by the performance arguments. The MariaDB website has a convenient tool for configuring the correct repositories in your Ubuntu distro. Using the tool, I came up with the following steps for installing the DB:

$ sudo apt-get install software-properties-common
$ sudo apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xF1656F24C74CD1D8
$ sudo add-apt-repository 'deb [arch=amd64,i386,ppc64el] http://nyc2.mirrors.digitalocean.com/mariadb/repo/10.2/ubuntu xenial main'
$ sudo apt-get update
$ sudo apt-get install mariadb-server

When the following screen comes up, make sure you provide a good secure password that is different from the password you used for your user account.

Setting up root password for MariaDB

Setting up root password for MariaDB

Next, lock down your MariaDB instance by running:

$ sudo mysql_secure_installation

Since you’ve already set up a secure password for your root user, you can safely answer “no” to the question asking you to create a new root password. Answer “Yes” to all of the other questions. Now we can set up a separate MariaDB account and database for our WordPress instance. At the command prompt type the following:

$ mysql -u root -p

Type in your password when prompted. This will open up a MariaDB shell session. Everything you type here is treated as a SQL query, so make sure you end every line with a semicolon! This is very easy to forget. Here are the commands you need to type in to create a new database, user, and assign privileges to that user:

MariaDB [(none)]> CREATE DATABASE mywpdb DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
MariaDB [(none)]> GRANT ALL ON mywpdb.* TO 'mywpdbuser'@'localhost' IDENTIFIED BY 'securepassword';
MariaDB [(none)]> FLUSH PRIVILEGES;
MariaDB [(none)]> quit

Note that although it’s customary to use ALL CAPS to write SQL statements like this, it is not strictly necessary. Also, where I’ve used “mywpdb” and “mywpdbuser” feel free to use your own database and user names.

Finally, it is recommended that you create a MariaDB sources.list file. This file will make sure that when your server periodically looks for updates, it will also check the MariaDB repository to keep it up to date. Create the file and open it for editing by typing the following at the command prompt:

$ sudo nano /etc/apt/sources.list.d/MariaDB.list

Copy and paste the following code into that file:

# MariaDB 10.2 repository list - created 2017-12-31 19:19 UTC
# http://downloads.mariadb.org/mariadb/repositories/
deb [arch=amd64,i386] http://nyc2.mirrors.digitalocean.com/mariadb/repo/10.2/ubuntu xenial main
deb-src http://nyc2.mirrors.digitalocean.com/mariadb/repo/10.2/ubuntu xenial main

Save and close the file by typing Ctrl + x.

Step 5: Install and Configure PHP7-FPM

One of the cool things about Ubuntu 16.04 is that it’s default PHP packages now default to version 7! Installing PHP is as simple as typing the following:

$ sudo apt-get install -y zip unzip php-fpm php-mysql php-xml php-gd php-mbstring php-zip php-curl

Note that this also installs the MySQL, XML, Curl and GD packages so that WordPress can interact with the database, support XMLRPC (important if you use Jetpack), and also automatically crop and resize images. It also installs zip/unzip (primarily because my favorite plugin for backups needs them).

Optionally, we can adjust our php.ini settings. Open /etc/php/7.0/fpm/php.ini using nano as follows:

$ sudo nano /etc/php/7.0/fpm/php.ini

You can search for the line you want to edit by hitting CTRL + W and then typing the text of the setting you’re looking for. I usually want to adjust the post_max_size and upload_max_filesize settings to something larger than their defaults of 8MB and 2MB, respectively (I set mine to 256MB). I also set the memory_limit property to 256M as well. I frequently find that in my WordPress sites I want to upload larger files. Once you’re done editing, hit CTRL + X to exit nano, and follow the prompts to save your changes. To get PHP to load the changes you need to restart it by typing:

$ sudo service php7.0-fpm restart

Step 6: Tell NGINX to use PHP7-FPM

Open up the configuration file for your default site for NGINX:

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

Edit the file so that it looks like this but change yoursite.com www.yoursite.com to reflect the URL for your website:

server {
  listen 80 default_server;
  listen [::]:80 default_server;

  root /var/www/html;
  index index.php index.html;

  server_name example.com www.example.com;

  location / {
    try_files $uri $uri/ =404;
  }

  location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php7.0-fpm.sock;
  }

  location ~ /\.ht {
    deny all;
  }
}

Save and exit this file, and then restart NGINX by typing the following:

$ sudo service nginx restart

In order to test out whether or not your changes worked, you can optionally create a basic PHP file at the root of your web server by typing:

$ echo "<?php phpinfo();" | sudo tee /var/www/html/index.php > /dev/null

Then you can go to a web browser and type in http://your-IP-address and you should get the auto-generated PHP Info page which looks something like this:

Default PHP Info page

Default PHP Info page

Woohoo!!! Now we’re getting somewhere. We’re going to be making a bunch of changes to our NGINX config in later steps for security and optimization, but this is the absolute minimum you need to do to get PHP7-FPM and NGINX playing well together.

Step 7: Set up SSL Certificates with LetsEncrypt

In the next step we’re going to add an SSL certificate to our site and then configure NGINX to use it. I recommend that you read DigitalOcean’s entire tutorial on securing NGINX on Ubuntu 16.04 with LetsEncrypt, but I’ll provide just the steps you need here. First, install LetsEncrypt:

$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx

Next, we’ll install our certs using:

$ sudo certbot --nginx

Follow the instructions. Assuming you entered your domain name in the Nginx config file for your site above, the certbot tool should be able to automatically detect what domains you’d like to generate certificates for. Make sure you pick a reliable email address for receiving notifications.

Next we’ll edit the configuration snippet for NGINX that was created by Certbot and which will contain all of our SSL parameters. Open the file as follows:

$ sudo nano /etc/letsencrypt/options-ssl-nginx.conf

Edit the file so that it looks like the one below. The top six or seven lines should have been created automatically for you by Certbot, and the ones below add the extra parameters we’ll need to take advantage of our heightened security profile:

# automatically added by Certbot
ssl_session_cache shared:le_nginx_SSL:1m;
ssl_session_timeout 1440m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA38$

# MANUALLY ADD THESE
ssl_ecdh_curve secp384r1;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

Save and exit this file.

It is important to note that SSL certificates from LetsEncrypt expire every 90 days. In order that you don’t have to log into your server every 3 months to renew your certs, we’re going to set up a CRON job to autorenew them. From the command line:

$ sudo crontab -e

Remove everything in the file and add the following lines:

30 2 * * 1 /usr/bin/certbot renew >> /var/log/le-renew.log
35 2 * * 1 /bin/systemctl reload nginx

This will update the LetsEncrypt client and then attempt to renew and load your certs (if necessary) every Monday. In case you’d like to test to make sure the automated renewal will work, you can use the following command to do a dry run:

$ sudo certbot renew --dry-run

Step 8: Install WordPress

Wow. All this work so far and we haven’t even installed WordPress yet! Let’s get to it. In the past, I’ve recommended using Subversion to do this, but more recently I’ve discovered the wonderful WordPress CLI. To install the CLI:

$ 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

Now you’ll be able to complete almost any WordPress related task you can think of from the command line. Before running any commands, though, we’re going to create a config file that will set some global variables. To create the file, open it in nano using:

$ nano ~/.wp-cli/config.yml

To that file add the following:

# Global parameter defaults
path: /var/www/html
url: https://example.com
user: admin

# Subcommand params
config create:
  dbuser: yourdbuser
  dbname: yourdbname
  dbprefix: wp_

core install:
  title: "My Example Site"
  admin_user: admin
  admin_email: admin@example.com

Make sure you change these values to match your website. In other words, the url should be to the site you’re setting up. It’s recommended that you  do NOT use “admin” as the admin username, that you do NOT use “root” as your database user, that you do NOT use “wp_” as the prefix for your DB tables because this will make your site easier to hack. Once that’s done, the following commands will:

  1. Download the WP core files
  2. Set up a wp-config.php file using the database we created earlier
  3. Install WP and create an admin user
  4. Set some default WP options
  5. Get rid of the “Hello, Dolly” plugin
  6. Install and activate some other useful plugins
$ sudo wp core download
$ sudo wp config create --dbpass="thepasswordtoyourdatabase"
$ sudo wp core install --admin_password="agoodadminpassword"
$ sudo wp option update timezone_string "America/New_York"
$ sudo wp rewrite structure '/%year%/%monthnum%/%day%/%postname%/' --hard
$ sudo wp rewrite flush --hard
$ sudo wp theme install slug-for-theme-you-want-to-install
$ sudo wp theme activate slug-for-theme-you-just-installed
$ sudo wp plugin delete hello
$ sudo wp plugin install nginx-helper
$ sudo wp plugin activate nginx-helper
$ sudo wp plugin install mailgun
$ sudo wp plugin activate mailgun
$ sudo wp plugin install gmail-smtp
$ sudo wp plugin activate gmail-smtp
$ sudo wp plugin install jetpack
$ sudo wp plugin activate jetpack

Hopefully, it’s obvious that you would NOT install both the mailgun and gmail-smtp plugins on the same install, and that slug-for-theme-you-want-to-install should be replaced the slug for a theme you actually want to install and activate. Installing things like Jetpack are optional, as are the settings changes like timezone and rewrite structure.

Next we have to update the ownership of the files so that our webserver can have full access:

$ sudo chown -R www-data:www-data /var/www/html

Now you can visit your domain in a web browser and login to WordPress installation as you normally would with the admin username/password you specified above.

Step 9: Configure WP Plugins and Set Up Email

The previous step may have installed and activated some plugins for you, but it did NOT configure them yet!

In order to take advantage of nginx caching made available by the custom version of nginx that we installed, you’ll need to go to Settings > Nginx Helper from the WP dashboard and check the box to “Enable Purge.” The default settings should be fine. Click “Save All Changes.”

Mailgun Setup

In Step 2 above, I showed the DNS settings you should set up in order to have Mailgun handle all of your email. The nice thing about doing this is you can avoid having to set up and maintain your own SMTP server on your droplet. Setting up a mail server like postfix, sendmail, or exif can be a real pain as most email providers these days are extremely sensitive about preventing spam. After you’ve set up your domain at Mailgun, go to Settings > Mailgun from the WP dashboard, copy and paste in your Mailgun domain name and API key, and then click “Save Changes” to get it set up. Click “Test Configuration” to make sure it is working. You may also want to use the Check Email plugin just to make sure that emails are being sent correctly.

GMail SMTP Setup

If you setup the GMail SMTP servers in your DNS in Step 2 above, you’ll want to have installed the GMail SMTP plugin for WP. The setup for this plugin is somewhat involved. I strongly urge you to follow the instructions on their documentation site.

Step 10: Securing and Optimizing WordPress

Here are some tips and strategies for securing and optimizing your WordPress install. Replace the content of  your /etc/nginx/sites-available/default file with the following:

fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

server {
  listen 80 default_server;
  listen [::]:80 default_server;
  listen 443 ssl http2 default_server;
  listen [::]:443 ssl http2 default_server;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
  include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

  # force redirect to HTTPS from HTTP
  if ($scheme != "https") {
    return 301 https://$host$request_uri;
  }

  client_max_body_size 256M;
  root /var/www/html;
  index index.php index.html;

  server_name example.com www.example.com;

  set $skip_cache 0;

  if ($request_method = POST) {
    set $skip_cache 1;
  }

  if ($query_string != "") {
    set $skip_cache 1;
  }

  if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
    set $skip_cache 1;
  }

  if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
    set $skip_cache 1;
  }

  location ~ /purge(/.*) {
    fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1";
  }

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

  # Turn off directory indexing
  autoindex off;

  # Deny access to htaccess and other hidden files
  location ~ /\. {
    deny  all;
  }

  # Deny access to wp-config.php file
  location = /wp-config.php {
    deny all;
  }

  # Deny access to revealing or potentially dangerous files in the /wp-content/ directory (including sub-folders)
  location ~* ^/wp-content/.*\.(txt|md|exe|sh|bak|inc|pot|po|mo|log|sql)$ {
    deny all;
  }

  # Stop php access except to needed files in wp-includes
  location ~* ^/wp-includes/.*(?<!(js/tinymce/wp-tinymce))\.php$ {
    internal; #internal allows ms-files.php rewrite in multisite to work
  }

  # Specifically locks down upload directories in case full wp-content rule below is skipped
  location ~* /(?:uploads|files)/.*\.php$ {
    deny all;
  }

  # Deny direct access to .php files in the /wp-content/ directory (including sub-folders).
  # Note this can break some poorly coded plugins/themes, replace the plugin or remove this block if it causes trouble
  location ~* ^/wp-content/.*\.php$ {
    deny all;
  }

  location = /favicon.ico {
    log_not_found off;
    access_log off;
  }

  location = /robots.txt {
    access_log off;
    log_not_found off;
  }

  location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php7.0-fpm.sock;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;
    fastcgi_cache WORDPRESS;
    fastcgi_cache_valid 60m;
    include fastcgi_params;
  }
}

This config file will take advantage of the advanced caching capabilities of our custom version of NGINX. It will also prevent visitors from accessing files that they shouldn’t be. The combined effect will be to make your site faster and more secure.

Step 11: Setup Admin Emails

In order to get email messages and notifications from the system itself, e.g. when there’s a problem with system updates (configured in Step 12 below), you need to have a program like postfix or sendmail running on your system. There are a number of different ways to configure this, but I’m going to show you one that will allow emails to be routed through your Mailgun SMTP server. It is based on this tutorial from the EasyEngine folks. First, install the necessary packages. When prompted about your server type, select “Internet Site”, and for your FQDN, the default should be acceptable. Then open the config file for editing:

$ sudo apt-get install postfix mailutils libsasl2-2 ca-certificates libsasl2-modules
$ sudo nano /etc/postfix/main.cf

You’ll need to edit the mydestination property and add a few properties. You can leave all of the rest of the defaults. I ended up adding the following:

mydestination = localhost.$myhostname, localhost
relayhost = [smtp.mailgun.org]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_CAfile = /etc/postfix/cacert.pem
smtp_use_tls = yes

If you are using GMail’s SMTP, you should edit the above slightly as follows:

mydestination = localhost.$myhostname, localhost
relayhost = [smtp.gmail.com]:465
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_CAfile = /etc/postfix/cacert.pem
smtp_use_tls = yes
smtp_tls_wrappermode = yes
smtp_tls_security_level = encrypt

Then open/create a file where you’ll store your SMTP credentials:

$ sudo nano /etc/postfix/sasl_passwd

And into this file add a single line:

[smtp.mailgun.org]:587 admin@example.com:PASSWORD

Or if you’re using GMail SMTP:

[smtp.gmail.com]:465 admin@example.com:PASSWORD

You’ll have to get the password for the postmaster account from your Mailgun dashboard. The password for the GMail example should be the password for the email address used. Next we need to lock down this file and tell postfix to use it by running the following:

$ sudo chmod 400 /etc/postfix/sasl_passwd
$ sudo postmap /etc/postfix/sasl_passwd
$ cat /etc/ssl/certs/thawte_Primary_Root_CA.pem | sudo tee -a /etc/postfix/cacert.pem

Finally, you can test your setup by reloading postfix then running the following:

$ sudo /etc/init.d/postfix reload
$ echo "Test mail from postfix" | mail -s "Test Postfix" you@example.com

If all has gone well, you should receive an email from the server at the address you typed on that last line. You can also check the logs at Mailgun to confirm that the message was routed through their servers.

Step 12: Final Server Tweaks

Finally, there are two more things we should do to keep our server up to date and healthy. The first is to make sure unattended upgrades are enabled:

$ sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

When I finished editing it, my file looked like this:

Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}-security";
        "${distro_id}:${distro_codename}-updates";
        "${distro_id}ESM:${distro_codename}";
};
Unattended-Upgrade::Mail "admin@yoursite.com";
//Unattended-Upgrade::MailOnlyOnError "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";

I also updated the /etc/apt/apt.conf.d/10periodic file to look like:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";

Lastly, I ran the following commands to make sure everything was up to date:

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get autoclean
$ sudo reboot

One more thing I’ve found to be useful is setting up a swapfile to handle situations where the server might otherwise run out of memory. Here is a good tutorial to walk you through the process. I found I needed to do this for some installations where MariaDB was crashing due to not having enough memory.

Conclusion

There are more plugins and tweaks you can make to improve the performance of your site. For example, you could host your images and other static resources on a CDN, and we didn’t talk about combining and minifying the CSS and JS files on the site. However, with all of the above, you should have made a very good start at having a screaming fast and secure website. Enjoy!

Leave a Reply for Morgan Benton Cancel Reply

Write a Comment

Comment

103 Comments

  1. Dude, this is great! I’m gonna give this a try soon… Question tho, what if I don’t wanna use Let’s encrypt? It’s still beta and I rather use an old-school but reliable certificate. Thanks!

    • @Ben, FWIW, I’ve found Let’s Encrypt to be pretty solid over the last 4-5 months that I’ve been using it. That being said, I’d consult the technical docs of the CA from which you buy your certificate. Sorry for the “RTM” response, but all of my past experience installing certificates has been automated by whatever web host I happened to be using at the time. I’m sure there are some pretty good tutorials for installing 3rd party certs on your own Ubuntu server out there somewhere. 🙂

  2. I sure have enjoyed working through your example. I’m impressed that you see the bleeding edge picture so well regarding performance enhancements. Thank you for taking the time to explain in such depth how you’ve developed WordPress into a fast experience.

    I’d like to suggest that upon completion of the steps here that once completed you should take a snapshot of your DigitalOcean image. This allows you to always go back to that point in time when you’ve got everything solid and working correctly. It’s like having that fall-back when or if everything goes ‘not good…’

    Once again, nice work. Thanks…

    • @John, yeah, given all the work that goes into one of these setups, it would be nice to have a base image to work from. The only drawback I can see is that each new install will have a different domain name, user creds, etc. Maybe this summer I’ll be feeling industrious enough to write a script that can automate the personalizable parts. Thanks for the comment!

  3. Hi! there the guide is pretty solid. I have few sites that I want to migrate my sites from shared hosting to Un-Managed DO VPS,this guide serves my purpose but all I want to Know is how can we add multiple sites to a single droplet with this setup it will be helpful if you can help me with this.

    TIA 🙂

    • Depends on how you want to add sites. If you want to use a WordPress multi-site setup, I don’t really have any experience with that. However, if you just want to have another, standalone WP site, the rough outline for doing that would be:

      1. Create a new directory for the site, e.g. /var/www/site2, and download WP into that directory as with the first site
      2. Create a copy of the NGINX config file: sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/site2
      3. Edit this file to point to the new WP install you created in step 1, and the new SSL certs that you’ll have to generate for the new site
      4. Create a symbolic link to this config file: sudo ln -s /etc/nginx/sites-available/site2 /etc/nginx/sites-enabled/site2
      5. Restart NGINX: sudo service nginx restart
      6. Create a new database with a new user like you did for the first site
      7. Visit the new URL and follow the WP installation and configuration steps as you did for the first site

      HTH!

      • Morgan you really done a very great article especially for newbies its really a very good start to find all the server configurations at one place.

        i simply followed all the steps mentioned on this page and was able to live my https site on vps easily.. but now i am facing tough time while moving another site to this vps.. i have exactly followed the steps you mentioned above for multiple sites, but unfortunately i think there is something missing you forgot to mention because of which the second site is not working.. whenever i open the second site, it redirects to the ist site..

        i am mostly getting errors/warnings regarding the fastcgi_cache settings

        fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
        fastcgi_cache_key “$scheme$request_method$host$request_uri”;
        fastcgi_cache_use_stale error timeout invalid_header http_500;
        fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

        can you please tell in detail whether we should add the above 4 fastcgi_cache statements in the vhost file of each site seperately or add these lines in the /etc/nginx/nginx.conf ..

        • Sorry for the delayed response. I have never tried to set up more than one site on the same VPS. That being said, the general approach is to add another config file for the second site in `/etc/nginx/sites-available` and symlink to that file in `/etc/nginx/sites-enabled`. Each of the site config files in `/etc/nginx/sites-available/` needs to specify the specific domain names that are handled by that server config. Also note that the flag `default_server` should only go into one of those config files. That is the site that gets served, for example, if someone visits your site by typing in the IP address directly. I’m sure there’s a good tutorial on this out there. If you find it, please drop a link to it here! Thanks!!

  4. There seems to be an error in line 41 of your cache config file. It ends in g$ which makes no sense – it must surely end in { – maybe it got cut off.

  5. RE: code in the “At the end of it all the config file for my site looked like this” section.
    Please include a raw file. I can’t access line 41.

    • Oops, yeah, looks like it got cut off. Should be fixed now. There’s a “copy” link in the toolbar at the top of the code block.

  6. And last.
    I have Internal Server Error 500 on wp-admin/options.php.

    /var/log/nginx/error.log
    PHP message: PHP Fatal error: Uncaught Error: Call to undefined function utf8_decode()$

    Server was missing php-xml package. I ran below command to install it.
    sudo apt-get install php7.0-xml

    All working now.

  7. Hello:
    Thanks for the time to write the tutorial. I has helped me a lot.

    Regarding the installation of packages for WordPress using the FTPS method. It is working ( at least in my setting ) “even” when there is “no” wp-user. WordPress is basically using www-data to upload and install the software. So you might not need to modify the wp-config.php adding the FTPS configuration and it would work.

    Cheers

  8. I have been looking for a procedure for installing WordPress with Nginx. Thanks! However, I got an error in Step 3 after creating the /etc/apt/sources.list.d/nginx.list file:

    root@linode1: /root
    ==> apt-get update
    Hit:1 http://mirrors.linode.com/ubuntu xenial InRelease
    o o o
    Fetched 103 kB in 0s (143 kB/s)
    Reading package lists… Done
    W: GPG error: http://download.opensuse.org/repositories/home:/rtCamp:/EasyEngine/xUbuntu_16.04 Release: The following signatures couldn’t be verified because the public key is not available: NO_PUBKEY 3050AC3CD2AE6F03
    W: The repository ‘http://download.opensuse.org/repositories/home:/rtCamp:/EasyEngine/xUbuntu_16.04 Release’ is not signed.
    N: Data from such a repository can’t be authenticated and is therefore potentially dangerous to use.
    N: See apt-secure(8) manpage for repository creation and user configuration details.

    Is there a step missing for getting a public key?

    • Hi George, Sorry for the delayed response. Looks like you’re working at Linode instead of DigitalOcean. That shouldn’t matter for most of the steps, however, it looks like both hosts maintain their own mirrors for various Ubuntu distros. My guess is that the problem you’re experiencing is due to a difference in the way that Linode manages their Ubuntu mirror. That being said, I think getting the GPG keys is a relatively straightforward process and it is likely documented somewhere in the Linode user forums. Feel free to post the link back to the solution you found if you got one. Thanks!

  9. Hello,

    Thank you for this guide. I’m trying it for the first time, and have run into an error towards the end of Step 7, just before adding the cron job to renew the certificates.

    Where you’ve said:
    If you do still want to allow non-secure HTTP traffic, please consult DigitalOcean’s blog post that I linked to above. Save and close this file. Then check the syntax and restart NGINX:
    sudo nginx -t
    sudo service nginx restart

    Upon doing sudo nginx -t, I get the following error:
    nginx: [emerg] the size 10485760 of shared memory zone “SSL” conflicts with already declared size 20971520 in /etc/nginx/snippets/ssl-params.conf:11
    nginx: configuration file /etc/nginx/nginx.conf test failed

    Can you please help with this?

    • Running `sudo nginx -t` checks that the syntax of your nginx config files is correct. The error you’re seeing indicates that you’ve specified the size of the shared memory zone “SSL” in more than one place, and that the sizes indicated conflict with one another. My best guess is that you’ve got two lines that specify the memory size, one in `/etc/nginx/nginx.conf` and one in `/etc/nginx/snippets/ssl-params.conf` that conflict with one another. You need to delete or comment out one of them.

  10. Seems like the mariadb install process has changed and I wasn’t prompted during install to set a root password as you indicated. But you can set this when running the mysql_secure_installation so all good… However, then when trying to mysql -u root -p I got “Access Denied”. Seemingly this is another security issue and running mysql -u root -p worked (explained in answer here: http://stackoverflow.com/a/35748657/79935)

    Just sharing in case others experience the same. Fingers crossed the rest of my setup goes to plan. Great tutorial so far 🙂

  11. I don’t suppose you have any tips for pretty URLs? I’m completely new to nginx and following this I have the site working for admin and homepage but all of my posts 404 I think because of the permalink structure. Any tips appreciated.

  12. A++++ Step by step to up “Super-fast Secure WordPress Install on DigitalOcean with NGINX, PHP7, and Ubuntu 16.04 LTS” Morgan.

    With regard to the server blocks, Is there anything I can do about someone who has pointed their domain at my ip?.

    I mean the right code on the default file (I serve only one site), concretely, the server blocks part.

    What Happens: notdesireddomain.com(without content, i’ve visited through ip) redirects to mydomain.com:80 (with content)
    then my config(as this entry explains step by step) redirects to mydomain:443(with content), this is what i want block.

    I guess, this is a not legacy SEO practice.

    Thanks.
    Great work @mcbenton.

      • Solved!

        Filled an abuse form to cloudfare.

        With regard to the server block I add another server block where the server_name contains the baddomain.com returing a 444 message.

        Thnaks.

    • MySQL and MariaDB are essentially the same thing. If you’ve already got MySQL installed on your system, it’s perfectly fine to just stick with that. I doubt you’ll notice any performance differences unless you have a very highly trafficked site. If you’re installing a fresh clean new install of WP on your server, though, I definitely recommend choosing MariaDB over MySQL. That being said, you do NOT want to have both of them running at the same time.

    • Glad you liked it! Those lines in the nginx config basically tell the server not to log or return a 404 for requests to http://yoursite.com/favicon.ico and /robots.txt. Not every site has a favicon or robots.txt file, and in any case it’s not necessary for the server to log these requests. The “try_files…” line is already included in the config file in my tutorial and is important to make permalinks work correctly. Essentially, the config file in my tutorial is much more comprehensive and does a better job at preventing malicious attacks. I’ll go ahead and add in those two lines about favicon/robots. Thanks for the tip!

      • Cool, sounds good. Followup question: I’m setting up mailgun, do I edit the CNAME/MX/TXT records at my DigitalOcean droplet, my domain registrar, or both?

        Right now, my domain is registered with Bluehost, and I pointed the nameservers to my VPS. It has it’s own A/CNAME/MX/TXT records. I tried one of the A records, and it still works. So should I delete all the records at Bluehost? Thanks!

        • okay, I asked DO support and they said to just change the DNS records at DO.

          Last question (hopefully): Your mailgun TXT record in your first picture says “mailo._domainkey”. I’m following the Mailgun tutorial and it says “k1._domainkey.mg..com. Why the difference, and which one should I use?

          • Use the one that they specify. They use different subdomains for different accounts. If you don’t use the one that they give you, your DNS won’t verify with their system.

  13. Morgan, thank you for all of the hard work that you put into this post! It only took me about 4 hours start to finish, switching between the computer and cooking Thanksgiving dinner. I skipped two steps:

    (1) forcing file downloads through FTPS, because I wasn’t sure of the username/password: does it need to be the same wpuser creds that I used when setting up MariaDB & WordPress? And,
    (2) setting up mail (I’ll do that later.)

    Now I’ll start building a real site on it so I can test it out. Again, thanks for sharing all of your work. We all appreciate it. Happy Thanksgiving (if you’re in the US, if not, happy Thursday).

  14. This may have been my mistake, but I had to put in an additional command to allow ssh on the firewall around Step 9/10 — I’d gotten disconnected from my VPS it was blocking any non-local connections from port 22 (ssh).

    To fix this I needed to:

    1) Login to my server via the Digital Ocean console
    2) Type sudo ufw status (gets firewall status)
    3) Type sudo ufw allow ssh (allows ssh connections)

    Then I was able to connect using Terminal on my Mac. Again, might have been caused by some user error on my part, but this got me back in so I could complete the steps.

  15. Morphatic,
    I was dying bro…You Helped me…All the resouces on the internet even some of the digital ocean Tutorials seems un updated…Many functions has been deprecated and that for a guy like me is too troublesome..I had to destroy my droplet atleast 25 times.. Thank you so much..

  16. Hey morphatic, I have just setup multiple sites in a droplet, everthing went on good. But i cant use fastcgi, it says duplicate directive in both sites… How do i resolve this?
    thank you..

    • Well, each line in your nginx config file is referred to as a directive. Without looking at your code, I couldn’t say for sure, but my guess is that you somehow copied some of the directives into your config file more than once. I recommend posting your config file code over on Stack Overflow. You’ll likely get an answer to your problem in less than an hour.

  17. Hey morphatic,
    Thank you for your article, Because of you my website is amazingly fast. You mentioned it could be more optimized with ” host your images and other static resources on a CDN, and we didn’t talk about combining and minifying the CSS and JS files”.

    Please write a another post to do this as well. I cant find better resources than your site

    • Prashant,

      You might take a look at CloudFlare, or WP Super Cache, which are both plugins that will allow you to move static resources to a CDN. I don’t know when I might get around to writing a tutorial, but googling for one should find you several good ones.

    • I just updated the tutorial at the very end to link to a tutorial that will set up a `swapfile` so that your server should be a bit more resilient to these attacks. The cheapest server option from Digital Ocean doesn’t really come with that much memory, unfortunately.

    • I had never heard of brotli until you asked this question. Can’t answer definitely one way or the other, but I’ll look into it. Thanks for asking!

  18. Morgan,

    Awesome job. Thank you for posting this. Any idea how I may go about upgrading this custom version of NGINX? I stopped the NGINX service and then attempted to do so with the following: apt-get upgrade nginx-custom, to no avail. From what I can tell running apt-get update doesn’t update NGINX.

    • Hi Ryan, my guess is that the folks at rtcamp (who maintain the custom nginx version) just haven’t uploaded any updates in their repo. So your `apt-get update` may be working, but just not finding any updates to install. My experience is that the rtcamp folks don’t update their servers to reflect every release that the nginx folks put out. You can see the date of their latest update by browsing the repo.

  19. I’m using 40GB-2GB DO droplet. do i need to setup swapfile?
    how is easyengine simple setup with two commands different from this method?

    • Hi Maggi, with 2GB, I would think you would be okay, but of course it all depends on how much traffic your site gets. Setting up swap only takes a couple of minutes and doesn’t really cost you anything. As for your other question, I’ve actually never used the whole easyengine simple setup, so I don’t know. If you try it and like it, please come back and leave us another note. I may want to update this blog post to reflect that method. 🙂

  20. Hello!

    Thank you very much for the tutorial
    I’ve ran into an issue though
    I’m getting an ERR_TO_MANY_REDIRECTS error, and despite clearing out browser cookies, it still persists.

    Any ideas?

    Thanks!

    • My guess is that the “Site Address (URL)” in your WP Admin > Settings > General section is pointing to the HTTP address for your site, but then NGINX is redirecting to the HTTPS address of your site, which creates an infinite loop. To fix this, you can either edit the Site URL manually in MySQL(MariaDB) or you can temporarily disable HTTPS in NGINX so that you can get into the admin section of your WP site and change the Site URL to HTTPS.

  21. Hey great post. I enjoyed reading it and implementing my instance. However, I installed LAMP and plan to install nginx infront of it as a reverse proxy. That is actually what WordPress site is using and what they recommend. So your statement “…even wordpress.com uses NGINX” might not be totally accurate in the way you are building it here. See – https://codex.wordpress.org/Nginx

    • Interesting read. Actually, I haven’t had any of the problems with permalinks that they discuss in that article and I’m not using Apache at all. Thanks for pointing this out, though.

  22. Hi! When I try to upgrade the server this is what it shows:
    The following packages have been kept back:
    nginx-custom nginx-ee
    […] 2 not upgraded.
    Is it something to be concerned?
    Thanks!

    • Hmmm… Not sure. If I run into that problem on my own machines, I’ll be sure to update if I find a workaround.

  23. Hello!, Its a amazing tutorial you have here.

    I have implemented the exact steps on my website with million users per day. I am running latest version of wordpress on digital ocean. Everything seems to work fine. But one issue is with logged in users that the face “Connection lost” error. I tried to disable heartbeat also but to no use. They sometimes get that the connection was refused by the server.

    • Hi Faraz, you’re welcome. Usually the one-click-install is a script built by a particular hosting provider, e.g. DigitalOcean. Some of them have added the ability to add SSL via LetsEncrypt with one click, but you’d have to check with the specific provider.

  24. Thanks a lot for the articulate instructions.
    I am in the process of migrating a WordPress (multi-site) website from a shared hosting to a Vultr VPS.
    It would be great if you can tell me where in the above instructions can I upload my backed up SQL database. Or should I go ahead install the new WordPress then work on dropping the SQL tables and uploading my own?

    My knowledge of SQL is limited.
    Appreciate any pointers! Thanks!

    • If I were you, I would find a tool for migrating WP multi-sites (like maybe BackupBuddy?) and not try to do it all manually.

  25. Hey Morgan,

    I know this tutorial is getting pretty old now but it is still the best I have found.

    I have a question about redirecting the http pages to https. Using your tutorial, I am getting a 301 redirect string (i.e. http://mysite.com > https://mysite.com > https://www.mysite.com)

    Do you know how I can modify the code to go directly from http://mysite.com http://www.mysite.com and https://mysite.com to the proper URL, which is https://www.mysite.com. Google says that redirect chains pass all benefit over, but a lot of testing by SEOs suggests that that is not entirely true so I’d like to get it fixed. Thanks!

    These are the 301 redirects I’m looking for:

    http://mysite.com > https://www.mysite.com
    http://www.mysite.com > https://www.mysite.com
    https://mysite.com > https://www.mysite.com

    • You might try updating the redirect portion of your nginx site config to:

      # force redirect to HTTPS from HTTP
      if ($scheme != "https") {
      return 301 https://www.$host$request_uri;
      }

      That will make sure any non-HTTPS requests will always be redirected directly to your URL using www.

  26. Hi,
    was using your tutorial for long time, now you change step 8 and its not working….
    tried with touch and nano…. not working.

    did all by step by step
    touch: cannot touch ‘/root/.wp-cli/config.yml’: No such file or directory

    -=-=-=-=
    copy paste code plugin that you have is a nightmare, I wish people who made it go to hell.
    Thanks

    • Hi Ramzan, Sorry about the copy/paste thing and also that the WP-CLI method for installing WP is not working for you. I’ll retry the steps and see if I can’t figure out what’s wrong. It may be a few days before I can get to it, though. In case you want to try it, I’ve been working on a script to automate the entire process. I’ve been planning to write a blog post about it, but haven’t gotten around to it, yet.

      • Sorry man, I was a bit angry last time.
        Just want to say that your tutorial is GREAT and amazing.

        thanks for your job done.

  27. hey.

    Still use your manual. Still amazing
    but there is still issue with certbot
    here is official information.
    https://certbot.eff.org/#ubuntuxenial-nginx

    Probably, I just used insted of “sudo certbot –nginx”
    this one
    “sudo certbot –authenticator webroot –installer nginx”
    then press 1 and add patch to my web.
    and its work. and dry run was fine as well.
    good luck.

  28. hi. there are still issues with WP-CLI, not the first server. and it’s clean server. and I cant install it.. some patches not complete. look.

    root@lol:~# wp –info
    OS: Linux 4.4.0-116-generic #140-Ubuntu SMP Mon Feb 12 21:23:04 UTC 2018 x86_64
    Shell: /bin/bash
    PHP binary: /usr/bin/php7.0
    PHP version: 7.0.28-0ubuntu0.16.04.1
    php.ini used: /etc/php/7.0/cli/php.ini
    WP-CLI root dir: phar://wp-cli.phar
    WP-CLI vendor dir: phar://wp-cli.phar/vendor
    WP_CLI phar path: /root
    WP-CLI packages dir:
    WP-CLI global config:
    WP-CLI project config:
    WP-CLI version: 1.5.0

    how to put manually correct path?

  29. just sort it out. need to create manually folder /.wp-cli/, so you can create a config file.

    these steps are very unclear in manual.

  30. when install manually some of the plugins, get this error

    Warning: Failed to create directory ‘/root/.wp-cli/cache/’: mkdir(): Permission denied.

    I know is not critical but anyway. tried to give permission to the folder

  31. Need to fix this in your automatic script installer on github – as it fails on step #9 and fails to go to the next step;
    certbot -n –agree-tos -m ${EMAIL} –nginx –redirect -d ${DOMAIN} -d ${WWW_DOMAIN}

    If I remove that line and put certbot –nginx for manual commands/tos/email etc it keeps running the script.

  32. Hey Morgan,

    Thank you for writing this guide. I recently moved from Gandi Simple Hosting which is a PaaS to Digital Ocean. This was super helpful and my website is now blazing fast. I ended up using MariaDB 10.3 and PHP 7.2 instead. Nonetheless, this saved me a lot of time and headache. Cheers!

  33. Hey Morgan,

    I just wanted to let you know of a problem. For those not using Yoast SEO for sitemaps, it returns a 404.

    I had to use these rewrite rules in the nginx config file inside the server block to get sitemaps from other plugins like The SEO Framework, Jetpack, Google XML sitemaps, etc working.

    rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.xml$ “/index.php?xml_sitemap=params=$2” last;
    rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.xml\.gz$ “/index.php?xml_sitemap=params=$2;zip=true” last;
    rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.html$ “/index.php?xml_sitemap=params=$2;html=true” last;
    rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.html.gz$ “/index.php?xml_sitemap=params=$2;html=true;zip=true” last;

      • Hey Morgan, another thing I noticed was that the admin emails weren’t working properly. Here’s what I had to do

        in /etc/aliases , I defined a new alias
        root: email@example.org

        where email@example.org is the email address where I want to receive admin emails.

        Then,
        sudo new aliases
        so that new aliases are configured

        Then the admin emails started working properly.

        Cheers!

    • Hi Diego! I don’t know. I haven’t tried it yet. I don’t expect that I will upgrade to 18.04 for at least a year. If I change my mind, I’ll be sure to blog about it. 🙂

  34. Hello!
    Fantastic tutorial, well written, visuals on point.
    One question, can with this Nginx setup can I run multiple WordPress sites?
    Kind Regards.

    • Thanks! Short answer, yes. Here’s some instructions to get you started:

      1. Create a copy of your nginx config file (/etc/nginx/sites-available/default) in the same directory (sites-available) and give it a new, short, memorable name, e.g. “site2”.
      2. In the new file:
        1. remove the four “fast_cgi” lines at the top (you only need those once per server),
        2. remove references to “default_server” (there is only one “default”),
        3. delete or comment out the “ssl_certificate” lines (lines 11-19 in final config file in the tutorial), since LetsEncrypt will recreate them for you when you add your new SSL cert for the new site
        4. change the “root” (see line 22 above) to a new directory, e.g. /var/www/site2
        5. set the “server_name” (line 25) to the domain name the new site will use, even if it’s only a subdomain
      3. `cd` into /etc/nginx/sites-enabled/ and symlink to the new config file: `sudo ln -s /etc/nginx/sites-available/site2 site2`
      4. make sure it’s configured correctly: `sudo nginx -t`
      5. Other than that, you can pretty much follow most of the steps above again. You obviously don’t need to re-install PHP or MariaDB, and you’d need to adjust file paths and such but it’s pretty straightforward once you get nginx configured correctly.

      Good luck!

  35. 3 years old but still good information… you going to update new test?

    do you test SlickStack script yet for LEMP stack bro

    • You’re right, it needs updating. Maybe I’ll get to it this summer…? No, I haven’t tested those things.

  36. Are these tips still current or is there something new and faster?

    Is this setting faster than setting up with EasyEngine?

    Thank you very much. Congratulations on your work. I’ve never seen so complete. Great

    • You’re welcome! I actually don’t know the answers to your questions. If you find out, come back and let us know!

Webmentions

  • Cara Install WordPress, NGINX, PHP7.2, Percona, Brotli, dan Letsencrypt di Ubuntu 16.04 November 4, 2020

    […] https://morphatic.com/2016/05/21/super-fast-secure-wordpress-install-on-digitalocean-with-nginx-php7&#8230; […]

  • Make a super fast and lightweight Wordpress on Ubuntu 18.04 with PHP 7.2, Nginx, and MariaDB - Alon Ganon's Personal Website November 4, 2020

    […] been building servers for a long while based on the ideas I learned a few years ago from morphatic.com, however I wanted to move on PHP 7.2 and I also wanted to begin a server migration project to have […]

  • VULTR VPS配置笔记 – Blog November 4, 2020

    […] 这一步开始安装wordpress了,还有一些安全配置,具体可以参考这篇文章,值得细看。 […]