๐ BookStack Backup Guide (Using systemd, with Auto-Cleanup)
This document explains how to back up a BookStack instance using a backup script, a systemd service, and a systemd timer, plus an automatic cleanup timer that deletes old backups.
It's work only for bookstack in docker if you have a bare metal install you nee to modify the script
No cron is used โ fully systemd-native.
โ What Gets Backed Up?
A full BookStack backup requires:
- Database dump (MySQL/MariaDB) โ contains all pages, books, users, settings
- Application files โ BookStack codebase
- Upload files โ images + attachments
- Storage files โ internal uploads, custom icons, etc.
๐ 1. Install the Backup Script
Create the script at:
/usr/local/bin/bookstack-backup.sh
Script content
Update
DB_NAME,DB_USER,DB_PASS,BACKUP_DIR, andBOOKSTACK_DIRto match your setup.
#!/usr/bin/env bash
set -euo pipefail
# === CONFIG ===
BACKUP_DIR="/backup/bookstack"
# Docker container + DB creds
DB_CONTAINER="bookstack-db"
DB_NAME="bookstack"
DB_USER="bookstack"
DB_PASS="YOUR_DB_PASSWORD"
# Host paths for bind mounts (adjust to yours)
APP_DIR="/opt/bookstack/app" # optional if you want the app code
UPLOADS_DIR="/opt/bookstack/uploads"
STORAGE_DIR="/opt/bookstack/storage"
DATE=$(date +"%Y-%m-%d_%H-%M-%S")
TARGET_DIR="${BACKUP_DIR}/${DATE}"
mkdir -p "$TARGET_DIR"
echo "[*] $(date) - Dumping database from container ${DB_CONTAINER}..."
docker exec "${DB_CONTAINER}" \
sh -c "mysqldump -u\"${DB_USER}\" -p\"${DB_PASS}\" \"${DB_NAME}\"" \
> "${TARGET_DIR}/db.sql"
echo "[*] $(date) - Copying BookStack upload/storage data..."
# Only copy app dir if it exists (in case you don't map it)
if [ -d "$APP_DIR" ]; then
rsync -a "$APP_DIR" "${TARGET_DIR}/app"
fi
rsync -a "$UPLOADS_DIR" "${TARGET_DIR}/uploads"
rsync -a "$STORAGE_DIR" "${TARGET_DIR}/storage"
echo "[*] $(date) - Creating archive..."
cd "$BACKUP_DIR"
tar -czf "bookstack_${DATE}.tar.gz" "$DATE"
echo "[*] $(date) - Cleaning up temp folder..."
rm -rf "$TARGET_DIR"
echo "[+] $(date) - Backup complete: ${BACKUP_DIR}/bookstack_${DATE}.tar.gz"
Make it executable:
chmod +x /usr/local/bin/bookstack-backup.sh
๐ ๏ธ 2. systemd Service (Backup Runner)
Create:
/etc/systemd/system/bookstack-backup.service
With:
[Unit]
Description=BookStack backup
Wants=network-online.target
After=network-online.target mariadb.service mysql.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/bookstack-backup.sh
User=root
Group=root
# Optional: keep system responsive
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
[Install]
WantedBy=multi-user.target
Test it manually:
systemctl daemon-reload
systemctl start bookstack-backup.service
journalctl -u bookstack-backup.service -e
โฐ 3. systemd Timer (Daily Backup at 02:00)
Create:
/etc/systemd/system/bookstack-backup.timer
With:
[Unit]
Description=Daily BookStack backup at 02:00
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
Unit=bookstack-backup.service
[Install]
WantedBy=timers.target
Enable the timer:
systemctl daemon-reload
systemctl enable --now bookstack-backup.timer
Check timers:
systemctl list-timers | grep bookstack
๐งน 4. Auto-Delete Old Backups (Cleanup Timer)
This step adds:
- a cleanup script that deletes backup files older than 30 days
- a systemd service to run the script
- a systemd timer to schedule cleanup (default: daily at 03:00)
You can adjust the retention days and schedule as needed.
4.1 Cleanup Script
Create:
/usr/local/bin/bookstack-backup-cleanup.sh
With:
#!/usr/bin/env bash
set -euo pipefail
# === CONFIG ===
BACKUP_DIR="/backup/bookstack"
RETENTION_DAYS=30
echo "[*] $(date) - Cleaning up BookStack backups older than ${RETENTION_DAYS} days in ${BACKUP_DIR}..."
if [ ! -d "$BACKUP_DIR" ]; then
echo "[!] Backup directory ${BACKUP_DIR} does not exist, nothing to clean."
exit 0
fi
# Delete .tar.gz archives older than RETENTION_DAYS
find "$BACKUP_DIR" -maxdepth 1 -type f -name "bookstack_*.tar.gz" -mtime +$RETENTION_DAYS -print -delete
# Optionally clean leftover dated directories (should normally not exist)
find "$BACKUP_DIR" -maxdepth 1 -type d -regex '.*/[0-9-]\{10\}_[0-9:-]\{8\}' -mtime +$RETENTION_DAYS -print -exec rm -rf {} +
echo "[+] $(date) - Cleanup complete."
Make it executable:
chmod +x /usr/local/bin/bookstack-backup-cleanup.sh
To change retention, edit
RETENTION_DAYS=30(for example, to 7 for one week).
4.2 Cleanup systemd Service
Create:
/etc/systemd/system/bookstack-backup-cleanup.service
With:
[Unit]
Description=Cleanup old BookStack backups
[Service]
Type=oneshot
ExecStart=/usr/local/bin/bookstack-backup-cleanup.sh
User=root
Group=root
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
[Install]
WantedBy=multi-user.target
Test it:
systemctl daemon-reload
systemctl start bookstack-backup-cleanup.service
journalctl -u bookstack-backup-cleanup.service -e
4.3 Cleanup systemd Timer (Daily at 03:00)
Create:
/etc/systemd/system/bookstack-backup-cleanup.timer
With:
[Unit]
Description=Daily cleanup of old BookStack backups at 03:00
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
Unit=bookstack-backup-cleanup.service
[Install]
WantedBy=timers.target
Enable the timer:
systemctl daemon-reload
systemctl enable --now bookstack-backup-cleanup.timer
Check:
systemctl list-timers | grep bookstack-backup-cleanup
๐ Restore Procedure (Dockerized BookStack)
From a backup file like:
/backup/bookstack/bookstack_YYYY-MM-DD_HH-MM-SS.tar.gz
0. Stop the stack (recommended)
From the directory where your docker-compose.yml lives:
docker compose down
# or
docker-compose down
1. Extract the backup archive
cd /backup/bookstack
tar -xzf bookstack_YYYY-MM-DD_HH-MM-SS.tar.gz
# This creates a directory like:
# /backup/bookstack/YYYY-MM-DD_HH-MM-SS
cd YYYY-MM-DD_HH-MM-SS
ls
# You should see:
# db.sql
# uploads/
# storage/
# app/ (only if you backed app dir too)
2. Restore BookStack files
Adjust paths if yours are different.
# Restore uploads
rsync -a uploads/ /opt/bookstack/uploads/
# Restore storage
rsync -a storage/ /opt/bookstack/storage/
# Restore app code (only if you backed it and actually use APP_DIR)
if [ -d app ]; then
rsync -a app/ /opt/bookstack/app/
fi
If your containers expect certain ownership/permissions, fix them now.
For many setups the web user inside the container is www-data (uid 33), but often simple chmod is enough since Docker abstracts it.
Example (optional):
chown -R root:root /opt/bookstack
# or: chown -R 1000:1000 /opt/bookstack
3. Start the stack again
cd /path/to/your/docker-compose/
docker compose up -d
# or
docker-compose up -d
Wait a few seconds for the DB container (bookstack-db) to be healthy.
4. Restore the database inside the DB container
Set your DB variables (must match what you used in backups / docker-compose):
DB_CONTAINER="bookstack-db"
DB_NAME="bookstack"
DB_USER="bookstack"
DB_PASS="YOUR_DB_PASSWORD"
Then run:
cd /backup/bookstack/YYYY-MM-DD_HH-MM-SS
docker exec -e MYSQL_PWD="$DB_PASS" -i "$DB_CONTAINER" \
mysql -u"$DB_USER" "$DB_NAME" < db.sql
-ipipes db.sql into the containerMYSQL_PWDavoids putting the password directly on the mysql command line- if your container uses .env vars like
MYSQL_USER,MYSQL_PASSWORD,MYSQL_DATABASE, you can map those instead of hard-coding.
5. Verify
- Open BookStack in your browser
- Check that:
- Pages & books exist
- Attachments and images load
- Users and settings look correct
๐ Recommended Extras
- Sync
/backup/bookstackto offsite storage (S3, Minio, Backblaze, rsync to another host) - Keep at least 7โ30 daily backups depending on your risk tolerance
- If using ZFS/Btrfs:
- Combine filesystem snapshots with this logical backup
- Optionally snapshot the backup dataset itself
โ Summary
| Component | Backed Up / Managed By |
|---|---|
| Database | mysqldump in bookstack-backup.sh |
| App Files | rsync in bookstack-backup.sh |
| Uploads & Storage | Included in app directory |
| Backup Automation | bookstack-backup.service + .timer |
| Cleanup Automation | bookstack-backup-cleanup.service + .timer |
| Retention | RETENTION_DAYS in cleanup script |
This setup is:
- Fully systemd-native (no cron)
- Includes daily backup + daily cleanup
- Easy to tweak for different times / retention periods.