How To Configure NGINX With SSL and HTTP/2

HTTP/2 is the newest version of the wildly popular Hyper Text Transport Protocol. Based on Google's experimental SPDY protocol, HTTP/2 provides better performance by introducing features like full request and response multiplexing, better compression of header fields, server push and request prioritization.

Some of the notable features of HTTP/2 is as follows:

  1. Binary Protocol - While HTTP/1.x was a text based protocol, HTTP/2 is a binary protocol resulting in less error during data transfer process.
  2. Multiplexed Streams - All HTTP/2 connections are multiplexed streams meaning multiple files can be transferred in a single stream of binary data.
  3. Compressed Header - HTTP/2 compresses header data in responses resulting in faster transfer of data.
  4. Server Push - This capability allows the server to send linked resources to the client automatically, greatly reducing the number of requests to the server.
  5. Stream Prioritization - HTTP/2 can prioritize data streams based on their type resulting in better bandwidth allocation where necessary.

If you want to learn more about the improvements in HTTP/2 this article by Kinsta may help.

While a significant upgrade over its predecessor, HTTP/2 is not as widely adapted as it should have been. In this article, I'll introduce you to some of the new features mentioned previously and I'll also show you how to enable HTTP/2 on an NGINX powered web server.

Table of Content

Prerequisites

Installing NGINX on Your Server

Going forward, I'm assuming that you have a running virtual private server and you have root access to your server via SSH. I'll be using a $5 cloud compute server on Vultr running Ubuntu 20.04 LTS as my operating system. If you want to be exactly in line with this tutorial, I would suggest you to use Ubuntu but as long as you know what you're doing, you may use anything.

I've also attached a custom domain to my server so that I can refer to it as nginx-in-action.farhan.info instead of the IP address. If you own a custom domain, using that is highly recommended.

Before installing NGINX, make sure your server is completely up-to-dated.

apt update && apt upgrade -y && apt dist-upgrade -y

# ... lots of scrolling text ...
# 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
# root@nginx-in-action:~

Pre-built binaries of NGINX is available for all major Linux distributions. You should be able to install them using your default package manager.

apt install nginx -y

NGINX should be (at least it does on Ubuntu) registered as a systemd service and should start automatically.

systemctl status nginx

# ● nginx.service - A high performance web server and a reverse proxy server
#      Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
#      Active: active (running) since Fri 2021-04-30 14:55:23 UTC; 16s ago
#        Docs: man:nginx(8)
#    Main PID: 56607 (nginx)
#       Tasks: 2 (limit: 1074)
#      Memory: 6.6M
#      CGroup: /system.slice/nginx.service
#              ├─56607 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
#              └─56608 nginx: worker process

If it doesn't for some reason, you can start by executing systemctl start nginx command. To enable auto sturtup for NGINX, you can execute the systemctl enable nginx command.

Finally, visit the server for a visual verification:

Welcome to nginx!

This means NGINX has been installed and running fine. Now you'll have to configure it to serve something else than this welcome page.

Serving Static Content With NGINX

Before you start editing your configuration, you need some static content. For this demo, I'll use the static-demo project from the following repository:

Git should come pre-installed on your server. If it doesn't, install git using your default package manager.

sudo apt install git -y

# Reading package lists... Done
# Building dependency tree       
# Reading state information... Done
# git is already the newest version (1:2.25.1-1ubuntu3.1).
# 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

On your server's root, there should be a /srv directory, meant for containing the files served by this server. Clone the above mentioned repository in there.

git clone https://github.com/fhsinchy/nginx-handbook-projects.git /srv/nginx-handbook-projects

# Cloning into '/srv/nginx-handbook-projects'...
# remote: Enumerating objects: 178, done.
# remote: Counting objects: 100% (178/178), done.
# remote: Compressing objects: 100% (130/130), done.
# remote: Total 178 (delta 30), reused 168 (delta 25), pack-reused 0
# Receiving objects: 100% (178/178), 153.73 KiB | 10.25 MiB/s, done.
# Resolving deltas: 100% (30/30), done.

Now that the content is ready to be served, its time for you to write the configuration file. For simplicity, I'll use the /etc/nginx/sites-available/default file as my configuration. Open the file using nano or vi if you fancy that.

nano /etc/nginx/sites-available/default

Update the file's content as follows:

server {
        listen 80;
        server_name nginx-in-action.farhan.info;  

        root /srv/nginx-handbook-projects/static-demo;
}

As you can see, the /srv/nginx-handbook-projects/static-demo; directory has been set as the root of this site and nginx-in-action.farhan.info has been set as the server name. If you do not have a custom domain set up, you can use your server's IP address as the server name here.

Test the configuration by executing nginx -t and reload the configuration by executing nginx -s reload commands.

nginx -t

# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

nginx -s reload

Finally visit your server and you should be greeted with a simple static HTML page.

static-demo

If everything has gone fine till now then you're ready to install a new SSL certificate on your server.

SSL Certification With Let's Encrypt and Certbot

In short, an SSL certificate is what allows a server to make the move from HTTP to HTTPS. These certificates are issued by a certificate authority (CA). Most of the authorities charge a fee for issuing certificates but nonprofit authorities such as Let's Encrypt, issues certificates for free.

If you want to understand the theory of SSL in a bit more detail, this article on the Cloudflare Learning Center may help.

Installing Certbot

Thanks to open-source tools like Certbot, installing a free certificate is dead easy. Head over to certbot.eff.org link. Now select the software and system that powers your server.

Software and System

I'm running NGINX on Ubuntu 20.04 and if you've been in line with this tutorial, you should have the same combination.

After selecting your combination of software and system, you'll be forwarded to a new page containing step by step instructions for installing certbot and a new SSL certificate.

Certbot - Ubuntufocal Nginx

The installation steps for certbot may differ from system to system but rest of the instructions should remain same. On Ubuntu, the recommended way is to use snap.

snap install --classic certbot

# certbot 1.14.0 from Certbot Project (certbot-eff✓) installed

certbot --version

# certbot 1.14.0

Certbot is now installed and ready to be used.

Installing a New SSL Certificate

Before you install a new certificate, make sure the NGINX configuration file contains all the necessary server names. Such as, if you want to install a new certificate for yourdomain.tld and www.yourdomain.tld, you'll have to include both of them in your configuration.

server {
    # ...
    server_name yourdomain.tld www.yourdomain.tld;
    # ...
}

Once you're happy with your configuration, you can install a newly provisioned certificate for your server. To do so, execute the certbot program with --nginx option.

certbot --nginx

# Saving debug log to /var/log/letsencrypt/letsencrypt.log
# Plugins selected: Authenticator nginx, Installer nginx
# Enter email address (used for urgent renewal and security notices)
#  (Enter 'c' to cancel): mail@farhan.info

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Please read the Terms of Service at
# https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
# agree in order to register with the ACME server. Do you agree?
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# (Y)es/(N)o: Y

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Would you be willing, once your first certificate is successfully issued, to
# share your email address with the Electronic Frontier Foundation, a founding
# partner of the Let's Encrypt project and the non-profit organization that
# develops Certbot? We'd like to send you email about our work encrypting the web,
# EFF news, campaigns, and ways to support digital freedom.
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# (Y)es/(N)o: N
# Account registered.

# Which names would you like to activate HTTPS for?
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# 1: nginx-in-action.farhan.info
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Select the appropriate numbers separated by commas and/or spaces, or leave input
# blank to select all options shown (Enter 'c' to cancel): 
# Requesting a certificate for nginx-in-action.farhan.info
# Performing the following challenges:
# http-01 challenge for nginx-in-action.farhan.info
# Waiting for verification...
# Cleaning up challenges
# Deploying Certificate to VirtualHost /etc/nginx/sites-enabled/default
# Redirecting all traffic on port 80 to ssl in /etc/nginx/sites-enabled/default

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Congratulations! You have successfully enabled
# https://nginx-in-action.farhan.info
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# IMPORTANT NOTES:
#  - Congratulations! Your certificate and chain have been saved at:
#    /etc/letsencrypt/live/nginx-in-action.farhan.info/fullchain.pem
#    Your key file has been saved at:
#    /etc/letsencrypt/live/nginx-in-action.farhan.info/privkey.pem
#    Your certificate will expire on 2021-07-30. To obtain a new or
#    tweaked version of this certificate in the future, simply run
#    certbot again with the "certonly" option. 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

You'll be asked for an emergency contact email address, license agreement and if you would like to receive emails from them or not.

The certbot program will automatically read the server names from your configuration file and show you a list of them. If you have multiple virtual hosts on your server, certbot will recognize them as well.

Finally if the installation is successful, you'll be congratulated by the program. To verify if everything's working or not, visit your server with HTTPS this time:

Verified by: Let's Encrypt

As you can see, HTTPS has been enabled successfully and you can confirm that the certificate is verified by Let's Encrypt authority. Later on, if you add new virtual hosts to this server with new domains or sub domains, you'll have to reinstall the certificates.

It's also possible to install wildcard certificate such as *.yourdomain.tld for some supported DNS managers. Detailed instructions can be found on the previously shown installation instruction page.

Certbot - Ubuntufocal Nginx Wildcard

A newly installed certificate will be valid for 90 days. After that, a renewal will be required. Certbot does the renewal automatically. You can execute certbot renew command with the --dry-run option to test out the auto renewal feature.

certbot renew --dry-run

# Saving debug log to /var/log/letsencrypt/letsencrypt.log

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Processing /etc/letsencrypt/renewal/nginx-in-action.farhan.info.conf
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Cert not due for renewal, but simulating renewal for dry run
# Plugins selected: Authenticator nginx, Installer nginx
# Account registered.
# Simulating renewal of an existing certificate for nginx-in-action.farhan.info
# Performing the following challenges:
# http-01 challenge for nginx-in-action.farhan.info
# Waiting for verification...
# Cleaning up challenges

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# new certificate deployed with reload of nginx server; fullchain is
# /etc/letsencrypt/live/nginx-in-action.farhan.info/fullchain.pem
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Congratulations, all simulated renewals succeeded: 
#   /etc/letsencrypt/live/nginx-in-action.farhan.info/fullchain.pem (success)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

The command will simulate a certificate renewal to test if it's correctly set up or not. If it succeeds you'll be congratulated by the program. This step ends the procedure of installing an SSL certificate on your server.

What Just Happened

To understand what certbot did behind the scenes, open up the /etc/nginx/sites-available/default file once again and see how its content has been altered.

server {

    server_name nginx-in-action.farhan.info;  

    root /srv/nginx-handbook-projects/static-demo;

    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/nginx-in-action.farhan.info/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/nginx-in-action.farhan.info/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 = nginx-in-action.farhan.info) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    listen [::]:80;

    server_name nginx-in-action.farhan.info;
    return 404; # managed by Certbot
}

As you can see, certbot has added quite a few lines here. I'll explain the notable ones.

server {
    # ...
    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    # ...
}

Like the 80 port, 443 is widely used for listening to HTTPS requests. By writing listen 443 ssl; certbot is instructing NGINX to listen for any HTTPS request on port 443. The listen [::]:443 ssl ipv6only=on; line is for handling IPV6 connections.

server {
    # ...
    ssl_certificate /etc/letsencrypt/live/nginx-in-action.farhan.info/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/nginx-in-action.farhan.info/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
    # ...
}

The ssl_certificate directive is used for indicating the location of the certificate and the private key file on your server. The /etc/letsencrypt/options-ssl-nginx.conf; includes some common directives necessary for SSL.

Finally the ssl_dhparam indicates to the file defining how OpenSSL is going to perform Diffie–Hellman key exchange. If you want to learn more about the purpose of /etc/letsencrypt/ssl-dhparams.pem; file, this stack exchange thread may help you.

server {
    if ($host = nginx-in-action.farhan.info) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    listen [::]:80;

    server_name nginx-in-action.farhan.info;
    return 404; # managed by Certbot
}

This newly added server block is responsible for redirecting any HTTP requests to HTTPS disabling HTTP access completely.

Enabling HTTP/2 With NGINX

Once you've successfully installed a valid SSL certificate on your server, you're ready to enable HTTP/2. SSL is a prerequisite for HTTP/2, so right off the bat you can see, security is not optional in HTTP/2.

HTTP/2 support for NGINX is provided by the ngx_http_v2_module module. Pre-built binaries of NGINX on most of the systems come with this module baked in. If you've built NGINX from source however, you'll have to include this module manually.

Before upgrading to HTTP/2, send a request to your server and see the current protocol version.

curl -I -L https://nginx-in-action.farhan.info

# HTTP/1.1 200 OK
# Server: nginx/1.18.0 (Ubuntu)
# Date: Sat, 01 May 2021 10:46:36 GMT
# Content-Type: text/html
# Content-Length: 960
# Last-Modified: Fri, 30 Apr 2021 20:14:48 GMT
# Connection: keep-alive
# ETag: "608c6538-3c0"
# Accept-Ranges: bytes

As you can see, by default the server is on HTTP/1.1 protocol. On the next step, we'll update the configuration file as necessary for enabling HTTP/2.

Updating The Configuration

To enable HTTP/2 on your server, open the /etc/nginx/sites-available/default file once again. Find wherever it says listen [::]:443 ssl ipv6only=on; or listen 443 ssl; and update them to listen [::]:443 ssl http2 ipv6only=on; and listen 443 ssl http2; respectively.

server {

    server_name nginx-in-action.farhan.info;  

    root /srv/nginx-handbook-projects/static-demo;

    listen [::]:443 ssl http2 ipv6only=on; # managed by Certbot
    listen 443 ssl http2; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/nginx-in-action.farhan.info/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/nginx-in-action.farhan.info/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 = nginx-in-action.farhan.info) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    listen [::]:80;

    server_name nginx-in-action.farhan.info;
    return 404; # managed by Certbot
}

Test the configuration file by executing niginx -t and reload the configuration by executing nginx -s reload commands. Now send a request to your server again.

curl -I -L https://nginx-in-action.farhan.info

# HTTP/2 200 
# server: nginx/1.18.0 (Ubuntu)
# date: Sat, 01 May 2021 09:03:10 GMT
# content-type: text/html
# content-length: 960
# last-modified: Fri, 30 Apr 2021 20:14:48 GMT
# etag: "608c6538-3c0"
# accept-ranges: bytes

As you can see, HTTP/2 has been enabled for any client supporting the new protocol.

Enabling Server Push

Server push is one of the many features that HTTP/2 brings to the table. Which means the server can push files to the client without the client having to request for them. In a HTTP/1.x server, a typical request for static content may look like as follows:

http-1x

But on a server push enabled HTTP/2 server, it may look like as follows:

http-2x

On a single request for the index.html file the server responds with the style.css file as well, minimizing the number of requests in the process.

In this section, I'll use an open-source HTTP client named Nghttp2 for testing the server.

apt install nghttp2-client -y

# Reading package lists... Done
# Building dependency tree       
# Reading state information... Done
# The following additional packages will be installed:
#   libev4 libjansson4 libjemalloc2
# The following NEW packages will be installed:
#   libev4 libjansson4 libjemalloc2 nghttp2-client
# 0 upgraded, 4 newly installed, 0 to remove and 0 not upgraded.
# Need to get 447 kB of archives.
# After this operation, 1,520 kB of additional disk space will be used.
# Get:1 http://archive.ubuntu.com/ubuntu focal/main amd64 libjansson4 amd64 2.12-1build1 [28.9 kB]
# Get:2 http://archive.ubuntu.com/ubuntu focal/universe amd64 libjemalloc2 amd64 5.2.1-1ubuntu1 [235 kB]
# Get:3 http://archive.ubuntu.com/ubuntu focal/universe amd64 libev4 amd64 1:4.31-1 [31.2 kB]
# Get:4 http://archive.ubuntu.com/ubuntu focal/universe amd64 nghttp2-client amd64 1.40.0-1build1 [152 kB]
# Fetched 447 kB in 1s (359 kB/s)     
# Selecting previously unselected package libjansson4:amd64.
# (Reading database ... 107613 files and directories currently installed.)
# Preparing to unpack .../libjansson4_2.12-1build1_amd64.deb ...
# Unpacking libjansson4:amd64 (2.12-1build1) ...
# Selecting previously unselected package libjemalloc2:amd64.
# Preparing to unpack .../libjemalloc2_5.2.1-1ubuntu1_amd64.deb ...
# Unpacking libjemalloc2:amd64 (5.2.1-1ubuntu1) ...
# Selecting previously unselected package libev4:amd64.
# Preparing to unpack .../libev4_1%3a4.31-1_amd64.deb ...
# Unpacking libev4:amd64 (1:4.31-1) ...
# Selecting previously unselected package nghttp2-client.
# Preparing to unpack .../nghttp2-client_1.40.0-1build1_amd64.deb ...
# Unpacking nghttp2-client (1.40.0-1build1) ...
# Setting up libev4:amd64 (1:4.31-1) ...
# Setting up libjemalloc2:amd64 (5.2.1-1ubuntu1) ...
# Setting up libjansson4:amd64 (2.12-1build1) ...
# Setting up nghttp2-client (1.40.0-1build1) ...
# Processing triggers for man-db (2.9.1-1) ...
# Processing triggers for libc-bin (2.31-0ubuntu9.2) ...

nghttp --version

# nghttp nghttp2/1.40.0

Lets test by sending a request to the server without server push.

nghttp --null-out --stat https://nginx-in-action.farhan.info/index.html

# id  responseEnd requestStart  process code size request path
#  13      +836us       +194us    642us  200  492 /index.html

nghttp --null-out --stat --get-assets https://nginx-in-action.farhan.info/index.html

# id  responseEnd requestStart  process code size request path
#  13      +836us       +194us    642us  200  492 /index.html
#  15     +3.11ms      +2.65ms    457us  200  45K /mini.min.css
#  17     +3.23ms      +2.65ms    578us  200  18K /the-nginx-handbook.jpg

On the first request --null-out means discard downloaded data and --stat means print statistics on terminal. On the second request --get-assets means also download assets such as stylesheets, images and scripts linked to this files. As a result you can tell by the requestStart times, the css file and image was downloaded shortly after the html file was downloaded.

Now, lets enable server push for stylesheets and images. Open /etc/nginx/sites-available/default file and update its content as follows:

server {

    server_name nginx-in-action.farhan.info;

    root /srv/nginx-handbook-projects/static-demo;

    listen [::]:443 ssl http2 ipv6only=on; # managed by Certbot
    listen 443 ssl http2; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/nginx-in-action.farhan.info/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/nginx-in-action.farhan.info/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

    location = /index.html {
        http2_push /mini.min.css;
        http2_push /the-nginx-handbook.jpg;
    }

    location = /about.html {
        http2_push /mini.min.css;
        http2_push /the-nginx-handbook.jpg;
    }

}
server {
    if ($host = nginx-in-action.farhan.info) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    listen [::]:80;

    server_name nginx-in-action.farhan.info;
    return 404; # managed by Certbot
}

Two location blocks have been added to exactly match /index.html and /about.html locations. The http2_push directive is used for sending back additional response. Now whenever NGINX receives a request for one of these two locations, it'll automatically send back the css and image file.

Test the configuration by executing nginx -t and reload the configuration by executing nginx -s reload commands.

nginx -t

# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

nginx -s reload

Now send another request to the server using nghttp and do not include --get-assets option.

nghttp --null-out --stat https://nginx-in-action.farhan.info/index.html

# id  responseEnd requestStart  process code size request path
#  13     +1.49ms       +254us   1.23ms  200  492 /index.html
#   2     +1.56ms *    +1.35ms    212us  200  45K /mini.min.css
#   4     +1.71ms *    +1.39ms    318us  200  18K /the-nginx-handbook.jpg

As you can see, although the assets were not requested, the server has sent them to the client. Looking at the time measurements, process time has gone down and the three responses ended almost simultaneously.

This was a very simple example of server push but depending on the necessities of your project, this configuration can become much complex. This article by Owen Garrett on the official NGINX blog can help you with more complex server push configuration.

Closing Thoughts

Although HTTP/2 is an improvement over its predecessor in many ways, its not all rainbows and sunshine. If configured wrong , HTTP/2 can hurt the server's stability than improving. So before enabling HTTP/2 make sure you understanding what you're doing. This article by Valentin Bartenev on the official NGINX blog can help you further.

Comments (2)

victor's photo

Great article! I would like to know if I use nginx as a reverse proxy for an express or go application, should I configure http2 in the express/go web server, in nginx the reverse proxy or in both express/go and nginx?

Farhan Hasin Chowdhury's photo

You should configure HTTP/2 in the reverse proxy. The article I linked in the Closing Thoughts section, contains an example.