Docker, Nginx and Certbot to Host multiple websites — Updated 2021 edition

When buying a beefy VPS for a low price, like the ones we offer, customers do tend to use it for more than one project. Don't get us wrong, we love that! Maximizing resource utilization is the name of the game at SSDNodes. This tutorial will help you run more than one websites (or web apps) on your VPS using nothing but free and open source software!

Requirements

In order to follow along you will need the following:

  1. At least one registered Domain Name (we will use example.com as a stand-in).
  2. A VPS with a public IP that you can SSH into, and have root access. If you don't already have this, headover to SSDNodes where we have servers for all your needs.
  3. Basic understanding of Linux, IP addresses, ports and Docker....Only very basic, I promise 🙂

We will be using the most popular GNU/Linux distro Ubuntu 20.04 for this tutorial, however, it works pretty well on other distributions too.

Setup

We will be working with the following hypothetical setup

  1. The websites we want to host are site1.example.com and site2.example.com
  2. The VPS has a public IP of 1.2.3.4
  3. The A records for both site1.example.com and site2.example.com are pointing at 1.2.3.4
  4. The operating system is assumed to be Ubuntu 20.04, although it is pretty easy to generalize to other distros

Let's install the two main packages that we will need in this setup:

$ sudo apt install docker.io nginx -y

Problem statement

In order to understand why we will be doing the below setup, it is important to understand the problem that involves using a single IP. A website is by convention served over port 80 (standard port for HTTP) and port 443 (for HTTPS) by your web server. When you go to your browser and type in site1.example.com it automatically reaches out to the port number 80 or 443. When trying to run multiple websites behind a single IP, you need a mechanism to share the ports 80, and 443, among many concurrent web servers. That's where reverse proxy comes in.

In order to serve more than one websites concurrently, you need a reverse proxy (like Nginx) to see the incoming request's required DNS and send it to appropriate backend server. So traffic for site1.example.com goes to site1's Docker container and request for site2.example.com goes to site2's Docker container.

The only problem is, if we have TLS enabled, and all the traffic is encrypted, then Nginx Reverse Proxy won't be able to read the incoming requests to ascertain which backend server it is meant for. The solution to this is that, we will configure the Nginx Reverse Proxy as our TLS termination point and the backend servers for sites 1 and 2 will get unencrypted traffic directly fed to them. Because all three websites are on the same VPS, this is not a security issue, but if the backend servers were elsewhere, instead of being just another Docker container, we will be introducing a security threat; Anyone between the reverse proxy and the backend sever will be able to snoop on this unencrypted traffic. But for a single VPS this is not an issue.

Our network diagram looks something like below:

Multiple website setup with Nginx and Docker containers

You can, of course, generalize this to have site3, site4, etc. Basically, having multiple websites on one server.

Use Docker to Create Multiple Websites on One Server

Your backend website can be anything from a WordPress blog to a custom web app written by you. As long as your app is packaged into a Docker image, its fair game. We will be using a very popular Content Management System called Ghost CMS which is written in NodeJS and it comes in an easy to use docker container. A look at their documentation shows that the container is configured to listen on port 2368. This will be used for Nginx configurations in the next step.

Your app may expose a different port number, so don't just copy the below commands, refer to the documentations for your specific app, and substitue the port number with 2368. WordPress Docker containers, for example, listen on port 80.

So let's create new Docker network, pull the Docker image and create Dockerized Ghost CMS instances:

$ docker network create myNetwork
$ docker pull ghost
$ docker run -dit --name site1.example.com --network myNetwork ghost
$ docker run -dit --name site2.example.com --network myNetwork ghost

Next we need to get the IP Addresses assigned to each of the containers:

$ docker inspect site1.example.com | grep IPAddress
"SecondaryIPAddresses": null,
"IPAddress": "",
"IPAddress": "172.18.0.2",
$ docker inspect site2.example.com | grep IPAddress
"SecondaryIPAddresses": null,
"IPAddress": "",
"IPAddress": "172.18.0.3",

Now we know the IP Addresses for individual websites, 172.18.0.2 and 172.18.0.3 and the port number, i.e, 2368, for both of them.

Configuring Nginx to Proxy Sites

Let's make sure that Nginx is running:

$ sudo systemctl enable --now nginx

Let's ensure that the DNS entries made at the your Domain Name's DNS service (like CloudFlare, Hover, Namecheap, etc) is actually valid. Go to your browser and visit site1.example.com and site2.example.com and for both the sites you will be shown this same page:

This is perfect. Now, let's configure Nginx. Since, we want Nginx to be a reverse proxy for our websites, and not serve the above page, let's clean up the default configuration.

On Ubuntu/Debian system remove the following file:

$ sudo rm /etc/nginx/sites-enabled/default.conf

The next setup is to add configurations for reverse proxy. Any file we add in the folder /etc/nginx/conf.d ending with .conf will get added to the Nginx configuration. So we will create two files in there, for each backend sites, respectively.

$ sudo vim /etc/nginx/cond.d/site1.example.com.conf

Contents of the file /etc/nginx/conf.d/site1.example.com.conf:

server {
    listen 80;
    listen [::]:80;

    server_name site1.example.com;
    location / {
        proxy_pass http://172.18.0.2:2368;
        proxy_buffering off;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
$ sudo vim /etc/nginx/cond.d/site1.example.com.conf

Contents of the file /etc/nginx/conf.d/site2.example.com.conf:

server {
    listen 80;
    listen [::]:80;

    server_name site2.example.com;
    location / {
        proxy_pass http://172.18.0.3:2368;
        proxy_buffering off;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Make sure you have substitued your actual domain name, and desired subdomains, in place of site1.example.com and site2.example.com. Since, these names will be automatically used to issue TLS (or SSL) certificates for your website.

Let's check if the configuration is valid:

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

Now, we can reload the Nginx server with our new configuration:

$ sudo systemctl reload nginx

This is it! Now if you visit http://site1.example.com/ghost and http://site2.example.com/ghost you can create two different admin accounts for both the websites. Once that is done, both the sites are now being served from the same VPS!

Site1Site2

Setting Up TLS/SSL

The last step in our multiple site configuration is TLS. We will be following instructions from Certbot's excellent documentation.

$ sudo apt install snapd -y;
$ sudo snap install core; sudo snap refresh core
$ sudo install --classic certbot
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot

The above 4 commands are just used to install certbot and make it ready to use. Let's now use certbot to get TLS certificates so our websites can be served over HTTPS. Certbot comes with a really useful flag certbot --nginx which automatically detects the domain names to be configured from your Nginx configuration file, and after successfully issuing the certificates, it modifies the Nginx configuration to redirect all unencrypted HTTP traffic to HTTPS, so you don't have to do any more configurations.

Let's run certbot. The parts highlighted in italics are the inputs you need to enter, when certbot prompts you for them. Please enter valid information here, i.e, substitute [email protected] with your actual email address, else certbot will error out. When it shows you a list of names for which you want to enable TLS/SSL make sure all your websites show up in that list (e.g, site1.example.com, site2.example.com) and just press ENTER to get TLS certificates for all of them.

$ sudo 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): [email protected]

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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: site1.example.com
2: site2.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 site1.example.com and site2.example.com
Performing the following challenges:
http-01 challenge for site1.example.com
http-01 challenge for site2.example.com
Waiting for verification...
Cleaning up challenges
Deploying Certificate to VirtualHost /etc/nginx/conf.d/site1.example.com.conf
Deploying Certificate to VirtualHost /etc/nginx/conf.d/site2.example.com.conf
Redirecting all traffic on port 80 to ssl in /etc/nginx/conf.d/site1.example.com.conf
Redirecting all traffic on port 80 to ssl in /etc/nginx/conf.d/site2.example.com.conf

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations! You have successfully enabled https://site1.example.com and
https://site2.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/site1.example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/site1.example.com/privkey.pem
Your certificate will expire on 2021-05-10. 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

If you have done everything correctly, visiting http://site1.example.com or http://site2.example.com would automatically redirect you to the HTTPS  versions of those sites.

Last step is to ensure that certbot is renews your certificates when the time comes. Add the following line at the end of /etc/crontab file.

0 0 * * * root certbot renew

This will run certbot renew command everyday at midnight, and when the certificates are due for renewal, it will automatically replace the old ones with new ones.

Conclusion

We are all done! You now have two websites configured to take advantage of a single server. If you would like to learn more about using Docker and configuring Ghost CMS head on over this article.