A container is a minimalist, isolated user-space environment that runs at the operating system level and shares system resources with other instances. Containers are designed to provide a portable and consistent runtime environment for applications, while consuming less resources than a traditional server or virtual machine. This allows for an overall better use of computing resources in multi-component, distributed applications, and highly-available systems.
Unlike virtual machines, which are typically built on top of computer-emulated hardware and run fully isolated operating systems, containers share resources from the host such as the kernel and the filesystem, which results in a smaller footprint.
Docker, a popular open source containerization tool, was largely instrumental in spurring the adoption of component-based application design with self-contained micro services. While other containerizing systems exist, Docker became popular for providing a more accessible and comprehensive interface around the technology.
To learn more about containers, you can refer to the following resources:
Use promo code DOCS10 for $10 credit on a new account.
Introduction
LXD (pronounced “Lex-Dee”) is a system container manager build on top of Linux Containers (LXC) supported by Canonical. The goal of LXD is to provide an experience similar to a virtual machine but through containerization rather than hardware virtualization. Compared to Docker for delivering applications, LXD offers nearly full operating-system functionality with additional features such as snapshots, live migrations, and storage management.
A reverse proxy is a server that sits between internal applications and external clients, forwarding client requests to the appropriate server. While many common applications, such as Node.js, are able to function as servers on their own, they may lack a number of advanced load balancing, security, and acceleration features.
This guide explains the creation of a reverse proxy in an LXD container in order to host multiple websites, each in their own additional containers. You will utilize NGINX and Apache web servers, while also relying on NGINX as a reverse proxy.
Please refer to the following diagram to understand the reverse proxy created in this guide.
In this guide you will:
Note
For simplicity, the term container is used throughout this guide to describe the LXD system containers.
Before You Begin
Complete A Beginner’s Guide to LXD: Setting Up an Apache Web Server In a Container. The guide instructs you to create a container called web with the Apache web server for testing purposes. Remove this container by running the following commands.
lxc stop web
lxc delete web
Note
This guide will use the hostnames apache1.example.com and nginx1.example.com for the two example websites. Replace these names with hostnames you own and setup their DNS entries to point them to the IP address of the server you created. For help with DNS see our DNS Manager Guide.
Creating the Containers
Create two containers called apache1 and nginx1, one with the Apache web server and another with the NGINX web server, respectively. For any additional websites, you may create new containers with your chosen web server software.
There are three containers, all in the RUNNING state – each with their own private IP address. Take note of the IP addresses (both IPv4 and IPv6) for the container proxy. You will need them to configure the proxy container in a later section.
Now that the containers have been created, the following steps will detail how to set up the web server software in the apache1 and nginx1 containers, and the proxy container so that the web servers are accessible from the internet.
Configuring the Apache Web Server Container
When using a reverse proxy in front of a web server, the web server does not know the IP addresses of visitors. The web server only sees the IP address of the reverse proxy. However, each web server has a way to identify the real remote IP address of a visitor. For Apache, this is performed with the Remote IP Apache module. For the module to work, the reverse proxy must be configured to pass the remote IP address’ information.
Start a shell in the apache1 container.
lxc exec apache1 -- sudo --user ubuntu --login
Update the package list in the apache1 container.
sudo apt update
Install the package apache2 in the container.
sudo apt install -y apache2
Create the file /etc/apache2/conf-available/remoteip.conf.
You can use the nano text editor by running the command sudo nano /etc/apache2/conf-available/remoteip.conf. Note, these are the IP addresses of the proxy container shown earlier, for both IPv4 and IPv6. Replace these with the IPs from your lxc list output.
Note
Instead of specifying the IP addresses, you can also use the hostname proxy.lxd. However, the RemoteIP Apache module is peculiar when using the hostname and will use only one of the two IP addresses (either IPv4 or IPv6), which means the Apache web server will not know the real source IP address for some connections. By listing explicitly both IPv4 and IPv6 addresses, you can be certain that RemoteIP will successfully accept the source IP information from all connections of the reverse proxy.
Enable the new remoteip.conf configuration.
sudo a2enconf remoteip
Enabling conf remoteip.
To activate the new configuration, you need to run:
systemctl reload apache2
Enable the remoteip Apache module.
sudo a2enmod remoteip
Enabling module remoteip.
To activate the new configuration, you need to run:
systemctl restart apache2
Edit the default web page for Apache to make a reference that it runs inside a LXD container.
sudo nano /var/www/html/index.html
Change the line “It works!” (line number 224) to “It works inside a LXD container!” Save and exit.
Restart the Apache web server.
sudo systemctl reload apache2
Exit back to the host.
exit
You have created and configured the Apache web server, but the server is not yet accessible from the Internet. It will become accessible after you configure the proxy container in a later section.
Creating the NGINX Web Server Container
Like Apache, NGINX does not know the IP addresses of visitors when using a reverse proxy in front of a web server. It only sees the IP address of the reverse proxy instead. Each NGINX web server software can identify the real remote IP address of a visitor with the Real IP module. For the module to work, the reverse proxy must be configured accordingly to pass the information regarding the remote IP addresses.
You can use the nano text editor by running the command sudo nano /etc/nginx/conf.d/real-ip.conf.
Note
You have specified the hostname of the reverse proxy, proxy.lxd. Each LXD container gets automatically a hostname, which is the name of the container plus the suffix .lxd. By specifying the set_real_ip_from field with proxy.lxd, you are instructing the NGINX web server to accept the real IP address information for each connection, as long as that connection originates from proxy.lxd. The real IP address information will be found in the HTTP header X-Real-IP in each connection.
Edit the default web page for NGINX to make a reference that it runs inside a LXD container.
sudo nano /var/www/html/index.nginx-debian.html
Change the line “Welcome to nginx!” (line number 14) to “Welcome to nginx running in a LXD system container!”. Save and exit.
Restart the NGINX web server.
sudo systemctl reload nginx
Exit back to the host.
exit
You have created and configured the NGINX web server, but the server is not accessible yet from the Internet. It will become accessible after you configure the proxy container in the next section.
Setting up the Reverse Proxy
In this section you will configure the container proxy. You will install NGINX and set it up as a reverse proxy, then add the appropriate LXD proxy device in order to expose both ports 80 and 443 to the internet.
Add LXD proxy devices to redirect connections from the internet to ports 80 (HTTP) and 443 (HTTPS) on the server to the respective ports at the proxy container.
Device myport80 added to proxy
Device myport443 added to proxy
The lxc config device add command takes as arguments:
Argument
Explanation
proxy
The name of the container.
myport80
A name for this proxy device.
proxy
The type of the LXD device (LXD proxy device).
listen=tcp:0.0.0.0:80
The proxy device will listen on the host (default) on port 80, protocol TCP, on all interfaces.
connect=tcp:127.0.0.1:80
The proxy device will connect to the container on port 80, protocol TCP, on the loopback interface. In previous versions of LXD you could have specified localhost here. However, in LXD 3.13 or newer, you can only specify IP addresses.
proxy_protocol=true
Request to enable the PROXY protocol so that the reverse proxy will get the originating IP address from the proxy device.
Note
If you want to remove a proxy device, use lxc config device remove. If you want to remove the above device myport80, run the following command:
lxc config device remove proxy myport80
Where proxy is the name of the container, and myport80 is the name of the device.
Start a shell in the proxy container.
lxc exec proxy -- sudo --user ubuntu --login
Update the package list.
sudo apt update
Install NGINX in the container.
sudo apt install -y nginx
Logout from the container.
logout
Direct Traffic to the Apache Web Server From the Reverse Proxy
The reverse proxy container is running and the NGINX package has been installed. To work as a reverse proxy, add the appropriate website configuration so that NGINX can identify (with server_name below) the appropriate hostname, and then pass (with proxy_pass below) the connection to the appropriate LXD container.
Start a shell in the proxy container.
lxc exec proxy -- sudo --user ubuntu --login
Create the file apache1.example.com in /etc/nginx/sites-available/ for the configuration of your first website.
You can run sudo nano /etc/nginx/sites-available/apache1.example.com to open up a text editor and add the configuration. Note, in this case you only need to edit the server_name to be the hostname of the website.
Restart the NGINX reverse proxy. By restarting the service, NGINX will read and apply the new site instructions just added to /etc/nginx/sites-enabled.
sudo systemctl reload nginx
Exit the proxy container and return back to the host.
logout
From your local computer, visit the URL of your website with your web browser. You should see the default Apache page:
Note
If you look at the Apache access.log file (default file /var/log/apache2/access.log), it will still show the private IP address of the proxy container instead of the real IP address. This issue is specific to the Apache web server and has to do with how the server prints the logs. Other software on the web server will be able to use the real IP. To fix this through the Apache logs, see the section Troubleshooting.
Direct Traffic to the NGINX Web Server From the Reverse Proxy
The reverse proxy container is running and the NGINX package has been installed. To work as a reverse proxy, you will add the appropriate website configuration so NGINX can identify (with server_name below) the appropriate hostname, and then pass (with proxy_pass below) the connection to the appropriate LXD container with the actual web server software.
Start a shell in the proxy container.
lxc exec proxy -- sudo --user ubuntu --login
Create the file nginx1.example.com in /etc/nginx/sites-available/ for the configuration of your second website.
You can run sudo nano /etc/nginx/sites-available/nginx1.example.com to create the configuration. Note, you only need to edit the fields server_name to be the hostname of the website.
Exit the proxy container and return back to the host.
logout
From your local computer, visit the URL of your website with your web browser. You should see the following default NGINX page.
Adding Support for HTTPS with Let’s Encrypt
Start a shell in the proxy container.
lxc exec proxy -- sudo --user ubuntu --login
Add the repository ppa:certbot/certbot by running the following command.
sudo add-apt-repository ppa:certbot/certbot
Output will look similar to the following.
This is the PPA for packages prepared by Debian Let's Encrypt Team and backported for Ubuntu(s).
More info: https://launchpad.net/~certbot/+archive/ubuntu/certbot
Press [ENTER] to continue or Ctrl-c to cancel adding it.
Get:1 http://security.ubuntu.com/ubuntu bionic-security InRelease [88.7 kB]
...
Fetched 3360 kB in 2s (2018 kB/s)
Reading package lists... Done
Install the following two packages to a) support the creation of Let’s Encrypt certificates; and b) auto-configure the NGINX reverse proxy to use Let’s Encrypt certificates. The packages are pulled from the newly-created repository.
sudo apt-get install certbot python-certbot-nginx
Note
This configures the reverse proxy to also act as a TLS Termination Proxy. Any HTTPS configuration is only found in the proxy container. By doing so, it is not necessary to perform any tasks inside the web server containers relating to certificates and Let’s Encrypt.
Run certbot as root with the --nginx parameter in order to perform the auto-configuration of Let’s Encrypt for the first website. You will be asked to supply a valid email address for urgent renewal and security notices. You will then be asked to accept the Terms of Service and whether you would like to be contacted by the Electronic Frontier Foundation in the future. Next, you will provide the website for which you are activating HTTPS. Finally, you can choose to set up a facility that automatically redirects HTTP connections to HTTPS connections.
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): myemail@example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing 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
Which names would you like to activate HTTPS for?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: apache1.example.com
2: nginx1.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): 1
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for apache1.example.com
Waiting for verification...
Cleaning up challenges
Deploying Certificate to VirtualHost /etc/nginx/sites-enabled/apache1.example.com
Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
Redirecting all traffic on port 80 to ssl in /etc/nginx/sites-enabled/apache1.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations! You have successfully enabled https://apache1.example.com
You should test your configuration at:
https://www.ssllabs.com/ssltest/analyze.html?d=apache1.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/apache1.example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/apache1.example.com/privkey.pem
Your cert will expire on 2019-10-07. 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"
- Your account credentials have been saved in your Certbot
configuration directory at /etc/letsencrypt. You should make a
secure backup of this folder now. This configuration directory will
also contain certificates and private keys obtained by Certbot so
making regular backups of this folder is ideal.
- 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
Run certbot as root with the --nginx parameter in order to perform the auto-configuration of Let’s Encrypt for the second website. This is the second time we run certbot, therefore we are asked directly to select the website to configure.
sudo certbot --nginx
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator nginx, Installer nginx
Which names would you like to activate HTTPS for?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: apache1.example.com
2: nginx1.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): 2
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for nginx1.example.com
Waiting for verification...
Cleaning up challenges
Deploying Certificate to VirtualHost /etc/nginx/sites-enabled/nginx1.example.com
Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
Redirecting all traffic on port 80 to ssl in /etc/nginx/sites-enabled/nginx1.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations! You have successfully enabled https://nginx1.example.com
You should test your configuration at:
https://www.ssllabs.com/ssltest/analyze.html?d=nginx1.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/nginx1.example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/nginx1.example.com/privkey.pem
Your cert will expire on 2019-10-07. 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
After adding all websites, perform a dry run in order to test the renewal of the certificates. Check that all websites are updating successfully to ensure the automated facility will update the certificates without further effort.
sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/apache1.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator nginx, Installer nginx
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for apache1.example.com
Waiting for verification...
Cleaning up challenges
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed with reload of nginx server; fullchain is
/etc/letsencrypt/live/apache1.example.com/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/nginx1.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator nginx, Installer nginx
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for nginx1.example.com
Waiting for verification...
Cleaning up challenges
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed with reload of nginx server; fullchain is
/etc/letsencrypt/live/nginx1.example.com/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
** (The test certificates below have not been saved.)
Congratulations, all renewals succeeded. The following certs have been renewed:
/etc/letsencrypt/live/apache1.example.com/fullchain.pem (success)
/etc/letsencrypt/live/nginx1.example.com/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
** (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
IMPORTANT NOTES:
- Your account credentials have been saved in your Certbot
configuration directory at /etc/letsencrypt. You should make a
secure backup of this folder now. This configuration directory will
also contain certificates and private keys obtained by Certbot so
making regular backups of this folder is ideal.
Note
The certbot package adds a systemd timer in order to activate the automated renewal of Let’s Encrypt certificates. You can view the details of this timer by running systemctl list-timers.
The certbot tool edits and changes the NGINX configuration files of your websites. In doing so, certbot does not obey initial listen directive (listen 80 proxy_protocol;) and does not add the proxy_protocol parameter to the newly added listen 443 ssl; lines. You must edit the configuration files for each website and append “proxy_protocol” to each “listen 443 ssl;” line.
listen 443 ssl proxy_protocol; # managed by Certbot
listen [::]:443 ssl proxy_protocol; # managed by Certbot
Note
Each website configuration file has two pairs of listen directives: HTTP and HTTPS, respectively. The first is the original pair for HTTP that was added in a previous section. The second pair was added by certbot for HTTPS. These are pairs because they they cover both IPv4 and IPv6. The notation [::] refers to IPv6. When adding the parameter proxy_protocol, add it before the ; on each line as shown above.
Restart NGINX.
sudo systemctl restart nginx
Troubleshooting
Browser Error “SSL_ERROR_RX_RECORD_TOO_LONG”
You have configured Certbot and created the appropriate Let’s Encrypt configuration for each website. But when you access the website from your browser, you get the following error.
Secure Connection Failed
An error occurred during a connection to apache1.example.com. SSL received a record that exceeded the maximum permissible length. Error code: SSL_ERROR_RX_RECORD_TOO_LONG
The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.
Please contact the website owners to inform them of this problem.
This error is caused when the NGINX reverse proxy in the proxy container does not have the proxy_protocol parameter in the listen 443 directives. Without the parameter, the reverse proxy does not consume the PROXY protocol information before it performs the HTTPS work. It mistakenly passes the PROXY protocol information to the HTTPS module, hence the record too long error.
Follow the instructions in the previous section and add proxy_protocol to all listen 443 directives. Finally, restart NGINX.
Error “Unable to connect” or “This site can’t be reached”
When you attempt to connect to the website from your local computer and receive Unable to connect or This site can’t be reached errors, it is likely the proxy devices have not been configured.
Run the following command on the host to verify whether LXD is listening and is able to accept connections to ports 80 (HTTP) and 443 (HTTPS).
sudo ss -ltp '( sport = :http || sport = :https )'
Note
The ss command is similar to netstat and lsof. It shows information about network connections. In this case, we use it to verify whether there is a service on ports 80 and 443, and which service it is. * -l, to display the listening sockets, * -t, to display only TCP sockets, * -p, to show which processes use those sockets, * ( sport = :http || sport = :https ), to show only ports 80 and 443 (HTTP and HTTPS, respectively).
In the following output we can verify that both ports 80 and 443 (HTTP and HTTPS, respectively) are in the LISTEN state. In the last column we verify that the process listening is lxd itself.
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:http *:* users:(("lxd",pid=1301,fd=7),("lxd",pid=1301,fd=5))
LISTEN 0 128 *:https *:* users:(("lxd",pid=1349,fd=7),("lxd",pid=1349,fd=5))
If you see a process listed other than lxd, stop that service and restart the proxy container. By restarting the proxy container, LXD will apply the proxy devices again.
The Apache access.log Shows the IP Address of the Proxy Container
You have set up the apache1 container and verified that it is accessible from the internet. But the logs at /var/log/apache2/access.log still show the private IP address of the proxy container, either the private IPv4 (10.x.x.x) or the private IPv6 addresses. What went wrong?
The default log formats for printing access logs in Apache only print the IP address of the host of the last hop (i.e. the proxy server). This is the %h format specifier as shown below.
Run the following command in the apache1 container to edit the configuration file httpd.conf and perform the change from %h to %a.
sudo nano /etc/apache2/apache2.conf
Reload the Apache web server service.
sudo systemctl reload apache2
Next Steps
You have set up a reverse proxy to host many websites on the same server and installed each website in a separate container. You can install static or dynamic websites in the containers. For dynamic websites, you may need additional configuration; check the respective documentation for setup using a reverse proxy. In addition, you may also use NGINX as a reverse proxy for non-HTTP(S) services.
More Information
You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.
Find answers, ask questions, and help others.
This guide is published under a CC BY-ND 4.0 license.
Use promo code DOCS10 for $10 credit on a new account.
Kubernetes and Docker
Kubernetes is a system that automates the deployment, scaling, and management of containerized applications. Containerizing an application requires a base image that can be used to create an instance of a container. Once an application’s image exists, you can push it to a centralized container registry that Kubernetes can use to deploy container instances in a cluster’s pods.
While Kubernetes supports several container runtimes, Docker is a very popular choice. Docker images are created using a Dockerfile that contains all commands, in their required order of execution, needed to build a given image. For example, a Dockerfile might contain instructions to install a specific operating system referencing another image, install an application’s dependencies, and execute configuration commands in the running container.
Docker Hub is a centralized container image registry that can host your images and make them available for sharing and deployment. You can also find and use official Docker images and vendor specific images. When combined with a remote version control service, like GitHub, Docker Hub allows you to automate building container images and trigger actions for further automation with other services and tooling.
Scope of This Guide
This guide will show you how to package a Hugo static site in a Docker container image, host the image on Docker Hub, and deploy the container image on a Kubernetes cluster running on Linode. This example, is meant to demonstrate how applications can be containerized using Docker to leverage the deployment and scaling power of Kubernetes.
Hugo is written in Go and is known for being extremely fast to compile sites, even very large ones. It is well-supported, well-documented, and has an active community. Some useful Hugo features include shortcodes, which are an easy way to include predefined templates inside of your Markdown, and built-in LiveReload web server, which allows you to preview your site changes locally as you make them.
Note
This guide was written using version 1.14 of Kubectl.
Before You Begin
Create a Kubernetes cluster with one worker node. This can be done in two ways:
Deploy a Kubernetes cluster using kubeadm.
You will need to deploy two Linodes. One will serve as the master node and the other will serve as a worker node.
Deploy a Kubernetes cluster using k8s-alpha CLI.
Create a GitHub account if you don’t already have one.
Create a Docker Hub account if you don’t already have one.
Set up the Development Environment
Development of your Hugo site and Docker image will take place locally on your personal computer. You will need to install Hugo, Docker CE, and Git, a version control software, on your personal computer to get started.
Use the How to Install Git on Linux, Mac or Windows guide for the steps needed to install Git.
Install Hugo. Hugo’s official documentation contains more information on installation methods, like Installing Hugo from Tarball. Below are installation instructions for common operating systems:
Debian/Ubuntu:
sudo apt-get install hugo
Fedora, Red Hat and CentOS:
sudo dnf install hugo
Mac, using Homebrew:
brew install hugo
These steps install Docker Community Edition (CE) using the official Ubuntu repositories. To install on another distribution, see the official installation page.
Remove any older installations of Docker that may be on your system:
sudo apt remove docker docker-engine docker.io
Make sure you have the necessary packages to allow the use of Docker’s repository:
For Ubuntu 19.04 if you get an E: Package 'docker-ce' has no installation candidate error this is because the stable version of docker for is not yet available. Therefore, you will need to use the edge / test repository.
Add your limited Linux user account to the docker group:
sudo usermod -aG docker $USER
Note
After entering the usermod command, you will need to close your SSH session and open a new one for this change to take effect.
Check that the installation was successful by running the built-in “Hello World” program:
docker run hello-world
Create a Hugo Site
Initialize the Hugo Site
In this section you will use the Hugo CLI (command line interface) to create your Hugo site and initialize a Hugo theme. Hugo’s CLI provides several useful commands for common tasks needed to build, configure, and interact with your Hugo site.
Create a new Hugo site on your local computer. This command will create a folder named example-site and scaffold Hugo’s directory structure inside it:
hugo new site example-site
Move into your Hugo site’s root directory:
cd example-site
You will use Git to add a theme to your Hugo site’s directory. Initialize your Hugo site’s directory as a Git repository:
git init
Install the Ananke theme as a submodule of your Hugo site’s Git repository. Git submodules allow one Git repository to be stored as a subdirectory of another Git repository, while still being able to maintain each repository’s version control information separately. The Ananke theme’s repository will be located in the ~/example-site/themes/ananke directory of your Hugo site.
Hugo has many available themes that can be installed as a submodule of your Hugo site’s directory.
Add the theme to your Hugo site’s configuration file. The configuration file (config.toml) is located at the root of your Hugo site’s directory.
echo 'theme = "ananke"' >> config.toml
Add Content to the Hugo Site
You can now begin to add content to your Hugo site. In this section you will add a new post to your Hugo site and generate the corresponding static file by building the Hugo site on your local computer.
Create a new content file for your site. This command will generate a Markdown file with an auto-populated date and title:
hugo new posts/my-first-post.md
You should see a similar output. Note that the file is located in the content/posts/ directory of your Hugo site:
/home/username/example-site/content/posts/my-first-post.md created
Open the Markdown file in the text editor of your choice to begin modifying its content; you can copy and paste the example snippet into your file, which contains an updated front matter section at the top and some example Markdown body text.
Set your desired value for title. Then, set the draft state to false and add your content below the --- in Markdown syntax, if desired:
---
title: "My First Post"
date: 2019-05-07T11:25:11-04:00
draft: false
---
# Kubernetes Objects
In Kubernetes, there are a number of objects that are abstractions of your Kubernetes system’s desired state. These objects represent your application, its networking, and disk resources – all of which together form your application. Kubernetes objects can describe:
- Which containerized applications are running on the cluster
- Application resources
- Policies that should be applied to the application
About front matter
Front matter is a collection of metadata about your content, and it is embedded at the top of your file within opening and closing --- delimiters.
Front matter is a powerful Hugo feature that provides a mechanism for passing data that is attached to a specific piece of content to Hugo’s rendering engine. Hugo accepts front matter in TOML, YAML, and JSON formats. In the example snippet, there is YAML front matter for the title, date, and draft state of the Markdown file. These variables will be referenced and displayed by your Hugo theme.
Once you have added your content, you can preview your changes by building and serving the site using Hugo’s built-in webserver:
hugo server
You will see a similar output:
                   | EN
+------------------+----+
Pages              | 11
Paginator pages    | 0
Non-page files     | 0
Static files       | 3
Processed images   | 0
Aliases            | 1
Sitemaps           | 1
Cleaned            | 0
Total in 7 ms
Watching for changes in /home/username/example-site/{content,data,layouts,static,themes}
Watching for config changes in /home/username/example-site/config.toml
Serving pages from memory
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop
The output will provide a URL to preview your site. Copy and paste the URL into a browser to access the site. In the above example Hugo’s web server URL is http://localhost:1313/.
When you are happy with your site’s content you can build the site:
hugo -v
Hugo will generate your site’s static HTML files and store them in a public directory that it will create inside your project. The static files that are generated by Hugo are the files that will be served to the internet through your Kubernetes cluster.
View the contents of your site’s public directory:
ls public
Your output should resemble the following example. When you built the site, the Markdown file you created and edited in steps 6 and 7 was used to generate its corresponding static HTML file in the public/posts/my-first-post/index.html directory.
The example Hugo site was initialized as a local Git repository in the previous section. You can now version control all content, theme, and configuration files with Git. Once you have used Git to track your local Hugo site files, you can easily push them to a remote Git repository, like GitHub or GitLab. Storing your Hugo site files on a remote Git repository opens up many possibilities for collaboration and automating Docker image builds. This guide will not cover automated builds, but you can learn more about it on Docker’s official documentation.
Add a .gitignore file to your Git repository. Any files or directories added to the .gitignore file will not be tracked by Git. The Docker image you will create in the next section will handle building your static site files. For this reason it is not necessary to track the public directory and its content.
echo 'public/' >> .gitignore
Display the state of your current working directory (root of your Hugo site):
git status
Stage all your files to be committed:
git add -A
Commit all your changes and add a meaningful commit message:
git commit -m 'Add content, theme, and config files.'
Note
Any time you complete work related to one logical change to the Hugo site, you should make sure you commit the changes to your Git repository. Keeping your commits attached to small changes makes it easier to understand the changes and to roll back to previous commits, if necessary. See the Getting Started with Git guide for more information.
Create a Docker Image
Create the Dockerfile
A Dockerfile contains the steps needed to build a Docker image. The Docker image provides the minimum set up and configuration necessary to deploy a container that satisfies its specific use case. The Hugo site’s minimum Docker container configuration requirements are an operating system, Hugo, the Hugo site’s content files, and the NGINX web server.
In your Hugo site’s root directory, create and open a file named Dockerfile using the text editor of your choice. Add the following content to the file. You can read the Dockerfile comments to learn what each command will execute in the Docker container.
#Install the container's OS.
FROM ubuntu:latest as HUGOINSTALL
# Install Hugo.
RUN apt-get update
RUN apt-get install hugo
# Copy the contents of the current working directory to the hugo-site# directory. The directory will be created if it doesn't exist.
COPY . /hugo-site
# Use Hugo to build the static site files.
RUN hugo -v --source=/hugo-site --destination=/hugo-site/public
# Install NGINX and deactivate NGINX's default index.html file.# Move the static site files to NGINX's html directory.# This directory is where the static site files will be served from by NGINX.
FROM nginx:stable-alpine
RUN mv /usr/share/nginx/html/index.html /usr/share/nginx/html/old-index.html
COPY --from=HUGOINSTALL /hugo-site/public/ /usr/share/nginx/html/
# The container will listen on port 80 using the TCP protocol.
EXPOSE 80
Add a .dockerignore file to your Hugo repository. It is important to ensure that your images are as small as possible to reduce the time it takes to build, pull, push, and deploy the container. The .dockerignore file excludes files and directories that are not necessary for the function of your container or that may contain sensitive information that you do not want to included in the image. Since the Docker image will build the static Hugo site files, you can ignore the public/ directory. You can also exclude any Git related files and directories because they are not needed on the running container.
Follow the steps 2 – 4 in the Version Control the Site with Git section to add any new files created in this section to your local git repository.
Build the Docker Image
You are now ready to build the Docker image. When Docker builds an image it incorporates the build context. A build context includes any files and directories located in the current working directory. By default, Docker assumes the current working directory is also the location of the Dockerfile.
Note
If you have not yet created a Docker Hub account, you will need to do so before proceeding with this section.
Build the Docker image and add a tag mydockerhubusername/hugo-site:v1 to the image. Ensure you are in the root directory of your Hugo site. The tag will make it easy to reference a specific image version when creating your Kubernetes deployment manifest. Replace mydockerhubusername with your Docker Hub username and hugo-site with a Docker repository name you prefer.
You should see a similar output. The entirety of the output has been removed for brevity:
Sending build context to Docker daemon 3.307MB
Step 1/10 : FROM ubuntu:latest as HUGOINSTALL
---> 94e814e2efa8
Step 2/10 : ENV HUGO_VERSION=0.55.4
---> Using cache
---> e651df397e32
...
Successfully built 50c590837916
Successfully tagged hugo-k8s:v1
View all locally available Docker images:
docker images
You should see the docker image hugo-site:v1 listed in the output:
REPOSITORY TAG IMAGE ID CREATED SIZE
hugo-k8s v1 50c590837916 1 day ago 16.5MB
Push your Hugo Site Repository to GitHub
You can push your local Hugo site’s Git repository to GitHub in order to set up Docker automated builds. Docker automated builds will build an image using a external repository as the build context and automatically push the image to your Docker Hub repository. This step is not necessary to complete this guide.
Host your Image on Docker Hub
Hosting your Hugo site’s image on Docker Hub will enable you to use the image in a Kubernetes cluster deployment. You will also be able to share the image with collaborators and the rest of the Docker community.
Log into your Docker Hub account via the command line on your local computer. Enter your username and password when prompted.
docker login
Push the local Docker image to Docker Hub. Replace mydockerhubusername/hugo-site:v1 with your image’s tag name.
docker push mydockerhubusername/hugo-site:v1
Navigate to Docker Hub to view your image on your account.
The url for your image repository should be similar to the following: https://cloud.docker.com/repository/docker/mydockerhubusername/hugo-site. Replace the username and repository name with your own.
Configure your Kubernetes Cluster
This section will use kubectl to configure and manage your Kubernetes cluster. If your cluster was deployed using kubeadm, you will need to log into your master node to execute the kubectl commands in this section. If, instead, you used the k8s-alpha CLI you can run all commands from your local computer.
In this section, you will create namespace, deployment, and service manifest files for your Hugo site deployment and apply them to your cluster with kubectl. Each manifest file creates different resources on the Kubernetes API that are used to create and the Hugo site’s pods on the worker nodes.
Create the Namespace
Namespaces provide a powerful way to logically partition your Kubernetes cluster and isolate components and resources to avoid collisions across the cluster. A common use-case is to encapsulate dev/testing/production environments with namespaces so that they can each utilize the same resource names across each stage of development.
Namespaces add a layer of complexity to a cluster that may not always be necessary. It is important to keep this in mind when formulating the architecture for a project’s application. This example will create a namespace for demonstration purposes, but it is not a requirement. One situation where a namespace would be beneficial, in the context of this guide, would be if you were a developer and wanted to manage Hugo sites for several clients with a single Kubernetes cluster.
Create a directory to store your Hugo site’s manifest files.
mkdir -p clientx/k8s-hugo/
Create the manifest file for your Hugo site’s namespace with the following content:
The manifest file declares the version of the API in use, the kind of resource that is being defined, and metadata about the resource. All manifest files should provide this information.
The key-value pair name: hugo-site defines the namespace object’s unique name.
Create the namespace from the ns-hugo-site.yaml manifest.
You should see the hugo-site namespace listed in the output:
NAME STATUS AGE
default Active 1d
hugo-site Active 1d
kube-public Active 1d
kube-system Active 1d
Create the Service
The service will group together all pods for the Hugo site, expose the same port on all pods to the internet, and load balance site traffic between all pods. It is best to create a service prior to any controllers (like a deployment) so that the Kubernetes scheduler can distribute the pods for the service as they are created by the controller.
The Hugo site’s service manifest file will use the NodePort method to get external traffic to the Hugo site service. NodePort opens a specific port on all the Nodes and any traffic that is sent to this port is forwarded to the service. Kubernetes will choose the port to open on the nodes if you do not provide one in your service manifest file. It is recommended to let Kubernetes handle the assignment. Kubernetes will choose a port in the default range, 30000-32767.
Note
The k8s-alpha CLI creates clusters that are pre-configured with useful Linode service integrations, like the Linode Cloud Controller Manager (CCM) which provides access to Linode’s load balancer service, NodeBalancers. In order to use Linode’s NodeBalancers you can use the LoadBalancer service type instead of NodePort in your Hugo site’s service manifest file. For more details, see the Kubernetes Cloud Controller Manager for Linode GitHub repository.
Create the manifest file for your service with the following content.
The spec key defines the Hugo site service object’s desired behavior. It will create a service that exposes TCP port 80 on any pod with the app: hugo-site label.
The exposed container port is defined by the targetPort:80 key-value pair.
View the service and its corresponding information:
kubectl get services -n hugo-site
Your output will resemble the following:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
hugo-site NodePort 10.108.110.6 80:30304/TCP 1d
Create the Deployment
A deployment is a controller that helps manage the state of your pods. The Hugo site deployment will define how many pods should be kept up and running with the Hugo site service and which container image should be used.
Create the manifest file for your Hugo site’s deployment. Copy the following contents to your file.
The deployment’s object spec states that the deployment should have 3 replica pods. This means at any given time the cluster will have 3 pods that run the Hugo site service.
The template field provides all the information needed to create actual pods.
The label app: hugo-site helps the deployment know which service pods to target.
The container field states that any containers connected to this deployment should use the Hugo site image mydockerhubusername/hugo-site:v1 that was created in the Build the Docker Image section of this guide.
imagePullPolicy: Always means that the container image will be pulled every time the pod is started.
containerPort: 80 states the port number to expose on the pod’s IP address. The system does not rely on this field to expose the container port, instead, it provides information about the network connections a container uses.
NAME READY UP-TO-DATE AVAILABLE AGE
hugo-site 3/3 3 3 1d
View the Hugo Site
After creating all required manifest files to configure your Hugo site’s Kubernetes cluster, you should be able to view the site using a worker node’s IP address and its exposed port.
Get your worker node’s external IP address. Copy down the EXTERNAL-IP value for any worker node in the cluster:
kubectl get nodes -o wide
Access the hugo-site services to view its exposed port.
kubectl get svc -n hugo-site
The output will resemble the following. Copy down the listed port number in the 30000-32767 range.
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
hugo-site NodePort 10.108.110.6 80:30304/TCP 1d
Open a browser window and enter in a worker node’s IP address and exposed port. An example url to your Hugo site would be, http://192.0.2.1:30304. Your Hugo site should appear.
If desired, you can purchase a domain name and use Linode’s DNS Manager to assign a domain name to the cluster’s worker node IP address.
Tear Down Your Cluster
To avoid being further billed for your Kubernetes cluster, tear down your cluster’s Linodes. If you have Linodes that existed for only part a monthly billing cycle, you’ll be billed at the hourly rate for that service. See How Hourly Billing Works to learn more.
Next Steps
Now that you are familiar with basic Kubernetes concepts, like configuring pods, grouping resources, and deploying services, you can deploy a Kubernetes cluster on Linode for production use by using the steps in the following guides:
More Information
You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.
Find answers, ask questions, and help others.
This guide is published under a CC BY-ND 4.0 license.