KB
Backup & Recovery

Passbolt Disaster Recovery Guide (Docker-Based Backup)

6 min read1118 words14 code blocks

At a glance#

  • Purpose: Back up a Docker-based Passbolt instance and restore it onto any Docker-capable machine.
  • Applies to: Passbolt CE and Pro running under Docker with a MariaDB or MySQL backend.
  • Risk: High — losing the server GPG key makes every stored credential permanently undecryptable.
  • Time: About 15 minutes to back up, about 1 hour to restore.

Overview#

Passbolt stores credentials encrypted with OpenPGP. A working restore therefore needs three things, and missing any one of them makes the other two worthless:

ComponentWhat it isIf lost
Database dumpUsers, groups, resources and encrypted secretsEverything is gone
Server GPG keysserverkey.asc and serverkey_private.ascDatabase restores but nothing can be decrypted
docker-compose.yamlService definition and environmentRebuildable, but slow and error-prone
Warning: The most common Passbolt disaster is a backup containing only the database. Without serverkey_private.asc the restored instance cannot decrypt a single stored password, and there is no recovery path. Verify the GPG keys are present in every backup.
Warning — credentials removed from this document. An earlier version of this article contained a real SMTP app password and a personal email address in the compose file below. They have been replaced with placeholders. If that app password was ever live, revoke and reissue it — it must be treated as compromised, having sat in plain text in shared documentation. Never store real credentials in the knowledge base; keep them in the password manager and use placeholders here.

Part 1 — Taking a backup#

Run this on the live Passbolt host, scheduled daily.

1. Create the backup script#

bash
sudo nano /usr/local/bin/passbolt-backup.sh
bash
#!/bin/bash
set -euo pipefail

# DB_PASSWORD comes from a root-only env file, never hard-coded here
source /root/.passbolt-backup.env

STACK_DIR="/opt/passbolt"
BACKUP_ROOT="/root/passbolt_backup"
DEST="${BACKUP_ROOT}/$(date +%F)"

DB_CONTAINER=$(docker compose -f "${STACK_DIR}/docker-compose.yaml" ps -q db)
APP_CONTAINER=$(docker compose -f "${STACK_DIR}/docker-compose.yaml" ps -q passbolt)

mkdir -p "${DEST}"

# 1. Database
docker exec "${DB_CONTAINER}" \
  mysqldump -u passbolt -p"${DB_PASSWORD}" passbolt > "${DEST}/passbolt-db.sql"

# 2. Server GPG keys - the irreplaceable part
docker cp "${APP_CONTAINER}:/etc/passbolt/gpg/serverkey.asc"         "${DEST}/"
docker cp "${APP_CONTAINER}:/etc/passbolt/gpg/serverkey_private.asc" "${DEST}/"

# 3. Stack definition
cp "${STACK_DIR}/docker-compose.yaml" "${DEST}/"

# 4. Fail loudly if anything is missing or empty
for f in passbolt-db.sql serverkey.asc serverkey_private.asc docker-compose.yaml; do
    if [ ! -s "${DEST}/${f}" ]; then
        echo "BACKUP FAILED: ${f} missing or empty" >&2
        exit 1
    fi
done

chmod 600 "${DEST}"/*
echo "Backup complete: ${DEST}"

Create the credentials file it sources:

bash
echo 'DB_PASSWORD=your-real-db-password' | sudo tee /root/.passbolt-backup.env
sudo chmod 600 /root/.passbolt-backup.env
sudo chmod 700 /usr/local/bin/passbolt-backup.sh
Note: Step 4 exists because the usual failure mode is silent. A mysqldump that fails still creates an empty file, and nobody notices until a restore is attempted months later.

2. Schedule it#

bash
sudo crontab -e
text
0 2 * * * /usr/local/bin/passbolt-backup.sh >> /var/log/passbolt-backup.log 2>&1

3. Copy backups off the host#

A backup stored only on the machine it protects is not a backup:

bash
rsync -avz /root/passbolt_backup/ backup-server:/backups/passbolt/
Warning: These files are the entire credential store plus the key that decrypts it. Encrypt them at rest, restrict access to as few people as possible, and never place them on shared storage other teams can read.

Part 2 — Restoring#

What you need#

A backup folder containing passbolt-db.sql, serverkey.asc, serverkey_private.asc and docker-compose.yaml, plus a machine with Docker and Docker Compose.

1. Prepare the restore directory#

bash
mkdir -p passbolt_restore/gpg
cd passbolt_restore

Arrange the files like this:

text
passbolt_restore/
├── docker-compose.yaml
├── passbolt-db.sql
└── gpg/
    ├── serverkey.asc
    └── serverkey_private.asc
bash
mv serverkey*.asc gpg/
chmod 600 gpg/serverkey_private.asc

The gpg/ subdirectory matters — it is bind-mounted into the container at /etc/passbolt/gpg.

2. Review the compose file#

yaml
services:
  db:
    image: mariadb:10.11
    restart: unless-stopped
    environment:
      MYSQL_RANDOM_ROOT_PASSWORD: "true"
      MYSQL_DATABASE: "passbolt"
      MYSQL_USER: "passbolt"
      MYSQL_PASSWORD: "<DB_PASSWORD>"
    volumes:
      - db_data:/var/lib/mysql

  passbolt:
    image: passbolt/passbolt:latest-ce
    restart: unless-stopped
    depends_on:
      - db
    environment:
      APP_FULL_BASE_URL: "<https://passbolt.example.com>"
      DATASOURCES_DEFAULT_HOST: "db"
      DATASOURCES_DEFAULT_USERNAME: "passbolt"
      DATASOURCES_DEFAULT_PASSWORD: "<DB_PASSWORD>"
      DATASOURCES_DEFAULT_DATABASE: "passbolt"
      EMAIL_DEFAULT_FROM_NAME: "Passbolt"
      EMAIL_DEFAULT_FROM: "<SMTP_FROM_ADDRESS>"
      EMAIL_TRANSPORT_DEFAULT_HOST: "<SMTP_HOST>"
      EMAIL_TRANSPORT_DEFAULT_PORT: 587
      EMAIL_TRANSPORT_DEFAULT_USERNAME: "<SMTP_USERNAME>"
      EMAIL_TRANSPORT_DEFAULT_PASSWORD: "<SMTP_APP_PASSWORD>"
      EMAIL_TRANSPORT_DEFAULT_TLS: "true"
    volumes:
      - ./gpg:/etc/passbolt/gpg
    command:
      ["/usr/bin/wait-for.sh", "-t", "0", "db:3306", "--", "/docker-entrypoint.sh"]
    ports:
      - "8080:80"
      - "8443:443"

volumes:
  db_data:
Warning: Replace every <PLACEHOLDER> with the real value from the password manager at restore time, and do not save the completed file back into the knowledge base.

Two things that commonly go wrong:

  • MYSQL_PASSWORD and DATASOURCES_DEFAULT_PASSWORD must match, or Passbolt cannot reach its own database.
  • APP_FULL_BASE_URL must match the URL users actually browse to. If it is wrong, Passbolt generates broken links in emails and the browser extension refuses to connect.

3. Start the stack#

bash
docker compose up -d
docker compose ps
docker compose logs -f passbolt

Allow 30–60 seconds for the database to initialise.

4. Restore the database#

bash
docker exec -i $(docker compose ps -q db) \
  mysql -u passbolt -p'<DB_PASSWORD>' passbolt < passbolt-db.sql

5. Restart the application#

Passbolt caches schema information at startup, so it must be restarted after the import:

bash
docker compose restart passbolt

6. Run Passbolt's health check#

bash
docker compose exec passbolt \
  su -c "/usr/share/php/passbolt/bin/cake passbolt healthcheck" -s /bin/sh www-data

Read the GPG section carefully. It should confirm the server key is present and that its fingerprint matches the configuration.

Verification#

A restore is not verified until you have decrypted something.

  1. Browse to the instance.
  2. Log in with an existing admin account.
  3. Open a stored password and reveal it. This is the real test — it proves the GPG private key restored correctly and the database is intact.
  4. Confirm the user and group lists match expectations.
Warning: Do not consider the DR plan proven until step 3 succeeds. A restore that reaches the login screen but cannot decrypt secrets is a failed restore, and that failure stays invisible until somebody urgently needs a password.

Rehearse the whole procedure on a scratch machine at least twice a year. An untested backup is an assumption, not a recovery plan.

Troubleshooting#

SymptomCause and fix
Login works but passwords will not decryptGPG keys missing or not mounted. Confirm ./gpg holds both .asc files and is mounted at /etc/passbolt/gpg.
Unable to connect to databaseMYSQL_PASSWORD and DATASOURCES_DEFAULT_PASSWORD differ.
Healthcheck reports a GPG fingerprint mismatchKeys and database dump came from different backups. Both must be from the same run.
Browser extension refuses to connectAPP_FULL_BASE_URL does not match the URL in the address bar.
Table doesn't exist errorsDump imported before Passbolt initialised the schema. Restart the app container and re-import.
Emails not sendingSMTP placeholders were never replaced. Check docker compose logs passbolt.
Import fails with Access deniedWrong database password in the mysql command.