This guide walks through deploying WordPress on Ubuntu 18.04 with a LEMP stack, TLS via Let's Encrypt, and a baseline set of production controls.

Operational context

  • When to use this: you need a self-managed WordPress instance with Nginx, database isolation, HTTPS, and firewall rules.
  • What it delivers: a working LEMP deployment with certificate automation and sensible defaults for a first production cut.
  • Tradeoff: this reflects Ubuntu 18.04-era package choices. Treat the commands as historical unless you are actually maintaining Ubuntu 18.04.

Still valid on newer Ubuntu versions?

The deployment shape is still useful: non-root SSH access, firewall rules, Nginx, PHP-FPM, a dedicated database user, WordPress salts, TLS, and post-install verification. The exact packages have changed.

Before reusing this on a newer Ubuntu release, check:

  • PHP package names and supported PHP version (php-fpm, php-mysql, and related extensions)
  • Certbot installation method; many current systems use Snap or distribution packages rather than the old PPA
  • MySQL versus MariaDB defaults, authentication plugins, and utf8mb4 support
  • whether your hosting provider already enables UFW, cloud firewalls, or managed TLS
  • backup and restore process before applying changes to an existing site

In particular, do not copy the package list blindly on current Ubuntu releases. php-mcrypt is obsolete and removed from modern PHP stacks, MySQL 8 commonly requires CREATE USER before GRANT, and Certbot PPA instructions have largely been replaced by distribution packages or Snap.

Index

WordPress on LEMP with Let's Encrypt

What is LEMP?

This is an acronym that describes a Linux operating system, with an Nginx (pronounced like “Engine-X”) web server, a MySQL / MariaDB database server and the dynamic processing is handled by PHP.

Linux Engine-X MySQL PHP

What is Let's Encrypt?

Let’s Encrypt is a Certificate Authority (CA) that provides an easy way to obtain and install free TLS/SSL certificates, thereby enabling encrypted HTTPS on web servers. It simplifies the process by providing a software client, Certbot, that attempts to automate most (if not all) of the required steps.

Prerequisites

  • A server (I run my servers on DigitalOcean *)
  • Ubuntu 18.04 installed
  • SSH access

* Signing up to DigitalOcean via that link, you receive a $50, 30-day credit as soon as you add a valid payment method to your account.

Installation steps

Step 1: Create a sudo user

Since we will be completing the steps in this guide using a non-root user with sudo privileges, we need to create a user for that.

Skip this step if you already have it

# Login as root
$ ssh root@your_server_ip

# Create a new user
$ adduser santa

# Set up sudo privileges
$ usermod -aG sudo santa

Highly recommended: configure SSH public key authentication and disable password login. You can follow the process in this Linuxize article.

Before you log out from your root session, you should test your new user login in a new terminal, on your local machine

# Login
$ ssh santa@your_server_ip

# Test sudo abilities
$ sudo ufw app list

Step 2: Set up the firewall

Ubuntu 18.04 ships with UFW enabled by default. Restrict inbound access to the services you actually need.

# Allow access on SSH port
$ sudo ufw allow 22

# Allow access on HTTP port for initial site access and Let's Encrypt HTTP-01 validation
$ sudo ufw allow 80

# Allow access on HTTPS port
$ sudo ufw allow 443

# Enable the firewall
$ sudo ufw enable

# Check firewall status
$ sudo ufw status

Step 3: Install Nginx web server

Installing Nginx is straightforward:

# Install Nginx
$ sudo apt-get update && sudo apt-get install nginx

Then open a browser tab, enter your server IP address, and you should see the default Nginx welcome page.

Step 4: Install MySQL database server

Here we're going to use MySQL, but alternatively you could also use MariaDB. More about it on this Linuxize article

# Install MySQL
$ sudo apt-get install mysql-server

# Configure the Installation
$ mysql_secure_installation

You’ll be prompted to answer some questions. We recommend you answer as follows:

NOTE: Make sure you remember your MySQL password, as you’ll be using it later on.

Enter current password for root (enter for none): Just press the Enter
Set root password? [Y/n]: Y
New password: Enter password
Re-enter new password: Repeat password
Remove anonymous users? [Y/n]: Y
Disallow root login remotely? [Y/n]: Y
Remove test database and access to it? [Y/n]: Y
Reload privilege tables now? [Y/n]: Y

Next, create the database, user, and password for WordPress.

# Login to your database server
$ mysql -u root -p

# Create a new database for WordPress
mysql> CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

# Create a separate MySQL user account that we will use exclusively to operate on our new database
# wordpressuser and password are placeholders; replace them with site-specific credentials
mysql> GRANT ALL ON wordpress.* TO 'wordpressuser'@'localhost' IDENTIFIED BY 'password';

# Flush the privileges so that the current instance of MySQL knows about the recent changes we’ve made
mysql> FLUSH PRIVILEGES;

# Exit out of MySQL console
mysql> EXIT;

On MySQL 8 and newer, create the user explicitly before granting permissions:

CREATE USER 'wordpressuser'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpressuser'@'localhost';
FLUSH PRIVILEGES;

Step 5: Install PHP

In this step we'll install the PHP extensions required for WordPress to run and for Nginx to process PHP (php-fpm).

# Install PHP packages
$ sudo apt-get install php-fpm php-mysql php-curl php-gd php-mbstring php-mcrypt php-xml php-xmlrpc php-intl php-zip

# Find out which PHP 7 version you have
$ php -v | head -1

php-mcrypt was already deprecated around PHP 7.2 and is not available on modern Ubuntu/PHP combinations. If your package manager cannot find it, remove it from the install command unless a legacy application specifically requires it.

We need to know which PHP 7 version, because next step is to configure the PHP Processor. Replace 7.2 with your version... i.e.: 7.0

# Open the main php-fpm pool configuration file
$ sudo nano /etc/php/7.2/fpm/pool.d/www.conf

In there you are to add or uncomment the line that contains the following: security.limit_extensions ending up with the following value: security.limit_extensions = .php

You can find more about the above, on this Server Fault thread

# Configure the PHP processor
$ sudo nano /etc/php/7.2/fpm/php.ini

And adjust the following values:

memory_limit = 256M
upload_max_filesize = 10M
cgi.fix_pathinfo = 0
max_execution_time = 360
date.timezone = Europe/Berlin

And finally

# Restart PHP
$ service php7.2-fpm restart

Step 6: Configure Nginx to serve PHP

We need a few minor adjustments to our Nginx server block files to be able to serve PHP / WordPress.

Replace your_domain.com with your actual domain.

# Create a few directories required for Nginx configuration
$ sudo mkdir /etc/nginx/sites-available && sudo mkdir /etc/nginx/sites-enabled

# Create your virtual host
$ sudo nano /etc/nginx/sites-available/your_domain.com

On Ubuntu's packaged Nginx, /etc/nginx/sites-available and /etc/nginx/sites-enabled usually already exist. Create them only if your install does not provide them.

This set of instructions will tell Nginx to listen for requests to your_domain.com and serve your WordPress site.

server {
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;

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

    server_name your_domain.com;

    client_max_body_size 10M;

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

    location ~ \.php$ {
        try_files $uri =404;

        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
        fastcgi_index index.php;
        include /etc/nginx/snippets/fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Disable logging for these static requests and will mark them as
    # highly cacheable since these are typically expensive resources to serve
    location = /favicon.ico { log_not_found off; access_log off; }
    location = /robots.txt { log_not_found off; access_log off; allow all; }
    location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
        expires max;
        log_not_found off;
    }

    # SECURITY : Deny all attempts to access PHP Files in the uploads directory
    location ~* /(?:uploads|files)/.*\.php$ {
        deny all;
    }

    # Deny access to .ht* files
    location ~ /\.ht {
        deny all;
    }
}

Use default_server only for the server block that should catch unmatched hostnames. If this host will run multiple sites, remove default_server from additional vhosts to avoid conflicts.

On newer Nginx versions, replace expires max with an explicit long-lived cache value, such as expires 30d;, when that is appropriate for your asset pipeline.

Last thing remaining is to enable the site and restart Nginx

# Create a symlink to enable your virtual host
$ sudo ln -s /etc/nginx/sites-available/your_domain.com /etc/nginx/sites-enabled/your_domain.com

# Restart nginx server
$ sudo service nginx restart

Step 7: Download and configure WordPress

# Download latest WordPress version
$ cd /tmp && wget https://wordpress.org/latest.tar.gz

# Unpack it
$ tar -zxvf latest.tar.gz

# Move it from tmp to /var/www
$ sudo rm -rf /var/www/html && sudo mv wordpress /var/www/html

# Generate WordPress secrets (Copy them somewhere)
$ curl -s https://api.wordpress.org/secret-key/1.1/salt/

# Edit WordPress configuration
$ sudo mv /var/www/html/wp-config-sample.php /var/www/html/wp-config.php && sudo nano /var/www/html/wp-config.php

These are WordPress configuration parameters that you need to adjust in order to establish a connection between your application and the database.

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', 'wordpress');

/** MySQL database username */
define('DB_USER', 'wordpressuser');

/** MySQL database password */
define('DB_PASSWORD', 'password');

/** MySQL hostname */
define('DB_HOST', 'localhost');

/** Database Charset to use in creating database tables. */
define('DB_CHARSET', 'utf8mb4');

/** The Database Collate type. Don't change this if in doubt. */
define('DB_COLLATE', '');

/**
 * Authentication Unique Keys and Salts.
 *
 * Change these to different unique phrases!
 * You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}
 * You can change these at any point in time to invalidate all existing cookies. This will force all users to have to log in again.
 *
 * @since 2.6.0
 */
define('AUTH_KEY',         'put your unique phrase here');
define('SECURE_AUTH_KEY',  'put your unique phrase here');
define('LOGGED_IN_KEY',    'put your unique phrase here');
define('NONCE_KEY',        'put your unique phrase here');
define('AUTH_SALT',        'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT',   'put your unique phrase here');
define('NONCE_SALT',       'put your unique phrase here');

Finally, set the permissions right. These values are a baseline; production deployments may use stricter ownership depending on how code is deployed and how updates are handled.

$ sudo chown -R www-data:www-data /var/www/html/ && sudo chmod -R 755 /var/www/html/

Step 8: Obtain and configure the SSL Certificate with Let's Encrypt

# First, add the Certbot repository
$ sudo add-apt-repository ppa:certbot/certbot

# Install Certbot and Certbot Nginx plugin
$ sudo apt-get update && sudo apt-get install python-certbot-nginx

# Obtain your free Let’s Encrypt SSL/TLS certificate
$ sudo certbot --nginx -m your@email.com -d your_domain.com

Follow the configuration steps followed by the command above and when it comes to Choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access. choose 2

Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2

After that, Let’s Encrypt Client has installed your certificate and configured your website to redirect all traffic to HTTPS.

If you check your site’s Nginx configuration file, /etc/nginx/sites-available/your_domain.com, you’ll notice that some modifications have been made by the Let’s Encrypt client.

. . .

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/your_domain.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

}

server {
    if ($host = your_domain.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    server_name your_domain.com;
    return 404; # managed by Certbot
}

Your WordPress site is now ready to be used over HTTPS.

Step 9: Finalize the WordPress Installation

The final step is to complete the WordPress installation through the web interface. To do this, just navigate in your browser to your domain (https://your_domain.com), and you’ll be guided through the easy process of installing WordPress.

Post-install verification

Before calling the server production-ready, verify:

  1. sudo ufw status shows only SSH, HTTP, and HTTPS exposed.
  2. sudo nginx -t passes and Nginx reloads cleanly.
  3. sudo systemctl status nginx and sudo systemctl status php*-fpm are healthy.
  4. sudo certbot renew --dry-run succeeds.
  5. WordPress admin login works over HTTPS.
  6. Media uploads work and files are owned by the expected web server user.
  7. The site URL and home URL use https://.
  8. Backups cover both the database and /var/www/html/wp-content/.

Next hardening steps

This guide gets the site online. For a production WordPress instance, continue with:

  • SSH key-only login and no root login
  • unattended security updates or a patching schedule
  • fail2ban or equivalent login-rate protection
  • regular database and file backups with restore tests
  • DISALLOW_FILE_EDIT in wp-config.php
  • PHP execution blocked in uploads
  • monitoring for certificate expiry, disk space, and PHP-FPM failures

For Apache-based deployments, see Hardening WordPress on LAMP (Apache) for a broader hardening checklist. Several controls there also apply to Nginx.

Related work

This secure LEMP baseline supports editorial and CMS platform delivery in Multilingual news platform and Large crowdlearning platform.