Perspective V Docs

Vaultwarden Guide

Vaultwarden password manager — single-container deployment behind the shared edge Traefik, backed by the shared Postgres.

Vaultwarden is the self-hosted password manager (a Rust, Bitwarden-compatible server). It runs as a single container behind the shared edge Traefik, using the shared Postgres. This page covers the VPS deployment as it was stood up; a dev bring-up is identical except for env values and domain.

Source Of Truth

  • Active compose: runtime/stacks/infrastructure/vaultwarden/docker-compose.yml
  • VPS env: runtime/environments/vps/infrastructure/vaultwarden/.env
  • Stack README: runtime/stacks/infrastructure/vaultwarden/README.md

There is no runtime/scripts/infrastructure/vaultwarden.{sh,bat} launcher; Vaultwarden is driven directly with docker compose.

Architecture

ContainerImagePortRole
vaultwardenvaultwarden/server:1.36.0 (pinned)80Web vault + API + WebSocket notifications
  • One internal HTTP listener on :80 serves the web vault, the API, and live-sync WebSocket. The legacy separate 3012 port is gone on modern images.
  • Networks: joins proxy (Traefik) and postgres-network (the DB).
  • TLS terminates at Traefik (websecure + Let's Encrypt); the backend is plain HTTP.
  • Persistent state lives in the vaultwarden-data volume mounted at /data (RSA key, attachments, sends, icon cache, config.json).
  • The image is pinned and Watchtower is disabled — a password manager should never auto-update under you.

Why native env names

Vaultwarden reads its own native variable names (DATABASE_URL, DOMAIN, ADMIN_TOKEN, …), not the VAULT_* convention used elsewhere in this repo. The env file is therefore written in Vaultwarden's names, with the raw DB credential parts kept as reference comments. Two consequences:

  • DATABASE_URL embeds the DB password percent-encoded ($→%24, &→%26, %→%25, *→%2A). The percent-encoding is only for the URL — the actual Postgres role keeps the raw password.
  • Compose ${...} interpolation does not read the service env_file:, so VAULT_HOST (used in the Traefik Host() label) must come from --env-file on the CLI.

Prerequisites

  • Shared proxy network and the edge Traefik running (see Edge).
  • Shared Postgres running on postgres-network (see Databases).
  • A DNS record for the external domain (vault.perspective-v.com) resolving to the host.

Setup

Unlike Zitadel, Vaultwarden does not create its own database or role — provision them first.

  1. Provision the database (one-time, against the postgres container; the raw, un-encoded password is used here as a SQL literal):

    docker exec -it postgres psql -U postgres
    CREATE ROLE vaultwarden_db_usr WITH LOGIN PASSWORD '<raw-db-password>';
    CREATE DATABASE vaultwarden_db OWNER vaultwarden_db_usr;
  2. Generate the admin token for the /admin panel and paste the $argon2id$... string (single-quoted) into ADMIN_TOKEN in the env file. Leave it empty to disable the panel.

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

    vaultwarden hash needs a real TTY. In a non-interactive shell, generate an equivalent Argon2id PHC string with any argon2 tool — Vaultwarden verifies against the parameters embedded in the string, so the exact m/t/p values need not match the bitwarden preset.

  3. Bring the stack up (the --env-file is required so ${VAULT_HOST} resolves in the Traefik label):

    cd runtime/stacks/infrastructure/vaultwarden
    docker compose --env-file ../../../environments/vps/infrastructure/vaultwarden/.env up -d
  4. Register the first user at https://vault.perspective-v.com/ while SIGNUPS_ALLOWED=true, then tighten signups (see Signup And Org-Ownership Policy) and re-run the bring-up command to apply.

SMTP And Deliverability

SMTP is configured from the same mxrouting mailbox the Zitadel stack uses (implicit TLS on port 465 → SMTP_SECURITY=force_tls):

Vaultwarden varValue
SMTP_HOST / SMTP_PORTfusion.mxrouting.net / 465
SMTP_SECURITYforce_tls
SMTP_FROM / SMTP_FROM_NAMEno-reply@perspective-v.com / Perspective-V Vault
SMTP_USERNAMEno-reply@perspective-v.com

Vaultwarden only contacts the SMTP server when it actually sends mail; a clean startup does not prove SMTP works. Test it from the /admin panel (Diagnostics → SMTP) or by triggering a verification email.

Outlook/Hotmail deliverability. Mail to consumer Outlook addresses was silently filtered even though mxrouting accepted it and SPF + DKIM both pass. Delivery to Gmail works. Root cause is Microsoft's aggressive filtering of a new sending domain combined with a missing DMARC record. The send path itself is correct.

Domain-auth status for perspective-v.com:

RecordStatus
SPF✅ v=spf1 include:mxroute.com -all (covers mxroute's sending IPs)
DKIM✅ selector x._domainkey.perspective-v.com published
DMARC❌ none — add _dmarc.perspective-v.com TXT "v=DMARC1; p=none; rua=mailto:postmaster@perspective-v.com" to improve Outlook deliverability

Creating Organizations

ORG_CREATION_USERS is unset, which means all users may create organizations (blank/all = allowed; none or an email allowlist would restrict it).

The "New organization" button is not on the main vault page in the current bundled web vault — it moved into the org/product switcher (top-left). If you can't find it, go straight to the route:

https://vault.perspective-v.com/#/create-organization

This always works and lands on the Free plan (Vaultwarden patches out the cloud billing step). The button being hidden is an upstream web-vault UI condition, not a backend permission issue.

Validate

docker inspect -f '{{.State.Health.Status}}' vaultwarden          # healthy
docker exec vaultwarden curl -fsS http://127.0.0.1:80/alive       # RFC3339 timestamp
curl -s -o /dev/null -w "%{http_code}\n" https://vault.perspective-v.com/alive   # 200

The web vault loads at https://vault.perspective-v.com/ with a valid Let's Encrypt certificate; /admin prompts for the admin token.

Signup And Org-Ownership Policy

SIGNUPS_ALLOWED is currently open to bootstrap the first accounts. The intended end state is Option C — only @perspective-v.com users own organizations; they invite external members (gmail, hotmail, …). The mechanics below are confirmed against the Vaultwarden 1.36.0 source, and they determine exactly how far that intent can be enforced declaratively.

How the three settings actually behave

SettingBehavior (verified in source)
SIGNUPS_DOMAINS_WHITELISTGates self-registration by the email's domain (exact match after @, lowercased, comma-separated). It overrides SIGNUPS_ALLOWED. Invited users bypass it entirely.
INVITATIONS_ALLOWEDOrg owners/admins can invite any email as a member, even one outside the whitelist and even when signups are off.
ORG_CREATION_USERSWho may create an organization. Blank/all = everyone, none = nobody, otherwise an exact, full-email allowlist (a@x.com,b@x.com). No domain wildcard — perspective-v.com as a value would match nobody.

The invite half of Option C is fully native: INVITATIONS_ALLOWED=true + invited users bypass the domain whitelist. The org-ownership half is the catch — because ORG_CREATION_USERS takes exact emails only, "any @perspective-v.com user may own an org" cannot be expressed in one setting. That forces a choice between two enforcement levels:

SIGNUPS_ALLOWED=true
SIGNUPS_DOMAINS_WHITELIST=perspective-v.com
INVITATIONS_ALLOWED=true
# ORG_CREATION_USERS left blank (all registered users may create orgs)

Only @perspective-v.com addresses can self-register, so in practice only perspective-v people obtain org-creating accounts; external users exist solely via invitation. Residual gap: an invited external member could also create their own organization, because ORG_CREATION_USERS=all applies to every existing account regardless of how it was created. Low risk (you control who gets invited) and zero ongoing maintenance.

Strict

SIGNUPS_ALLOWED=true
SIGNUPS_DOMAINS_WHITELIST=perspective-v.com
INVITATIONS_ALLOWED=true
ORG_CREATION_USERS=owner1@perspective-v.com,owner2@perspective-v.com

Only the explicitly listed emails can create organizations — an invited external member is structurally barred. Cost: the allowlist has no wildcard, so every new perspective-v owner must be added to ORG_CREATION_USERS by hand and the stack re-upped.

Open decision: pragmatic vs strict. Both pin self-registration to @perspective-v.com and allow external members by invite — that part is settled. The only undecided point is whether to also hard-restrict org creation to a maintained email allowlist (strict) or accept that any registered user may create one (pragmatic). Until chosen, signups remain fully open; apply either block above and re-run the bring-up command.

Maintenance

# Apply env changes (e.g. after editing signup policy or SMTP)
docker compose --env-file ../../../environments/vps/infrastructure/vaultwarden/.env up -d

# Update: bump the image tag in docker-compose.yml, then re-up
docker compose --env-file ../../../environments/vps/infrastructure/vaultwarden/.env up -d

# Stop (keeps data); add -v ONLY to also delete the vaultwarden-data volume
docker compose --env-file ../../../environments/vps/infrastructure/vaultwarden/.env down

Important Rules

  • The image is pinned — updates are deliberate. Bump the tag, review the Vaultwarden release notes, then re-up.
  • Never delete the vaultwarden-data volume without a backup — it holds the RSA key and all attachments/sends. Losing the RSA key invalidates sessions.
  • Changing DOMAIN after users register invalidates WebAuthn/passkeys registered against the old origin.
  • ADMIN_TOKEN is single-quoted in the env file because the Argon2 PHC string contains $.

On this page