The reverse proxy is assumed to live outside this project (Caddy, nginx, Traefik, whatever the operator already runs). The reference compose stands up only the server: image-pinned via RM_VERSION, named volume for operator state, localhost-bound so the proxy reaches it on loopback. docs/reverse-proxy.md covers what the proxy must forward — the X-Forwarded-* headers, Host, and Connection: upgrade for the agent WebSocket and live-log streams — plus the RM_TRUSTED_PROXY CIDR rule that gates header trust. Worked examples for Caddy, nginx (with the websocket upgrade map + 1h proxy_read_timeout for live logs), and Traefik.
4.4 KiB
Running behind a reverse proxy
The restic-manager server is HTTP-only by design (see spec.md §11):
TLS termination, public hostname, ACME, HSTS, and edge-level rate
limiting all belong to a reverse proxy that you already operate
outside this project. The reference compose in deploy/docker-compose.yml
stands up only the server; this page covers what your proxy needs
to do to make the rest of it work.
What the proxy must forward
The server reads four headers when (and only when) the immediate peer
matches RM_TRUSTED_PROXY:
| Header | Value | Why |
|---|---|---|
X-Forwarded-For |
The original client IP (single value, or comma chain) | Rate-limit keys, audit log entries, and OIDC redirect-URI checks all use the real client IP. |
X-Forwarded-Proto |
https |
The server emits absolute URLs (e.g. OIDC redirect URIs) using this. |
Host |
The public hostname clients use | Cookies are scoped to this; RM_BASE_URL must match. |
Connection/Upgrade |
Pass through unchanged | The agent connects on /ws/agent and the live-log viewer connects on /api/jobs/{id}/stream — both are WebSockets and need Upgrade: websocket to survive the hop. |
Set RM_TRUSTED_PROXY to the CIDR (or comma-separated list of CIDRs)
the proxy connects from. Anything outside that range has its
X-Forwarded-* headers ignored, so a stray request that bypasses the
proxy can't spoof the client IP.
Example: Caddy
restic.example.com {
# Caddy's default reverse_proxy preserves Host, sets
# X-Forwarded-For/Proto, and passes Connection: upgrade through,
# so a single directive covers HTTP + WebSocket.
reverse_proxy 127.0.0.1:8080
encode zstd gzip
}
RM_TRUSTED_PROXY=127.0.0.1/32 if Caddy and the server share the
host; the docker-bridge CIDR (commonly 172.16.0.0/12) if Caddy
runs in another container on the default bridge network.
Example: nginx
server {
listen 443 ssl http2;
server_name restic.example.com;
ssl_certificate /etc/ssl/restic.example.com.fullchain.pem;
ssl_certificate_key /etc/ssl/restic.example.com.key.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
# WebSocket support — agent + live-log endpoints need this.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Trusted-proxy headers.
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# Live job logs are long-running streams. Bump read timeouts
# so nginx doesn't drop them mid-backup.
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}
}
# Standard websocket upgrade map (define once at the http {} level).
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
RM_TRUSTED_PROXY for the same-host case: 127.0.0.1/32.
Example: Traefik (label-based)
labels:
- "traefik.enable=true"
- "traefik.http.routers.restic-manager.rule=Host(`restic.example.com`)"
- "traefik.http.routers.restic-manager.entrypoints=websecure"
- "traefik.http.routers.restic-manager.tls.certresolver=letsencrypt"
- "traefik.http.services.restic-manager.loadbalancer.server.port=8080"
Traefik handles X-Forwarded-* and Connection: upgrade by default.
RM_TRUSTED_PROXY should be the docker network the Traefik container
shares with the server (commonly 172.16.0.0/12 for the default
bridge, or whatever your overlay network's CIDR is).
Sanity-checking the wiring
After bringing the stack up:
curl -fsS https://restic.example.com/healthz— should return 200.- The login page should report HTTPS in the address bar; cookies
set after login should carry the
Secureflag. - Check the server log for the
config resolvedline:trusted_proxiesmust include the IP/CIDR your proxy actually connects from. - Enrol a test agent — the WebSocket handshake hitting
/ws/agentconfirmsUpgradeis being forwarded correctly.
If any of those fail, the proxy is the first place to look — the server itself is intentionally minimal.