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.
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>
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.
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.
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
.
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.