DB-유

Running Bitwarden (bitwarden_rs) on Synology

· synology, docker, bitwarden

I wanted my passwords at home, not on somebody else's server. Bitwarden's official server is a pile of ASP.NET containers with Microsoft SQL Server behind them, far too heavy for a NAS. bitwarden_rs is an unofficial Rust reimplementation of the server API on SQLite. It's one container, a few dozen megabytes of RAM, and the official apps and extensions work with it unchanged.

I run it on DSM 6.2 behind the built-in reverse proxy with a valid certificate, which the clients insist on; they refuse plain HTTP except on localhost. That means a DNS name like vault.example.com with a certificate in Control Panel → Security → Certificate (Let's Encrypt via DSM works if port 80 is reachable), and either 443 forwarded to the NAS or a VPN for outside access. Docker and SSH are assumed.

Folder and compose file

sudo mkdir -p /volume1/docker/bitwarden/data

/volume1/docker/bitwarden/docker-compose.yml:

version: '2'
services:
  bitwarden:
    image: bitwardenrs/server:latest
    container_name: bitwarden
    restart: unless-stopped
    ports:
      - "8480:80"
      - "3012:3012"
    environment:
      DOMAIN: https://vault.example.com
      SIGNUPS_ALLOWED: "true"
      WEBSOCKET_ENABLED: "true"
      ADMIN_TOKEN: a-long-random-string
      LOG_FILE: /data/bitwarden.log
      SMTP_HOST: smtp.example.com
      SMTP_FROM: vault@example.com
      SMTP_PORT: "587"
      SMTP_SSL: "true"
      SMTP_USERNAME: vault@example.com
      SMTP_PASSWORD: secret
    volumes:
      - /volume1/docker/bitwarden/data:/data
  • Port 8480 is the web and API port; 3012 is the WebSocket port used for live sync between clients.
  • DOMAIN must be the exact external URL, otherwise attachments and two-factor enrolment generate wrong links.
  • ADMIN_TOKEN enables the /admin page; generate it with openssl rand -base64 48.
  • SMTP is optional but needed for invitations and email two-factor.
cd /volume1/docker/bitwarden
sudo docker-compose up -d

DSM reverse proxy

Control Panel → Application Portal → Reverse Proxy → Create. Two rules are needed, one for the application and one for the WebSocket path.

Rule 1:

Description:  bitwarden
Source:       HTTPS, vault.example.com, port 443, HSTS on
Destination:  HTTP, localhost, port 8480

Rule 2:

Description:  bitwarden-notifications
Source:       HTTPS, vault.example.com, port 443, path /notifications/hub
Destination:  HTTP, localhost, port 3012

In rule 2, open Custom Header → Create → WebSocket, which adds the Upgrade and Connection headers. DSM 6.2.2 and later offer this in the reverse proxy dialog directly. Then in Control Panel → Security → Certificate → Configure, assign the vault.example.com certificate to both reverse proxy entries.

Open the firewall for 443 from wherever clients will connect, and on the router forward 443 to the NAS. Do not forward 8480 or 3012; they should only be reached through the proxy.

Accounts, then lock signups

Browse to https://vault.example.com, click Create Account, and register with a strong master password. Repeat for each household member or colleague. Then close registration by changing the compose file:

      SIGNUPS_ALLOWED: "false"

and docker-compose up -d. From now on, new users can only join by invitation from the admin page at https://vault.example.com/admin, which asks for the ADMIN_TOKEN.

Enable two-factor on your account under Settings → Two-step Login in the web vault; TOTP works out of the box, and YubiKey and Duo need extra environment variables documented in the project wiki.

Pointing the clients at it

In the Bitwarden browser extension or mobile app, before logging in, open Settings (the gear on the login screen) and set Server URL to https://vault.example.com. Log in. The extension icon shows the vault; add a login and check it appears in the mobile app within a few seconds, which proves the WebSocket rule works.

Backups

Don't skip this section; it's the whole point of owning the server. Everything is in /volume1/docker/bitwarden/data: db.sqlite3, the attachments folder, rsa_key.* used to sign tokens, and config.json from the admin page. SQLite must be copied consistently, so use its backup command from a Task Scheduler script running nightly as root:

#!/bin/sh
DEST=/volume1/backup/bitwarden
mkdir -p "$DEST"
/usr/local/bin/docker exec bitwarden sqlite3 /data/db.sqlite3 ".backup '/data/db-backup.sqlite3'"
cp /volume1/docker/bitwarden/data/db-backup.sqlite3 "$DEST/db-$(date +%F).sqlite3"
cp -r /volume1/docker/bitwarden/data/attachments "$DEST/"
cp /volume1/docker/bitwarden/data/rsa_key* "$DEST/"
find "$DEST" -name 'db-*.sqlite3' -mtime +30 -delete

Then include /volume1/backup in Hyper Backup with client-side encryption. Also export the vault from the web UI occasionally as an encrypted JSON; it is the recovery path that needs no server at all.

Is it alive?

curl -s https://vault.example.com/alive
sudo docker logs bitwarden | tail -20

/alive returns the current time in quotes when the API is up. In the browser, the web vault loads with a padlock and no certificate warning. In the extension, Settings → Sync → Sync Vault Now completes without error. Change an item on one device and confirm it appears on another without a manual sync.

Errors

  • The web vault loads but login fails with "An error has occurred. Cannot read property of undefined" or similar. DOMAIN does not match the URL in the browser, often http versus https or a missing subdomain. Fix it and restart the container.
  • Live sync does not work; changes appear only after a manual sync. The WebSocket rule is missing, has the wrong path, or lacks the WebSocket custom headers. Check with docker logs bitwarden | grep -i websocket for connection attempts.
  • Mobile app says "Failed to fetch" while the browser works. The phone is off the LAN and 443 is not forwarded, or the DSM firewall blocks the phone's network. Test with mobile data.
  • Error: database is locked in the container log during backups. The sqlite3 binary is running against a busy database; the .backup command handles this, but plain cp of db.sqlite3 does not. Use the script above rather than copying the live file.

The container itself has been trouble-free. What I check now and then is that the certificate renewed and that the nightly backup is actually producing files.