Opens Claude Code with this recipe loaded — it won't run anything until you review the prompt and press Enter. No Claude Code? Use Copy command.

Experimental: this pattern has not yet been validated end-to-end by a maintainer. Apply extra scrutiny and report what breaks.

Move your family off 1Password to self-hosted Vaultwarden

Context

Use this pattern when you want a self-hosted password manager for a household: a handful of users, shared credentials (streaming, utilities, the kids’ school portal), and a plan for what happens if the person running the server is unavailable. Vaultwarden is a lightweight Rust reimplementation of the Bitwarden server API — you self-host the server, and everyone uses the official Bitwarden apps and browser extensions against it. Features that are paid in Bitwarden (organizations, emergency access) are included for free.

This is a pattern where operational discipline matters more than the install. A password manager is the one service where losing data or serving a broken TLS setup actually hurts. Take the backup section seriously before you move real credentials in.

Decisions explained

Why Vaultwarden over the official Bitwarden server. The official server is a multi-container .NET deployment aimed at enterprises and wants far more RAM, plus a paid license for the family features. Vaultwarden implements nearly the complete Bitwarden client API in a single small container with SQLite, is actively maintained, and explicitly targets “individuals, families, and small organizations”. Every official Bitwarden client works with it.

Why a public domain instead of Tailscale-only. Browser extensions and mobile autofill need to reach the server from every device your family uses, including devices you don’t control (work laptops, in-laws’ phones). Requiring Tailscale on every family device is a non-starter. Vaultwarden’s security model assumes a hostile network anyway: the vault is end-to-end encrypted, the server only stores ciphertext. Expose it publicly over HTTPS, disable signups, and keep the admin panel locked down.

Why SQLite. It is the most widely used and tested backend, recommended by the project for most users, and makes backups a single-file affair. A family will never hit its limits.

Why Dokploy. Consistent with the rest of Thuishaven: Traefik routing and Let’s Encrypt certificates come from the dokploy-bootstrap pattern; this pattern only adds a service.

Step-by-step

Assumes a server bootstrapped with dokploy-bootstrap and a DNS record for your chosen hostname (e.g. vault.example.com) pointing at the server.

1. Generate the admin token

The admin panel (/admin) is enabled by setting ADMIN_TOKEN. Store it as an Argon2 hash, not plaintext. On any machine with Docker:

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

Enter a long random password (generate one with openssl rand -base64 32 and save it somewhere safe — this is what you’ll type into /admin). The command prints an ADMIN_TOKEN='$argon2id$...' line. Keep the hash for step 2.

2. Deploy via the Dokploy API

This pattern is API-first: an agent drives the deploy over the Dokploy HTTP API using DOKPLOY_API_TOKEN / DOKPLOY_URL from dokploy-bootstrap step 9. A condensed click-through fallback for humans is at the end. The call sequence is stable; confirm exact endpoint and field names at $DOKPLOY_URL/api/swagger (versioned — written against Dokploy 0.29.8; the API path is pending a clean end-to-end run, see issue #2).

First the environment. DOMAIN must be the exact public URL including https://; ADMIN_TOKEN is the Argon2 hash from step 1:

DOMAIN=https://vault.example.com
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$...'   # the hash from step 1
SIGNUPS_ALLOWED=true            # temporarily — turned off in step 4
SMTP_HOST=smtp.example.com
SMTP_FROM=vault@example.com
SMTP_PORT=587                   # prefer 587; 465/25 are blocked outbound on many VPS providers (incl. Hetzner)
SMTP_SECURITY=starttls
SMTP_USERNAME=vault@example.com
SMTP_PASSWORD=your-smtp-password

Then create, configure, and deploy:

auth header:  x-api-key: $DOKPLOY_API_TOKEN     base: $DOKPLOY_URL/api

project.create                  { name: "family" }                      -> projectId
application.create              { projectId, name: "vaultwarden" }       -> applicationId
application.saveDockerProvider  { applicationId, dockerImage: "vaultwarden/server:1.36.0" }
mounts.create                   { serviceId: applicationId, type: "volume",
                                  mountPath: "/data" }   # persists db, attachments, keys
application.saveEnvironment     { applicationId, env: <the env block above> }
domain.create                  { applicationId, host: <your host>, port: 80,
                                 https: true, certificateType: "letsencrypt" }
application.deploy             { applicationId }
  • Pin the image (vaultwarden/server:1.36.0) — never latest for a password manager; read release notes before upgrading.
  • The /data volume is mandatory: SQLite database, attachments, RSA keys, and admin config all live there, and you lose the vault on redeploy without it.
  • Container port is 80 (Vaultwarden listens on 80 internally). WebSocket live-sync runs over the same port; Traefik forwards the upgrade headers automatically.
  • If you ever move the env into a compose file, escape every $ in the Argon2 hash as $$.
  • Dokploy gives the app a random container name; resolve applicationId → appName via application.one to find its logs.
  • CDN-proxied DNS: DNS-only during first certificate issuance, then re-proxy with SSL mode Full (strict). See dokploy-bootstrap step 6.
  • SMTP is effectively required: organization invites, email verification, and emergency-access notifications all go through email.

3. Create your account and the family organization

  1. Open https://vault.example.com, create your own account. Use a strong master password — it cannot be recovered, only reset by deleting the account.
  2. In the web vault: New organization (e.g. “Family”). Create collections for shared credential groups, e.g. Shared — Household, Shared — Streaming, Shared — Kids.
  3. Invite family members from Organization → Members. Invites arrive by email (this is why SMTP matters) and expire after 5 days. Each member creates their own account with their own master password; you confirm them after they accept.
  4. Move shared credentials into collections; per-member access is controlled per collection.

4. Lock down signups

Once everyone has an account, set in the Environment tab:

SIGNUPS_ALLOWED=false

and redeploy. Organization invites keep working (INVITATIONS_ALLOWED defaults to true), so you can still add people later — but strangers who find your instance can no longer register.

Note: settings previously saved through the /admin panel persist in /data/config.json and override environment variables. If a setting doesn’t seem to take effect, check the admin panel.

5. Set up emergency access

Vaultwarden supports Bitwarden’s Emergency Access (enabled by default). For each adult:

  1. Web vault → Settings → Emergency accessAdd emergency contact.
  2. Grant your partner (or another trusted person) Takeover or View access with a waiting period (e.g. 7 days).
  3. The contact accepts the invite; you confirm them.

This answers “what if the server admin is hit by a bus”: the vault can be recovered by family without anyone knowing your master password. Test the request flow once so both sides know what it looks like.

6. Configure clients

On every device, install the official Bitwarden app or browser extension. Before logging in: on the login screen, open the server/region selector (“Logging in on”), choose Self-hosted, and enter https://vault.example.com as the Server URL. Then log in normally. CLI: bw config server https://vault.example.com.

7. Migrate from 1Password

  1. Export from 1Password (per vault, .1pux or CSV).
  2. In the Vaultwarden web vault: Tools → Import data, select the matching 1Password format, import into your personal vault (or directly into an organization collection for shared items).
  3. Spot-check entries with TOTP seeds, custom fields, and attachments — these are the usual casualties of export formats. Attachments don’t survive any export; re-upload them manually.
  4. Have each family member do the same with their own export.
  5. Only after a week or two of real use: delete the 1Password data and cancel the subscription.

8. Automate backups

Back up the SQLite database with SQLite’s online backup (never a plain cp of a live database), plus the attachments and keys. Vaultwarden ships a built-in backup command. On the server, create /usr/local/bin/vaultwarden-backup.sh:

#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR=/var/backups/vaultwarden
CONTAINER=$(docker ps --filter ancestor=vaultwarden/server:1.36.0 --format '{{.Names}}' | head -n1)
mkdir -p "$BACKUP_DIR"
# Writes a consistent db backup inside /data, then copy the whole data dir out
docker exec "$CONTAINER" /vaultwarden backup
docker cp "$CONTAINER":/data "$BACKUP_DIR/data-$(date +%F)"
tar -czf "$BACKUP_DIR/vaultwarden-$(date +%F).tar.gz" -C "$BACKUP_DIR" "data-$(date +%F)"
rm -rf "$BACKUP_DIR/data-$(date +%F)"
# Keep 14 days
find "$BACKUP_DIR" -name 'vaultwarden-*.tar.gz' -mtime +14 -delete
chmod +x /usr/local/bin/vaultwarden-backup.sh
(crontab -l 2>/dev/null; echo "30 3 * * * /usr/local/bin/vaultwarden-backup.sh") | crontab -

Then get the archives off the server — sync /var/backups/vaultwarden to external storage (rclone to any S3/Backblaze/Drive target, or a restic repository). A backup on the same disk as the database is not a backup. The archive contains your encrypted vault plus config.json (which holds the admin token hash) — treat it as sensitive even though vault items are ciphertext.

Manual UI fallback

No API token, or prefer clicking? The deploy (step 2) in the Dokploy UI, condensed:

  1. Service — create a project (e.g. family), add an Application vaultwarden, provider Docker, image vaultwarden/server:1.36.0.
  2. Volume — add a persistent volume mounted at /data (mandatory — that’s the whole vault).
  3. Env — paste the env block from step 2 into the Environment tab.
  4. Domain — in the Domains tab add your host, container port 80, HTTPS via Let’s Encrypt; deploy. (Mind the CDN-proxied DNS note in step 2.)

The web-vault actions (account, organization, clients, migration — steps 3, 6, 7) are inherently GUI/client work regardless of path.

Verification

The assertions in the frontmatter are the source of truth. The two scriptable ones exit non-zero on failure:

#!/usr/bin/env bash
# verify.sh — DOMAIN must be exported (hostname, no scheme).
set -euo pipefail
: "${DOMAIN:?}"
curl -fsS --max-time 10 "https://$DOMAIN" >/dev/null         && echo "✓ site-https"
curl -fsS --max-time 10 "https://$DOMAIN/alive" | grep -q .  && echo "✓ serving-vaultwarden"
echo "All scriptable assertions passed."

A password manager’s important checks are inherently manual: true — they need real clients and a real second person. Do these by hand:

  • clients-sync — create an item on your phone; it appears in the browser extension within seconds (confirms WebSocket sync). Autofill works from a browser extension and mobile app pointed at your server URL.
  • member-isolation — a second family member logs in and sees shared collections but not your personal vault.
  • signups-locked — with SIGNUPS_ALLOWED=false deployed, the registration page refuses new sign-ups.
  • admin-accepts-tokenhttps://${DOMAIN}/admin asks for the admin password and accepts the one generated in step 1.
  • backup-restores — run the backup script, extract the archive on another machine, and verify the database opens: sqlite3 db.sqlite3 'select count(*) from users;'. An untested backup is a hypothesis.

Gotchas

  • HTTPS is mandatory, not a nicety. The web vault uses Web Crypto APIs that browsers only expose in secure contexts. Plain HTTP gives you a broken login page, not a degraded experience.
  • DOMAIN mismatch breaks invites. Invite links and several client flows are built from DOMAIN. It must match the public URL exactly, scheme included.
  • $ escaping in the Argon2 hash. The PHC hash is full of $ characters. In Dokploy’s env tab a single-quoted value is fine, but in a docker-compose file each $ must be doubled ($$) or compose will try variable interpolation and the token silently won’t match.
  • config.json overrides environment variables. Anything ever saved via the admin panel persists in /data/config.json and wins over env vars after restarts. If an env change seems ignored, that’s why.
  • Live-copying SQLite corrupts backups. A cp of db.sqlite3 while the server runs can produce a silently corrupt copy. Always go through /vaultwarden backup (built in since 1.32.1) or sqlite3 ... ".backup ...". When restoring, delete any db.sqlite3-wal file alongside the restored database first.
  • Master passwords are unrecoverable. No SMTP reset emails will save a forgotten master password. Emergency Access (step 5) is the designed recovery path — set it up before you need it.
  • Don’t use the latest tag. Read the release notes before upgrading a password manager. Vaultwarden releases occasionally include security fixes you want promptly, and rarely, behavior changes (e.g. 2FA remember tokens were invalidated in 1.35.5) you want to know about.

Maintenance notes

  • Updates: check vaultwarden releases roughly monthly; security advisories are called out prominently. Update by bumping the image tag in Dokploy and redeploying (also update the tag in the backup script). Vaultwarden runs database migrations automatically on startup.
  • Backups: verify quarterly that you can actually restore — extract the newest archive and open the database. An untested backup is a hypothesis.
  • Admin panel: you rarely need it after setup. For extra hardening, remove ADMIN_TOKEN (and the admin_token key in config.json) to disable /admin entirely, or block the /admin path at the proxy, and re-enable when needed.
  • Certificate renewal is automatic via Traefik/Let’s Encrypt; if clients suddenly refuse to connect, an expired certificate is the first thing to check.
  • Watch disk space: attachments and icon cache live in /data and grow slowly; the database itself stays small.