I often talk about self-hosting on this blog, and I'm certainly a big fan of being able to control my own data and systems wherever possible (and feasible). I've recently switched from using Nginx to Traefik as a reverse proxy for my server and for terminating TLS connections.
In this post I'll talk a little about why and how I made this change.
My (until recent) setup
I self-host a number of services; including Nextcloud for file storage and sync, Gitea for git syncing, FreshRSS for RSS feed aggregation and management, Monica for relationship organisation, and a few other things too.
To date, I've mostly relied on Nginx for providing a reverse proxy, enabling me to run many of these services on a single VPS. I run everything, including Nginx, using Docker containers, which I find much more convenient and easy to manage.
Individual services are managed using Docker Compose, which allows me to keep services that rely on multiple containers (such as those that depend on both a web server and a backend database server) logically separated and organised. I use a single
nginx.conf file, used by my Nginx container, to manage these services as virtual hosts and for configuring TLS certificates.
Why I wanted a change
This setup works well and has served me well for several years. However, it isn't without its drawbacks;
- The basic system relies on manual and semi-tedious config entries to my
nginx.conf, including HTTP -> HTTPS redirects, enumerating virtual hosts, and other deeper Nginx stuff I don't always fully understand.
- TLS certificate provisioning and renewal: I either need to do this manually or remember to setup (and then trust) a cron job to renew things properly.
- Domain verification for Let's Encrypt: configuring webroots and interception of ACME requests for each virtual host.
- Needing to restart (or automate the restart) of the webserver after receiving new certificates.
Most of these pain points are related to the reverse proxy setup itself, rather than the individual services (which can happily run indefinitely). To me, it felt like the webserver side of things should be the most "boring" and least time-consuming, when in reality it is the opposite.
I wanted to find a new solution that would help me solve these issues and allow me to focus more on maintaining and working on the services themselves.
Traefik as a reverse proxy
Traefik provides a proxy system that I've used before in Kubernetes setups. It acts as a reverse proxy, TLS terminator, load balancer, and much more. It's designed to work well with microservices running in containers, and whilst Nginx can certainly achieve all of this too, the nice thing about Traefik is that it does this for you dynamically and automatically.
It can easily be configured using Docker Compose, and all the TLS provisioning is handled automatically via Docker labels. The documentation covers the setup well and in detail, but below I will run through (roughly) how I made the switch from Nginx to Traefik.
Step 1: Firstly, stop all of your running services
If you use Compose, you can simply run
docker-compose down to take your services down. At the very least, make sure you are shutting down your Nginx container.
Note: If your user is not in the
docker group you may need to use
sudo to run
Step 2: Next, write a new Docker Compose file
Create a new directory to house your Traefik setup, and in here write a new
docker-compose.yml file, as shown below. In this example I am using Traefik to route requests through to Nextcloud and FreshRSS instances:
version: '3' services: # The Traefik service reverse-proxy: image: traefik:v2.6 command: # Tell Traefik that we're working in a Docker environment # This allows it to dynamically pull out the configuration - "--providers.docker" # Standard ports for HTTP/S - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" # ACME TLS certificate resolver setup. Remember to change your email address here - "--firstname.lastname@example.org" - "--certificatesresolvers.myresolver.acme.storage=/etc/traefik/acme/acme.json" - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web" ports: # Map our web traffic ports to the host network - "80:80" - "443:443" volumes: # Mounting this volume is important to ensure certificates can be persisted - ./acme:/etc/traefik/acme # This volume is to enable Traefik to communicate with Docker - /var/run/docker.sock:/var/run/docker.sock # The FreshRSS service # (see https://github.com/FreshRSS/FreshRSS/tree/edge/Docker for more info) freshrss: image: freshrss/freshrss environment: - "CRON_MIN=3,33" - "TZ=Europe/London" volumes: - ./freshrss_data:/var/www/FreshRSS/data - ./freshrss_extensions:/var/www/FreshRSS/extensions labels: # Here we add a label to tell Traefik how to route to this service by domain # Remember to change this host value. - traefik.http.routers.freshrss.rule=Host(`rss.example.com`) # These two labels tell Traefik to setup TLS for this service - traefik.http.routers.freshrss.tls=true - traefik.http.routers.freshrss.tls.certresolver=myresolver restart: always # The Nextcloud service # (see https://hub.docker.com/_/nextcloud for more info) nextclouddb: image: mariadb:10.5 command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW restart: always volumes: - ./nextcloud-data/db:/var/lib/mysql environment: # Remember to change these values: - MYSQL_ROOT_PASSWORD=<PASSWORD> - MYSQL_PASSWORD=<PASSWORD> - MYSQL_DATABASE=nextcloud - MYSQL_USER=nextcloud nextcloud: image: nextcloud:22 volumes: - ./nextcloud-data/storage:/var/www/html restart: always labels: # Here we add a label to tell Traefik how to route to this service by domain # Remember to change this host value. - traefik.http.routers.nextcloud.rule=Host(`nextcloud.example.com`) # These two labels tell Traefik to setup TLS for this service - traefik.http.routers.nextcloud.tls=true - traefik.http.routers.nextcloud.tls.certresolver=myresolver
The above may seem quite verbose, but this is literally all you need. After running
docker-compose up -d (and you've changed your domains and passwords, etc.) your services will be up and running with TLS certificates automatically provisioned.
I've added comments to the file to explain some of the concepts further, but below I'll add a few extra notes:
- I've setup four services: the Traefik reverse proxy itself, a
freshrsscontainer, and two containers for Nextcloud:
nextcloud(the software itself), and
nextclouddb(the MariaDB database for Nextcloud).
nextclouddbdoesn't need to (and shouldn't, for security) be exposed to the internet, we do not add any Traefik routing labels to this service.
- Adding a mount for the
acme.jsonstorage is important in your Traefik setup to avoid hitting Let's Encrypt rate limits.
- I added volumes for FreshRSS and Nextcloud so that data can be persisted.
- Notice how all Traefik routing and TLS configuration on services is simply handled with labels.
Whilst the above setup achieves what we need, it is simple. There is much more you can do, such as route using specific paths and manage load balancing. For more information, please see the documentation.
More complex services
As mentioned earlier, I also run Gitea as a service on my VPS. Gitea serves a GitHub-style web UI in addition to a git endpoint. Since I interact with the git endpoints over SSH, this service ends up relying on two ingress points: one for HTTP web traffic and another for SSH git traffic.
When I first set this up, Traefik was routing web traffic through to Gitea's SSH port, which obviously caused problems. As such, I needed to add extra configuration to tell Traefik which port to use, as described below:
... gitea: image: gitea/gitea:latest restart: always environment: - USER_UID=1000 - USER_GID=1000 volumes: - ./gitea_data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro expose: - "3000" ports: # Ensure we map port 22 to the host for incoming SSH git traffic - "22:22" labels: # The below three labels are as described earlier - traefik.http.routers.gitea.rule=Host(`git.example.com`) - traefik.http.routers.gitea.tls=true - traefik.http.routers.gitea.tls.certresolver=myresolver # Add a fourth label to tell Traefik where to route web traffic to - traefik.http.services.gitea.loadbalancer.server.port=3000
docker-compose up -d again, your Gitea instance will be running, along with its web UI and git endpoint.
Note: if you use port 22 for standard SSH to your host server, you can map a different port for your SSH git traffic. For example,
Having made this switch, I find things much more logically organised and robust. The reverse proxy is just as performant (at least, for my use) as Nginx and I get extra peace of mind in that I can trust Traefik to handle web traffic, routing, and certificate renewals without any manual intervention.