29 October 2021

In this article I show how you can host multiple websites in containers on a single server. Each containers exposes a different port, which is mapped using a reverse proxy. I am using an AlmaLinux 8 server but the same principles apply to any other Linux distribution.

Create the websites

To start we need some websites. I will use two sites: example.com and example.net.

$ cat /home/example/sites/example.com/index.php
<!DOCTYPE html>
<html lang="en-gb">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>example.com</title>
  </head>

  <body>
    <h1>Hello, World</h1>
    <p>It is currently <?php echo date("h:i"); ?>. All things are moving toward their end, and time is running out!</p>
  </body>
</html>

$ cat /home/example/sites/example.net/index.php
<!DOCTYPE html>
<html lang="en-gb">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>example.net</title>
  </head>

  <body>
    <h1>G'day</h1>
    <p>Your number is <?php echo rand(1, 10); ?>. This could be your Lucky Number, but please note that terms and conditions apply.</p>
  </body>
</html>

Installing Podman and Nginx

You can use either Docker or Podman for the containers. I have a slight preference for Podman, as it avoids running commands as root. However, either will work. If you use Docker, just replace podman with docker in the commands that follow (and run them with root privileges).

On RHEL8-based servers you can install both Podman and Nginx with the following command:

# dnf install podman nginx

The Nginx service won’t be enabled automatically. To start the service and make sure it is always started when the system boots you can use this command:

# systemctl enable --now nginx

Alternatively, use systemctl start if don’t want Nginx to always start at boot.

And it is always a good idea to check the status:

# systemctl is-active nginx
active

If you need a systemctl refresher, my article about managing services on Linux servers covers all the basics.

Update /etc/hosts

I obviously don’t have control over the DNS for example.com and example.net. They are just example domains. To view the websites as they appear on your server you can add the domains to your hosts file:

$ cat /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4 example.com example.net
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6 example.com example.net

Use ping to make sure that the domains now resolve to your localhost:

$ ping -c 3 example.com
PING example.com(localhost (::1)) 56 data bytes
64 bytes from localhost (::1): icmp_seq=1 ttl=64 time=0.078 ms
64 bytes from localhost (::1): icmp_seq=2 ttl=64 time=0.075 ms
64 bytes from localhost (::1): icmp_seq=3 ttl=64 time=0.076 ms

--- example.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2027ms
rtt min/avg/max/mdev = 0.075/0.076/0.078/0.007 ms

Obviously, you can skip this step if you do have control over the DNS for your domains.

Run the containers

Next we need to containerise the two websites. For this article I will just pull the php-apache Docker image and spin up two containers (no need for a Dockerfile):

$ podman pull php:7.4.25-apache-bullseye
...
$ podman run \
-d \
-p 8080:80 \
--name example.com \
--volume /home/example/sites/example.com/:/var/www/html:Z \
php:7.4.25-apache-bullseye

$ podman run \
-d \
-p 8081:80 \
--name example.net \
--volume /home/example/sites/example.net/:/var/www/html:Z \
php:7.4.25-apache-bullseye

There are two things to note here. Firstly, example.com uses port 8080 and example.net uses port 8081. I want both to be accessible over port 80, which is where the Nginx reverse proxy comes in.

Secondly, I appended :Z to the --volume argument. This gives the website directories on the server the correct SELinux context (container_file_t). That is not going to be sufficient for this example, as Nginx expects the httpd_t context. There are workarounds, but they are beyond the scope of this article. If you are following along on a RHEL-based server then you probably want to temporary disable SELinux using setenforce 0. You can enable SELinux again with setenforce 1.

Configure Nginx

There are two common ways to configure multiple websites in Nginx. The Debian/Ubuntu way is to use configuration files for individual virtual hosts in /etc/nginx/sites-available and to then create a symbolic links in /etc/nginx/sites-enabled. On RHEL-based servers you don’t get these directories by default. Instead, you can put individual configuration files in /etc/nginx/conf.d. The directory is included via the main configuration file (/etc/nginx/nginx.conf). I personally prefer the latter approach, simply because storing custom configuration files in a conf.d directory is the standard approach on Linux servers. There is no superior way of doing things though – either approach is perfectly fine.

There are lots of options for Nginx config files. For this article I just want Nginx to act as reverse proxy that sends requests for example.com to localhost:8080 and requests for example.com requests for example.net to localhost:8081. The following configuration files do just that:

# cat /etc/nginx/conf.d/example.com.conf
server {
  listen 80;
  server_name example.com;

  location / {
    proxy_pass http://localhost:8080;
  }
}

# cat /etc/nginx/conf.d/example.net.conf
server {
  listen 80;
  server_name example.net;

  location / {
    proxy_pass http://localhost:8081;
  }
}

For each domain you use server_name to define the domain and proxy_pass to map the container. Again, this is a very basic example. I have left out security headers, caching option and many other common directives related to proxies. I just want to show the bare minimum you need to get a reverse proxy working. There is much more information about Nginx reverse proxies in the official documentation.

Next, you can remove the server block from the main /etc/nginx/nginx.conf file. Please do make a copy of the original file first – that way you have something to revert to if everything breaks:

# cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf_$(date +"%Y%m%d%H%M%S")

For this tutorial I use the below nginx.conf file. Note that the custom .conf files I created are included on the last line of the file:

# cat /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

}

And finally, here I check the syntax of the configuration file and reload the Nginx service:

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

# systemctl reload nginx

And that’s it! You should now be able to view the two websites without specifying port 8080 or 8081. If you don’t have a graphical environment then you can use curl or lynx to view the websites:

$ lynx -dump example.com
                                  Hello, World

   It is currently 12:22. All things are moving toward their end, and time
   is running out!

$ lynx -dump example.net
                                     G'day

   Your number is 5. This could be your Lucky Number, but please note that
   terms and conditions apply.