Having worked out how to handle TLS traffic on my K3S setup, it is time to achieve the same goal on Docker. In this guide, I’ll show you how to set up a Raspberry Pi running Docker Swarm, SWAG (Secure Web Application Gateway), and Let’s Encrypt to secure your containerized applications with free TLS certificates. While I’ll use Jellyfin as an example, this approach works for most containerized applications.
Getting started
Prerequisites
- A Raspberry Pi with Docker and Docker Swarm.
- A public domain managed, for example, by Cloudflare.
- Containerized applications, such as Jellyfin, running on Docker.
- SWAG to handle reverse proxy and TLS certificates.
Folder structure
All my Docker containers data is stored on BTRFS volume under /opt/my_pool
where I have created a folder for each container and the nas-stack.yml
file with all the Docker instructions.
/mnt/my_pool/docker
├── jellyfin
├── nas-stack.yml
└── swag
Docker Swarm
I opted for Docker Swarm instead of Docker Compose primarily for its better handling of secrets. With Docker Swarm, secrets are securely stored and only made available inside the container at runtime, mounted at /run/secrets/<mysecret>
. This ensures that sensitive information, like passwords or API keys, is never exposed in clear text on the host.
In contrast, Docker Compose often requires bind-mounting secrets, which means they are stored in clear text on the host filesystem, making them more vulnerable if the host is compromised.
Initiate the Docker cluster
To initiate a Docker Swarm on your system, use the following command:
docker swarm init
This command sets up the current machine as the Swarm manager.
Create a Secret
To add a secret to your Docker Swarm, use the following command:
echo "your_secret_value" | docker secret create my_secret_name -
Personally, I don’t like this approach because your secret is stored in your bash history, but unfortunately it’s necessary because you can get trailing space problems with other methods. So I suggest you remove the line from your history with history -d <line_number>
.
The other approach, which I will come to later, is to import the whole file as a secret:
docker secret create my_secret_name /path/to/your/secret_file
Again, the secret is in plain text on the file system, so if you don’t need it, delete the file!
SWAG
SWAG (Secure Web Application Gateway) is a Docker container that simplifies setting up a secure reverse proxy with automatic SSL/TLS certificates from Let’s Encrypt. A key benefit of SWAG is that it comes with pre-built TLS configurations for many popular containerized applications, such as Jellyfin, Nextcloud, and more. This means you can quickly secure your applications without needing to manually configure HTTPS. SWAG also handles certificate renewal and provides a robust Nginx-based reverse proxy to ensure your web traffic is encrypted and secure.
I decided to include all my containers on the same stack and use a single overlay network for all of them.
DNS validation and secrets
To get a TLS certificate from Let’s Encrypt you need to prove that you own the domain, I use DNS challenge as a validation method (more here), this implies that you need to store the API token in a configuration file to update the TXT record at your DNS provider (in my case Cloudflare).
Each provider has its own configuration file, stored in swag/config/dns-conf/
, this is the default configuration for Cloudflare:
# Instructions: https://github.com/certbot/certbot/blob/master/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py#L20
# Replace with your values
# With global api key:
#dns_cloudflare_email = cloudflare@example.com
#dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234567
# With token (comment out both lines above and uncomment below):
dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567
The strongly recommended authentication method is to use API token rather than Global API key as you can grant more granular permissions. In this case only Edit: Zone.DNS is required.
As explained above, the secret will be in plain text on your host server, to avoid this I have decided to save the whole file as docker secret and delete it from the filesystem:
# create a Docker secret
docker secret cloudflare.ini swag/config/dns-conf/cloudflare.ini
# delete host file
rm swag/config/dns-conf/cloudflare.ini
We will reference the secret in the nas-stack.yml
file with the following code:
secrets:
cloudflare.ini:
external: true
Reverse proxy
SWAG contains Nginx acting as a reverse proxy for managing incoming web traffic. The developers, to simplify our lives, have already prepared configuration files for popular apps and services such as Jellyfin. These configuration files are hosted on GitHub and are automatically pulled into the /swag/config/nginx/proxy-confs
folder as inactive sample files (jellyfin.subdomain.conf.sample
), ready to be customized and activated as needed.
In my case the file works out of the box and we just need to rename it with the command cp jellyfin.subdomain.conf.sample jellyfin.subdomain.conf
, but in some scenarios it is necessary to edit it according to your needs.
For example, in the Jellyfin configuration file you will find the following help:
# make sure that your jellyfin container is named jellyfin
# make sure that your dns has a cname set for jellyfin
# if jellyfin is running in bridge mode and the container is named "jellyfin", the below config should work as is
# if not, replace the line "set $upstream_app jellyfin;" with "set $upstream_app <containername>;"
# or "set $upstream_app <HOSTIP>;" for host mode, HOSTIP being the IP address of jellyfin
# in jellyfin settings, under "Advanced/Networking" add subdomain.mydomain.tld as a known proxy
Deploy the stack
As mentioned earlier, the nas-stack.yml
file contains all the instructions for deploying the Docker stack on the Swarm cluster. If we split it, we have:
- The
secrets
section, which contains the Cloudflare API token used by the SWAG service to generate TLS certificates. - The
services
section, which defines the containers (such as Jellyfin and SWAG) with their configurations, such as images, volumes and ports. - The overlay
networks
, where containers communicate internally and some services are exposed to the home network for external access.
version: "3"
secrets:
cloudflare.ini:
external: true
services:
swag:
image: lscr.io/linuxserver/swag:latest
cap_add:
- NET_ADMIN
hostname: swag-container
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- URL=example.com
- VALIDATION=dns
- SUBDOMAINS=jellyfin
- CERTPROVIDER=
- DNSPLUGIN=cloudflare
- EMAIL=mail@example.com
- ONLY_SUBDOMAINS=true
secrets:
- source: cloudflare.ini
target: /config/dns-conf/cloudflare.ini
volumes:
- /mnt/my_pool/docker/swag/config:/config
ports:
- target: 443
published: 443
protocol: tcp
mode: host
networks:
- swarm-network
deploy:
mode: global
labels:
- com.centurylinklabs.watchtower.enable=true
jellyfin:
image: jellyfin/jellyfin
hostname: jellyfin
volumes:
- /mnt/my_pool/docker/jellyfin/config:/config
- /mnt/my_pool/docker/jellyfin/cache:/cache
- type: bind
source: /mnt/my_pool/media
target: /media
user: "1000:1000"
networks:
- swarm-network
deploy:
mode: global
networks:
swarm-network:
driver: overlay
The SWAG container has some environment variables that need to be set to generate the TLS certificates:
URL
- The web address of the domain. This is used both for generating the TLS certificate and for DNS validation.SUBDOMAINS
- List all the subdomains you want your TLS certificate to cover, or use wildcard to cover them all. This will result in a certificate for*.example.com
.VALIDATION
- Specifies the method the container will use to confirm your ownership of the domain. We will use DNS validation.DNSPLUGIN
- Here I’ve selected Cloudflare as my DNS provider, which will handle the DNS validation.EMAIL
- Enter your email address to receive your free Let’s Encrypt SSL certificate for your domain.
The list of environment variables is available in the official SWAG documentation.
Also the Jellyfin container has its own configurations, but I will not cover them as it is outside the scope of this article, refer to the official documentation of the container application you need to use the appropriate settings.
The last step is to deploy the cluster and enjoy your container with a shiny free TLS certificate:
docker stack deploy -c nas-stack.yml nas
Troubleshooting
Here are some useful command to troubleshoot your cluster environment if things goes wrong:
# Get stack services status
docker service ls
# Get container logs
docker service logs <service-name>
Credits
I would like to thanks for inspiring me to write this article: