Convention Over Configuration: Adding a Service With Zero Router Labels
No Host(...) rule appears anywhere in the traefik-selfhosted repo — the hostname for every service is derived from the container name by Traefik's defaultRule, and that one convention is most of what makes adding a service cheap.
grep -r "Host(" traefik-selfhosted/ returns nothing. Every service on nissaar.com gets a working hostname without a single Host(...) router rule written anywhere in the repo.
One convention — container name maps to subdomain — replaces a router rule per service.
What normally goes into a Traefik label
The textbook way to expose a container through Traefik's Docker provider is a label like:
labels:
- traefik.http.routers.myapp.rule=Host(`myapp.example.com`)That's a reasonable default, and it's what most Traefik documentation shows first. It also means every new service needs someone to type the hostname correctly, keep it consistent with whatever naming scheme the rest of the fleet uses, and not typo the domain.
What defaultRule does instead
Traefik's static configuration accepts a defaultRule template on the Docker provider, evaluated against each discovered container using Go's templating with the container's own metadata available as fields. On vmi2633427 that template effectively reduces to: take the container's name, append .nissaar.com, and route there. So this is the entire router configuration needed for a new service:
services:
whoami:
image: traefik/whoami
container_name: whoami
networks: [frontend]
labels:
- traefik.enable=true
- traefik.http.routers.whoami.middlewares=default@file
networks:
frontend:
external: truetraefik.enable=true is the only thing that flips this container from invisible to routed. The hostname whoami.nissaar.com falls out of the container name automatically — no router rule, no hostname to remember to update, no place for it to drift out of sync with what the container is actually called.
The one line that still needs to be explicit
The middlewares label is not optional in practice, even though the rule itself is implicit: middlewares=default@file attaches the shared chain — security headers, identity-header stripping, and custom error pages — defined once in data/dynamic-route/middlewares.yml via the file provider. Convention handles routing; middleware attachment is still a deliberate, per-service line, and that split is intentional. Getting a hostname for free is low-risk. Getting security headers for free, silently, for a container someone forgot needed different treatment, is not something you want implicit.
What convention doesn't cover
defaultRule derives the route, not the DNS record. Each new hostname still needs a proxied A/CNAME entry created by hand in Cloudflare — Traefik has no way to create DNS records, and nothing in this stack tries to make it do so. That split surfaced its own gap: traefik.nissaar.com, the host referenced in the dashboard's ping and api router labels, was never actually given a DNS record, so the dashboard and healthcheck endpoint are unreachable despite being fully configured on the Traefik side. The convention makes the routing half of "add a service" essentially free; it does nothing to remind you about the DNS half, which is exactly the kind of thing that goes unnoticed until you go looking for it.
Why this is worth the implicitness
The trade being made here is legibility of an individual label versus the cost of typing (and eventually drifting) a hostname per service. For a homelab-scale deployment where every service already follows <name>.nissaar.com, the convention removes a whole category of copy-paste mistake — the router rule that still says Host(old-service-name.nissaar.com) after a rename. Fewer places for a hostname to be written down is fewer places for it to be wrong.
Series: The Self-Hosting Stack. Next: the two-command smoke test that runs before any real restart.