One front door for every internal tool: a self-hosting pattern
How to run task boards, dashboards, a password vault and more behind one reverse proxy: wildcard DNS, containers under systemd, layered access, safe renames.

Every operations team accumulates small tools. A task board, a dashboard server, a container console, a password vault, a couple of internal web apps somebody wrote. Each one starts life on its own port with its own certificate and its own idea of authentication, and within a year nobody is sure which of them are exposed, which are patched, or which still have a valid certificate. This is the pattern we use to run a whole set of internal tools as one well-kept service: one front door, one way to start things, one access policy.
The shape of the stack
The pattern has four layers, and each has one job:
- DNS: a wildcard name, such as
*.tools.example.com, pointing at one host. - Reverse proxy: Traefik on that host, terminating TLS and routing each hostname to the right container.
- Containers: each tool in its own Podman container on a private container network, with no ports published to the host except the proxy's.
- Supervision: systemd starts, restarts and orders everything, so the stack survives a reboot with no human involved.
With this in place, adding a tool is a short checklist rather than a project: start a container on the network, add a route, done. There is no new firewall hole, no new DNS record and no new certificate request.
DNS: one wildcard, then forget about it
A wildcard record means tasks.tools.example.com, grafana.tools.example.com and vault.tools.example.com all resolve to the proxy host without anyone touching DNS again. Pair it with a wildcard certificate for the same name, and every new tool is born with valid TLS.
Keep the wildcard on a dedicated subdomain. You do not want anything.example.com landing on your tooling host.
The proxy: routes as files you can read
Traefik can discover routes from container labels or from plain configuration files. For a small, long-lived stack we prefer files: they are easy to review, easy to put in version control, and they do not change because someone restarted a container with different labels. A route for a task manager looks like this:
http:
routers:
tasks:
rule: "Host(`tasks.tools.example.com`)"
entryPoints: [websecure]
service: tasks
middlewares: [mgmt-only]
tls: {}
services:
tasks:
loadBalancer:
servers: [{ url: "http://tasks:3456" }]
middlewares:
mgmt-only:
ipAllowList:
sourceRange: ["192.0.2.0/24"]
The middleware is the important part. The proxy is the single place where you decide who may reach a tool, so put the decision there.
Access: two locks, not one
Internal tools have their own logins, and their quality varies. Do not rely on them alone. Layer two controls in front:
- A source allow-list at the proxy, limited to the networks your team actually works from. Expand every CIDR before you write it and read it back afterwards. A mistyped prefix length can turn a single office range into a large part of the internet:
python3 -c "import ipaddress,sys; n=ipaddress.ip_network(sys.argv[1],strict=False); print(n, n[0], '->', n[-1], n.num_addresses)" 192.0.2.0/24
- An authentication middleware (basic auth over TLS at minimum, or forward authentication to your identity provider) for tools that should not be reachable even from inside without a second credential.
The password vault deserves special thought. A Bitwarden-compatible server such as Vaultwarden stores vault contents encrypted by the clients, so the server never holds them in readable form. That is a strong property, but it does not remove the need to restrict who can reach the server.
Containers under systemd
Running containers by hand works until the first reboot. Podman integrates with systemd through Quadlet: you describe the container in a small unit file and systemd treats it like any other service.
# /etc/containers/systemd/tasks.container
[Unit]
Description=Task manager
After=network-online.target
[Container]
Image=docker.io/vikunja/vikunja:<pinned-tag>
Network=tools.network
Volume=/srv/tools/tasks:/app/vikunja/files:Z
[Service]
Restart=always
[Install]
WantedBy=multi-user.target
After systemctl daemon-reload, systemctl start tasks runs it, and it starts on boot from then on. Pin image tags rather than following latest, so an upgrade is a decision you make, with a dump and a rollback ready, rather than something that happens to you at the next restart.
Keep SELinux enforcing. Label each volume for the container that owns it (:Z for a private volume, :z for one that more than one container must share).
Renaming without breaking anything
Sooner or later a tool moves: a new host, a new domain, a tidier name. Keep the old name working by having the proxy answer it with a permanent redirect to the new one. Use 308 Permanent Redirect rather than 301, because a 308 tells clients to repeat the same method and body at the new address.
There is a catch that bites scripts, not browsers. Browsers follow redirects. Many command-line clients and libraries do not, unless told to. A script that sends a POST to the old name gets a 308 back, does not follow it, and moves on. Nothing was created, nothing failed loudly, and if the script only checks that something answered, it reports success. With curl, use -L to follow, and always check the status code and the body:
curl -s -o out.json -w '%{http_code}\n' -X POST https://tasks.tools.example.com/api/v1/...
A response body of the words "Permanent Redirect" where you expected JSON is the tell. When you rename, search your scripts and integrations for the old name and update them, and keep the redirect only as a safety net.
Backups, monitoring and patching
Monitor each route from outside the host for the expected page, not merely for an answer, and patch one tool at a time with the previous image kept until the new one has run for a while.
A checklist for your own tooling
- Can you list every internal tool, its hostname and who may reach it?
- Does any tool publish its own port to the host or the internet, bypassing the proxy?
- Are all your allow-list ranges as narrow as you think? Expand them and look.
- Do your containers come back on their own after a reboot?
- Do your scripts check status codes, or only that something answered?
- When did you last restore your password vault?
One front door, one policy and one way of starting things turns a drawer of loose tools into a service you can reason about.
If you would like your team's tools run this way, on your own instance, our engineers can help. Talk to an engineer
Published 22 October 2026.
All field notes