Contents13 sections
- Prerequisites
- Step 1: install Docker
- Step 2: create the Duck DNS name
- Step 3: get a Caddy build with the Duck DNS module
- Step 4: write compose.yaml
- Step 5: write the Caddyfile
- Step 6: start it
- Step 7: create your account, then close sign-ups
- Step 8: decide on the admin panel
- Step 9: connect the Bitwarden apps
- Troubleshooting
- Keep it updated and backed up
- What to do next
To run Vaultwarden on a Raspberry Pi with working HTTPS, run it in Docker Compose next to a Caddy container, and have Caddy get a Let's Encrypt certificate through the DNS challenge with a free Duck DNS name that points at the Pi's private IP. You get a trusted certificate without opening a single port to the internet. This is the "Caddy with DNS challenge" setup from Vaultwarden's own wiki. The steps below follow it, and the few places this guide changes it are called out.
Why bother with a certificate for a box on your LAN? The README is blunt: the web vault needs the browser's Web Crypto API, which only works in a secure context, so it "will only work if you enable HTTPS." The FAQ adds that some platforms won't accept self-signed certificates, and it recommends Let's Encrypt.
For what Vaultwarden is and isn't, read the Vaultwarden profile first. The current release is 1.37.3, published 13 September 2026.
Prerequisites
- A Raspberry Pi 3, 4 or 5 running 64-bit Raspberry Pi OS. This matters more than it used to. Docker's docs say Engine v28 is the last major version with packages for 32-bit Raspberry Pi OS (armhf). They also say ARMv6 boards (Pi 1 and the original Pi Zero/Zero W) are no longer supported by the official packages. For 64-bit, Docker says to follow its Debian instructions.
- A fixed LAN IP for the Pi, for example a DHCP reservation on your router, because the Duck DNS record will point at it.
- A free Duck DNS account (duckdns.org). If you already have a domain on Cloudflare DNS, the wiki covers that too (see the note in step 3).
- SSH access to the Pi and about half an hour.
This setup is LAN-only by design. To reach the vault away from home, you'd connect back over a VPN. Opening Vaultwarden to the internet is a different setup (the wiki's HTTP-challenge example), with a bigger attack surface.
Step 1: install Docker
Docker's convenience script is the quickest route. Docker notes it isn't recommended for production and says to examine scripts before running them. The repository method on the same page is the careful alternative.
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh --dry-run # optional: preview what it will do
sudo sh get-docker.sh
sudo docker run hello-worldExpected result: hello-world "downloads a test image and runs it in a container. When the container runs, it prints a confirmation message and exits."
To run docker without sudo, add yourself to the docker group, then log out and back in (Docker's post-install page):
sudo usermod -aG docker $USERStep 2: create the Duck DNS name
Sign in to Duck DNS, create a subdomain (the wiki's example is my-vw.duckdns.org), and set its IP to the Pi's private LAN address, such as 192.168.1.100. Copy your account token, a UUID-style string. Caddy needs it to prove to Let's Encrypt that you control the name.
Pointing a public DNS name at a private IP is the trick that makes this work. Let's Encrypt never has to reach the Pi. It only checks a DNS record that Caddy creates through the Duck DNS API.
Step 3: get a Caddy build with the Duck DNS module
The stock caddy:2 image doesn't include DNS-challenge modules. The wiki's easiest route is to download a custom build from caddyserver.com/download: pick Linux arm64 as the platform, tick github.com/caddy-dns/duckdns, and download.
Make a project folder, put the binary in it, rename it caddy, and make it executable:
mkdir ~/vaultwarden && cd ~/vaultwarden
# move the downloaded binary here and rename it to "caddy", then:
chmod a+x caddyAlternative (adapted): if you'd rather build the binary on the Pi, Caddy's Docker Hub page documents a two-stage Dockerfile on the :builder image. This version swaps in the Duck DNS module in place of Caddy's example modules:
FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/duckdns
FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddyIf you go this way, replace image: caddy:2 with build: . in the Compose file below and drop the ./caddy:/usr/bin/caddy volume line.
Using Cloudflare instead of Duck DNS? Tick github.com/caddy-dns/cloudflare instead, and use dns cloudflare {$CLOUDFLARE_API_TOKEN} in the Caddyfile. The wiki lists the token permissions: Zone / DNS / Edit plus Zone / Zone / Read, scoped to your zone.
Step 4: write compose.yaml
This is the wiki's "Caddy with DNS challenge" file. Two changes: SIGNUPS_ALLOWED is added (taken from the wiki's minimal template) so you can create your first account, and the data path is ./vw-data rather than the example's /vw-data, so everything lives in one folder.
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: always
environment:
DOMAIN: "https://my-vw.duckdns.org" # Your domain
SIGNUPS_ALLOWED: "true" # set to "false" after step 7
volumes:
- ./vw-data:/data
caddy:
image: caddy:2
container_name: caddy
restart: always
ports:
- 80:80
- 443:443
- 443:443/udp # Needed for HTTP/3.
volumes:
- ./caddy:/usr/bin/caddy # Your custom build of Caddy.
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-config:/config
- ./caddy-data:/data
environment:
DOMAIN: "https://my-vw.duckdns.org" # Your domain.
EMAIL: "admin@example.com" # The email address to use for ACME registration.
DUCKDNS_TOKEN: "<token>" # Your Duck DNS token.
LOG_FILE: "/data/access.log"The token is a credential. Keep this file readable only by you, and don't commit it anywhere.
Step 5: write the Caddyfile
Straight from the wiki, and it "does not need to be modified":
{$DOMAIN} {
log {
level INFO
output file {$LOG_FILE} {
roll_size 10MB
roll_keep 10
}
}
# Use the ACME DNS-01 challenge to get a cert for the configured domain.
tls {
dns duckdns {$DUCKDNS_TOKEN}
}
# This setting may have compatibility issues with some browsers
# (e.g., attachment downloading on Firefox). Try disabling this
# if you encounter issues.
encode zstd gzip
# Proxy everything to Rocket
reverse_proxy vaultwarden:80
}Vaultwarden itself publishes no ports here. Only Caddy is exposed, and it reaches Vaultwarden over the private network Compose creates.
Step 6: start it
docker compose up -d
docker compose logs -f caddyExpected result: the wiki says the first start "takes a few seconds to solve the DNS challenge and obtain the HTTPS certificates." Then open https://my-vw.duckdns.org from a device on your network. You should see the Bitwarden web vault login page, with no certificate warning. If it doesn't load, the wiki's advice is to check Caddy's output first.
Step 7: create your account, then close sign-ups
Create your account (and your household's) in the web vault. Then, as the wiki's template comment says, deactivate sign-ups "so that no strangers can register." Change the line in compose.yaml:
SIGNUPS_ALLOWED: "false"While you're there, the Hardening Guide suggests two more switches:
INVITATIONS_ALLOWED: "false" # stops org owners inviting new users
SHOW_PASSWORD_HINT: "false" # hides hints on the login pageVaultwarden shows password hints on the page itself when there's no email service, and the guide notes attackers could use that for password guessing. Apply the changes:
docker compose up -dStep 8: decide on the admin panel
The /admin panel can manage users and settings, and it can invite users even with registration closed. It's off unless you set a token. If you don't need it, leave ADMIN_TOKEN unset. That is the wiki's documented way to keep it disabled.
If you want it, don't put a plain password in the file. Generate an Argon2id PHC hash with Vaultwarden's built-in command:
docker exec -it vaultwarden /vaultwarden hashIt asks for the password twice and prints a string beginning $argon2id$. In compose.yaml, every $ must be doubled to $$ or Compose will try to interpolate it (the wiki's rule, with its example value):
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$UUZxK1FZMkZoRHFQRlVrTXZvS0E3bHpNQW55c2dBN2NORzdsa0Nxd1JhND0$$cUoId+JBUsJutlG4rfDZayExfjq4TCt48aBc9qsc3UIRun docker compose up -d again, then log in at /admin with the password you hashed, not the hash. Admin sessions last 20 minutes by default.
Step 9: connect the Bitwarden apps
In the browser extension or mobile app, on the login screen, open Logging in on, choose Self-hosted, and enter https://my-vw.duckdns.org as the Server URL (Bitwarden's "Connect individual clients" guide). The desktop app has the same option.
Troubleshooting
These are the issues the Vaultwarden docs themselves describe.
- The name doesn't resolve at home, but works on mobile data. Some routers (the wiki names FritzBox) and resolvers such as Unbound have DNS rebind protection, which blocks public names that resolve to private IPs. Add an exception for your Duck DNS name.
- Clients refuse to log in or complain about the certificate. The FAQ says this is usually a self-signed certificate. That's the problem the DNS challenge solves, so check that Caddy actually got a Let's Encrypt certificate in its logs.
- Attachments won't download in Firefox. The Caddyfile's own comment flags
encode zstd gzipas a possible cause. Remove that line and restart Caddy. - The whole
$argon2id$...string works as the admin password. The wiki says that means you're on a Vaultwarden version too old to support Argon2id hashes. Update. - You need to kick out every admin session. Changing the token doesn't end existing sessions. The wiki's fix is to delete
rsa_key.pemfrom the data folder and restart. Note that this key also signs user logins.
Keep it updated and backed up
Updating is the wiki's one-liner:
docker compose pull && docker compose up -dDo it promptly. The official Bitwarden clients update themselves, and an out-of-date server can break them. For backups (db.sqlite3 via SQLite's backup command, attachments/, the rsa_key* files), follow the backup section of the Vaultwarden profile. On a Pi that boots from an SD card, keep a copy off the board.
What to do next
- Moving from another password manager? Migrate LastPass or Chrome passwords to Bitwarden works the same way against your new server.
- Still deciding between this and the official server (including Bitwarden lite)? Read Vaultwarden vs Bitwarden.
- Put network-wide ad blocking on the same Pi with Pi-hole or AdGuard Home, and get alerts when the vault goes down with Uptime Kuma.
- More ideas for the board: Raspberry Pi self-hosted apps.
Sources (15)ShowHide
- Vaultwarden wiki: Using Docker Compose (Caddy with HTTP and DNS challenge) · accessed 2026-09-29
- Vaultwarden wiki: Running a private vaultwarden instance with Let's Encrypt certs · accessed 2026-09-29
- Vaultwarden README (HTTPS and secure-context requirement, Compose example) · accessed 2026-09-29
- Vaultwarden wiki: Enabling admin page (ADMIN_TOKEN, Argon2 PHC hash, $$ escaping) · accessed 2026-09-29
- Vaultwarden wiki: Disable registration of new users · accessed 2026-09-29
- Vaultwarden wiki: Disable invitations · accessed 2026-09-29
- Vaultwarden wiki: Password hint display · accessed 2026-09-29
- Vaultwarden wiki: Hardening Guide · accessed 2026-09-29
- Vaultwarden wiki: FAQs (clients and certificates) · accessed 2026-09-29
- Docker Docs: Install Docker Engine on Debian (convenience script, hello-world check) · accessed 2026-09-29
- Docker Docs: Install Docker Engine on Raspberry Pi OS (32-bit / armhf) deprecation notice · accessed 2026-09-29
- Docker Docs: Linux post-installation steps · accessed 2026-09-29
- Caddy image docs on Docker Hub (the :builder image and xcaddy) · accessed 2026-09-29
- Bitwarden Help: Connect individual clients to a self-hosted server · accessed 2026-09-29
- GitHub releases API: dani-garcia/vaultwarden (1.37.3, 2026-09-13), as recorded in the Vaultwarden profile · accessed 2026-09-29