Setting up a LEMP server from scratch

Posted on: 01/03/2021. Last updated at: 07/09/2021


In this post I'll cover what steps you need to take in order to install and configure a LEMP stack on Ubuntu 20.04. LEMP stands for:

  • Linux (Ubuntu 20.04)
  • Nginx (pronounced Engine X)
  • MySQL
  • PHP

Configuring Ubuntu 20.04 and Security Setup

For this tutorial I'm using Digital Ocean to create my server. You can use whichever provider you want, but you will need to have root access and your public key added to the server before starting.

Creating a new user

First we will create a new user and give them sudo privileges

adduser youruser

Enter a memorable but secure password. Remember, this is most likely going to be the user you SSH in with and run most sudo commands under.

Once you have created a new user you'll want to add this user to the sudo group, which will allow you to run sudo commands.

usermod -a -G sudo youruser

The -a and -G flags do as follows:

append the user to the supplemental GROUPS mentioned by the -G option without removing the user from other groups

You can then confirm your user has been added to the sudo group by running:

groups youruser

You should see something like the following:

youruser : youruser sudo

Now, assuming when you created your server, your public keys will have been added to the ~/root/.ssh/authorized_keys file. We want to copy these public keys into our new users authorized_keys folder.

To do that we can use rsync to copy over the directory, its contents and set the correct ownership in one command!

rsync --archive --chown=youruser:youruser ~/.ssh /home/youruser

--archive makes sure your permissions and attributes are the same in the destination directory and --chown changes the user and group who owns the authrorized_keys file to be youruser.

Lets test this out. Open up a new terminal window, making sure you keep the original one (logged in as root) open too. and SSH into the server as your newly created user.

ssh youruser@youripaddress

If it works, congratulations! You've sucessfully:

  • Created a new user and gave that sudo priviledges
  • Copied over your existing SSH keys from your root user into your new users authorized_keys
  • Successfully SSH'd into your server as the new user.

Removing Root Access

To make our server more secure, we should turn off root login via SSH. From now on we should only login with our newly created user. Lets start by editing the /etc/ssh/sshd_config file.

vim /etc/ssh/sshd_config

We want to set PermitRootLogin to no. Scroll down to find it or press /PermitRootLoginand hit return to perform a search for it. It should be set to yes, change this to no.

PermitRootLogin no

Next we want to search for PasswordAuthentication. It may already be set to no or commented out. If its set to no already save the file and quit, else uncomment the line and make sure its set to no.

PasswordAuthentication no

This makes sure you can only login to your server using SSH and no other method. Also because you made a change to this file you need to restart the ssh service for the changes to be picked up.

sudo service ssh restart

Try logging in as your root user and you should see "permission denied (publickey)"

Adding Firewall Rules

Moving forward, we should now be logged in as your new user. You'll need to prepend sudo to most of your commands.

Ubuntu makes use of UFW - UncomplicatedFirewall and this makes it quite easy to set up rules to allow and deny traffic coming in and out of our server.

As it stands we only want to allow:

  • SSH
  • HTTP Port 80
  • HTTPS Port 443

Adding this is very simple - type in the following:

sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable

You might get a message saying: "Command may disrupt existing ssh connections. Proceed with operation (y|n)?" just type y and hit enter.

Now your firewall rules should be enabled, to test this out you can type:

sudo ufw status

You should see all the rules you enabled displayed. This also persists after you shutdown or restart your server, so no need to worry about having to do this each time.

Installing Fail2Ban

Fail2Ban keeps an eye on your authentication logs and blocks successive failed attempts and also logs them down. Great for security!

sudo apt-install -y fail2ban

Now we need to enable it by copying a configuration file.

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Check that the service is running

sudo systemctl status fail2ban 

And that should be it. For most people this setup is ample to get you started.

Installing Unattended Upgrades

Unattended upgrades installs secuirty updates automatically on your server without you having to do anything. It's important to note that it won't upgrade any packages or programs, such as PHP, nginx etc. That is still up to you.

sudo apt-get install unattended-upgrades

It may be that its already installed and thats fine, we'll configure it next.

cd /etc/apt/apt.conf.d
sudo vim 50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}";
        "${distro_id}:${distro_codename}-security";
        // Extended Security Maintenance; doesn't necessarily exist for
        // every release and this system may not have it installed, but if
        // available, the policy for updates is such that unattended-upgrades
        // should also install from here by default.
        "${distro_id}ESMApps:${distro_codename}-apps-security";
        "${distro_id}ESM:${distro_codename}-infra-security";
//      "${distro_id}:${distro_codename}-updates";
//      "${distro_id}:${distro_codename}-proposed";
//      "${distro_id}:${distro_codename}-backports";
};

You want to make sure that

  • ${distro_id}:${distro_codename},
  • ${distro_id}:${distro_codename}-security,
  • ${distro_id}ESMApps:${distro_codename}-apps-security
  • and ${distro_id}ESM:${distro_codename}-infra-security

are uncommented and comment out:

  • ${distro_id}:${distro_codename}-updates
  • ${distro_id}:${distro_codename}-proposed
  • ${distro_id}:${distro_codename}-backports

This will probably already be setup for you, if so leave it as-is and save the file and exit.

Next we want to change the 10periodic file

and add the following lines:

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

This will now check for security updates once a day.

Installing and Configuring NGINX

If you haven't done so already, lets update the servers package list and then install nginx.

sudo apt update
sudo apt install -y nginx

Nginx should have started automatically, you can check this by typing:

sudo service nginx status

If not you can enable it:

sudo service nginx start

If everything is good so far, lets test that nginx is running correctly:

curl -I localhost

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu
Date: Mon, 08 Mar 2021 14:39:19 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Mon, 08 Mar 2021 14:38:35 GMT
Connection: keep-alive
ETag: "604636eb-264"
Accept-Ranges: bytes

If you get a 200 OK response you're good to go! We'll park the nginx setup for now and move onto installing MySQL and PHP, which we'll link back into our nginx setup.

Installing MySQL 8

Start by installing MySQL server:

sudo apt install -y mysql-server

Next we can run a security script that will lock down access and tighten up some default settings.

sudo mysql_secure_installation

You'll be asked if you want to setup the "Validate Password Plugin" I generally skip this as I use a password generator, but this plugin will enforce password rules that you set and will reject any which don't meet the requirements.

After this you'll be asked to set a root user password. This is separate from your Ubuntu root user password. Pick something secure and then confirm it again. After this, press y to accept all the other questions.

You can now test your installation works by running:

sudo mysql

show databases;

Creating a new MySQL user

Next we'll create a new database and a user who can access it. Still using the same session or if not, just do sudo mysql again.

CREATE DATABASE your_database
CREATE USER 'your_user'@'%' IDENTIFIED WITH mysql_native_password BY 'yourSecurePassword'

Now we should grant read and write permissions to our new user for this new database and only this database.

GRANT ALL ON your_database.* TO 'your_user'@'%';

Your new user can now only access the newly created database, but cannot access or modifiy any other databases.

Quit out of your current mysql session and lets test if our new user can connect successfully.

mysql -u dan -p

the -u flag lets you specify the user you want to connect as and the -p flag will give you a secure password prompt.

Once logged in, execute a SHOW DATABASES; command and you should see the new database you created!

Installing and Configuring PHP 8

Next we'll install PHP 8 and configure it to work with Nginx via PHP-FPM. At the time of writing the supported version of PHP is v7.4. If you want to use 7.4 simply skip the next part about enabling the PHP repository and replace any instance of php8.0 with php7.4

First to get PHP 8 we need to enable a separate repository from Ondřej Surý who maintains several versions of PHP.

sudo apt install -y software-properties-common
sudo add-apt-repository ppa:ondrej/php

Next we'll install PHP 8 and a few other packages which will make working with frameworks such as Laravel easier for us.

sudo apt install -y php8.0-fpm php8.0-mysql php8.0-mbstring php8.0-xml php8.0-bcmath

Now test that you have PHP installed by running php -v you should see PHP 8.0.3 or something similar.

Setup Nginx to use the PHP-FPM package

Nginx allows us to run multiple websites from our server and this is achieved by using "server blocks". Each server block can be a separate domain with its own config. Lets start by making our first one.

You should create a root directory of your site in the following location:

sudo mkdir /var/www/www.example.com

You can replace www.example.com with your own domain with or without the www.

Next we want to take ownership of the directory. The www-data is the default user / group for which the PHP-FPM process runs as. You can use Linux permissons to control what the process can do on your server.

The -R flag means recursive and will apply the changes to all folders within /var/www/www.example.com.

sudo chown -R www-data:www-data /var/www/www.example.com

Now we want to configure and enable our new site. Nginx has two main directories where this is done. /etc/nginx/sites-available and /etc/nginx/sites-enabled. Sites Available can contain many websites and when you are ready to active them, you can symlink the config file over to the Sites Enabled directory. Let's do that.

sudo vim /etc/nginx/sites-available/www.example.com

This will create an empty file which you can paste the following into:

server {
    listen 80;
    server_name www.example.com;
    root /var/www/www.example.com;

    index index.html index.htm index.php;

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

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

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

Its a small file, but there's quite a lot going on, lets try to break it down:

Directive Meaning
listen

What port should Nginx listen on. The default is port 80 (http), this could also be 443 (https)

server_name

Defines what hostname Nginx should look for when the request comes in using the Host header

root

The document root where your website lives. In our example its /var/www/www.example.com

index

Is the order of what files Nginx will try to read if a file name and extenstion is not passed with the request

location /

The location block allows Nginx to try and match a file path or a regex pattern against the incoming URI request.

The try_files directive will check for the existance of files or folders which match the the URI request. Nginx will return a 404 if nothing can be found.

location ~ \.php$

If you have a request which has a .php file extenstion, Nginx will hand over the request to the PHP processor (php-fpm8.0 in our case)

location ~ /\.ht

If a request for a .htaccess file comes in, we can choose what happens here. Since Nginx doesn't handle .htaccess files, we can safely deny access to them.

Save the file and then we can activate the site by symlinking the sites-available file over to sites-enabled.

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

Nginx comes with a "default" site enabled by... default. It's the "Welcome to nginx!" page you'll see if you visit the IP of your webserver now in your browser. We should disable this as we don't need it anymore.

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

If you ever need to re-enable the default site, just do the symbolic link above but use default instead of you domain name: sudo ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default

Nginx has a handy command to test that your configuration files are valid and is something you should run everytime you edit a config file.

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If this fails then fix whatever it states and after that, you should reload nginx to pick up the new configurations and activate your site.

sudo system nginx reload

If you visit your domain (assuming you have DNS setup for it) or the IP address, you'll probably get a 403 Forbidden. This is because the root of your site /var/www/www.example.com has no files in it!

We can add a quick PHP file in there and test it works.

sudo vim /var/www/www.example.com/index.php
<h1>Hello World</h1>
<?php echo date('d/m/Y'); ?>



Picture of Daniel Ord
Daniel Ord

Building things for the Web. Follow me on Twitter or checkout my GitHub.