Saltar al contenido
ES EN

Run Vaultwarden behind Caddy with local HTTPS on Docker Compose

Deploy Vaultwarden behind Caddy with pinned versions, closed signups, an admin panel guarded by a hashed token, and backups you restore to prove they work.

Lab · Self-hosting

50 min intermediate level validated configuration

Lab architectureComponents: Browser / Bitwarden client, Caddy, Vaultwarden, vw-data, backups.HTTPSreverse_proxy HTTPreads/writesvaultwarden backupBrowser / Bitwarden client: step 3Browser / Bitwarde…https://localhostCaddy: step 1Caddycontainer :443, local CAVaultwarden: step 1Vaultwardencontainer :80 internalvw-data: step 1vw-dataSQLite and attachmentsbackups: step 4backupsrestored copies
  1. Browser / Bitwarden client · step 3 https://localhost
  2. Caddy · step 1 container :443, local CA
  3. Vaultwarden · step 1 container :80 internal
  4. vw-data · step 1 SQLite and attachments
  5. backups · step 4 restored copies

What you will build and why

Vaultwarden is a Bitwarden-compatible server written in Rust: it keeps your passwords on your own machine and works with the official Bitwarden clients. In this lab you run it with Docker Compose next to Caddy, which handles HTTPS, with both images pinned to an exact version so an update never catches you off guard.

You follow a defensive order: HTTPS first (the admin panel should never be enabled without it), then closed signups once your account exists, an admin token stored as an argon2id hash and, last, a backup that you boot in a throwaway container to prove it is usable. Everything lives in a single directory and goes away with one command.

Always replace the CHANGE_ME_… placeholders with your own values. For home use without a public domain, Caddy issues certificates from its own local CA for internal hostnames such as localhost; the first time it may ask for your password to install its root certificate in the trust store (this happens once per root certificate), and clients that do not trust that CA will show security errors.

Lab: Run Vaultwarden behind Caddy with local HTTPS on Docker Compose

Requirements

SystemLinux (Debian 12 / Ubuntu 24.04) or macOS 14
Vaultwarden (imagen vaultwarden/server)1.37.4
Caddy (imagen caddy)2.11.7
Docker Compose (plugin docker compose)5.6.1
RAM2 GB
Disk5 GB

Step by step

  1. Create the project, the Caddyfile and the Compose file with pinned versions

    Create a working directory and, before running anything with Compose, create the Caddyfile: the Compose file mounts it as a file, and if it does not exist when you run docker compose up, Docker creates a directory with that name and Caddy fails to start. That is why the Caddyfile comes first in this step.

    Caddy serves every site with a defined host over HTTPS and, for localhost, signs certificates with its own local CA. The log block with roll_size and roll_keep rotates the access log inside Caddy itself. header_up X-Real-IP passes the client's real IP to Vaultwarden so it shows up in its logs. Caddy reads the {$DOMAIN} and {$LOG_FILE} variables from its environment, defined in the caddy service of the Compose file.

    Both images carry an exact tag (vaultwarden/server:1.37.4 and caddy:2.11.7) instead of latest: an upgrade becomes your decision, not a side effect of pull. Vaultwarden publishes no port on the host: only Caddy reaches it, over the private network Compose creates. SIGNUPS_ALLOWED starts as true only so you can create your account; we close it in step 3. DOMAIN must be the https URL, because Vaultwarden needs to know it runs over https to handle attachments properly. If you own a domain, replace https://localhost in both DOMAIN variables with it (CHANGE_ME_DOMAIN); with a public DNS name Caddy would use a public CA, which requires ports 80 and 443 to be reachable from the outside. Port 80 is published for the HTTP-to-HTTPS redirect, which Caddy applies by default.

    mkdir -p ~/vaultwarden && cd ~/vaultwarden
    mkdir -p vw-data caddy-config caddy-data backups

    File Caddyfile

    {$DOMAIN} {
    	log {
    		level INFO
    		output file {$LOG_FILE} {
    			roll_size 10MB
    			roll_keep 10
    		}
    	}
    
    	encode zstd gzip
    
    	reverse_proxy vaultwarden:80 {
    		header_up X-Real-IP {remote_host}
    	}
    }

    File compose.yaml

    services:
      vaultwarden:
        image: vaultwarden/server:1.37.4
        container_name: vaultwarden
        restart: always
        environment:
          DOMAIN: "https://localhost"
          SIGNUPS_ALLOWED: "true"
        volumes:
          - ./vw-data:/data
    
      caddy:
        image: caddy:2.11.7
        container_name: caddy
        restart: always
        ports:
          - 80:80
          - 443:443
          - 443:443/udp
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile:ro
          - ./caddy-config:/config
          - ./caddy-data:/data
        environment:
          DOMAIN: "https://localhost"
          LOG_FILE: "/data/access.log"
  2. Start the containers

    With the Caddyfile and compose.yaml in the directory, bring the project up. Compose creates the private network, pulls the two pinned images and starts both services. Check Caddy's logs to confirm there are no configuration errors.

    docker compose up -d
    docker compose logs --tail 20 caddy
  3. Create your account, close signups and protect the admin panel

    Open https://localhost, accept the local certificate warning (or install Caddy's CA as trusted) and create your account with a long master password. Then close signups: in the next Compose file SIGNUPS_ALLOWED becomes "false", so nobody else can register.

    The /admin panel is only enabled if you define ADMIN_TOKEN. Turn on HTTPS first (you already have it) and store the token as an argon2id hash (PHC string) rather than plain text. Generate the hash with the built-in vaultwarden hash command: it asks for the password twice and prints the hash. The password you create is the one you will use to log in to /admin; the panel session lasts 20 minutes by default (ADMIN_SESSION_LIFETIME changes it, but do not lengthen it without a reason).

    When you put the hash in environment, Vaultwarden's documentation says every $ in the hash must be escaped by doubling it to $$; otherwise Compose tries to interpolate it. Write the hash straight into the Compose file with the $$, replacing CHANGE_ME_ARGON2_PHC_STRING with the generated hash with every $ doubled. Then apply the changes by recreating the container: without this step signups would stay open and the panel disabled.

    docker run --rm -it vaultwarden/server:1.37.4 /vaultwarden hash

    File compose.yaml

    services:
      vaultwarden:
        image: vaultwarden/server:1.37.4
        container_name: vaultwarden
        restart: always
        environment:
          DOMAIN: "https://localhost"
          SIGNUPS_ALLOWED: "false"
          ADMIN_TOKEN: CHANGE_ME_ARGON2_PHC_STRING
        volumes:
          - ./vw-data:/data
    
      caddy:
        image: caddy:2.11.7
        container_name: caddy
        restart: always
        ports:
          - 80:80
          - 443:443
          - 443:443/udp
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile:ro
          - ./caddy-config:/config
          - ./caddy-data:/data
        environment:
          DOMAIN: "https://localhost"
          LOG_FILE: "/data/access.log"

    Replace every CHANGE_ME_… value with your own before starting: never keep example passwords or keys.

    docker compose up -d
  4. Take a backup and restore it to prove it works

    The data lives in ./vw-data: the SQLite database (db.sqlite3), attachments (attachments), config.json if you have used the panel, the rsa_key* files and, optionally, sends. Do not copy a live database with cp: the documentation recommends SQLite's online backup API. Since version 1.32.1 there is a built-in command, vaultwarden backup, which saves you from installing sqlite3 (the image does not ship it). The command leaves a SQLite backup file in the data folder.

    Copy that backup file and the rsa_key* files to ./backups. The attachments and sends directories and the config.json file may not exist: according to the documentation, attachments is absent if an attachment was never created, and config.json only exists if you configured something from the panel. That is why they are copied separately with a prior check: skip whichever does not exist. If you encrypt the copy, do it above all for config.json, which may hold the admin token and SMTP credentials in clear text.

    A backup you have never restored is not a backup. To check it, mount the copy as the data folder of a throwaway Vaultwarden container without Caddy (Vaultwarden uses db.sqlite3 by default, so rename the backup to that name in the test folder). The container runs in the background with -d; read its logs with docker logs and, when done, stop it with docker stop (since it was created with --rm, it removes itself).

    docker exec -it vaultwarden /vaultwarden backup
    ls -l vw-data
    mkdir -p backups/vw-copia
    cp vw-data/db_*.sqlite3 backups/vw-copia/
    cp -a vw-data/rsa_key* backups/vw-copia/
    # attachments, sends and config.json may not exist: copy only the ones you see in the listing
    [ -d vw-data/attachments ] && cp -a vw-data/attachments backups/vw-copia/
    [ -d vw-data/sends ] && cp -a vw-data/sends backups/vw-copia/
    [ -f vw-data/config.json ] && cp -a vw-data/config.json backups/vw-copia/
    ls -l backups/vw-copia

    Expected output (illustrative: the exact format may vary)

    -rw-r--r-- 1 root root  ... db.sqlite3
    -rw-r--r-- 1 root root  ... db_<marca-de-tiempo>.sqlite3
    drwxr-xr-x 2 root root  ... icon_cache
    -rw------- 1 root root  ... rsa_key.pem

    Expected output (illustrative: the exact format may vary)

    -rw-r--r-- 1 root root  ... db_<marca-de-tiempo>.sqlite3
    -rw------- 1 root root  ... rsa_key.pem
    mkdir -p backups/prueba-restauracion
    cp -a backups/vw-copia/. backups/prueba-restauracion/
    cp backups/prueba-restauracion/db_*.sqlite3 backups/prueba-restauracion/db.sqlite3
    docker run -d --rm --name vw-restore-test -v "$(pwd)/backups/prueba-restauracion:/data" vaultwarden/server:1.37.4
    docker logs vw-restore-test
    docker stop vw-restore-test

Check that it works

  1. Check that HTTPS responds

    Request the site's headers over HTTPS. Use --insecure only if you have not yet trusted Caddy's local CA; with the CA installed you can leave it out.

    curl --insecure --head https://localhost

    Expected output (illustrative: the exact format may vary)

    HTTP/2 200
  2. Check that signups are closed and the client works

    Open the Vaultwarden site and try to create another account: it should not let you. Then, in the client (browser extension or app), choose “Self-hosted” and enter https://localhost as the server URL. Log in with your account and save a test item. If the client does not accept the certificate, install Caddy's root CA as trusted on that device.

  3. Verify the backup

    Check that the timestamped backup exists and is not empty, and that the throwaway container from step 4 started without errors in the logs that docker logs printed. Also review the main service's logs.

    ls -l backups/vw-copia/db_*.sqlite3
    docker compose logs --tail 20 vaultwarden
If something fails

The browser or the client shows a certificate error.

That is expected with a local CA: it is only trusted where Caddy's root certificate is installed, which it stores in its data directory under pki/authorities/local. In this lab that directory is mounted at ./caddy-data, so look there for root.crt (the exact path may include an intermediate subdirectory) and install it as trusted on each device, or use your own domain with a certificate from a public CA.

find caddy-data -name root.crt

Compose warns that the argon2id, v or m variables are not set and the panel rejects your password.

The hash contains $ symbols that Compose tries to interpolate. With the hash written in environment, every $ must be doubled to $$, as Vaultwarden documents.

Caddy will not start because ports 80 or 443 are taken, or a directory named Caddyfile appears.

If the ports are taken, another service on the host is using them: stop it or change Caddy's port mapping. If you change the external port, the DOMAIN variable must include the port (for example https://localhost:8443) in both services, and clients must use that URL. If Docker created a Caddyfile directory because you ran up before creating the file, delete it, create the file from step 1 and start again.

docker compose logs --tail 30 caddy

Attachments do not download properly in Firefox.

The Vaultwarden wiki's own example warns that encode zstd gzip can cause compatibility problems with some browsers and suggests turning it off. Comment out that line in the Caddyfile and recreate the Caddy container with docker compose up -d --force-recreate caddy.

Harden the setup

Close the admin panel when you are not using it

To disable it, remove ADMIN_TOKEN from the Compose file (and do not define DISABLE_ADMIN_TOKEN), also delete the admin_token key from vw-data/config.json if that file exists, and recreate the container. If you suspect someone holds a valid panel session, delete rsa_key.pem from the data folder and restart Vaultwarden: the key is regenerated and sessions are invalidated.

Keep versions pinned and upgrade on purpose

To upgrade, change the tag in compose.yaml, read the release notes (1.37.4 includes security fixes) and run docker compose pull followed by docker compose up -d. Take a backup first and restore it as in step 4. Use a recent version of the Compose plugin (here 5.6.1, the latest shown in the release notes); the lab only uses basic Compose commands.

docker compose pull
docker compose up -d

Keep copies off the machine and encrypt the sensitive parts

A copy on the same disk does not protect you from a disk failure. Duplicate it on another machine and encrypt at least config.json, which may hold the admin token and SMTP credentials in clear text.

Clean-up: undo the lab

Remove the lab

Before deleting anything, copy backups somewhere else if you want to keep your data: it lives inside the project directory and will be lost with it. Then stop and remove the Compose containers and network, and finally the project directory. You can also remove the “Caddy Local Authority” certificate from your devices' trust stores.

cd ~/vaultwarden
docker compose down
cd ..
rm -rf vaultwarden

What is verified

  • Caddyfile: no automatic validator, review it by hand
  • compose.yaml syntax (YAML parser and Compose structure)
  • compose.yaml syntax (YAML parser and Compose structure)
  • Syntax of 12 command block(s) (bash -n in an isolated container, not executed)
  • Versions (3), options (5) and config keys (11) checked against 19 official documentation pages
  • Running the whole lab end to end: not executed

Automatic checks run on 28 October 2026. Items marked "·" were not executed: check them in your own environment.

Sources

Article generated with AI.larebelion

Byline

· Chief editor · English edition · London

“A password vault is only as safe as its backup: restore it before you need it.”

Comentarios