Protect Private Admin Tools With Caddy Basic Auth
Use Caddy basic auth to put a password wall in front of private Docker admin tools without accidentally exposing a dashboard to the internet.
An admin dashboard is not an app you should casually put on a public subdomain.
I have seen this go wrong in the most boring way possible: a useful local tool gets a nice URL, HTTPS is enabled, and everyone feels secure. Then someone discovers that the dashboard has no authentication, weak authentication, or an old image with a bad day. TLS protected the trip to the server. It did not decide who gets in.
Caddy’s basic_auth is a good small lock for private tools: dashboards, internal documentation, a one-person status page, or a temporary admin interface. It is not a replacement for application authentication or a VPN, but it is a practical first boundary when you need one now.
The setup we are protecting
This example puts a tiny internal dashboard behind Caddy. The application stays on a private Docker network. Caddy is the only container publishing ports to the host.
services:
caddy:
image: caddy:2.8.4-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- proxy
admin-tool:
image: louislam/uptime-kuma:1.23.16
container_name: admin-tool
restart: unless-stopped
volumes:
- ./uptime-kuma:/app/data
networks:
- proxy
# No ports: section. Caddy is the entry point.
networks:
proxy:
volumes:
caddy_data:
caddy_config:
Pin versions you have tested. Using latest on an internet-facing reverse proxy is the kind of convenience that turns a routine restart into an incident.
If the tool does not need to be public at all, use a VPN first. WireGuard and Tailscale are better answers for a small trusted group because they remove the login page from the public internet entirely.
Create a password hash, not a plain password
Caddy expects a hashed password in its configuration. Generate it from the Caddy image so the command is repeatable and you never paste a clear-text password into the Caddyfile.
docker run --rm caddy:2.8.4-alpine caddy hash-password --algorithm bcrypt --plaintext 'use-a-long-unique-password'
Copy the output, which starts with $2a$ or $2b$. Treat it as a secret anyway. A hash is safer than plain text, but it still deserves to stay out of screenshots, public repositories, and shell history where possible.
For a server you access from other networks, a VPN is the cleaner boundary. It also keeps your SSH and other management surfaces off café Wi-Fi.
🚀NordVPN
Secure your server with a reliable VPN.
Affiliate link — we may earn a commission at no extra cost to you.
Add the Caddyfile
Create Caddyfile beside your Compose file and replace the domain, username, and hash.
status.example.com {
basic_auth {
admin $2b$12$replace.this.with.your.generated.bcrypt.hash
}
reverse_proxy admin-tool:3001
}
That is enough for a browser login prompt before Caddy proxies any request. Caddy also obtains and renews HTTPS certificates automatically when the DNS record points to this server and ports 80 and 443 are reachable.
Start the stack and watch the proxy logs:
docker compose up -d
docker compose logs --tail=100 caddy
Open https://status.example.com in a private browser window. You should receive a credentials prompt before the application UI appears. Then verify that the app is not reachable directly through a host port:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
Only Caddy should show 0.0.0.0:80->80/tcp and 0.0.0.0:443->443/tcp. If the admin tool shows 3001 on the host, remove that ports: entry and recreate the stack.
The shortcut I do not recommend
It is tempting to give every internal tool the same Caddy login. That is fast, but it makes one leaked password a master key for your homelab.
Use separate credentials or separate subdomains for tools with different risk levels. More importantly, use the application’s own account system when it has one. Caddy basic auth is a front-door check, not identity management, audit logging, MFA, or role-based access control.
For tools that can change infrastructure, I use two boundaries: private network access through a VPN, then the app’s own MFA or strong authentication. A password prompt alone is fine for a low-risk personal dashboard. It is not enough for Portainer, a NAS admin panel, or anything that can destroy data.
Keep credentials out of Git
A Caddyfile containing a bcrypt hash should not become a public repository artifact. The simplest approach is to keep it in a directory outside your project repository and mount it read-only.
volumes:
- /opt/caddy-private/Caddyfile:/etc/caddy/Caddyfile:ro
Another option is a deployment-time template backed by your secret manager. Do not put a clear password in an environment variable just because it feels tidier. Environment variables are often visible in inspection output, process tooling, and backups.
If you change the Caddyfile, validate it before reloading:
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
A short exposure checklist
Before calling an admin tool protected, check the boring things:
- Caddy is the only service with host ports.
- The app has no direct
ports:mapping. - The Caddyfile contains a bcrypt hash, never the plain password.
- The domain has valid HTTPS and the login prompt appears in a private browser session.
- Sensitive tools require a VPN and their own application authentication.
- You update Caddy and the admin tool deliberately, not by surprise.
For broader Docker isolation, pair this with Docker network segmentation and avoid direct Docker socket mounts. A login wall does not help much if the dashboard can already control the whole host.
Is basic auth enough?
For a personal read-only dashboard that must be reachable from the internet, it is a meaningful improvement over exposing it naked. For an infrastructure control panel, no.
Start by removing direct host ports from one low-risk tool, put it behind Caddy basic auth, and confirm it works. Then decide whether the tool actually belongs behind a VPN instead. The best public admin interface is often the one you never made public.
FAQ
Does Caddy basic auth replace the app’s own login?
No. Keep the app’s own authentication enabled. Basic auth adds a gate in front of it; it does not provide user roles, MFA, or useful per-user audit trails.
Can I use this with a local-only service?
Yes. Caddy can proxy to a container name on the same Docker network, as in the example. You do not need to publish the upstream service port to the host.
Why not use a VPN instead?
For an admin interface, I usually would. A VPN reduces the public attack surface much more than a password prompt. Basic auth is useful when a low-risk page needs limited public reachability or while you are migrating an existing setup.
What should I do next?
Audit docker ps for dashboards with published ports. Move one behind Caddy, verify its direct port disappears, and put high-risk tools behind a VPN plus their native authentication.
Stay in the loop 📬
Get self-hosting tutorials, tool reviews, and infrastructure tips delivered to your inbox. No spam, unsubscribe anytime.
Join 0 self-hosters. Free forever.