# Verified compose for Vaultwarden with Caddy, tested 2026-10-02 by the SelfHostBench lab. # Lab host: Intel(R) Xeon(R) Processor @ 2.10GHz, 4 vCPU, 15.7 GiB RAM, x86_64, Docker 29.6.2. # Floating image tags below were replaced by the version resolved at test time; # the registry digest is in the comment on each image line. # Measurements: vaultwarden-caddy.json (schema 2). # Companion env file: vaultwarden-caddy.env (save it as .env next to this file). # Secrets are not published: the lab's values are public. In vaultwarden-caddy.env: # VAULTWARDEN_ADMIN_TOKEN is commented out. Uncomment it and set your own value (the comment above it explains how). # Vaultwarden (unofficial Bitwarden-compatible server, written in Rust) behind Caddy. # Caddy terminates HTTPS and is the only service that publishes ports. The web vault needs HTTPS. # The lab has no public domain, so the Caddyfile (inline, under `configs`) uses `tls internal`: # Caddy signs the certificate with its own CA. To use a real domain, see the comments in the Caddyfile. # Needs Docker Compose 2.23.1 or newer (inline `configs` content). name: vaultwarden-caddy services: vaultwarden: image: vaultwarden/server:1.37.3 # sha256:1587c45feaa479f1f5e8af3b00eded36bff77bcf1880cf8dbf0541706dd470e0 (version 1.37.3, from label:org.opencontainers.image.version) restart: unless-stopped environment: # The public URL of the vault. Set DOMAIN in the .env file. DOMAIN: "https://${DOMAIN}" # false after the first account exists. New users can then only join by admin invite. SIGNUPS_ALLOWED: "false" # Argon2 PHC hash of the admin password, generated with `vaultwarden hash`. It lives in the # .env file, because Compose would otherwise read the $ signs in the hash as variables. ADMIN_TOKEN: "${VAULTWARDEN_ADMIN_TOKEN}" volumes: # Everything lives here: database, attachments, sends, RSA keys, config.json. Back this up. - vw-data:/data/ # The image already defines a check (/healthcheck.sh, every 60 s). This keeps that script and # tightens the timing, so Caddy is not held back for up to a minute at start. healthcheck: test: ["CMD", "/healthcheck.sh"] interval: 30s timeout: 10s retries: 3 start_period: 30s start_interval: 2s caddy: image: caddy:2.11.4 # sha256:0c994536bddb66445885237f1a5dcc1916bccea922661c76b4e9fc24061f9b52 (version v2.11.4, from label:org.opencontainers.image.version) restart: unless-stopped depends_on: vaultwarden: condition: service_healthy ports: - "80:80" # Redirects to HTTPS. Also used for the Let's Encrypt HTTP challenge. - "443:443" - "443:443/udp" # HTTP/3 environment: DOMAIN: "${DOMAIN}" configs: - source: caddyfile target: /etc/caddy/Caddyfile volumes: # Certificates and Caddy's internal CA. Without these, every re-create issues new ones. - caddy-data:/data - caddy-config:/config networks: default: # Lets other containers on this network reach the vault by its public name. aliases: - "${DOMAIN}" configs: caddyfile: # Written inside a Compose file, so every Caddy {$VAR} is spelled {$$VAR} here. content: | {$$DOMAIN} { # Self-signed by Caddy's own CA: browsers will warn until you trust it. Lab use only. # With a real domain that points at this host (ports 80 and 443 open), delete this # line: Caddy then gets a Let's Encrypt certificate by itself. tls internal # The wiki's Caddy example enables this. Remove it if attachment downloads fail in Firefox. encode zstd gzip # One proxy line for everything, as in the Vaultwarden wiki's Caddy example. reverse_proxy vaultwarden:80 { # Passes the client address to Vaultwarden for its logs. header_up X-Real-IP {remote_host} } } volumes: vw-data: caddy-data: caddy-config: