Vaultwarden behind Caddy

Verified · Caddy 2.8

Config + gotchas checked against the official Vaultwarden docs and the issues people actually hit · 2026-06-07

A Caddy reverse-proxy setup for Vaultwarden, with automatic HTTPS on vault.example.com. Copy it, swap the domain — the gotchas that trip people up are solved below.

New to reverse proxies? Start with the Caddy + Docker guide.

Caddyfile
# Caddyfile for Vaultwarden
# Replace vault.example.com with your real domain. Caddy issues TLS automatically.

vault.example.com {
	encode zstd gzip
	header {
		Strict-Transport-Security "max-age=31536000;"
	}

	reverse_proxy /notifications/hub vaultwarden:8080 {
		header_up Host {host}
		header_up X-Real-IP {remote_host}
	}

	reverse_proxy vaultwarden:8080 {
		header_up Host {host}
		header_up X-Real-IP {remote_host}
	}
}

Replace vault.example.com with your domain. Your DNS A record must already point at the server.

Gotchas

What trips people up

WebSocket notifications never fire — clients only update on manual sync.

Vaultwarden serves real-time sync over `/notifications/hub`. That path must be reverse-proxied to the WebSocket explicitly; the Caddyfile in this stack already adds the route.

Clients reject login with "We could not process your request".

Bitwarden clients require a valid public TLS certificate — self-signed is refused. Caddy issues a real Let’s Encrypt cert automatically once your DNS A record points at the server and ports 80/443 are reachable.

Deploy

How to deploy

  1. Create the shared proxy network once: `docker network create proxy`.
  2. Start the stack: `docker compose up -d`.
  3. Caddy obtains the TLS certificate on first request — confirm DNS for your domain resolves to this server and ports 80/443 are open.

Runnable

Full runnable stack

Vaultwarden + Caddy + the shared network, in one compose file — so it runs on the first try, not after you stitch the proxy in yourself. Save the Caddyfile above next to it.

docker-compose.yml (app + Caddy)
# docker-compose.yml — Vaultwarden + Caddy, ready to run.
# Pin caddy to a digest for production: caddy:2.8-alpine@sha256:<digest>
# 1. Save the Caddyfile above as ./Caddyfile
# 2. docker network create proxy
# 3. docker compose up -d
services:
  vaultwarden:
    image: docker.io/vaultwarden/server:1.32.5-alpine@sha256:76d46d32ba4120b022e0a69487f9fd79fc52e2765b1650c5c51a5dd912a3c288
    restart: unless-stopped
    environment:
      DOMAIN: https://${VAULTWARDEN_HOST}
      SIGNUPS_ALLOWED: "false"
      ADMIN_TOKEN: ${ADMIN_TOKEN}
      WEBSOCKET_ENABLED: "true"
      SENDS_ALLOWED: "true"
      EMERGENCY_ACCESS_ALLOWED: "true"
      PASSWORD_ITERATIONS: "600000"
      SHOW_PASSWORD_HINT: "false"
      LOG_FILE: /data/vaultwarden.log
      LOG_LEVEL: warn
      ROCKET_PORT: "8080"
    volumes:
      - vaultwarden-data:/data
    healthcheck:
      test:
        - CMD
        - wget
        - -qO-
        - http://127.0.0.1:8080/alive
      interval: 30s
      timeout: 5s
      retries: 3
    networks:
      - default
      - proxy
  caddy:
    image: caddy:2.8-alpine
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - proxy
volumes:
  vaultwarden-data: {}
  caddy-data: {}
  caddy-config: {}
networks:
  proxy:
    external: true

Full Vaultwarden stack (compose + env + docs)