Install LEMP Stack on Ubuntu 20.04 LTS

Author: Paul Sueno
Created: 2/15/2021

 

Set up a modified LEMP stack on Ubuntu 20.04. The modified is using PostgreSQL rather than MariaDB/MySQL. This blog is Part 1 of 2.

What is LEMP? It's a common way to implement a web service. Hosting web sites alone won't get you too far without a database and way to process the data. There are four essential aspects: the operating system, web server, database and server side scripting. This is where an LEMP stack comes in. It's the combination of Linux, (E)Nginx, MySQL (or similar database) and PHP scripting.

This blog will focus on setting up Nginx with TLS/SSL encryption by Let's Encrypt (Certbot). It will also show how to use nftables firewall for webhosting. Most online tutorials used LEMP stacks with Mariadb or MySQL. One of the web apps I needed to run required PostgreSQL. I'm going to start with the web server here. A separate blog will cover incorporating SQL and PHP into my LEMP (or LEPP?) stacks.

Nginx and Ubuntu

As open source web servers go, Nginx (pronounced "Engine X") is a very popular alternative to Apache. The ServerGuy wrote a great comparison between the two. Do note that Nginx is open source and availabe free. Top search results for Nginx return the paid-support product, which I do not use. If you decide to stick with the free open source Nginx, follow along.

The web server must be installed on an operating system. This tutorial will be based on Nginx version 1.18.0 on Ubuntu 20.04 LTS.

  • NOTE: There is a separate tutorial for the modified LEMP stack on Ubuntu 18.04.
  • If you haven't done so already, go ahead and set up an Ubuntu 20.04 server. See my tutorial on setting up a LUKS encrypted Ubuntu server on the cloud.
  • Come back here afterwards.

Go ahead and ssh or Putty into your server. And let's begin!

Install by package manager

As you may know, installing applications and programs on Ubuntu (or any other Linux flavor) are much easier through a package manager. The most commonly used one for Ubuntu is apt.

Make sure the package repositories are updated. As long as no other essential services are running, go ahead and upgrade the server as well. Once your server has a handful of essential services configured and running, I recommend frequent backups. Be careful of upgrades. And if a service doesn't run quite right after a system upgrade, you may want to rollback from one of the backups. Here are the commands to update and upgrade by apt.

sudo apt update
sudo apt upgrade

I installed Nginx along with the extras package.

sudo apt install nginx nginx-extras

Initial Nginx configuration

The basic configuration files for Nginx are found in /etc/nginx/. I scour the web regularly for help with customizing my server. Nginx put together a good intro site for use of their server. Because I host my apps on Linode, here's their intro to using Nginx.

I host several apps on one host with one ip address. I take advantage of Nginx's name-based virtual hosting via their server_name directive. I created separate .conf configuration files for each site/app. These are located in /etc/nginx/sites-available/ and symbolically linked to /etc/nginx/sites-enabled/. These files tell the server how to handle http(s) requests.

The files that are to be hosted and shared on the web are located in /usr/share/nginx/ folder. I also create symbolic links from my hosted app folders to here. So remember, the configuration files are located in /etc/nginx/, while the hosted files should be located in /usr/share/nginx/. Some people who are used to Apache's default, have the hosted directories located in /var/www/html/. Use whichever you want. Be consistent. But I'll be using the former.

Let's back up the initial install files, in case we mess up. After backing up, I'll delete the default configuration file and create my own.

sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.orig
sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.orig
sudo rm /etc/nginx/sites-enabled/default
sudo cp /usr/share/nginx/html/index.html /usr/share/nginx/html/index.html.orig 

To help with the configuration, let's also get your network address. Get this by the command ifconfig. Look for the addresses listed by your public-facing or wide-area-network (WAN) interface. My WAN interface device is named enp0sn (where n is a number). The ipv4 address is noted next to inet. Use the ipv6 next to inet6 that does not start with fe80:. If you have it, the address that starts with fe80: is called link-local and used to identify and manage the network. Here's a great explanation by dotcom-tools.

Something else you need (beyond the scope of this tutorial) is to set up your domain registrar to point requests to your site over to your server. You will need to create A/AAAA records in your DNS. Domain name registrar services are a dime-a-dozen out there. I separated my web server from my domain name registrar. Many reasons to do this, but some reasons to keep them all in one. Read Nuts and Bolts Blog for more info on this topic.

Enter this command to create our own default Nginx configuration file: sudo nano /etc/nginx/conf.d/default.conf. Here's an example of my file. You will need your own ip addresses and host.domain.com as registered in your DNS (see above discussion). Search for keywords in the Nginx documentation for guidance on the configuration files. Let's start it off with a site for unencrypted connections only. Remember to replace your own host/domain names and ip addresses!!

server {
  listen 80;
  listen [::]:80;
  server_name host.domain.com domain.com xxx.xxx.xxx.xxx [ffff:eeee:dddd::1234];
  root /usr/share/nginx/html;
  index index.php index.html;
  location / {
    try_files $uri $uri/ /index.html =404;
  }
}

Modify nftables firewall

If you followed along after my Ubuntu install tutorial, your firewall should be nftables (rather than ufw or iptables). We need to allow traffic to our http(s) ports. These are ports 80 (http) for unencrypted traffic and 443 (https) for TLS/SSL encrypted traffic.

Edit the nftables configuration file by running sudo nano /etc/nftables.conf. Add the line below the SSH service, should be around line 79.

tcp dport {80,443} counter accept comment "web"

Reload the nftables firewall by running sudo nft -f /etc/nftables.conf.

Test unencrypted web server

Now that the Nginx web server is running and the firewall allows http traffic, let's test it out. In your browser, go to http://domain.com or http://[ip address] (using your own domain name or ip address).

Nginx Sample Site

Now try going to https://domain.com (notice the s in https and use your own domain name) to see how the browser treats encrypted traffic without a valid certificate. Not good, ya? Well, let's fix that.

Certbot - Let's Encrypt - TLS

Encrypted web traffic is the standard now. It used to be cost-prohibitive for smaller operations and hobbyists. A team of non-profits partnered together to help us all out with free (or donation supported) verified TLS certificates. This is all thanks to Let's Encrypt, ISRG and Certbot via Electronic Frontier Foundation. So, if you have the means, go ahead and give a tax-deductible donation to one or all of these non-profit organizations.

Let's install the package via snap.

sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Certbot renews their certificates relatively frequently. There are several methods for you to implement to allow this to happen in the background. I'll show you the webroot method.

Create the "hidden" folder that will have the webroot file available for certbot to find.

sudo mkdir -p /usr/share/nginx/html/.well-known/acme-challenge
sudo chown -R www-data:www-data /usr/share/nginx/html

If unsure if you you set this up correctly, then test it out. Make a simple html file, e.g., sudo nano /usr/share/nginx/html/.well-known/acme-challenge/test.html.

<h4>Header</h4>
<p>This is a test paragraph.</p>

Go to a web browser and test it out: http://[host.domain.com]/.well-known/acme-challenge/test.html. If it loads correctly, go ahead and delete this test file: sudo rm /usr/share/nginx/html/.well-known/acme-challenge/test.html.

I will create certificates for the following hosts (or subdomains) along with the primary domain. These will include www.domain.com, domain.com and host.domain.com. Again, replace your own host and domain names, but would definitely include www as one of your subdomains. In the command below, the trailing \ tells Ubuntu bash that the command continues to the next line. Answer any prompts that come up.

sudo certbot certonly --webroot \
-w /usr/share/nginx/html -d www.domain.com -d domain.com -d host.domain.com

Certbot package installer set up automated certificate renewals. However, your services on the server that rely on the TLS certificate need to know whenver a new one is loaded. We will take advantage of the "hooks" used by certbot when certificates are renewed.

Let's create a script to renew this and future services after Let's Encrypt renews our TLS certificate. Run sudo nano /etc/letsencrypt/renewal-hooks/post/servers-reload.sh.

#!/bin/sh
service nginx reload /etc/letsencrypt/renewal-hooks/post/servers-reload.sh

We have to make this file executable.

sudo chmod 774 /etc/letsencrypt/renewal-hooks/post/servers-reload.sh

Your Let's Encrypt/Certbot TLS certificates are saved in /etc/letsencrypt/live/www.domain.com/ folder. This is as long as you wrote the www subdomain first when you created the certbot certificate initially.

Nginx - TLS configuration

Now that we have our own verified TLS certificates, we have to tell our Ubuntu services that they can now use them. Let's start by enhancing the security of the certificates by creating a Diffie-Hellman group. The following command will take several minutes.

sudo openssl dhparam -out /etc/ssl/private/dhparam.pem 4096
...
...
...
sudo chmod 600 /etc/ssl/private/dhparam.pem

Now let's edit the Nginx configuration file. I based my TLS cipher suite used by Nginx on the file they keep up to date on github. The link breaks sometimes. You can always find it by Binging for the keywords github certbot options ssl nginx conf. Copy the contents of github file. Create the nginx TLS/SSL configuration file: sudo nano /etc/letsencrypt/options-ssl-nginx.conf. Paste the contents of the github file into the editor.

We will redirect all traffic except for certbot webroot verification from unencrypted http traffic to encrypted https. We'll also forward traffic from unencrypted domain.com over to encrypted www.domain.com. This keeps things consistent and minimizes duplicate search results. For guidance on SSL configuration, feel free to use the Mozilla SSL Configuration Generator (use current Nginx and OpenSSL versions). Again, fill in your own information in the example below. It will not work if you leave it as domain.com.

sudo nano /etc/nginx/conf.d/default.conf

 

server {
  listen 80;
  listen [::]:80;
  server_name www.domain.com domain.com host.domain.com xxx.xxx.xxx.xxx [ffff:eeee:dddd::1234];
  location ^~ /.well-known/acme-challenge/ {
    default_type "text/plain";
    root /usr/share/nginx/html;
  }
  location / {
    return 301 https://www.domain.com$request_uri;
  }
}
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name domain.com name.domain.com;
  ssl_certificate /etc/letsencrypt/live/www.domain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/www.domain.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/www.domain.com/chain.pem;
  ssl_dhparam /etc/ssl/private/dhparam.pem;
  ssl_stapling on;
  ssl_stapling_verify on;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  return 301 https://www.domain.com$request_uri;
}
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name www.domain.com;

  ssl_certificate /etc/letsencrypt/live/www.domain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/www.domain.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/www.domain.com/chain.pem;
  ssl_dhparam /etc/ssl/private/dhparam.pem;
  ssl_stapling on;
  ssl_stapling_verify on;
  include /etc/letsencrypt/options-ssl-nginx.conf;

  root /usr/share/nginx/html;
  index index.html index.php;

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

For whatever reason, I came across an odd Nginx error. It didn't happen with prior installations. After a few refreshes, I wasn't able to access my webserver through any web browser. There would be error log entries in (cat /var/log/nginx/error.log) like the following:

2021/02/15 09:01:01 [info] 1234#1234: Using 32768KiB of shared memory for nchan in /etc/nginx/nginx.conf:63
2021/02/15 09:02:02 [alert] 5678#5678: *16 open socket #3 left in connection 7
2021/02/15 09:02:02 [alert] 5678#5678: aborting

I don't use nchan, and so I decided to remove the Nginx module for nchan. The error messages are gone. Not the prettiest of fixes, but it works.

sudo rm -rf /etc/nginx/modules-enabled/50-mod-nchan.conf
sudo service nginx restart

Test encrypted web server

And voila. In your browser type in http://domain.com, replacing your own domain name and using unencrypted http on purpose. If you want, press F12 before going to your site. You can then see how the initial requests are ultimately forwarded to the encrypted https port and virtual host server_name www.domain.com.

Once other apps are installed on the server, we can modify the steps above to create other virtual hosts. For example, we can direct mail.domain.com to Dovecot, Postfix and Webmail services; drive.domain.com to a file sharing service; and db.domain.com to a web interface to manage your database (e.g., phppgadmin).

Next step is to complete our modified LEMP stack by adding a database service and server side scripting.

 
 
media,300x250,768336750
media,300x250,618013813
media,300x250,605020959
media,300x250,746683857
media,300x250,684991714
media,300x250,213191128
media,320x50,464634581
media,320x50,577852834

Suenotek Blog

Seattle, Washington

Cookies | Privacy | Policy

About | Contact Us