Bitwarden Lite Docker Backup

Hi There,

I am using Bitwarden Lite as Docker Image to run Bitwarden. I am now looking into the topic of how to backup my instance. For Bitwarden Lite, one needs to do the backups himself.

I have read the Article about backup of the normal on prem version, but this does not really help me, because the file structure seems to be different to my files.

Does anyone know, which files from the bitwarden folder in my Docker I need to backup for a full desaster recovery?

Should I copy the whole `data` folder mapped as volume from /volume1/docker/bw/data:/etc/bitwarden?

Next to the Files, I would just also do an export of the database.. Is that sufficient?

Depends on how you have Bitwarden Lite configured. Technically as long as you have a standalone exported backup of your vault you’d be fine with disaster recovery (imo), but if you want a full backup you could just backup the entire container. If you run a non-sqlite database in another container then you’d have to back that up as well.

1 Like

Thanks for your inputs. The thing with “having an exported backup of my vault” is, that I cannot automate this process as far as I know? So, I would periodically need to open the admin interface and create an export, to also have newly added Passwords in the backup. Or is there a way to automate that?

When you talk about “entire container”, do you mean the folder which is mapped to my host system?

I am running a MariaDB in another Container, so I guess, I would also need to backup that..

BR Jan

Why not use a disk imaging app (e.g., Carbon Copy Cloner)?

I haven’t actually tried to restore the program and database folders after deleting them to see if that’s sufficient, since I don’t mind making a standalone vault backup. Yes, I was referring to the folders that are mapped to your host system, both the program itself and the database mappings. I have my doubts the recovery would be smooth since many programs that backup folder structures filter files they don’t feel are necessary, but it can’t hurt. If you are running on a computer I like @grb’s idea of disk imaging. I image my Windows computers and those can be up and running as if nothing happened in about 90 seconds after a failure.

I’ve actually done some digging into this as part of setting up Bitwarden Lite as part of considering migrating across from 1Password.

Where I landed on achieving this was:

  1. Using rclone to mount a cloud location for the backup (easiest way to achieve this).
  2. Performing a scheduled backup, including:
    1. Stopping Bitwarden’s docker instance (and restarting when complete, or it fails)
    2. Going into the Docker folders for the Container, creating a backup of the DB file, with a date and time stamp, and encrypting it.
    3. Uploading of the new encrypted file to the cloud mount point.
    4. Rotating any existing backup files within defined parameters.

This is the code I ended up with, and hope it helps you with what you’re seeking to achieve.

Create encryption key

# Age keys (root‑only)
sudo mkdir -p /root/age && sudo chmod 700 /root/age
sudo age-keygen -o /root/age/keys.txt
sudo chmod 600 /root/age/keys.txt
# Derive the public key
sudo age-keygen -y /root/age/keys.txt


Backup script

#!/usr/bin/env bash
set -euo pipefail
umask 027

# --- Configuration ---
RCLONE_MOUNT="/mnt/foldername"
WORK_DIR="/opt/bitwarden/backups/work"
KEEP_LOCAL_DAYS=7
KEEP_REMOTE_DAYS=90
IN="/usr/bin/docker"
VOL="bitwarden_bw_etc"    # confirm with `docker volume ls`
AGE_KEYFILE="/root/age/keys.txt"

# --- Derived names ---
STAMP="$(date +%Y-%m-%d_%H-%M-%S)"
BASENAME="bitwarden_${STAMP}"
TAR_GZ="${WORK_DIR}/${BASENAME}.tar.gz"
SEALED="${TAR_GZ}.age"
SUM="${SEALED}.sha256"

# --- Prep ---
mkdir -p "${WORK_DIR}"
cd /opt/bitwarden

# Resolve Age recipient from key file
if [ -r "$AGE_KEYFILE" ]; then
  RECIPIENT=$(sed -n 's/^# public key: //p' "$AGE_KEYFILE" | head -n1)
fi
: "${RECIPIENT:?Age recipient not found; run: sudo age-keygen -o /root/age/keys.txt && sed -n 's/^# public key: //p' /root/age/keys.txt}"

# Always bring Bitwarden back up on exit
finish() { sudo "$IN" compose start bitwarden >/dev/null 2>&1 || true; }
trap finish EXIT

# 1) Stop briefly for a consistent SQLite snapshot
sudo "$IN" compose stop bitwarden || true

# 2) Snapshot the named volume -> gzipped tar (NO shell, NO redirects)
sudo "$IN" run --rm \
  -v "$VOL:/data:ro" \
  -v "$WORK_DIR:/out" \
  alpine:3.20 \
  tar -C /data -czf "/out/$(basename "$TAR_GZ")" .

# 3) Start Bitwarden again ASAP
sudo "$IN" compose start bitwarden || true

# 4) Encrypt with Age
age -r "$RECIPIENT" -o "$SEALED" "$TAR_GZ"

# 5) Checksum
sha256sum "$SEALED" > "$SUM"

# 6) Copy to OneDrive (if mounted)
if mountpoint -q "${RCLONE_MOUNT}"; then
  cp -f "$SEALED" "$SUM" "${RCLONE_MOUNT}/"
else
  echo "WARNING: ${RCLONE_MOUNT} not mounted; leaving encrypted backup locally at $SEALED" >&2
fi

# 7) Local retention
find "${WORK_DIR}" -type f -name 'bitwarden_*.tar.gz'            -mtime "+$KEEP_LOCAL_DAYS" -delete || true
find "${WORK_DIR}" -type f -name 'bitwarden_*.tar.gz.age'        -mtime "+$KEEP_LOCAL_DAYS" -delete || true
find "${WORK_DIR}" -type f -name 'bitwarden_*.tar.gz.age.sha256' -mtime "+$KEEP_LOCAL_DAYS" -delete || true

# 8) Remote retention
if mountpoint -q "${RCLONE_MOUNT}"; then
  find "${RCLONE_MOUNT}" -type f -name 'bitwarden_*.tar.gz.age'            -mtime "+$KEEP_REMOTE_DAYS" -delete || true
  find "${RCLONE_MOUNT}" -type f -name 'bitwarden_*.tar.gz.age.sha256'     -mtime "+$KEEP_REMOTE_DAYS" -delete || true
fi

echo "Backup complete: $SEALED"

Backup service using /etc/systemd/system/bitwarden-backup.service:

[Unit]
Description=Bitwarden encrypted backup (tar.gz + Age → OneDrive)
After=docker.service rclone-mount-bitwarden.service
Requires=docker.service rclone-mount-bitwarden.service
OnFailure=bitwarden-backup-alert@%n.service

[Service]
Type=oneshot
WorkingDirectory=/opt/bitwarden
TimeoutStartSec=1200
ExecStart=/usr/local/sbin/bitwarden-backup.sh
StandardOutput=journal
StandardError=journal
SyslogIdentifier=bitwarden-backup

Backup timer using /etc/systemd/system/bitwarden-backup.timer:

[Unit]
Description=Run Bitwarden backup daily
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
[Install]
WantedBy=timers.target


Enabling the backup:

sudo systemctl daemon-reload
sudo systemctl enable --now bitwarden-backup.timer
systemctl list-timers | grep -i bitwarden-backup || true


Getting the latest backup from my cloud mount point, and decrypting for restoration (inc. stopping and starting Bitwarden lite):


# 1) Copy the desired .tar.gz.age from remote location to a working dir
cp /mnt/folder/bitwarden_YYYY-mm-dd_HH-MM-SS.tar.gz.age /opt/bitwarden/backups/work/
# 2) Decrypt & extract
cd /opt/bitwarden/backups/work
age -d -i /root/age/keys.txt -o restore.tar.gz bitwarden_*.tar.gz.age
mkdir -p restore && tar -C restore -xzf restore.tar.gz
# 3) Stop the app
cd /opt/bitwarden && sudo docker compose stop bitwarden
# 4) Replace contents of the named volume
VOL="bitwarden_bw_etc"
sudo docker run --rm -v "$VOL:/data" -v "/opt/bitwarden/backups/work/restore:/src" alpine:3.20 \
  sh -lc 'cp -a /src/. /data/ && chown -R 1000:1000 /data'
# 5) Start the app
sudo docker compose start bitwarden


Decrypting the backup separately (if needed for manual testing):

  LATEST=$(ls -1t /opt/bitwarden/backups/work/bitwarden_*.tar.gz.age | head -n1)
  sudo age -d -i /root/age/keys.txt -o /opt/bitwarden/backups/work/_restore.tar.gz "$LATEST"
  tar -tzf /opt/bitwarden/backups/work/_restore.tar.gz | head
  rm -f /opt/bitwarden/backups/work/_restore.tar.gz