Migrating from Apache to NGINX on Ubuntu with WordPress

Apache has been around forever. It’s been the dominant web server since the beginning of the Dot-com era and if you’re a Linux admin, you’ve most likely crossed paths with Apache at some point. Enter NGINX.

NGINX hasn’t been around for nearly as long as Apache, but it has established itself as the second most used web server platform in the world. I myself have been slow to give NGINX a look, but have heard that it beats Apache in terms of performance, efficiency & scaling. Sounding good so far?

For those hesitant to devote time to learn an entirely new platform, I’ve found the transition to be fairly easy with a lot of crossover with Apache in terms of configuration and management. It’s liken to the similarities between MySQL and MariaDB.

In this guide, I’ll show you how to migrate an existing Apache web server to NGINX with a focus on WordPress. I’ll be using Ubuntu 18.04 LTS as the base OS.

Stable Is Always Best, Right?

The first decision you’ll need to make before installing NGINX is which branch you’d like to run. While most people in a production environment would opt for the Stable Branch (1.6) based on name alone, it might be in your best interest to opt for the current Mainline Branch (1.7) instead. Unlike other software, the branch opposite of Stable is by no means experimental. The Mainline branch is heavily tested and receives updates and feature improvements more frequently than the Stable branch. If you need more convincing, get it straight from the horse’s mouth in this NGINX Blog Post.

Installing NGINX

First up, we’ll need to add the NGINX Ubuntu Repository. If you opted for Mainline, go ahead and run the following.

sudo add-apt-repository ppa:nginx/mainline

*TIP: If you haven’t been persuaded to go with Mainline and you’d rather opt for the Stable branch, just change mainline to stable in the previous command.

A Tiny Bit of Downtime

At the end of the installation, NGINX starts automatically and like Apache, it installs a default instance on port 80 which Apache is most likely using. This means if we have Apache running during the installation, it’ll end with an error caused by this conflict. While I know nobody likes to take a downtime, its best to shutdown Apache during the installation. The entire process should be relatively quick though.

sudo systemctl stop apache2

With Apache stopped, go ahead and install NGINX.

sudo apt install nginx

Now we can shutdown NGINX and restart Apache.

sudo systemctl stop nginx
sudo systemctl start apache2

With Apache running again, we can take our time with the rest of the NGINX setup. See, I told you it’d be quick.

One More PHP Module

Since you’re coming from a working instance of Apache, you should already have all of the necessary PHP modules installed to run WordPress. However with NGINX, there is one additional PHP module you’ll need to install and that is php-fpm. NGINX doesn’t include native PHP processing in its base build and relies on the PHP-FPM module to handle it.

sudo apt install php-fpm

*NOTE: If you’ve made any changes to the Apache PHP config file located in /etc/php/7.x/apache2/php.ini, you’ll need to migrate those changes over to /etc/php/7.x/fpm/php.ini. After making any changes, restart the PHP-FPM service by running sudo systemctl restart php7.x-fpm.

With NGINX and all of its prerequisites installed, we can move on to configuring NGINX.

Configuring NGINX

The first thing we’ll do is disable the default NGINX site by removing the link to the site config from /etc/nginx/sites-enabled. Look familiar?

sudo rm /etc/nginx/sites-enabled/default

Now we’ll create a WordPress global config which contains common settings that can be used by any WordPress site hosted on the server.

Create a WordPress Global Config

The WordPress global config will reside in /etc/nginx/wordpress.conf.

sudo nano /etc/nginx/wordpress.conf

Add the following into the wordpress.conf config file and Save.

*Note: One important difference to note between Apache and NGINX are that every directive, aka setting, within an NGINX config must end with a semi-colon.

# Enable PHP Processing
location ~ \.php$ {
	include snippets/fastcgi-php.conf;
	fastcgi_pass unix:/run/php/php7.2-fpm.sock;

# Disables Favicon Logging & Errors
location = /favicon.ico {
	log_not_found off;
	access_log off;

# Disables Robots.txt Logging & Errors
location = /robots.txt {
	allow all;
	log_not_found off;
	access_log off;

# Denies Access to Hidden Files
location ~ /\. {
	deny all;

# Blocks PHP files in Upload Directory
location ~* /(?:uploads|files)/.*\.php$ {
	deny all;

# Enable WordPress Permalinks Support (default .htaccess replacement)
location / {
	try_files $uri $uri/ /index.php?$args;

Each section within this global config is called a Location Block. They each target specific directories and/or files within your site. I’ve added comments above each section to provide a short explanation of what each section does.

*NOTE: It’s important to note that for each incoming request, NGINX applies location blocks based on the best possible match and not the order in which they are written. However, the individual rules within each location block are processed from top to bottom stopping after the first match. Digital Ocean has a very detailed writeup on how NGINX applies it’s rules if you’d like to read up on it.

Say Bye-Bye to .htaccess

The last location block in the WordPress global config is of particular importance because it addresses one of the biggest issues you’ll encounter when migrating from Apache to NGINX.

NGINX doesn’t support .htaccess files

The directive in the final location block, try_files $uri $uri/ /index.php?$args;, is equivalent to the default .htaccess file most users have in the root directory of their Apache hosted WordPress sites which normally looks like this.

Default WordPress .htaccess

# BEGIN WordPress

RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

# END WordPress

As for the purpose of this .htaccess file and the equivalent NGINX directive, in short, they are necessary for WordPress Permalinks to work on their respective platforms. Follow the link if you’re unfamiliar with Permalinks.

*NOTE: If you use .htaccess files for other reasons like restricting site access, you can create separate location blocks like the ones used in the global config to restict access to different directories. I’ll talk more about this when we create the individual site config later in the guide.

Some FastCGI Tweaks

Based on my real world testing, I’ve found that these two settings were necessary for some pages in my WordPress sites to load properly. These tweaks will most likely be beneficial to all your NGINX hosted sites so instead of adding it to the WordPress global config, we’ll add it directly to /etc/nginx/fastcgi.conf.

sudo nano /etc/nginx/fastcgi.conf

Add the following to the bottom of the fastcgi.conf config and Save.

fastcgi_buffer_size 128k;
fastcgi_buffers 256 16k;

These two settings increase the number and size of the FastCGI buffers. You may need to adjust these settings based on the amount of RAM available to your web server as well as the size of your content and the amount of traffic your site does. I myself came across this tweak because I was getting 502 Bad Gateway errors on various pages.

Looking Back at Apache

Next locate your Apache WordPress config in /etc/apache/sites-available. We’ll be using it as a reference to create the NGINX WordPress config.

Below is an example Apache config for a WordPress site with SSL enabled.

Example Apache Config

<VirtualHost *:80>
	ServerName www.examplesite.com
	ServerAlias examplesite.com
	Redirect 301 / https://www.examplesite.com/
	DocumentRoot /var/www/www.examplesite.com
	DirectoryIndex index.php index.html
	ErrorLog ${APACHE_LOG_DIR}/error.log
	TransferLog ${APACHE_LOG_DIR}/access.log

	<Directory /var/www/www.examplesite.com>
		Options FollowSymLinks
		Options -Indexes
		AllowOverride All
		Require all granted
<IfModule mod_ssl.c>
	<VirtualHost *:443>
		ServerName www.examplesite.com
		ServerAlias examplesite.com
		DocumentRoot /var/www/www.examplesite.com
		DirectoryIndex index.php index.html
		ErrorLog ${APACHE_LOG_DIR}/error.log
		TransferLog ${APACHE_LOG_DIR}/access.log

		SSLEngine on
		SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
		SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
		<FilesMatch "\.(cgi|shtml|phtml|php)$">
			SSLOptions +StdEnvVars

		<Directory /usr/lib/cgi-bin>
			SSLOptions +StdEnvVars
		BrowserMatch "MSIE [2-6]" \
			nokeepalive ssl-unclean-shutdown \
			downgrade-1.0 force-response-1.0
		BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown

		<Directory /var/www/www.examplesite.com>
			Options FollowSymLinks
			Options -Indexes
			AllowOverride All
			Require all granted

Create the WordPress Site Config

Based on the previous Apache config, we’ll create a new WordPress site config located in /etc/nginx/sites-available/www.examplesite.com.conf. Unlike the WordPress global config we created earlier, this config will only apply to a specific WordPress site.

The config is broken up into several Server Blocks which are the NGINX equivalent to Apache Virtual Hosts.

*NOTE: Server Blocks are one level above Location Blocks within the NGINX config structure meaning that a Server Block can contain multiple Location Blocks.

sudo nano /etc/nginx/sites-available/www.examplesite.com.conf

Copy the following settings into the www.examplesite.com.conf config file and Save.

# Redirect HTTP -> HTTPS
server {
    listen 80;
    server_name www.examplesite.com examplesite.com;
    return 301 https://www.examplesite.com$request_uri;

# Redirect Root Domain -> WWW
server {
    listen 443 ssl http2;
    server_name examplesite.com;
    return 301 https://www.examplesite.com$request_uri;
	# SSL parameters
	ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

server {
    listen 443 ssl http2;
    server_name www.examplesite.com;

    root /var/www/www.examplesite.com;
    index index.php;

	# WordPress Global Config
    include wordpress.conf;
	# SSL parameters
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

    # Log files
    access_log /var/log/nginx/www.examplesite.com.access.log;
    error_log /var/log/nginx/www.examplesite.com.error.log;

The first server block in the config redirects all HTTP (Port 80) requests to HTTPS (Port 443). There are a couple of ways to accomplish this with Apache including using a directive within the Virtual Host config as shown in our Apache example. The other way is with the use of .htaccess files which we already know NGINX does not support. The NGINX method displayed in the first server block has you covered either way.

The second server block redirects any requests to the root domain name to the www subdomain instead, i.e. examplesite.com to www.examplesite.com. Some people prefer the exact opposite, redirecting www requests to the root domain. This can be accomplished by swapping all instances of www.examplesite.com with examplesite.com and vice versa.

The last server block defines the actual site including the root directory of your existing WordPress site and a directive to include the WordPress global config we previously created.

Notably missing in the NGINX config are the equivalent Directory sections from the Apache config. It turns out that none of these settings are actually needed by NGINX. The reason is Symlinks are enabled by default, Directing Listing (Indexing) is disabled by default, Overrides which are used to allow .htaccess files are useless and access to the site is already wide open.

As mentioned earlier, if you are looking to restrict access to your site, NGINX has allow and deny rules that can target IP addresses and ranges. These rules can be defined either in a server block if you intend to restrict access to the entire site or in a location block if you want to restrict certain portions of your site like /wp-admin, the WordPress Admin Dashboard. Have a look at this NGINX Doc for info on IP restrictions and this JournalDev Post on location syntax.

NGINX & SSL Certificate Chains

I was debating whether to include this in the guide since it doesn’t apply to everyone, but it is important to those affected when making the transition to NGINX so I decided to add it in. If you’re using a self-signed cert or aren’t using SSL altogether, you can just skip over this section.

The example WordPress config is shown with a self-signed SSL certificate. However, if you purchased an SSL cert, there’s one difference you need to be aware of when migrating from Apache. Intermediate CA (Certificate Authority) certificates need to be merged into the same file as your SSL certificate.

It’s very possible that you’ve never dealt with this issue before as most browsers will just accept SSL certificates as valid even without the Intermediate CA certificate chain, but depending on your SSL vendor and the certificate you purchased, some browsers may still consider your site untrusted even with a valid certificate.

In Apache, you could specify a separate file within your Virtual Host config which contains the Intermediate CA certificates by using the SSLCertificateChainFile directive. NGINX doesn’t have an equivalent option so you need to combine your SSL certificate and the Intermdiate CA certificates into one file.

If your SSL vendor provided their Intermediate CA certificates in a separate file, just append that file to your SSL certificate and move the bundled SSL certificates to your SSL directory by doing the following.

sudo bash -c 'cat ssl-certificate.crt ca-certificates.crt > ssl-bundle.pem'
sudo mv ssl-bundle.pem /etc/ssl/certs

Once this is done, just change the ssl_certificate directive in your site config to point to the ssl-bundle.pem instead of your SSL cert.

ssl_certificate /etc/ssl/certs/ssl-bundle.pem

*NOTE: When creating the SSL certificate bundle, the order of the certificates matters. Your site’s SSL certificate must come first followed by the lowest level Intermediate CA continuing up to the Root CA. If your CA certficates already came in a single merged file, they should already be in the proper order and just need to be appended to your site’s SSL certificate. If not, you may need to ask your SSL vendor for assistance or do some trial and error.

Enable the WordPress Site

Now that we’ve created our WordPress site config, it’s time to enable it. Keep in mind the site will still be running on Apache until we actually start the NGINX service.

sudo ln -s /etc/nginx/sites-available/www.examplesite.com.conf /etc/nginx/sites-enabled/

If you ever need to disable the site for any reason, just remove the symbolic link from /etc/nginx/sites-enabled. This is the same method used by Apache to enable and disable sites.

*NOTE: You may be used to using the a2ensite and a2dissite commands to enable and disable sites in Apache. Unfortunately, NGINX doesn’t have anything similar built-in. There are third party scripts out there that can serve this purpose, but I would make sure to test them out before using them in production.

Handing Things Over to NGINX

Before we shutdown Apache one last time, we want to make sure NGINX is 100% ready to handle the job. We can do that by testing the NGINX config files for any syntax errors.

sudo nginx -t

If any of the configs do contain syntax errors, this test will tell us which config bombed and the exact line number of the error. If there are no errors because you are of superior intelligence, the test should be successful.

With all of the configs checked, we’ll now take another short downtime while we shutdown Apache and officially hand the reins over to NGINX.

sudo systemctl stop apache2
sudo systemctl start nginx

Testing Your WordPress Site

This first thing you should do after activating NGINX is open a browser and make sure to clear your browser’s cache. The last thing we want is a cached version of your site throwing a monkey wrench into your testing.

If your WordPress site launches successfully, the next thing you should do is click on various links and test out basic site functionality. Also, make sure to login to the WordPress dashboard to ensure you still have admin access.

If you find any problems with the site running on NGINX, you can quickly revert back to Apache by stopping the NGINX service (sudo systemctl stop nginx) and starting Apache (sudo systemctl start apache2).

Saying Farewell to Apache

If you’ve completed your lengthy & arduous testing process and everything checks out, you can disable Apache from automatically starting up with the server and have NGINX take its place.

sudo systemctl disable apache2
sudo systemctl enable nginx

It’s ok to leave Apache installed on the server as long as its not started while NGINX is running. However, if you’re tight on disk space, and you’re supremely confident in your NGINX installation, you can officially bury Apache by purging it from your server altogether.

sudo apt purge apache2*
sudo apt autoremove
sudo rm -rf /etc/apache2

Pat Yourself on the Back..Just Not Too Hard

While this guide will help you migrate from Apache to a working NGINX web server, there’s still a lot left to learn. There are tweaks and settings I haven’t covered which can improve the performance and security of your NGINX server as well as a ton of features which I encourage you to research. I myself have learned a lot just in writing this guide and am excited to delve deeper into the #2 most popular web server in the world.

12 Responses to Migrating from Apache to NGINX on Ubuntu with WordPress

  1. solved it by adding 100+ location blocks to wordpress.conf instead of earth.com.conf

    added some multi-wordpress rewrite as well in separate multisite.conf

    thank for great tutorial. I saved it for future reference.

  2. as you said in article, i found “Default WordPress .htaccess” under each wordpress home directory.

  3. This is one of the amazing articles.

    My site has multiple wordpress installations. After migrating to nginx by using your tutorial, the root wordpress works fine. e.g. http://www.blahblahblah.com is working ok. but http://www.blahblahblah.com/sub-wordpress1 is not working.

    Can you help me to fix this?

    • Avatar Postmin (Admin)
      Postmin (Admin) says:

      Just wanted to clarify things. Do you want to create separate NGINX instances for each WordPress site or a single Multisite WordPress installation.

      • single domain has multiple worpress. For example, http://www.earth.com (example), now i have separate worpdress installation for every country , such as http://www.earth.com/usa, http://www.earth.com/canada etc. each has separate wordpresspress installation in subdirectory /usa, /canada, /australia, etc. http://www.earth.com works as as hub to all those wordpress sites.

        i Followed every step you said, now my http://www.earth.com is working fine but posts for http://www.earth.com/usa, http://www.earth.com/canada is showing 404.

        as you are an expert, I seek your advice what type of nginx instance is appropriate for such case.

        • Avatar Postmin (Admin)
          Postmin (Admin) says:

          Using subdirectories for separate WordPress instances is more complicated than your average setup. Usually you would host separate instances of WordPress on different subdomains like usa.earth.com, canada.earth.com.

          However, if you’d rather stick to using subdirectories, I believe you can add separate locations for each WordPress instance within the site config, below the “include wordpress.conf;” line.

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

          I can’t guarantee everything within WordPress will work so you probably want to do some thorough testing if this works.

          • its not in subdomain usa.earth.com, its on sub-directory earth.com/usa

            So, shall it try to add location block to global wordpress.conf for each subdirectory?:
            location /usa {
            try_files $uri $uri/ /usa/index.php?$args;
            location /canada {
            try_files $uri $uri/ /canada/index.php?$args;

          • Avatar Postmin (Admin)
            Postmin (Admin) says:

            No, you add that into your site config, earth.com.conf

          • there is no location block yet in earth.com.conf. all are server block..

            OK. I will add location block now and will let you know the outcomes.

          • added location {
            } for one subdirectory wordpress

            when i tried to start nginx, it tells me that “location” directive i snot allowed here in /etc/nginx/site-enabled/earth.com.conf:37

          • Avatar Postmin (Admin)
            Postmin (Admin) says:

            It would probably help you to do some research on the basic syntax for an NGINX config file. NGINX provides a WordPress example config at the link below.