Turn any service running on http://192.168.x.x:3000 into https://recall.lan — a real, trusted HTTPS domain that works on every device on your network. No public domain required, no Let's Encrypt, no cert files to manage.
Stack: Caddy (reverse proxy + local CA) · Pi-hole or AdGuard Home (local DNS)
Most tutorials put the reverse proxy on a separate machine — a router, a Pi, a dedicated proxy box. This creates a gap:
Your service ──── unencrypted LAN ──── Caddy ──── HTTPS ──── your devices
The first hop travels the network unencrypted. Anyone on your LAN can read it.
Running Caddy on the same machine as the service closes that gap:
Your service ──── localhost ──── Caddy ──── HTTPS ──── your devices
The only unencrypted segment is localhost — it never leaves the machine. This is the correct architecture for a self-hosted service you actually care about securing.
- A Linux server running the service you want to expose (any port)
- Caddy installed on the same machine as the service
- Pi-hole or AdGuard Home on your network (recommended), or a router that allows custom DNS entries
# Debian / Ubuntu
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddyVerify:
caddy version
sudo systemctl status caddyEdit /etc/caddy/Caddyfile:
recall.lan {
tls internal
reverse_proxy localhost:3000
}
Replace recall.lan with whatever domain you want and 3000 with your service's port.
tls internal is the key directive — Caddy generates its own local certificate authority and issues a cert automatically. No files to manage, no expiry to track, no external services contacted.
Multiple services work the same way, just add more blocks:
recall.lan {
tls internal
reverse_proxy localhost:3000
}
grafana.lan {
tls internal
reverse_proxy localhost:3001
}
home.lan {
tls internal
reverse_proxy localhost:8080
}
Reload Caddy after any change:
sudo systemctl reload caddyCheck logs if something doesn't come up:
sudo journalctl -u caddy -n 50Caddy generates a root CA at first startup. You need to install it once on every device that should trust your .lan domains.
Find the cert on your server:
sudo cat /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crtCopy it to your local machine:
scp user@your-server-ip:/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt ~/caddy-root.crtThen install it on each device:
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain ~/caddy-root.crtDouble-click caddy-root.crt → Install Certificate → Local Machine → Place all certificates in the following store → Trusted Root Certification Authorities → Finish.
sudo cp caddy-root.crt /usr/local/share/ca-certificates/caddy-root.crt
sudo update-ca-certificatesTransfer the file to the device, then:
Settings → Security → Install from storage → select the .crt file.
On Android 14+: Settings → Security & Privacy → More security & privacy → Install from storage
- AirDrop
caddy-root.crtto the device (or serve it via a local web server and open it in Safari) - Settings → General → VPN & Device Management → tap the profile → Install
- Settings → General → About → Certificate Trust Settings → toggle full trust for the Caddy certificate
You only do this once per device. All future .lan domains you add will be trusted automatically because they all share the same root CA.
Pick one method.
Local DNS → DNS Records → add:
| Domain | IP |
|---|---|
recall.lan |
your server's LAN IP |
Every device using Pi-hole as its DNS resolver will resolve the domain automatically — no per-device config.
Filters → DNS Rewrites → add recall.lan → your server's LAN IP.
Same result as Pi-hole.
Some routers (pfSense, OPNsense, OpenWRT, UniFi) support custom DNS entries in their DHCP/DNS settings. Check your router's documentation. This works network-wide like Pi-hole without needing a separate DNS server.
Add to /etc/hosts on each device:
192.168.x.x recall.lan
This works but requires manual updates on every device every time you add a new service. Use one of the above options instead if you can.
Open https://recall.lan in your browser. You should see a padlock — cert issued by Caddy Local Authority, no warnings.
Every new service you add is two lines in the Caddyfile and a systemctl reload caddy.
Browser shows "Your connection is not private" The root CA isn't trusted on this device yet. Go back to Step 3.
recall.lan doesn't resolve
DNS isn't set up yet, or your device isn't using the right DNS server. Check Step 4. You can verify with:
nslookup recall.lanCaddy won't start
Check the logs: sudo journalctl -u caddy -n 50. Common cause: another process is on port 80 or 443.
Service unreachable through Caddy but works directly
Confirm the service is actually listening on localhost and not binding to a specific interface. Test with: curl http://localhost:3000