Vaultwarden
Vaultwarden is a self-hosted, Bitwarden-compatible password manager. It is a single container with a SQLite database, which makes it the best first stack to deploy: one service, one secret, one volume, one route.
What you'll learn
- A standalone secret read from a file inside the container.
- A drive-backed volume that follows the container between nodes.
- A hardened network with
allowed: []. - An action that doubles as a pre-replication hook (
volume_sync), so the copy of the database on other nodes is consistent. - A public HTTPS proxy route.
Prerequisites
Cluster. Networks public (type public, domain example.com) and wan (type egress), and the replicated drive apps. See the cluster prerequisites.
DNS. vault.example.com. It is a public route, so the name must sit inside a Cloudflare zone you configured if you want Clouduler to publish the record.
Secrets. Create this one before the first deploy:
| Name | What it is | Suggested setting |
|---|---|---|
vaultwarden_admin_token | Token for the /admin panel. | Paste string with a password you will keep, if you want to use /admin. Otherwise Generate, charset alphanumeric, length 64. A generated value is never shown again. |
Configs. None.
Volumes. apps/vaultwarden holds the whole data directory (/data): the database, attachments and the backup snapshots.
Stack YAML
# Vaultwarden: self-hosted Bitwarden-compatible password manager.
# Served at https://vault.example.com; admin panel at /admin.
networks:
# Hardened: the vault has no reason to talk to any other stack. allowed: []
# denies everything except the stack itself on public: the proxy returns 403
# for other hosts' routes on public, and the host firewall drops other overlay
# traffic. wan has no allowed list, so icon fetching, breach checks and the
# push relay keep working.
public:
allowed: []
wan: {}
secrets:
vaultwarden_admin_token:
services:
vaultwarden:
image: vaultwarden/server:latest
volumes:
- apps/vaultwarden:/data
secrets: [vaultwarden_admin_token]
environment:
LOG_LEVEL: warn
ENABLE_DB_WAL: "false"
ADMIN_TOKEN_FILE: /run/secrets/vaultwarden_admin_token
networks: [public, wan]
restart: unless-stopped
deploy:
replicas: 1
resources:
limits:
memory: 256M
x-clouduler:
# Backup writes a consistent db_*.sqlite3 snapshot into /data. It runs
# before every replication of the volume, and only the 3 newest snapshots
# are kept.
actions:
Backup:
cmd: /vaultwarden backup && ls -1t /data/db_*.sqlite3 | tail -n +4 | xargs -r rm -f
timeout: 1m
volume_sync:
vaultwarden:
before: Backup
proxy:
- host: vault
network: public
port: 80
tls: true
Config files
None. This stack has no configs: block.
Walkthrough
networks.public.allowed: []lets the stack reach nothing else throughpublic. The proxy answers 403 for other stacks' routes, and the firewall drops other overlay traffic. See Allowed hosts and ports.networks.wan: {}is an egress network with noallowedlist, so outbound traffic is unrestricted. The vault needs it for icons and breach checks.secrets:with a name and no value declares a standalone secret. The stack refers to it by name only, so it must already exist in the cluster. Nothing is bundled from a file.secrets: [vaultwarden_admin_token]on the service mounts it at/run/secrets/vaultwarden_admin_token.ADMIN_TOKEN_FILEpoints the container at that path, so the token never appears in the environment.apps/vaultwarden:/datais a drive-backed volume:appsis the drive,vaultwardenis the volume inside it. Whatever is written to/datais replicated to every node that holds theappsdrive, so the container can start on another node if its node fails. See Drives and volumes.restart: unless-stoppedis the Docker restart policy. Without it the policy isno.x-clouduler.actions.Backupis a named command. It shows up as a Run button in the Service actions panel of the container's page (open it from a row in the stack's Containers tab; admins only), andvolume_synccan use it as a hook.volume_sync.vaultwarden.before: BackuprunsBackupinside the running container before each scheduled replication of the volume namedvaultwarden. The key is the volume name, not the drive name. If the hook fails, that replication is skipped.x-clouduler.proxy:host: vaultgets thepublicnetwork's domain appended, so the route isvault.example.com.port: 80is the container port. See Proxy and TLS.
Deploy
Create the secret first, then deploy.
- Web UI
- CLI / config
- Open Settings, Secrets and configs, click Create secret, name it
vaultwarden_admin_token, choose Paste string (or Generate) and save. - Open Stacks, click Deploy stack, name it
vaultwarden, paste the YAML into the Spec editor. - Wait for Spec is valid, check What it needs lists the secret, the drive and the networks, then click Deploy vaultwarden.
printf '%s' 'choose-a-long-admin-password' | clouduler secret set vaultwarden_admin_token
clouduler stack deploy ./vaultwarden.yaml
clouduler stack list
First run
-
Open
https://vault.example.comand create your account. This stack does not close sign-up, so create your account first, then check the Vaultwarden documentation for theSIGNUPS_ALLOWEDsetting to close it. -
Open
https://vault.example.com/adminand enter the admin token. If you generated the token and did not copy it, set a new value (saving an existing secret name overwrites it) and restart the instance. -
Open the container from the stack's Containers tab and run the Backup action once from Service actions, then confirm a snapshot exists:
clouduler exec vaultwarden.vaultwarden.0 -- ls -1 /data -
In your Bitwarden clients, set the self-hosted server URL to
https://vault.example.com.
Quirks
- Memory. 256M is a comfortable limit for a handful of users. There is no healthcheck, so a hung process that is still running is not reported unhealthy.
- Admin token. A plain token works. Upstream recommends storing an Argon2 hash instead.
- Pin the image.
latestis convenient to start with. Pin a version once it works, and update from the Updates page.