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
Tap a component to see its step.
- Browser / Bitwarden client · step 3 https://localhost
- Caddy · step 1 container :443, local CA
- Vaultwarden · step 1 container :80 internal
- vw-data · step 1 SQLite and attachments
- 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.

Requirements
Step by step
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 rundocker 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. Thelogblock withroll_sizeandroll_keeprotates the access log inside Caddy itself.header_up X-Real-IPpasses 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 thecaddyservice of the Compose file.Both images carry an exact tag (
vaultwarden/server:1.37.4andcaddy:2.11.7) instead oflatest: an upgrade becomes your decision, not a side effect ofpull. Vaultwarden publishes no port on the host: only Caddy reaches it, over the private network Compose creates.SIGNUPS_ALLOWEDstarts astrueonly so you can create your account; we close it in step 3.DOMAINmust be the https URL, because Vaultwarden needs to know it runs over https to handle attachments properly. If you own a domain, replacehttps://localhostin bothDOMAINvariables 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 backupsFile
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.yamlservices: 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"Start the containers
With the
Caddyfileandcompose.yamlin 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 caddyCreate 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 fileSIGNUPS_ALLOWEDbecomes"false", so nobody else can register.The
/adminpanel is only enabled if you defineADMIN_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-invaultwarden hashcommand: 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_LIFETIMEchanges 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$$, replacingCHANGE_ME_ARGON2_PHC_STRINGwith 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 hashFile
compose.yamlservices: 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 -dTake a backup and restore it to prove it works
The data lives in
./vw-data: the SQLite database (db.sqlite3), attachments (attachments),config.jsonif you have used the panel, thersa_key*files and, optionally,sends. Do not copy a live database withcp: 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 installingsqlite3(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. Theattachmentsandsendsdirectories and theconfig.jsonfile may not exist: according to the documentation,attachmentsis absent if an attachment was never created, andconfig.jsononly 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 forconfig.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.sqlite3by default, so rename the backup to that name in the test folder). The container runs in the background with-d; read its logs withdocker logsand, when done, stop it withdocker 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-copiaExpected 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.pemExpected output (illustrative: the exact format may vary)
-rw-r--r-- 1 root root ... db_<marca-de-tiempo>.sqlite3 -rw------- 1 root root ... rsa_key.pemmkdir -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
Check that HTTPS responds
Request the site's headers over HTTPS. Use
--insecureonly if you have not yet trusted Caddy's local CA; with the CA installed you can leave it out.curl --insecure --head https://localhostExpected output (illustrative: the exact format may vary)
HTTP/2 200Check 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://localhostas 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.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 logsprinted. 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.crtCompose 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 caddyAttachments 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 -dKeep 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 vaultwardenWhat 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
- Vaultwarden: Using Docker Compose github.com
- Vaultwarden: Enabling admin page github.com
- Vaultwarden: Backing up your vault github.com
- Caddy: Automatic HTTPS caddyserver.com
- Vaultwarden: Releases github.com
Did this lab work for you?
Rate it from 1 to 5 stars. One vote per person; you can change it any time.
Article generated with AI.larebelion


Publicar un comentario