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:
- Ubuntu 16.04 LTS
“Xenial Xerus” was launched 21 April 2016, and is the most recent LTS version of Ubuntu. - 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. - 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. - 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:
- 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.
- Since we’re using NGINX, we don’t need Apache, and don’t have to worry about uninstalling it.
- 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:
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.
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:
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:
- Download the WP core files
- Set up a
wp-config.php
file using the database we created earlier - Install WP and create an admin user
- Set some default WP options
- Get rid of the “Hello, Dolly” plugin
- 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!
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. 🙂
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!
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:
/var/www/site2
, and download WP into that directory as with the first sitesudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/site2
sudo ln -s /etc/nginx/sites-available/site2 /etc/nginx/sites-enabled/site2
sudo service nginx restart
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!!
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.
Good catch! It should be fixed now. Thanks!!!
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.
Hi,
I found little issue in code for /etc/nginx/sites-available/default
Change to this please:)
location / {
try_files $uri $uri/ /index.php?$args;
}
Good catch! Fixed.
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.
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
I’ll check it out… Glad it helped!
$ 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
gained the gpg error, for not having signatures verified, thought installation works, but it seems bit strange. Should there be some key adding things for this?
I don’t recall having to do this. See my response to George Wilder for other thoughts on this issue.
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!
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.
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 🙂
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.
This bit from the site config file is supposed to take care of it:
“`
location / {
try_files $uri $uri/ /index.php?$args;
}
“`
Here is a link to a reference: https://www.digitalocean.com/community/questions/enabling-nginx-mod_rewrite
Ah perfect, I hadn’t moved onto the caching and purging section yet as just wanted to get WP working as standard first so hadn’t updated that line yet. Thanks so much!
Probably one of the best article if not the best about this topic. Thank you.
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.
Wow. I’ve never heard of this situation before. Here’s a ServerFault post that discusses some options of what you can do in this situation. Good luck!
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.
Hi,
Do we have to stop MySql since we have MariaDB?
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.
Hi! Thanks for the tutorial.
I’ve been looking at the DigitalOcean tutorial alongside yours here: https://www.digitalocean.com/community/tutorials/how-to-install-wordpress-with-lemp-on-ubuntu-16-04
They seem to have an extra step configuring Nginx: Step 2: Adjust Nginx’s Configuration to Correctly Handle WordPress. Is this unnecessary with your method?
Thanks!
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.
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).
Thank you! Glad it helped, and hope your Thanksgiving dinner was not infused with code smell!
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.
Yeah, pretty sure you missed a step when following the Initial Server Setup with Ubuntu 16.04 tutorial. Sucks to get locked out of your server! Thanks for posting instructions on how to recover for others who might have done the same thing. (BTW, I’ve done this before, too… 🙂 )
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..
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.
Maybe OTP, but anyway I like colors in your bash shell. Could you share the code pls?
🙂
I’m using the Crayon Syntax Highlighter with the Obsidian theme.
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 am having alot of xmlrpc attack..website going down every day(error establishing connection database)….tootally pissed
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.
Are you planning to at brotli support for this setup in the future?
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!
Thank you so much, Your really wonderful – im following @twitter you from now 🙂
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.
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. 🙂
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.
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.
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.
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.
Getting key expired. How to fix?
http://download.opensuse.org/repositories/home:/rtCamp:/EasyEngine/xUbuntu_16.04/Release.gpg The following signatures were invalid: KEYEXPIRED
Nevermind, fixed it with:
sudo apt-key adv –keyserver keyserver.ubuntu.com –recv 3050AC3CD2AE6F03
resource:
http://community.rtcamp.com/t/repo-key-expired-error-for-easyengine-on-ubuntu-16-04/9227/5
Hey, thanks! I’d been wrestling with that issue myself for a while and hadn’t had time to track down a solution.
@Morgan thanks for sharing this wonderful article. Is it possible to add letsencrypt on wordpress one click install ?
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.
Hi Morgan,
What changes I need to do in nginx config or else for using this setup for WordPress Multisite?
Thanks,
Sincerely,
Sergey Komlev.
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.
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.
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.
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.
Hi Morgan, have you seen Varnish? would it be good?
Hi Diego, Do you mean Varnish Cache? No, I have never heard of it until you mentioned it. I have no idea if it would be good or not for this kind of scenario.
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?
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.
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
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.
Hi Damian, Thanks for the feedback. I’ll check this out. Cheers!
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!
Yeah, I need to update the tutorial to reflect the latest versions of the software…
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;
Thanks Varun! I’ll test these out and add them to the tutorial.
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!
need to change php7.0-fpm to php7.2-fpm
Hi Morgan
Is the setup the same using ubuntu 18.04?
Thanks a lot
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. 🙂
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:
Good luck!
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.
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!
looking for update! for new ubuntu.
this guide is amazing
Thanks Morgan!
when you plan to update this one?
I can tell you a few improvements as well.