···21212222This is the single most important reason I had for migrating away from NGINX.
23232424-In a homelab environment where services are being spun up/down, created, moved between hosts, renamed...having to 1) remember to update NGINX config and then restart it 2) keep track of IP:PORT or specific config per application is a pain. NGINX was designed during a time when network topology wasn't so dynamic, it's not built with a homelab use-case in mind. It's also not built with today's paradigms in mind such as [first-class environmental substitution](https://www.baeldung.com/linux/nginx-config-environment-variables) or [easy-to-read config validation.](https://github.com/dvershinin/gixy)
2424+In a homelab environment where services are being spun up/down, created, moved between hosts, renamed, etc...having to 1) remember to update NGINX config and then restart it and 2) keep track of IP:PORT or specific config per application is a pain. NGINX was designed during a time when network topology wasn't so dynamic, so it's not built with a homelab use-case in mind. It's also not built with today's paradigms in mind, such as [first-class environmental substitution](https://www.baeldung.com/linux/nginx-config-environment-variables) or [easy-to-read config validation.](https://github.com/dvershinin/gixy)
25252626##### NGINX Service Configuration Ownership
27272828-As my homelab continues to grow I have gravitated towards each Stack owning its own configuration. Through mechanisms like `environment` in `compose.yaml` or configs committed to git alongside `compose.yaml` etc.. having the service-as-code live next to all the data needed run the service makes it more portable and reduces the cognitive scope required to configure it. NGINX config files need to be physically accessible to it and that is not a *feasible* option when services run on other hosts.
2828+As my homelab continues to grow I have gravitated towards each Stack owning its own configuration. Through mechanisms like `environment` in `compose.yaml` or configs committed to git alongside `compose.yaml` etc., having the service-as-code live next to all the data needed run the service makes it more portable and reduces the cognitive scope required to configure it. NGINX config files need to be physically accessible to it and that is not a *feasible* option when services run on other hosts.
29293030##### SWAG is Tightly Coupled and Opinionated
31313232-LSIO does an excellent job making setup with NGINX easy by using [SWAG](https://docs.linuxserver.io/general/swag/). For simple setups and users just dipping their toes into the space it's a fantastic tool for getting started quickly without requiring any hand holding.
3232+LSIO does an excellent job making setup with NGINX easy by using [SWAG](https://docs.linuxserver.io/general/swag/). For simple setups and users just dipping their toes into the space it's a fantastic tool for getting started quickly without requiring any hand-holding.
33333434-However, it has shortfalls which appear for more complex use-cases. Some of these are limitations of nginx such as needing the user to [edit .ini files for DNS ACME challenge while other solutions only need ENVs](http://localhost:4000/posts/migrating-to-traefik/#wildcards).
3434+However, it has shortfalls which appear for more complex use-cases. Some of these are limitations of nginx, such as needing the user to [edit .ini files for DNS ACME challenge, while other solutions only need ENVs](http://localhost:4000/posts/migrating-to-traefik/#wildcards).
35353636-Others are due to the reality of limited developer-hours needing to fulfill only the most common use-case, like LSIO's [cloudflare docker mod](https://github.com/linuxserver/docker-mods/tree/universal-cloudflared) configuration only working with one domain even though a tunnel can be used for multiple domains -- one domain is the most common use-case and easiest to script for. In this scenario "fixing" the problem means refactoring the entire stack to remove universal-cloudflare and implementing your own `cloudflared` container.
3636+Others are due to the reality of limited developer-hours needing to fulfill only the most common use-case, like LSIO's [cloudflare docker mod](https://github.com/linuxserver/docker-mods/tree/universal-cloudflared) configuration only working with one domain even though a tunnel can be used for multiple domains -- one domain is the most common use-case and easiest to script for. In this scenario, "fixing" the problem means refactoring the entire stack to remove universal-cloudflare and implementing your own `cloudflared` container.
37373838-If most scenarios end with the user having to implement the decoupled solution anyways...why not consider other reverse proxy solutions since we aren't tied to SWAG anymore?
3838+If most scenarios end with the user having to implement the decoupled solution anyway...why not consider other reverse proxy solutions since we aren't tied to SWAG anymore?
39394040##### Lack of Dashboard
4141···47474848SWAG offers the docker mod [swag-auto-proxy](https://github.com/linuxserver/docker-mods/tree/swag-auto-proxy) which generates nginx confs for services discovered by docker label on the same machine NGINX is running on. I forked this as [swag-auto-proxy-multi](https://github.com/FoxxMD/docker-mods/tree/swag-auto-proxy-multi) and wrote new functionality to enable it to work with multiple hosts using [docker-socket-proxy](https://docs.linuxserver.io/images/docker-socket-proxy/).
49495050-While this does work it's not *good.* The inner workings are a mess of bash scripts that generate nginx confs and reload *the entire application* every time new changes are made. It's also barebones compared to applications like Traefik that have [service discovery as a first-class feature.](https://traefik.io/glossary/service-discovery/)
5050+While this does work, it's not *good.* The inner workings are a mess of bash scripts that generate nginx confs and reload *the entire application* every time new changes are made. It's also barebones compared to applications like Traefik that have [service discovery as a first-class feature.](https://traefik.io/glossary/service-discovery/)
51515252### Requirements/Spec
53535454Before YOLO'ing another reverse-proxy solution I came up with a list of requirements that needed to met. I have 60+ stacks, over 100 containers, running on 7 machines.
55555656-If I am going to switch and do all the work to get most of these served then the new solution was going to have to 1) have **feature parity** with SWAG + features I use with it (auth) and 2) be **easier** to implement with all my machines than the current swag + auto-proxy setup.
5656+If I am going to switch and do all the work to get most of these served, then the new solution was going to have to 1) have **feature parity** with SWAG + features I use with it (auth) and 2) be **easier** to implement with all my machines than the current swag + auto-proxy setup.
57575858The requirements:
5959···85858686I eventually settled on [Traefik](https://traefik.io/) after due-diligence gave me enough confidence to think I could satisfy all of the [Requirement](#requirementsspec).
87878888-The actual implementation is always more difficult than how it looks on paper but in the end it met every requirement and ended up being easy to maintain and work with!
8888+The actual implementation is always more difficult than how it looks on paper, but in the end, it met every requirement and ended up being easy to maintain and work with!
89899090In each section below I lightly cover the differences between SWAG/Traefik, what was needed to migrate to Traefik, and how I implemented it along with examples, if necessary.
9191···280280281281Traefik requires setting up a [cert resolver and entrypoint](https://doc.traefik.io/traefik/https/acme/#configuration-examples) which can be done via file or as labels on a docker container (not recommended).
282282283283-One advantage Traefik has is that it will [automatically generate certs](https://doc.traefik.io/traefik/https/acme/#domain-definition) for **all domains** found during service discovery. That is, if you have the label
283283+One advantage Traefik has is that it will [automatically generate certs](https://doc.traefik.io/traefik/https/acme/#domain-definition) for **all domains** found during service discovery (that is, if you have the label)
284284285285```yaml
286286labels:
···304304305305#### Wildcards
306306307307-But this is where Traefik really shines. To use wildcard certs with Traefik we add a few more lines to our existing static config, specifying the dns challenge provider and explicitly defining the domains:
307307+But this is where Traefik really shines. To use wildcard certs with Traefik, we add a few more lines to our existing static config, specifying the dns challenge provider and explicitly defining the domains:
308308309309```diff
310310entryPoints
···366366367367#### Cloudflare Tunnels {#certs-and-cf-tunnels}
368368369369-When using [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) no cert generation/management is required.
369369+When using [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), no cert generation/management is required.
370370371371<details markdown="1">
372372···451451452452* Separating access logs from regular Traefik logs
453453 * Troubleshooting traefik-specific issues from logs is much easier (less noise in container logs)
454454- * Write to file persists access logs after restart
454454+ * Write to file persists the access logs after restart
455455* Exposing/using `log-tail` container enables
456456 * [traefik log acquistion](https://docs.crowdsec.net/docs/next/log_processor/data_sources/docker) to be done with a docker connection locally or remotely, and by container name. Instead of needing to mount log folders/files into a crowdsec container (locally only) and having to deal with permissions.
457457 * access logs are consumable/viewable in other apps (when using `json` access log format, readable in [Dozzle](https://dozzle.dev/) or Logdy)
···732732733733### Cloudflare Tunnels Integration
734734735735-CF Tunnels setup is markedly different than SWAG but functionally the same once setup.
735735+CF Tunnels setup is markedly different than SWAG but functionally the same once set up.
736736737737For CF Tunnels with SWAG there are two [LSIO docker mods](https://docs.linuxserver.io/general/container-customization/#docker-mods) that are used:
738738···825825826826Finally, we need to configure traefik to substitute the value of the header `Cf-Connecting-IP` CF Tunnel attaches to our traffic into the `X-Forwarded-For` header. This will ensure that logs/metrics and downstream applications see the IP of the actual origin host rather than CF's edge server IPs.
827827828828-To this we first install the [traefik plugin](https://doc.traefik.io/traefik/plugins/) [cloudflarewarp](https://github.com/PseudoResonance/cloudflarewarp) by defining it in our **static config**:
828828+To do this we first install the [traefik plugin](https://doc.traefik.io/traefik/plugins/) [cloudflarewarp](https://github.com/PseudoResonance/cloudflarewarp) by defining it in our **static config**:
829829830830```yaml
831831# add this to the /etc/traefik/traefik.yaml example above
···853853```
854854{: file="/config/dynamic/global.yaml" }
855855856856-To use with the entrypoint we setup earlier in our static config add `entryPoints.cf.http.middlewares` with our middlename@provider:
856856+To use with the entrypoint we setup earlier in our static config add `entryPoints.cf.http.middlewares` with our `middleware@provider`:
857857858858```yaml
859859# ... building on previous static config
···960960service:
961961 # ...
962962 authentik-proxy:
963963- image: ghcr.io/goauthentik/proxy:${AUTHENTIK_TAG:-2024.10.5}
963963+ image: ghcr.io/goauthentik/proxy:${AUTHENTIK_TAG:-2024.10.5}are
964964 # ...
965965 environment:
966966- AUTHENTIK_HOST: https://your-authentik.tld
···992992993993Finally, the easy part! In [Authentik's Traefik guide](https://docs.goauthentik.io/docs/add-secure-apps/providers/proxy/server_traefik) setup the authentik middleware using the **Standalone traefik** sample for `http.middlewares.authentik` in a [dyanamic file config](https://doc.traefik.io/traefik/providers/file/), which creates the middleware `authentik@file`.
994994995995-Alternatively, if you included all the labels from the **docker-compose** sample for `authentik-proxy` then it is already setup (`traefik.http.middlewares.authentik.forwardauth.*`) and can be used with the middleware `authentik-proxy@docker`.
995995+Alternatively, if you included all the labels from the **docker-compose** sample for `authentik-proxy`, then it is already setup (`traefik.http.middlewares.authentik.forwardauth.*`) and can be used with the middleware `authentik-proxy@docker`.
996996997997### Service Discovery
998998···1072107210731073#### Multi-Host Docker Discovery
1074107410751075-If you have multiple machines running Docker and want Traefik to route to all of them you have a few choices.
10751075+If you have multiple machines running Docker and want Traefik to route to all of them, you have a few choices.
1076107610771077-##### Dynamic files and Docker Discovery {#multi-host-files}
10771077+##### Dynamic files and Docker Discovery {#multi-host-files}are
1078107810791079-Use [**Dynamic files**](#nginx-equivalent) if it's only one or two, unchanging services. Using this approach as well as the [docker provider](#docker-discovery) for services on the same machine is *doable* if the "other host" services are exceptions/unchanging and can be reached over the bridge network (host IP:PORT).
10791079+Use [**Dynamic files**](#nginx-equivalent) if it's only one or two unchanging services. Using this approach as well as the [docker provider](#docker-discovery) for services on the same machine is *doable* if the "other host" services are exceptions/unchanging and can be reached over the bridge network (host IP:PORT).
1080108010811081##### Docker Swarm
10821082···11361136```
11371137{: file="/etc/traefik/traefik.yaml"}
1138113811391139-Next, on each docker host create a new stack for kop. I prefer to connect it to docker using [docker-socket-proxy](https://docs.linuxserver.io/images/docker-socket-proxy) since it only needs limited, read-only capabilities.
11391139+Next, on each docker host, create a new stack for kop. I prefer to connect it to docker using [docker-socket-proxy](https://docs.linuxserver.io/images/docker-socket-proxy) since it only needs limited, read-only capabilities.
1140114011411141```yaml
11421142services:
···1171117111721172**Watch out for port used with traefik!**
1173117311741174-If you are using **host/bridge IP** for the container then the port must be published and the port for traefik must be the "external" port...
11741174+If you are using **host/bridge IP** for the container, then the port must be published and the port for traefik must be the "external" port...
1175117511761176```yaml
11771177services:
···1750175017511751___
1752175217531753-## Footnotes17531753+## Footnotes