# Vaultwarden Docker Compose: A Working Setup, Explained

> A Vaultwarden Docker Compose file with Caddy and HTTPS, plus what the quickstart skips: HTTPS, closed sign-ups, a hashed admin token and a full backup.

1. [Home](/)
2. [Blog](/blog)
3. Vaultwarden Docker Compose: A Working Setup, Explained

DeploymentOctober 29, 202611 min read

# Vaultwarden Docker Compose: A Working Setup, Explained

A Vaultwarden Docker Compose file with Caddy and automatic HTTPS, and what the README example leaves out: HTTPS in front of it, closed sign-ups, a hashed admin token, and why changes in /admin beat your compose file. Plus what a complete backup contains and how to update without losing the vault.

## The short version

A Vaultwarden Docker Compose setup that is safe to leave running has two services: vaultwarden/server, pinned to a version, and a reverse proxy such as Caddy that serves it over HTTPS, because the web vault refuses to work over plain http. Set DOMAIN to the public address and SIGNUPS\_ALLOWED=false so only invited people get accounts, and give ADMIN\_TOKEN an Argon2 hash from vaultwarden hash instead of plain text. Settings saved in /admin go to config.json and override the environment. A backup is the database copy from vaultwarden backup plus rsa\_key.pem, attachments, sends and config.json.

[Teo Marquardt](/about#author)· Facts last checked October 26, 2026

The Compose example in the Vaultwarden README is a dozen lines, and it works. It also uses the `latest` tag, binds the container to `127.0.0.1:8000` with nothing serving HTTPS in front of it, leaves registration open to anyone who finds the URL and sets no admin token. Publish the port to reach it from another machine over plain http, and the web vault won't let you log in. The example is meant as a starting point, and that's fine. A password manager is just a bad place to discover defaults after other people already depend on it.

Below is a complete Vaultwarden Docker Compose setup with Caddy for HTTPS, followed by the settings around it and the exact message you get when each one is missing. Every variable name and log line here was checked against the Vaultwarden source as of writing, with 1.37.4 as the current release.

## A working Vaultwarden Docker Compose file

Three files in one directory. You need a server with ports 80 and 443 open and a DNS record pointing your domain at it. Compose reads `.env` on its own:

bash

```
# .env
VW_VERSION=1.37.4
VW_DOMAIN=vault.example.eu

# Paste the line that `vaultwarden hash` prints, single quotes included.
# See the section on the admin token.
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'
```

yaml

```
# docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:${VW_VERSION}
    restart: unless-stopped
    environment:
      DOMAIN: https://${VW_DOMAIN}
      SIGNUPS_ALLOWED: "false"
      ADMIN_TOKEN: ${ADMIN_TOKEN}
    volumes:
      - ./vw-data:/data
    # No ports: only Caddy talks to it, over the Compose network.

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    environment:
      VW_DOMAIN: ${VW_DOMAIN}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:
```

text

```
# Caddyfile
{$VW_DOMAIN} {
	encode zstd gzip
	reverse_proxy vaultwarden:80
}
```

`docker compose up -d` starts both. On first start Caddy requests a Let's Encrypt certificate for the domain and renews it from then on. That's the whole reason it's here instead of nginx: no certbot, no renewal cron. The vault data goes to `./vw-data` next to the Compose file rather than a named volume, so you can see every file you'll need to back up. And the version is pinned, so `docker compose pull` never upgrades the server behind your back.

### Which port does Vaultwarden use?

Inside the official image, port 80 (`ROCKET_PORT=80` is set in the Dockerfile). The `.env.template` in the repository mentions 8000, which is the default when you run the binary outside Docker. Live sync between devices goes over WebSocket on that same port, and `ENABLE_WEBSOCKET` is on by default. Guides written before mid-2023 map a second port, 3012, for WebSocket. Current versions don't use it, so leave it out. In the setup above Vaultwarden publishes no ports at all; the only ways in are 80 and 443 on Caddy.

## HTTPS is not optional

A question that comes up again and again in Vaultwarden threads goes like this: the container runs, the page loads on `http://server-ip:8000`, and creating an account or logging in does nothing useful. The web vault encrypts and decrypts your data in the browser with the Web Crypto API, and browsers only provide that API on secure origins. Vaultwarden's build of the web vault checks for it and tells you directly:

### This browser requires HTTPS to use the web vault

text

```
This browser requires HTTPS to use the web vault
Check the Vaultwarden wiki for details on how to enable it
```

There's no setting that turns this off. Browsers refuse to give the page those crypto functions, so the fix is a certificate. On a public server, Caddy as above or any proxy with Let's Encrypt. On a home network with no public domain, you still need HTTPS: a domain you own with a DNS challenge, Caddy's internal CA with its root installed on your devices, or a tunnel service that gives you an HTTPS address. Passkeys and security keys add one more constraint. They're bound to the domain they were registered on, so choose the final address before anyone registers one.

## Close sign-ups before anyone finds the URL

`SIGNUPS_ALLOWED` defaults to `true`. With the README example, anyone who reaches your domain can create an account on your server. They can't read anyone else's vault, since every vault is encrypted with its owner's master password. But they get free storage on your disk and a login form to probe, and open registration is the first thing people probe on a public instance.

Set `SIGNUPS_ALLOWED: "false"` from the first start, as in the file above, and add people through invitations. Open `/admin`, go to Users, and invite an email address. Invited addresses can still register while sign-ups are closed. Without SMTP configured no invitation email goes out, and the person simply registers on your URL with the invited address. If everyone you want shares a company domain, `SIGNUPS_DOMAINS_WHITELIST=yourcompany.eu` lets those addresses register without an invite. While the whitelist is set, `SIGNUPS_ALLOWED` is ignored: only the listed domains can self-register, even with `SIGNUPS_ALLOWED=true`. Invitations still work.

### Registration not allowed or user already exists

This is what a stranger, or you on a second attempt, sees when trying to register on a closed server. The wording is deliberately vague: Vaultwarden returns the same error whether the address isn't invited or already has an account, so the form can't be used to check who has one. If an invited person sees it, check for a typo in the invited address and whether they already registered once.

## The Vaultwarden admin token

The admin panel at `/admin` exists only when `ADMIN_TOKEN` is set, and the token is the panel's password. It's a separate secret from any user's master password. Without it nobody can use the panel, and `DISABLE_ADMIN_TOKEN=true` leaves the panel open to anyone who can reach it, so you'd only use that behind a proxy that does its own authentication.

### You are using a plain text ADMIN\_TOKEN which is insecure

Most guides set `ADMIN_TOKEN` to a long random string. That still works, and Vaultwarden prints this at every start:

text

```
[NOTICE] You are using a plain text `ADMIN_TOKEN` which is insecure.
Please generate a secure Argon2 PHC string by using `vaultwarden hash` or `argon2`.
```

The fix is to store a hash of the token instead of the token itself, so a leaked `.env` or `docker inspect` output doesn't hand over the admin panel. The image ships the tool. Run it once before the first start, type the password you'll use for `/admin` twice, and it prints a ready line:

bash

```
docker run --rm -it vaultwarden/server:1.37.4 /vaultwarden hash
# Generate an Argon2id PHC string using the 'bitwarden' preset:
# Password:
# Confirm Password:
#
# ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'
```

Paste that line into `.env` as it is. The single quotes matter. Compose treats a single-quoted value in `.env` literally, while an unquoted or double-quoted one goes through variable interpolation, and the `$` signs in the hash would be read as variable names. If you put the hash straight into `docker-compose.yml` instead, every `$` has to be written as `$$`. On the login page at `/admin` you then type the password, not the hash. `vaultwarden hash --preset owasp` gives a lighter hash if the default makes logins slow on a small machine.

### Settings saved in /admin override your environment

This one costs people an evening. The settings page in `/admin` covers most of what you'd otherwise set in Compose: SMTP, sign-ups, invitation rules, even the admin token itself. Press Save there once and Vaultwarden writes the whole form to `/data/config.json`. From then on that file wins over environment variables for every setting on the page. You change `SIGNUPS_ALLOWED` in Compose, recreate the container, and nothing happens. The logs do tell you:

text

```
[WARNING] The following environment variables are being overridden by the config.json file.
[WARNING] Please use the admin panel to make changes to them:
[WARNING] SIGNUPS_ALLOWED, ADMIN_TOKEN, ...
```

Pick one place for settings

Either keep configuration in Compose and never press Save in /admin, or switch to the admin panel and remove the duplicated variables from Compose. To go back to the environment, use Reset defaults at the bottom of the settings page, which deletes config.json. And since config.json can hold your SMTP password and the admin token, it belongs in the backup.

## Vaultwarden Docker environment variables that matter

Vaultwarden has well over a hundred settings, all listed with comments in `.env.template` in the repository. For a family or a small team, these are the ones whose absence you'll notice:

| Variable                                                       | Default | What goes wrong without it                                                                                                                |
| -------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| DOMAIN                                                         | not set | Attachment downloads, links in emails and security keys (WebAuthn/U2F) can break, because Vaultwarden doesn't know its own public address |
| SIGNUPS\_ALLOWED                                               | true    | Anyone who finds the URL can create an account                                                                                            |
| ADMIN\_TOKEN                                                   | not set | No /admin panel, so no invitations or user management; in plain text, a NOTICE at every start                                             |
| SMTP\_HOST, SMTP\_FROM, SMTP\_USERNAME, SMTP\_PASSWORD         | not set | No invitation emails, no email two-step login, no emergency access, no password hints                                                     |
| PUSH\_ENABLED, PUSH\_INSTALLATION\_ID, PUSH\_INSTALLATION\_KEY | off     | Mobile apps only pick up changes when they sync, not instantly                                                                            |

### Push notifications through the EU relay

The browser extensions and desktop apps get changes live over WebSocket. The iOS and Android apps can't keep a socket open in the background, so instant updates go through the push relay operated by Bitwarden, Inc. You request an installation ID and key at bitwarden.com/host, which is free. Pick the European Union data region there, and point Vaultwarden at the EU relay to match, because the default URIs are the US ones:

yaml

```
      PUSH_ENABLED: "true"
      PUSH_INSTALLATION_ID: ${PUSH_INSTALLATION_ID}
      PUSH_INSTALLATION_KEY: ${PUSH_INSTALLATION_KEY}
      PUSH_RELAY_URI: https://api.bitwarden.eu
      PUSH_IDENTITY_URI: https://identity.bitwarden.eu
```

The relay only learns that a device should sync; the change itself stays between your server and the app. Skip push entirely and the mobile apps still work; they pick up changes when you open them or pull to refresh.

## Vaultwarden backup: more than db.sqlite3

By default Vaultwarden keeps everything in `/data`, which in this setup is `./vw-data`. The database is SQLite in write-ahead-log mode, so copying `db.sqlite3` while the server runs can give you a file that's missing its most recent writes. Vaultwarden has a command for a consistent copy:

bash

```
docker compose exec vaultwarden /vaultwarden backup
# Backup to '/data/db_20261029_031500.sqlite3' was successful
```

It writes a snapshot with `VACUUM INTO` next to the live database, so it lands in `./vw-data` on the host. It works for SQLite only. With PostgreSQL or MySQL you'd use that database's own dump tool. The same button exists in `/admin` as Backup Database. Then copy the snapshot together with the files around it, and move the copies off the server:

| Path in /data                 | Holds                                         | If it's missing from the backup                 |
| ----------------------------- | --------------------------------------------- | ----------------------------------------------- |
| db\_\*.sqlite3 (the snapshot) | Accounts, vault items, folders, organizations | Nothing to restore                              |
| rsa\_key.pem                  | The key that signs login tokens               | Every device is signed out once; no data lost   |
| attachments/                  | Files attached to vault items                 | The items stay, the files are gone              |
| sends/                        | Files shared with Send                        | Open Send links stop working                    |
| config.json                   | Settings saved in /admin                      | Settings fall back to the environment variables |

`icon_cache/` can be skipped; Vaultwarden downloads website icons again. To restore, stop the container, delete any `db.sqlite3-wal` and `db.sqlite3-shm` files so stale journal data isn't replayed over the snapshot, put the snapshot back as `db.sqlite3` alongside the other files, and start it again. As a second layer, each user can export an encrypted JSON from the web vault under Tools. That export restores into any Bitwarden-compatible server, and it's the copy you want if the server, the disk and the backups disappear at the same time.

## Update Vaultwarden in Docker Compose

With the version pinned in `.env`, an update is deliberate and has an order:

bash

```
docker compose exec vaultwarden /vaultwarden backup
# set VW_VERSION to the new release in .env
docker compose pull vaultwarden
docker compose up -d vaultwarden
docker compose logs -f vaultwarden
```

Vaultwarden migrates the database on start, and migrations aren't reversed if you go back to an older tag. Going back means restoring the snapshot you took first and starting the old version. Read the release notes before a jump across several versions, and keep the snapshot until the new version has run for a few days. It's the same routine as for any stateful container. The [n8n Docker Compose guide](/blog/n8n-docker-compose) walks through it for a setup with a separate database.

## Where to run it

Vaultwarden is light. On our own template deploy it idles at about 35 MB of memory and barely touches the CPU, so hardware is never the question. Where it runs decides who does the upkeep:

* At home, on a NAS or a small box. Nothing leaves your network, but HTTPS still needs solving, and away from home you need a VPN or a tunnel to reach the vault. When the box is down, devices keep working from their local copy and can't sync.
* On a VPS, for example a small Hetzner instance in Germany. You get a public address and full control, and you also get OS updates, the firewall, SSH keys and off-server backups. The [VPS vs PaaS cost breakdown](/blog/vps-vs-paas-cost) puts numbers on that upkeep.
* On a managed platform that runs the container for you. You keep the Vaultwarden configuration and hand over the server. [Coolify vs managed PaaS](/blog/coolify-vs-managed-paas) covers the middle ground of running your own platform on a VPS.

Vault items are encrypted on your devices, so the server never sees a password. It does hold email addresses, organization names and IP addresses in its logs, and that's personal data under the GDPR. If you run Vaultwarden for a team, the server's location and operator matter. The [data residency guide](/blog/gdpr-data-residency) explains what to check.

## How Runsite handles it

If you want your own Vaultwarden without running the server, [the Vaultwarden template on Runsite](/templates/vaultwarden) deploys the official `vaultwarden/server` image behind HTTPS on its own URL, with a persistent disk at `/data`, on servers in Germany. Public sign-ups are off before the first start, and the template generates a random admin token for your deploy, so you invite yourself at `/admin` and register. That token is plain text, so the NOTICE described above shows in the logs; paste an Argon2 hash from `vaultwarden hash` into `ADMIN_TOKEN` to replace it. It copies a repository into your GitHub account and pins the Vaultwarden version there, so an update is a one-line commit. If the disk is missing or read-only, the start script stops with an error instead of running a vault that would lose its data on the next deploy.

Backups are still yours to take: the disk isn't snapshotted for you, and the `vaultwarden backup` routine above works the same way from the service shell. The repository also includes a `docker-compose.yml`, so the same setup runs on your laptop or on any server.

## The short version

* Run `vaultwarden/server` pinned to a version, with `/data` on a volume you can see, and no published ports except through the proxy.
* Serve it over HTTPS. Without it you get `This browser requires HTTPS to use the web vault`, on a home network too.
* Set `DOMAIN` to the public address and `SIGNUPS_ALLOWED=false` from the first start, then invite people from `/admin`.
* Generate `ADMIN_TOKEN` with `vaultwarden hash` and keep it single-quoted in `.env`. Settings saved in `/admin` go to `config.json` and override Compose.
* Back up the `vaultwarden backup` snapshot with `rsa_key.pem`, `attachments/`, `sends/` and `config.json`, off the server, and take one before every update.

Related service

## Vaultwarden template on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Vaultwarden template](/templates/vaultwarden)

[Back to all articles](/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What are the system requirements for Vaultwarden?

Very low. Vaultwarden idles at tens of megabytes of memory (about 35 MB on our own deploy) and uses almost no CPU, so a Raspberry Pi, a small NAS or the smallest VPS plan is enough for a family or a small team. Disk use depends mostly on attachments. What it does require is HTTPS on a domain or a trusted certificate, because the web vault does not work over plain http.

### Is Vaultwarden completely free?

Yes. Vaultwarden is open source under the AGPL-3.0 and costs nothing to run beyond the server. It works with the free official Bitwarden apps and includes features Bitwarden sells in paid plans, such as TOTP codes in vault items, attachments, vault health reports and organizations. Push notifications for mobile apps need an installation ID and key from bitwarden.com/host, which are also free.

### Which is better, Bitwarden or Vaultwarden?

Vaultwarden is an independent, unofficial server written in Rust that works with the Bitwarden clients; Bitwarden, Inc. offers a hosted service and its own self-hosted server, with official support and paid licences for business features. Vaultwarden uses far fewer resources and is popular for families and small teams who self-host. It is not affiliated with Bitwarden, and its support comes from the community rather than a vendor.

### Can I run Vaultwarden without a domain?

Only with a certificate the browser trusts. On a home network you can use Caddy's internal certificate authority and install its root certificate on every device, or use a tunnel or VPN service that provides an HTTPS address. A plain IP address over http loads the page, but the web vault shows This browser requires HTTPS to use the web vault and will not let you log in.

Keep reading

## Related articles

[Deployment10 min readn8n Docker Compose with PostgreSQL: The Settings That MatterA working n8n Docker Compose file with PostgreSQL, and the settings the quickstart leaves out: the encryption key, the webhook URL, the proxy hop count, the timezone and a pinned version. What breaks without each one, how to update without losing credentials, and what to back up.Oct 24, 2026Read](/blog/n8n-docker-compose)[Deployment14 min readVPS vs PaaS Cost: What a €5 Server Actually Costs YouThe price gap between a raw VPS and a managed platform is smaller than the pricing pages suggest. This itemises both sides: the hidden line items, the jobs, and who the processor is.Aug 19, 2026Read](/blog/vps-vs-paas-cost)[Deployment13 min readCoolify and Self-Hosted PaaS vs Managed: What You're Taking OnWhat you actually take on when the PaaS layer runs on your own server: the maintenance ledger, the one box that holds everything, and when it stops paying off.Aug 10, 2026Read](/blog/coolify-vs-managed-paas)

## Your app deserves to be online

€5 of credit on signup. Deploy in under a minute. No credit card needed.

[Start deploying](https://dashboard.runsite.app/login)[View documentation](https://docs.runsite.app)

---

Source: https://runsite.app/blog/vaultwarden-docker-compose
