Self-hosted Bitwarden-compatible password manager using Vaultwarden with a PostgreSQL backend. This template follows the repository hardening baseline: non-root containers, cap_drop: ALL, no-new-privileges, private IPC, read-only app root filesystem, internal database network, resource limits, and localhost-only web binding.
Vaultwarden is the community Rust implementation of the Bitwarden server API. It is not affiliated with Bitwarden, Inc.
Browsers / Bitwarden clients
|
Reverse proxy with HTTPS
|
127.0.0.1:8080
|
[vaultwarden-front] -- Vaultwarden (port 8080)
|
[vaultwarden-db] -- PostgreSQL 18
| Service | Image | Purpose |
|---|---|---|
vaultwarden |
vaultwarden/server:1.36.0-alpine |
Web vault, Bitwarden-compatible API, admin panel |
postgres |
postgres:18.4 |
PostgreSQL database on an internal-only Docker network |
| Port | Binding | Purpose |
|---|---|---|
${VAULTWARDEN_PORT:-8080} |
127.0.0.1 |
Reverse proxy target for web vault, API, and WebSocket notifications |
Vaultwarden 1.31+ serves WebSocket notifications on the main HTTP port, so this template does not expose the old 3012 listener.
cd vaultwarden
cp .env.example .envGenerate secrets:
openssl rand -hex 32 # POSTGRES_PASSWORD
openssl rand -hex 32 # POSTGRES_NON_ROOT_PASSWORD
docker run --rm -it vaultwarden/server:1.36.0-alpine /vaultwarden hash --preset owaspPaste the generated Argon2 PHC string into ADMIN_TOKEN in .env, keeping single quotes around it:
ADMIN_TOKEN='$argon2id$v=19$m=19456,t=2,p=1$...'Create networks and data directories:
docker network create vaultwarden-front
docker network create --internal vaultwarden-db
mkdir -p data/app data/db
sudo chown -R "$(id -u):$(id -g)" dataDeploy:
docker compose up -dPoint your reverse proxy at http://127.0.0.1:8080 and set DOMAIN=https://vault.example.com to the exact public URL.
The secure default is SIGNUPS_ALLOWED=false and INVITATIONS_ALLOWED=false.
For a fresh instance, use one of these bootstrap paths:
- Temporarily set
SIGNUPS_ALLOWED=true, create the owner account, then set it back tofalseand recreate the container. - Keep public signups disabled and use
/adminwithADMIN_TOKENto manage users. Configure SMTP first if you want email invitations to work cleanly.
Do not leave public signups enabled on an internet-facing password manager.
Caddy example:
vault.example.com {
reverse_proxy http://127.0.0.1:8080
}The proxy must preserve WebSocket upgrades for /notifications/hub; Caddy does this automatically. If your proxy writes access logs to disk or forwards them to a log store, redact access_token query parameters under /notifications/*.
ADMIN_TOKENshould be an Argon2 PHC string generated byvaultwarden hash, not a plain text password.DOMAINmust be HTTPS for Web Crypto, WebAuthn/U2F, attachments, and client links.HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=trueprotects internal services from favicon-fetch SSRF-style probes.ORG_CREATION_USERS=noneis the locked-down default. Change it to a comma-separated email allowlist orallif users need to create organizations.- SMTP is optional, but recommended for invitations, account verification, email 2FA, emergency access, and device emails.
Back up both PostgreSQL and data/app. The app directory contains attachments, Sends, icon cache, RSA keys, and local config overrides created by the admin panel.
mkdir -p backups
set -a
. ./.env
set +a
docker compose exec -T postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > "backups/vaultwarden-$(date +%Y%m%d).sql"
tar -czf "backups/vaultwarden-data-$(date +%Y%m%d).tar.gz" data/app .envStore backups encrypted and test restores before relying on them.
Images are pinned. Update by changing tags through Renovate or manually, then:
docker compose pull
docker compose up -d
docker compose logs -f vaultwardenCheck upstream release notes before major upgrades. Password-manager updates are worth treating with more suspicion than a casual dashboard bump.