14. Install the Nginx web server

by Double Bastion - Updated March 18, 2022

Install Nginx from the Debian repository:

apt-get update
apt-get install nginx

After installation, restart Nginx:

systemctl restart nginx

You can check its status by running:

systemctl status nginx

The output should state:

Active: active (running)

You will need to allow HTTP and HTTPS connections to your server, so open the port 80 and 443 in firewall using ufw:

ufw allow 80
ufw allow 443

Verify the change by running:

ufw status

You should see the 80 and 443 ports opened for both IPv4 and IPv6 traffic:

To                         Action      From
--                         ------      ----
80                         ALLOW       Anywhere
443                        ALLOW       Anywhere
80 (v6)                    ALLOW       Anywhere (v6)
443 (v6)                   ALLOW       Anywhere (v6)

14.1. The “Failed to parse PID from file /run/nginx.pid: Invalid argument” notice

If your server has one CPU core, after you run systemctl restart nginx and then systemctl status nginx, you will see the following line in the output of the status command:

systemd[1]: nginx.service: Failed to parse PID from file /run/nginx.pid: Invalid argument

This is because systemd is expecting the /run/nginx.pid file to be created before Nginx has the time to create it. This is a harmless bug that you can ignore, since the /run/nginx.pid file is actually created. In general, this notice appears on systems with only one CPU core. To make this notice disappear, run the following commands:

mkdir /etc/systemd/system/nginx.service.d
printf "[Service]\nExecStartPost=/bin/sleep 0.1\n" > /etc/systemd/system/nginx.service.d/override.conf
systemctl daemon-reload
systemctl restart nginx

14.2. Disable direct access by IP address to websites

If a user or a bot requests the public IPv4 address of your server in a browser, like this: (where is your IPv4 address), by default, Nginx will serve the index page of the first website configured in the server blocks configuration file (/etc/nginx/sites-enabled/0-conf), which is not what you want. Therefore, you have to add a catch-all server block right at the top of the /etc/nginx/sites-enabled/0-conf file, so that Nginx knows how to treat all the requests for or .

You have to create a ‘dummy’ self-signed SSL/TLS certificate. First create the directory to hold the certificate and key files and switch to it:

mkdir -p /etc/nginx/ssl/dum
cd /etc/nginx/ssl/dum

Create the server.key and server.cert files in one command by running:

openssl req -x509 -nodes -days 73000 -newkey rsa:4096 -keyout server.key -out server.cert

A few questions will follow. Since this is a self-signed ‘dummy’ SSL/TLS certificate that won’t be used to serve any websites, you can leave all the fields empty by pressing Enter:

Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

The command from above generates a self-signed certificate valid for 73000 days (200 years). This is the proper duration for all the self-signed certificates, which don’t need to be trusted by browsers, if you don’t want to have issues when you will forget to generate new certificates after the 13 months of ‘normal’ validity.

Create a directory to hold a simple error page that will be served to all the visitors who will request the IPv4 address of the server:

mkdir /var/www/varerrors

Create the error page:

nano /var/www/varerrors/error.html

Add the following content iniside this file:

<html><head></head><body><p>Access denied!</p></body></html>

Then create the server blocks configuration file by running:

nano /etc/nginx/sites-available/0-conf

Add the following catch-all block at the top of this file:

server {
    listen      80 default_server;
    listen [::]:80 default_server;
    listen      443 ssl default_server;
    listen [::]:443 ssl default_server;

    root /var/www/varerrors;
    index error.html;

    ssl_certificate      /etc/nginx/ssl/dum/server.cert;
    ssl_certificate_key  /etc/nginx/ssl/dum/server.key;

    access_log /var/log/nginx/nginx.access.log;
    error_log  /var/log/nginx/nginx.error.log notice;

Create the necessary symbolic link for this file, in the /etc/nginx/sites-enabled directory:

ln -s /etc/nginx/sites-available/0-conf /etc/nginx/sites-enabled

Delete the default symbolic link from the /etc/nginx/sites-enabled directory, since now you have a new configuration file link there:

rm /etc/nginx/sites-enabled/default

By using the catch-all server block from above, you instructed Nginx to serve the ‘Access denied!’ page to all the visitors who will request or , where is the public IPv4 address of your server.

Restart Nginx to apply the new configuration:

systemctl restart nginx

Please note that Nginx will serve all the websites configured in all the configuration files located in the /etc/nginx/sites-enabled directory. So, you can have one or multiple files in /etc/nginx/sites-enabled, with different names of your choice. However, it’s better to configure all the websites in one big configuration file, which we called 0-conf. When you’ll want to modify something, you’ll know precisely what file to edit. To use multiple configuration files, with specific names, in /etc/nginx/sites-enabled, would only make sense if you host a very large number of websites, like 100 or more, in which case the unique configuration file would become too large to allow easy editing.

14.3. Configure Nginx

14.3.1. General configuration

First make a copy of the /etc/nginx/nginx.conf file:

cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf_orig

Then open /etc/nginx/nginx.conf:

nano /etc/nginx/nginx.conf

Comment out the ssl_protocols and ssl_prefer_server_ciphers directives. We’ll include these SSL directives in the server blocks within the /etc/nginx/sites-enabled/0-conf file. Therefore, make them look like this:

# ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
# ssl_prefer_server_ciphers on;

Since the access logs and error logs will be defined for each individual website in the server block, comment out the access_log and error_log directives also, to make them look like this:

# access_log /var/log/nginx/access.log;
# error_log /var/log/nginx/error.log;

There are other changes that you will need to make. The final /etc/nginx/nginx.conf file should look like this:

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;

http {

        # Basic Settings

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        # server_tokens off;

        server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;
        client_max_body_size 200M;
        fastcgi_read_timeout 600;

        # SSL Settings

#       ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
#       ssl_prefer_server_ciphers on;

        # Logging Settings

#       access_log /var/log/nginx/access.log;
#       error_log /var/log/nginx/error.log;

        # Gzip Settings

        gzip on;
        gzip_static on;
        gzip_vary on;
        gzip_proxied any;
        gzip_comp_level 6;
        gzip_buffers 16 8k;
        gzip_http_version 1.1;
        gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/jpg image/jpeg image/x-icon image/bmp image/png image/gif image/svg+xml;

        # Virtual Host Configs

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;

        map $http_upgrade $connection_upgrade {
                default upgrade;
                ''              close;

Next, make a copy of the original /etc/nginx/fastcgi_params file:

cp /etc/nginx/fastcgi_params /etc/nginx/fastcgi_params_orig

Open /etc/nginx/fastcgi_params:

nano /etc/nginx/fastcgi_params

Search for the following line:

fastcgi_param  HTTPS              $https if_not_empty;

Right below it, add the following two lines, which are necessary for Nginx to interact with the FastCGI service:

fastcgi_param  SCRIPT_FILENAME    $request_filename;
fastcgi_param  PATH_INFO          $fastcgi_path_info;

Restart the PHP processor:

systemctl restart php7.4-fpm

Also restart Nginx:

systemctl restart nginx

14.3.2. Domains with or without ‘www’

In all the examples that will follow we’ll suppose that your websites have domains of the form www.example.com, and that you want to redirect all the requests for example.com to www.example.com.

As about what type of domain is better: non-www or www, the reality is that www adds some flexibility with DNS settings and in the past, with older browsers, there was also a cookie problem for non-www domains in the sense that when the static files of a website, like images and media files, were hosted on a subdomain of the non-www domain, to serve them faster, the cookies set for the non-www domain were accepted by browsers and then sent in HTTP requests to the subdomain of the static files as well. This meant sending requests to the static files subdomain that were unnecessarily large and slowed down the process of serving static files (see https://www.yes-www.org/why-use-www/). With newer browsers, this problem disappears, because the cookies set for the non-www domains aren’t automatically passed to the subdomains. From a SEO perspective there is no advantage of using one over the other. The most important thing is to choose one from the beginning and stick to it for ever, to avoid the ‘duplicate content’ issue. Therefore, you can choose the form that you prefer and then make the necessary redirects from non-www to www or viceversa. These redirects, which have to be done correctly in Nginx, as we’ll show in this guide, are obligatory, because it’s certain that some visitors will request the non-www instead of the www domain and the other way round. The idea is to always get the correct form loaded in the visitors’ browsers, regardless if they add www or not when they make the request.

14.3.3. Configure Nginx to serve a website over HTTP

If you intend to serve all your websites over HTTPS, you can skip this chapter.

We’ll configure Nginx to serve the website www.example-http-only.com over HTTP.

Please note that today, almost all websites need to be served over HTTPS. This is because in general, a website needs to be edited frequently and thus it needs a Content Management System (CMS) like WordPress. When the administrator logs in to the CMS to edit it, they should send their log in credentials through an encrypted connection, which means through HTTPS, otherwise the credentials could be intercepted in transit. Also, the speed improvements brought by HTTP/2 are only available for HTTPS connections. In addition to this, the search engines tend to give priority to HTTPS websites which are considered more secure. Let alone the fact that the visitors tend to trust HTTPS websites more than plain HTTP ones. Also, if in the future the owner decides to sell products on their website, the HTTPS connection will be a must. Buyers won’t introduce their credit card numbers on plain HTTP websites. These being said, the only situation where somebody would want to serve a plain HTTP website is for very simple static websites, such as presentation websites consisting of a few pages that the owner will very rarely edit.

Nevertheless, since this guide is complete, we will describe how to configure Nginx to serve websites over HTTP, before explaining how to configure it to serve websites over HTTPS, which is the usual case.

First create the /var/www/example-http-only.com directory, where the new website will have its files:

mkdir /var/www/example-http-only.com

Create the robots.txt file in the /var/www/example-http-only.com directory:

cd /var/www/example-http-only.com
nano robots.txt

Add the following content inside this file, to allow search engines to crawl and index the website:

User-agent: *

The most common method to upload the files of a website to a directory is by using FTP. Thus, you can use FileZilla to connect to your remote server and upload all the website’s files inside the /var/www/example-http-only.com directory.

Next, open the /etc/nginx/sites-available/0-conf file:

nano /etc/nginx/sites-available/0-conf

At the bottom of this file, add the following blocks:

server {
          listen  80;
          listen  [::]:80;
          server_name  example-http-only.com;
          return       301 http://www.example-http-only.com$request_uri;

server {
          listen 80;
          listen [::]:80;
          server_name  www.example-http-only.com;
          root /var/www/example-http-only.com;
          index index.php index.html;
          location = /robots.txt {
              allow all;

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

              # prevent image hotlinking
              location ~ .(jpg|jpeg|png|svg|gif)$ {
                    valid_referers none blocked ~.google. ~.bing. ~.yahoo. example-http-only.com *.example-http-only.com;
                    if ($invalid_referer) {
                        return   403;

          location ~ \.php$ {
              try_files $uri =404;
              fastcgi_split_path_info ^(.+\.php)(/.+)$;
              include fastcgi_params;
              fastcgi_index index.php;
              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
              fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;

          # Browser caching
          location ~* \.(css|js|gif|jpeg|jpg|png|ico)$ {
              expires 72h;
              add_header Pragma public;
              add_header Cache-Control "public";

          access_log /var/log/sites/example-http-only.com/access.log;
          error_log  /var/log/nginx/example-http-only.com.error.log notice;

A few things to be noted here:

listen [::]:80; is needed if you want to make the website accessible over IPv6. If you don’t want to make the website accessible over IPv6 or your server doesn’t have IPv6 connectivity, you can remove this directive. In general, you’ll want to have the website accessible over both IPv4 and IPv6, since IPv6 is widely used and it seems it will be the norm in the future.

– the fastcgi directives are used to proxy requests for PHP code execution to PHP-FPM, via the FastCGI protocol.

Replace example-http-only.com with your domain. Please note that the first server block redirects all the requests for example-http-only.com to www.example-http-only.com. This is the way to redirect requests for non-www domains to www domains. This assumes that you want to use a domain that includes www, and you have added a DNS A record for www.example-http-only.com, apart from the DNS A record for example-http-only.com . If on the contrary, your site is example-http-only.com and you want to redirect all the requests for www.example-http-only.com to example-http-only.com, you can do that in a similar manner.

Of course, if you have a really simple website, made up of a few html files, without any php file, you can remove the location ~ \.php$ block.

The example given above also assumes that the website is made up of only a few php or html files, without any database. If the website is more complex so that it needs a database, then there is no reason why you should avoid using WordPress to set it up and, most importantly, to maintain it in the most time-efficient manner. On the other hand, if you use WordPress, there is no reason why you should avoid using HTTPS, since using HTTPS will bring so many benefits, it will not decrease page loading speed and it can be implemented using a free of charge, self-renewing Let’s Encrypt SSL/TLS certificate, as we’ll explain further below.

14.4. Install phpMyAdmin

phpMyAdmin is a popular web application for MySQL/MariaDB administration. It can be used to easily manage databases, tables, columns, users, user priviledges, etc. Its web interface offers a more comfortable interaction with MySQL/MariaDB databases than the command line, while also offering the ability to execute any SQL query in an embeded console.

We’ll use phpMyAdmin to create all the databases needed for WordPress websites and different applications, therefore, before describing how to install HTTPS websites with WordPress, we have to explain how to install, configure and upgrade phpMyAdmin.

First, you have to download the last stable version from the official website. On the homepage of the official website right-click on the ‘Download’ green button in the upper right corner, select ‘Copy link location’ to copy to clipboard the URL of the last stable version. Then run:

cd /tmp
wget https://files.phpmyadmin.net/phpMyAdmin/5.1.0/phpMyAdmin-5.1.0-all-languages.zip

where https://files.phpmyadmin.net/phpMyAdmin/5.1.0/phpMyAdmin-5.1.0-all-languages.zip is the URL of the latest stable version of phpMyAdmin that you copied to clipboard. Next, extract the archive:

unzip phpMyAdmin-5.1.0-all-languages.zip

Move the uncompressed phpMyAdmin-5.1.0-all-languages directory to /usr/share, and when doing this, change its name to phpmyadmin:

mv phpMyAdmin-5.1.0-all-languages /usr/share/phpmyadmin

Remove the phpMyAdmin-5.1.0-all-languages.zip archive, which is no longer needed:

rm phpMyAdmin-5.1.0-all-languages.zip

Create a phpmyadmin directory in /var/lib and a tmp directory inside it, where phpMyAdmin will store its temporary files:

mkdir -p /var/lib/phpmyadmin/tmp

Change ownership for the /var/lib/phpmyadmin directory, so that the web server user, www-data, can write to it:

chown -R www-data:www-data /var/lib/phpmyadmin

Among the files extracted from the downloaded archive there is a sample configuration file. Copy it to a new file called config.inc.php:

cp /usr/share/phpmyadmin/config.sample.inc.php /usr/share/phpmyadmin/config.inc.php

Next, create a random string by running:

openssl rand -base64 24

The resulting string will be 32 characters long. Copy it to a file, to use it later. Then open the configuration file for editing:

nano /usr/share/phpmyadmin/config.inc.php

Scroll down to the line that begins with $cfg['blowfish_secret'] and enter the string of random characters created earlier between the single quotation marks like below:

$cfg['blowfish_secret'] = '12acZBEqMhQa+S6jzwqC0TDi4/Oib2rn'; /* YOU MUST FILL IN THIS FOR COOKIE AUTH! */

This string will be used internally by the AES algorighm, to encrypt the phpMyAdmin users’ passwords. Next, look for the section /* Authentication type */ and make sure it contains the following lines:

/* Authentication type */
$cfg['Servers'][$i]['auth_type'] = 'cookie';
$cfg['Servers'][$i]['AllowRoot'] = false;

If one of the two lines is missing, add it.

Also uncomment the following lines:

/* Storage database and tables */
$cfg['Servers'][$i]['pmadb'] = 'phpmyadmin';
$cfg['Servers'][$i]['bookmarktable'] = 'pma__bookmark';
$cfg['Servers'][$i]['relation'] = 'pma__relation';
$cfg['Servers'][$i]['table_info'] = 'pma__table_info';
$cfg['Servers'][$i]['table_coords'] = 'pma__table_coords';
$cfg['Servers'][$i]['pdf_pages'] = 'pma__pdf_pages';
$cfg['Servers'][$i]['column_info'] = 'pma__column_info';
$cfg['Servers'][$i]['history'] = 'pma__history';
$cfg['Servers'][$i]['table_uiprefs'] = 'pma__table_uiprefs';
$cfg['Servers'][$i]['tracking'] = 'pma__tracking';
$cfg['Servers'][$i]['userconfig'] = 'pma__userconfig';
$cfg['Servers'][$i]['recent'] = 'pma__recent';
$cfg['Servers'][$i]['favorite'] = 'pma__favorite';
$cfg['Servers'][$i]['users'] = 'pma__users';
$cfg['Servers'][$i]['usergroups'] = 'pma__usergroups';
$cfg['Servers'][$i]['navigationhiding'] = 'pma__navigationhiding';
$cfg['Servers'][$i]['savedsearches'] = 'pma__savedsearches';
$cfg['Servers'][$i]['central_columns'] = 'pma__central_columns';
$cfg['Servers'][$i]['designer_settings'] = 'pma__designer_settings';
$cfg['Servers'][$i]['export_templates'] = 'pma__export_templates';

Next, add the following line at the bottom of the file, to configure phpMyAdmin to use the /var/lib/phpmyadmin/tmp directory as its temporary directory, which will be used as a templates cache for faster page loading:

$cfg['TempDir'] = '/var/lib/phpmyadmin/tmp';

To prevent being logged out after 24 minutes of inactivity, you can increase the session timeout to 180 minutes by adding the following line at the bottom of the configuration file:

$cfg['LoginCookieValidity'] = 10800;

In the /usr/share/phpmyadmin/sql directory there is a file called create_tables.sql. This file contains all the commands necessary to create the phpmyadmin database and all the tables mentioned above in the /* Storage database and tables */ list. You will use the create_tables.sql file to create the needed database and tables. Run the following command:

mariadb < /usr/share/phpmyadmin/sql/create_tables.sql

Since you disabled logging in as root to phpMyAdmin, you won’t be able to use the root user to log in to phpMyAdmin and create databases, edit tables, etc. Instead of root you’ll use the username created earlier in the Install the MariaDB Database Management System chapter, to perform all the needed actions inside phpMyAdmin. In our example, that user was called walter.

14.4.1. Protect phpMyAdmin’s login page using basic HTTP authentication

To restrict access to phpMyAdmin’s login page, first you have to generate user passwords for HTTP authentication. To generate passwords with the htpasswd tool, you need to first install the apache2-utils package, so run:

apt-get install apache2-utils

Create the directory to hold the password files:

mkdir /etc/nginx/htpass

Now, if you want to give permission to the user verner, run the following command:

htpasswd -c /etc/nginx/htpass/pmapasswd verner

This command will prompt you to type and retype a password for the user verner and will create a file called pmapasswd in which it will store the username followed by the password, hashed with the default MD5 algorithm. The content of the file will look similar to this:


Change ownership and permissions for the password file:

cd /etc/nginx/htpass

chown www-data:root pmapasswd

chmod 400 pmapasswd

Of course, you can name the password file pmapasswd in any way you prefer, but it’s recommended to give it a suggestive name, so as to easily distinguish it from other password files in the same directory.

To add the credentials for a new user jerry to the /etc/nginx/htpass/pmapasswd file, you should run a similar command but without the -c option, because the file has been already created:

htpasswd /etc/nginx/htpass/pmapasswd jerry

Please note that when the username and password used for basic HTTP authentication are sent from the browser to the server, they are Base64 encoded but not encrypted, so, to increase security for basic HTTP authentication, you should always use it in conjunction with the HTTPS protocol.

14.5.Configure Nginx for phpMyAdmin

We will configure Nginx to serve phpMyAdmin on a subdirectory of mail.example.com, where example.com is the main domain hosted on your server.

14.5.1. Add A and AAAA DNS records formail.example.com

First of all, you have to add the A and AAAA DNS records for mail.example.com. Earlier, in the ‘Add DNS records using Hurricane Electric’s free DNS services’ chapter, we explained how to add A and AAAA records for www.example.com. The records for mail.example.com can be added in the same way as for www.example.com. You’ll just have to enter mail instead of www. Of course, you don’t have to use the DNS services offered by Hurricane Electric. As explained before, it’s recommended to use Hurricane Electric’s free DNS services only as a last resort, if neither your hosting provider, nor the registrar of your domain offer free DNS services and you don’t want to host your own DNS servers by installing and maintaining BIND on two (or more) of your own servers.

14.5.2. Generate the Diffie-Hellman (DH) parameter

Please note that there is no need to generate multiple Diffie-Hellman parameters. It’s enough to generate only one for your entire server and mention it in the Nginx server blocks of all the websites served over HTTPS, as we’ll describe below.

Diffie–Hellman key exchange (DH) is a method of securely exchanging cryptographic keys over a public channel. We’ll generate a new set of DH parameters, to strengthen the security of the HTTPS connection:

openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

Wait about 1 minute, until the process ends. Below we’ll show how to include the ssl_dhparam directive in the Nginx server block.

Restrict permissions to the /etc/nginx/ssl/dhparam.pem file:

chmod 640 /etc/nginx/ssl/dhparam.pem

14.5.3. Install a Let’s Encrypt SSL/TLS certificate for mail.example.com

To serve any website over HTTPS you need a SSL/TLS certificate issued by a Certificate Authority (CA). We’ll describe below how to obtain and renew for mail.example.com the most popular free SSL/TLS certificates which are the Let’s Encrypt certificates.

Let’s Encrypt is a certificate authority run by Internet Security Research Group (ISRG), that offers free of charge SSL/TLS certificates. The Let’s Encrypt certificates have a 90 day validity and they can be automatically renewed by using a cron job.

14.5.4. Install Certbot, the Let’s Encrypt client

Certbot is a software tool used to obtain and renew Let’s Encrypt SSL/TLS certificates. Although the Certbot documentation recommends to first install snapd and then use it to install Certbot, the best way to obtain Certbot is by installing it from the official Debian repositories. Installing the additional python-certbot-nginx package, as the same documentation recommends, is really not necessary, since you don’t want this plugin to change Nginx configuration. Also, if you try to clone the official github repository of Certbot and use the certbot-auto script to obtain and renew the Let’s Encrypt SSL/TLS certificates, this won’t work, although for earlier versions of Certbot, it used to work. Therefore, to install Certbot, run:

apt-get install certbot

14.5.5. Configure a Nginx server block

The Let’s Encrypt verification process will look for certain verification files created by Certbot in the .well-known/acme-challenge directory, located in the server’s webroot.

This means that these files must be publicly accessible. For this we need to include a location /.well-known/acme-challenge directive in the server block for the website for which we request the SSL/TLS certificate. Open the /etc/nginx/sites-enabled/0-conf file:

nano /etc/nginx/sites-enabled/0-conf

At the bottom of this file add the following temporary server block that we’ll use to obtain the Let’s Encrypt SSL/TLS certificate for mail.example.com. After we obtain the certificate we’ll replace this block with a proper, complete server block necessary to serve the website over HTTPS:

server {

listen 80;

listen [::]:80;

server_name mail.example.com;

location /.well-known/acme-challenge {

root /var/www;



Reload Nginx to apply the changes:

systemctl reload nginx

Please note the location /.well-known/acme-challenge directive.

14.5.6. Get the SSL/TLS certificate for mail.example.com

To obtain a SSL/TLS certificate valid for mail.example.com run:

certbot certonly –agree-tos –webroot -w /var/www/ -d mail.example.com

The first time you run this command you will be asked for an email address where you can receive important renewal and security notices:

Enter email address (used for urgent renewal and security notices)

(Enter ‘c’ to cancel):

Here it’s recommended to enter one of your email addresses, because the messages sent by the Let’s Encrypt servers can be really useful, such as an SSL/TLS certificate approaching expiration date and Certbot being unable to renew it because of some Nginx misconfiguration, etc.

If the SSL/TLS certificate is successfully generated, the output will look like this:

– Congratulations! Your certificate and chain have been saved at:


Your key file has been saved at:


Your cert will expire on 2021-09-20. To obtain a new or tweaked

version of this certificate in the future, simply run certbot

again. To non-interactively renew *all* of your certificates, run

“certbot renew”

– If you like Certbot, please consider supporting our work by:

Donating to ISRG / Let’s Encrypt: https://letsencrypt.org/donate

Donating to EFF: https://eff.org/donate-le

The private key for the certificate will be:


The intermediate certificates used for OCSP stapling will be:


The certificate and intermediate certificate concatenated in the correct order will be:


The certificate (not to be used in Nginx configuration; you’ll use the fullchain.pem instead) will be:


These files are in fact symlinks to the most recent version of the respective files, which are located in /etc/letsencrypt/archive/mail.example.com . When you renew the Let’s Encrypt certificates, the Certbot client will automatically update the symlinks to point to the most recent version of the files. The generated files and symlinks must be left in their default locations.

14.5.7. Update the server blocks configuration file

Next edit the /etc/nginx/sites-enabled/0-conf file:

nano /etc/nginx/sites-enabled/0-conf

Replace the simplified server block for mail.example.com mentioned above with the following server blocks:

server {
    listen  80;
    listen [::]:80;
    server_name mail.example.com;
    return  301 https://mail.example.com$request_uri;

server {
    listen      443 ssl http2;
    listen [::]:443 ssl http2;
    server_name mail.example.com;
    root /var/www/mail.example.com;
    index index.php;

    ssl_certificate /etc/letsencrypt/live/mail.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mail.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/mail.example.com/chain.pem;
    ssl_dhparam /etc/nginx/ssl/dhparam.pem;

    ssl_session_timeout 4h;
    ssl_session_cache shared:SSL:40m;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    ssl_stapling on;
    ssl_stapling_verify on;
    add_header Strict-Transport-Security "max-age=63072000" always;
    # The following line allows integrating Roundcube with Roundpin; if you don't want to be able to open Roundcube inside Roundpin, replace the following line with:  add_header X-Frame-Options SAMEORIGIN;
    add_header Content-Security-Policy "frame-ancestors https://*.example.com";

    add_header X-Content-Type-Options nosniff;
    add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive";

    location = /robots.txt {
       allow all;

    location / {
                auth_basic 'Restricted';
                auth_basic_user_file /etc/nginx/htpass/mail.example.com;
                try_files $uri $uri/ /index.php?$args;

    location /.well-known/acme-challenge {
       root /var/www;

    location ~ \.php$ {
       try_files $uri =404;
       fastcgi_split_path_info ^(.+\.php)(/.+)$;
       include fastcgi_params;
       fastcgi_index index.php;
       fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
       fastcgi_param HTTPS on;
       fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;

    location /net-mrdblog {
       alias /usr/share/phpmyadmin;
       index index.php;
       auth_basic "Restricted";
       auth_basic_user_file /etc/nginx/htpass/pmapasswd;
       location ~ ^/net-mrdblog/(.+\.php)$ {
                       alias /usr/share/phpmyadmin/$1;
                       fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
                       fastcgi_param HTTPS on;
                       fastcgi_index index.php;
                       fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                       include /etc/nginx/fastcgi_params;

       location ~* ^/net-mrdblog/(.+.(jpg|jpeg|gif|css|png|js|ico|html|xml|txt))$ {
              alias /usr/share/phpmyadmin/$1;

    access_log  /var/log/sites/mail.example.com/access.log;  
    error_log  /var/log/sites/mail.example.com/errors.log notice;

Replace example.com with your own domain. Also replace net-mrdblog with a custom name for the directory that will be used as an alias for phpMyAdmin’s installation directory (/usr/share/phpmyadmin). The name of this directory will be part of the URL used to log in to phpMyAdmin. It’s important to have a custom name for this directory, because it will be the target of vicious attacks. So, replace net-mrdblog with a name of your choice that you can easily remember but radically different from phpmyadmin, myadmin, phpma, etc., so that it will be very difficult to guess for an attacker. Below is an extract from a real log from one of our servers, showing an attacker trying to access phpMyAdmin’s login page by guessing the name of the installation directory: – – [25/Jun/2020:05:44:49 +0300] “GET /myadmin/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:49 +0300] “GET /xampp/phpmyadmin/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:50 +0300] “GET /phpMyadmin_bak/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:50 +0300] “GET /www/phpMyAdmin/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:50 +0300] “GET /tools/phpMyAdmin/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:51 +0300] “GET /phpmyadmin-old/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:51 +0300] “GET /phpMyAdminold/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:52 +0300] “GET /phpMyAdmin.old/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:52 +0300] “GET /pma-old/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:52 +0300] “GET /claroline/phpMyAdmin/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:53 +0300] “GET /phpma/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:54 +0300] “GET /phpmyadmin/phpmyadmin/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:54 +0300] “GET /phpMyAdmin/phpMyAdmin/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:54 +0300] “GET /phpMyAbmin/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:55 +0300] “GET /phpMyAdmin__/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:55 +0300] “GET /phpMyAdmin+++—/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:56 +0300] “GET /v/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:56 +0300] “GET /phpmyadm1n/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:56 +0300] “GET /phpMyAdm1n/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:57 +0300] “GET /phpMyadmi/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:58 +0300] “GET /phpMyAdmion/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:58 +0300] “GET /s/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:58 +0300] “GET /MyAdmin/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0” – – [25/Jun/2020:05:44:59 +0300] “GET /phpMyAdmin1/index.php HTTP/1.1″ 444 0 “-” “Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0”

Please also note the auth_basic “Restricted”; directive by which you restricted access to the /var/www/mail.example.com directory and also to the phpMyAdmin login page. The pmapasswd file mentioned in the auth_basic_user_file /etc/nginx/htpass/pmapasswd; directive is the password file created earlier, containing the name and the hashed password of the user(s) who will have access to the phpMyAdmin login page. The /etc/nginx/htpass/mail.example.com file mentioned in the location / { block is the password file containing the usernames and hashed passwords of all the users who will have access to Roundcube’s login page, which you’ll configure later. For the moment, create a mail.example.com file with the same user(s) as the pmapasswd file by running:

htpasswd -c /etc/nginx/htpass/mail.example.com verner
New password:
Re-type new password:
chown www-data:root /etc/nginx/htpass/mail.example.com
chmod 400 /etc/nginx/htpass/mail.example.com

where verner is the user from the /etc/nginx/htpass/pmapasswd file.

Next create the /var/www/mail.example.com directory, which you’ll use later to store Roundcube’s files:

mkdir /var/www/mail.example.com

Create the access logs directory:

mkdir -p /var/log/sites/mail.example.com

Create a robots.txt file inside the /var/www/mail.example.com directory:

nano /var/www/mail.example.com/robots.txt

Add the following content inside this file, to discourage search engines from indexing the login page:

User-agent: *

Disallow: /

The robots.txt file may seem superfluous in a setup where the content of the /var/www/mail.example.com directory will be accessible only to logged in users and the login page will be protected with basic HTTP authentication, but in the situation where you disable HTTP authentication temporarily for testing purposes, etc., the search engines can access mail.example.com and index the login page, which is not what you want. The web interface of Roundcube, which will be accessible on mail.example.com, should only be accessed by its intended users.

Rload Nginx:

systemctl reload nginx

Then access:


Replace example.com with your own domain, and net-mrdblog with the custom name of the directory used as alias of /usr/share/phpmyadmin. First, you’ll have to enter the username and password that you set up earlier when creating the /etc/nginx/htpass/pmapasswd file. Then you will see the phpMyAdmin login page:

You should be able to log in to phpMyAdmin with the user created earlier in the Install the MariaDB Database Management System chapter, specifically for this purpose (walter in our example).

14.5.8. Automate the Let’s Encrypt SSL/TLS certificates renewal

The certificates from Let’s Encrypt are valid for 90 days. This limited validity duration doesn’t create any problems, because the certificates renewal process can be fully automated by setting up a simple cron job. Open the crontab file by running:

crontab -e

Enter the following lines in the crontab file:

# Update all the Let’s Encrypt SSL/TLS certificates that need to be renewed, every Monday at 4:12 AM

12 4 * * 1 certbot renew –quiet –post-hook “systemctl reload nginx” >> /var/log/letsencrypt/le-renew.log

This runs the certificates renewal process every Monday at 4:12 AM (you can run it more frequently if you want to), and will renew all the certificates that are up for renewal. If at least one certificate is renewed, the command in the –post-hook argument will be executed (Nginx will be reloaded). If no certificates are renewed, the command will be ignored.

14.5.9. Configure Fail2ban to protect phpMyAdmin against brute-force attacks

Navigate to Fail2ban’s filter.d directory:

cd /etc/fail2ban/filter.d

Create a file called phpmyadmin.conf:

nano phpmyadmin.conf

Add the following lines inside this file:


failregex = ^<HOST> .* \”POST /net-mrdblog/index.php HTTP/2.0\” 200 31.*$

^<HOST> .* \”GET /net-mrdblog HTTP/2.0\” 401 195 .*$

ignoreregex =

Replace net-mrdblog with the custom name of the directory used as alias of /usr/share/phpmyadmin. Please note that the first failregex line protects the phpMyAdmin login page against brute-force attacks, while the second line protects the HTTP authentication dialog window, which can be itself subject to brute-force attacks. The username and password used for HTTP authentication should be different from the username and password used to log in to phpMyAdmin.

Then open /etc/fail2ban/jail.local:

nano /etc/fail2ban/jail.local

Add the following section below the [lighttpd-auth] section:


enabled = true

filter = phpmyadmin

logpath = /var/log/sites/mail.example.com/access.log

port = 80,443

findtime = 3600

maxretry = 3

bantime = 604800

Don’t forget to replace example.com with your actual domain. Reload Fail2ban:

systemctl reload fail2ban

14.5.10. Upgrading phpMyAdmin

Before upgrading phpMyAdmin to a new version, it’s recommended to verify if the new version has been tested and confirmed to function well within the suite of applications described in this guide. Once we test an application and confirm that it works well, we include it on this page.

After you backup the phpmyadmin database and the /usr/share/phpmyadmin directory, you can upgrade phpMyAdmin by following again all the steps needed to install phpMyAdmin described above, up to the Protect phpMyAdmin’s login page using basic HTTP authentication chapter. When you will run the /usr/share/phpmyadmin/sql/create_tables.sql script again, it will not overwrite the phpmyadmin database. It will just create new tables or update the existing tables if needed.

14.6. Upgrading Nginx

Since Nginx has been installed from the official Debian repository, to upgrade it, all you need to do is to run apt-get update && apt-get dist-upgrade with a specific frequency, as described in the Maintenance steps chapter. This command will upgrade Nginx if there is a new version available. During these upgrades, the configuration changes implemented as described above, will be preserved.

You can send your questions and comments to: