Skip to content

Reverse proxy and HTTPS

Lantern serves plain HTTP on port 3000. Something in front of it has to give it HTTPS: a reverse proxy with a certificate, such as Traefik or Caddy, or Tailscale for a household that keeps Lantern private. This guide sets up each of them. Installing Lantern Home is the rest of an install.

Over plain HTTP, browsers turn off what a family hub depends on:

  • the service worker behind offline mode;
  • Web Push notifications;
  • the wake lock that keeps a display’s screen on;
  • the Secure cookies that Lantern’s sign-in and displays use.

Lantern still opens over HTTP from another machine, with a notice on every page saying so (SET-5). It’s for trying it out, not for living with.

Never publish Lantern’s port to the internet. Only the proxy, or Tailscale, should reach it.

Setting in .envWhy
APP_BASE_URLThe HTTPS address people use, such as https://lantern.example.com. Links, cookies, and sign-in are built from it, never from what a request claims
APP_PUBLISH127.0.0.1:3000 when the proxy runs on the same machine, so port 3000 isn’t open to the network. A proxy in a container on the same Docker network reaches web:3000 directly, so the port isn’t needed at all
APP_TRUST_PROXYWhere the proxy’s requests come from, as Lantern sees them. Lantern then reads each person’s own address from X-Forwarded-For, which sign-in’s rate limits are counted by. Leave it empty and every sign-in is counted together: safe, but one person’s typing slows everyone else’s

To find the address, look at the Docker network Lantern’s containers are on:

Terminal window
docker network inspect lantern_default -f '{{range .IPAM.Config}}{{.Subnet}} {{end}}'

lantern_default is the default network of a project in a folder named lantern. A proxy on the host reaches Lantern through that network’s gateway, and a proxy container through its own address on the network. Its subnet, such as 172.18.0.0/16, covers both. Never trust every address (0.0.0.0/0): anyone could then claim any address and dodge the limits.

The stream. Every screen keeps one request open at /api/v1/stream, through which changes arrive (SPEC Section 4.4). The proxy must pass it through as it comes, without buffering. Lantern sends a heartbeat every 25 seconds, so an idle timeout of a minute or more is enough. Traefik and Caddy do both by default.

Headers. Lantern sends HSTS, nosniff, Referrer-Policy, and X-Frame-Options itself (docs/security-checklist.md), so the proxy needn’t add them. It must accept TLS 1.2 or later only, which Traefik and Caddy do by default.

For a Traefik that already runs in Docker with a certificate resolver (here letsencrypt, on an entry point websecure) and a network of its own (here traefik). Add a docker-compose.override.yml beside Lantern’s docker-compose.yml, which Compose merges in, so the release’s file stays as it came:

# docker-compose.override.yml: Lantern behind Traefik.
services:
web:
networks: [default, traefik]
labels:
- traefik.enable=true
- traefik.docker.network=traefik
- traefik.http.routers.lantern.rule=Host(`lantern.example.com`)
- traefik.http.routers.lantern.entrypoints=websecure
- traefik.http.routers.lantern.tls.certresolver=letsencrypt
- traefik.http.services.lantern.loadbalancer.server.port=3000
networks:
traefik:
external: true

In .env:

  • APP_BASE_URL=https://lantern.example.com;

  • APP_PUBLISH=127.0.0.1:3000, since Traefik reaches web on its network;

  • APP_TRUST_PROXY set to the traefik network’s subnet:

    Terminal window
    docker network inspect traefik -f '{{range .IPAM.Config}}{{.Subnet}} {{end}}'

Then docker compose up -d.

Caddy gets a certificate from Let’s Encrypt by itself, so the name must point at the machine, with ports 80 and 443 open to it. With Caddy on the same machine, the whole Caddyfile is:

lantern.example.com {
reverse_proxy 127.0.0.1:3000
}

In .env:

  • APP_BASE_URL=https://lantern.example.com;
  • APP_PUBLISH=127.0.0.1:3000;
  • APP_TRUST_PROXY set to the subnet of lantern_default, as above.

Reload Caddy (systemctl reload caddy) and start Lantern.

Caddy streams /api/v1/stream as it comes, since it flushes text/event-stream responses at once, and it sets X-Forwarded-For itself, ignoring one a client sent.

For a household that keeps Lantern on its tailnet, with nothing open to the internet. Every phone, computer, and display then needs Tailscale too.

  1. Turn on HTTPS certificates for the tailnet. tailscale serve asks for it the first time if it isn’t on.

  2. On the machine that runs Lantern:

    Terminal window
    tailscale serve --bg localhost:3000

    It serves https://<machine>.<tailnet>.ts.net with a certificate Tailscale manages. tailscale serve --https=443 off stops it.

  3. In .env:

    • APP_BASE_URL=https://<machine>.<tailnet>.ts.net;
    • APP_PUBLISH=127.0.0.1:3000;
    • APP_TRUST_PROXY left empty: Tailscale doesn’t document sending X-Forwarded-For, and everyone who can reach Lantern is on the tailnet anyway.
  • https://lantern.example.com/healthz answers with Lantern’s version.
  • A browser shows no HTTPS notice on the sign-in page.
  • After signing in, More → Notifications offers notifications. They’re unavailable over plain HTTP.
  • A change made on one device shows on another within a couple of seconds. If it doesn’t, the proxy is buffering the stream.