Caddy reverse proxy in front of Foodvore for LAN-only access
A short Caddyfile that gives you HTTPS on your LAN without exposing anything to the internet.
Why bother
Foodvore (when launched with the optional --companion flag on a desktop or Pi) exposes a small web companion at http://0.0.0.0:7891. It’s plain HTTP, no authentication, no TLS. Fine if it never leaves localhost. Less fine if you want to access it from your phone on the LAN.
We put a Caddy in front of it. Caddy gets you:
- TLS on the LAN, with a cert from a private CA or Tailscale.
- A stable hostname instead of
192.168.1.42:7891. - Basic auth for the LAN-but-shared case.
- Logs that aren’t on Foodvore’s stdout.
Total config: about 30 lines.
Setup
Foodvore companion runs on the host as a systemd service. Caddy runs in Docker so it can manage its own data dir cleanly.
# /etc/systemd/system/foodvore-companion.service
[Unit]
Description=Foodvore Companion
After=network.target
[Service]
ExecStart=/usr/local/bin/foodvore-companion --port 7891 --bind 127.0.0.1
Restart=on-failure
User=foodvore
[Install]
WantedBy=multi-user.target
Bind to 127.0.0.1 so Foodvore is unreachable from the LAN directly. Caddy is the only thing the LAN sees.
Caddyfile
foodvore.lan.example.org {
encode gzip zstd
basic_auth {
# generate with: caddy hash-password
admin $2a$14$REPLACE_ME_WITH_REAL_HASH
}
reverse_proxy 127.0.0.1:7891 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
flush_interval -1
}
log {
output file /var/log/caddy/foodvore.log {
roll_size 5MB
roll_keep 10
}
format json
}
header {
Strict-Transport-Security "max-age=31536000"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
Three things worth flagging:
flush_interval -1disables Caddy’s response buffering. Foodvore streams its food-search responses, and buffering visibly slows the search-as-you-type experience.basic_authis fine for LAN. If you’re crossing the public internet (you shouldn’t be — use Tailscale), use Caddy’s@autheliastyle integration instead.X-Frame-Options DENYbecause nothing should be embedding your tracker.
TLS
Two paths.
Tailscale path. Run tailscale cert foodvore.tail-scale.example.ts.net on the host. Mount the resulting .crt and .key into the Caddy container and reference them with tls /etc/caddy/foodvore.crt /etc/caddy/foodvore.key.
Private CA path. Generate a CA with openssl or step-ca, sign a leaf for foodvore.lan.example.org, install the CA on every device that will use it. More setup, but less external dependency.
If your LAN actually resolves foodvore.lan.example.org to your host (via dnsmasq or your router’s local zones), Caddy can also try ZeroSSL/Let’s Encrypt with the DNS-01 challenge using a real domain you control — see Caddy’s docs.
Why not nginx
Personal preference. nginx with TLS, basic auth, and gzip needs roughly 80 lines of config to do the same thing. Caddy needs 30. The fewer YAML-shaped things in your config, the fewer things break in three months.
We have an nginx + certbot guide for people who prefer that path.
What we don’t do
We don’t expose this to the public internet. The Foodvore companion is not designed for hostile traffic. The codebase has had two sql-injection-adjacent issues in 2024 that the maintainer fixed inside a week, but the surface area is small enough that we’d rather just keep the whole thing on the LAN.
For mobile access from outside the LAN, see Tailscale for self-hosted nutrition.
Verifying
curl -k https://foodvore.lan.example.org/healthz
# 401 with HTTP/2 + basic-auth challenge
curl -k -u admin:correct-password https://foodvore.lan.example.org/healthz
# 200 OK
Log into Caddy’s logs:
docker compose logs caddy | tail -50
You’ll see the bypassed basic-auth attempt and the successful one with separate request IDs.
References
- Caddy: caddyserver.com/docs
- Foodvore companion mode: linked in the project README (varies by fork)
- TLS for self-hosted nutrition apps
- Tailscale for self-hosted nutrition