Sign inSign up

cuza/bitwarden-sync

By cuza

•Updated 11 days ago

Daily encrypted Bitwarden and Vaultwarden backups, with an optional mirror into a second vault

Image
0

1.2K

cuza/bitwarden-sync repository overview

⁠bitwarden-sync

bitwarden-sync runs once a day and does one or both of these jobs for a Bitwarden or Vaultwarden account:

  • It writes password-encrypted exports of your personal vault and of each organization that lets you export its vault.
  • It mirrors your personal vault into a second account. The second account can be on the same server or a different one, so a Vaultwarden vault can mirror into bitwarden.com or the other way around.

Backups are on by default. Setting any DEST_BW_* variable turns the mirror on.

⁠Setup

Each example below has two files. Put docker-compose.yml and .env in the same folder, then start the container:

docker compose up -d
docker compose logs -f

The container checks its settings when it starts, including that the backups folder is writable and that the source and destination are different accounts, then logs the time of the next run. A missing or invalid setting stops the container with a message that names the problem. To run once right away, use docker compose run --rm -e RUN_AT= bitwarden-sync.

The client ID and secret for each account come from the web vault, under Settings, Security, Keys, View API key.

⁠Examples

⁠Backup only

This example writes an encrypted backup every night at 03:00 and keeps the last 30.

docker-compose.yml:

services:
  bitwarden-sync:
    image: cuza/bitwarden-sync:latest
    restart: unless-stopped
    env_file: .env
    tmpfs: /tmp
    volumes:
      - ./backups:/backups

.env:

SOURCE_BW_SERVER=https://vault.example.com
SOURCE_BW_CLIENTID=user.00000000-0000-0000-0000-000000000000
SOURCE_BW_CLIENTSECRET=your-api-key-secret
SOURCE_BW_PASSWORD=your-master-password
BACKUP_PASSWORD=a-long-passphrase-for-the-backup-files
BACKUP_KEEP=30
RUN_AT=03:00
TZ=America/Toronto

Run mkdir -p backups before the first start, so the folder belongs to your user and not to root.

⁠Mirror only

This example copies the personal vault of vault.example.com into a bitwarden.com account every night and writes no backup files. It needs no volumes: section.

docker-compose.yml:

services:
  bitwarden-sync:
    image: cuza/bitwarden-sync:latest
    restart: unless-stopped
    env_file: .env
    tmpfs: /tmp

.env:

SOURCE_BW_SERVER=https://vault.example.com
SOURCE_BW_CLIENTID=user.00000000-0000-0000-0000-000000000000
SOURCE_BW_CLIENTSECRET=your-api-key-secret
SOURCE_BW_PASSWORD=your-master-password
DEST_BW_SERVER=https://vault.bitwarden.com
DEST_BW_CLIENTID=user.11111111-1111-1111-1111-111111111111
DEST_BW_CLIENTSECRET=other-api-key-secret
DEST_BW_PASSWORD=other-master-password
SKIP_BACKUP=true
RUN_AT=03:00
TZ=America/Toronto
⁠Backup and mirror

This example writes the backup first and then updates the mirror. If the mirror step fails, that night's backup still exists.

docker-compose.yml is the same as in "Backup only". .env:

SOURCE_BW_SERVER=https://vault.example.com
SOURCE_BW_CLIENTID=user.00000000-0000-0000-0000-000000000000
SOURCE_BW_CLIENTSECRET=your-api-key-secret
SOURCE_BW_PASSWORD=your-master-password
BACKUP_PASSWORD=a-long-passphrase-for-the-backup-files
BACKUP_KEEP=30
DEST_BW_SERVER=https://vault.bitwarden.com
DEST_BW_CLIENTID=user.11111111-1111-1111-1111-111111111111
DEST_BW_CLIENTSECRET=other-api-key-secret
DEST_BW_PASSWORD=other-master-password
RUN_AT=03:00
TZ=America/Toronto

⁠Settings

VariableDefaultPurpose
SOURCE_BW_SERVERhttps://vault.bitwarden.comURL of the vault to back up and copy from.
SOURCE_BW_CLIENTID, SOURCE_BW_CLIENTSECRETrequiredAPI key of the source account.
SOURCE_BW_PASSWORDrequiredMaster password of the source account.
BACKUP_PASSWORDrequired unless SKIP_BACKUP=truePassphrase that encrypts the backup files.
SKIP_BACKUPunsetSet to true to turn backups off. The mirror must be on.
BACKUP_KEEP30Number of backup runs to keep. 0 keeps every run.
DEST_BW_SERVERhttps://vault.bitwarden.comURL of the vault to mirror into.
DEST_BW_CLIENTID, DEST_BW_CLIENTSECRET, DEST_BW_PASSWORDunsetAPI key and master password of the account to mirror into. Setting any DEST_BW_* variable turns the mirror on.
RUN_AT03:00Daily run time as HH:MM. An empty value runs once and exits.
TZUTCTime zone for RUN_AT, such as America/New_York.

Keep all settings in .env. A value set under environment: in the compose file overrides the same variable in .env. Write values without quotes, because docker run --env-file keeps quotes as part of the value.

⁠Backups

Each run writes a folder named after its start time, such as backups/2026-10-23T030000/. The folder holds personal.json and one org-<id>.json file per organization. If you own or administer an organization, its export must succeed or the run fails. If you are an ordinary member and the export fails, which usually means the organization doesn't grant you export permission, the run skips it and logs a warning.

The script writes each run into a temporary folder and renames it only after every export succeeds. A failed run leaves the older backups as they were. If the source vault returns zero items, the run stops before it writes or deletes anything.

To restore a backup, open any Bitwarden or Vaultwarden web vault and go to Tools, Import data. Choose "Bitwarden (json)", upload the file and enter BACKUP_PASSWORD. Keep that passphrase somewhere outside the vault, because nobody can open the backup files without it.

⁠Mirror

The mirror copies personal items and folders. It notes which personal items and folders the destination holds, imports a plain JSON export of the source, and then deletes the noted items with the server's bulk-delete API, 500 items per request. If the import fails, the destination keeps its previous copy. If a delete fails, the destination holds duplicates until the next run removes them. Organization items stay untouched on both sides. If the source export has no personal items, the run stops before it changes the destination.

The plain export lives in /tmp. The compose file mounts /tmp as tmpfs, so the export stays in memory, and the script deletes it when the run ends.

Every run re-imports the whole personal vault, so item IDs in the destination change each time, and edits made in the destination are lost on the next run.

⁠Limits

The Bitwarden export format leaves out file attachments and Sends, so this image doesn't back them up or copy them.

⁠Image

The image uses node:24-alpine and Bitwarden CLI 2026.10.0, with builds for linux/amd64 and linux/arm64. It runs as the node user with uid 1000, so the backups folder must be writable by that uid. dumb-init runs as the first process, so docker stop ends the container at once.

Tag summary

Content type

Image

Digest

sha256:54140d987…

Size

77.4 MB

Last updated

11 days ago

docker pull cuza/bitwarden-sync