Passbolt Disaster Recovery Guide (Docker-Based Backup)
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:
| Component | What it is | If lost |
|---|---|---|
| Database dump | Users, groups, resources and encrypted secrets | Everything is gone |
| Server GPG keys | serverkey.asc and serverkey_private.asc | Database restores but nothing can be decrypted |
docker-compose.yaml | Service definition and environment | Rebuildable, 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#
sudo nano /usr/local/bin/passbolt-backup.sh#!/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:
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.shNote: 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#
sudo crontab -e0 2 * * * /usr/local/bin/passbolt-backup.sh >> /var/log/passbolt-backup.log 2>&13. Copy backups off the host#
A backup stored only on the machine it protects is not a backup:
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#
mkdir -p passbolt_restore/gpg
cd passbolt_restoreArrange the files like this:
passbolt_restore/
├── docker-compose.yaml
├── passbolt-db.sql
└── gpg/
├── serverkey.asc
└── serverkey_private.ascmv serverkey*.asc gpg/
chmod 600 gpg/serverkey_private.ascThe gpg/ subdirectory matters — it is bind-mounted into the container at /etc/passbolt/gpg.
2. Review the compose file#
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_PASSWORDandDATASOURCES_DEFAULT_PASSWORDmust match, or Passbolt cannot reach its own database.APP_FULL_BASE_URLmust 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#
docker compose up -d
docker compose ps
docker compose logs -f passboltAllow 30–60 seconds for the database to initialise.
4. Restore the database#
docker exec -i $(docker compose ps -q db) \
mysql -u passbolt -p'<DB_PASSWORD>' passbolt < passbolt-db.sql5. Restart the application#
Passbolt caches schema information at startup, so it must be restarted after the import:
docker compose restart passbolt6. Run Passbolt's health check#
docker compose exec passbolt \
su -c "/usr/share/php/passbolt/bin/cake passbolt healthcheck" -s /bin/sh www-dataRead 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.
- Browse to the instance.
- Log in with an existing admin account.
- 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.
- 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#
| Symptom | Cause and fix |
|---|---|
| Login works but passwords will not decrypt | GPG keys missing or not mounted. Confirm ./gpg holds both .asc files and is mounted at /etc/passbolt/gpg. |
Unable to connect to database | MYSQL_PASSWORD and DATASOURCES_DEFAULT_PASSWORD differ. |
| Healthcheck reports a GPG fingerprint mismatch | Keys and database dump came from different backups. Both must be from the same run. |
| Browser extension refuses to connect | APP_FULL_BASE_URL does not match the URL in the address bar. |
Table doesn't exist errors | Dump imported before Passbolt initialised the schema. Restart the app container and re-import. |
| Emails not sending | SMTP placeholders were never replaced. Check docker compose logs passbolt. |
Import fails with Access denied | Wrong database password in the mysql command. |