A lightweight, automated backup solution using Docker, Restic, and Backblaze B2. This container automatically backs up your data to Backblaze B2 cloud storage with intelligent scheduling, automatic restore capabilities, and comprehensive health monitoring.
- π Configurable Startup Backup: Optional backup on container start (default: enabled)
- π Auto-Restore: Automatically detects empty directories and restores from latest snapshot
- β° Simple Scheduler: Cron-like syntax without the complexity (no cron daemon!)
- βοΈ Backblaze B2 Integration: Secure, cost-effective cloud storage
- π Encryption: End-to-end encryption via Restic
- π¦ Incremental Backups: Efficient deduplication and compression
- β»οΈ Smart Retention Management: Automatic cleanup across all container restarts
- π₯ Comprehensive Health Monitoring: Automatic failure detection with detailed diagnostics
- π€ User Management: Configurable user permissions to match host system
- π³ Lightweight: Alpine Linux base (~50MB image)
- βοΈ Environment-Driven: Fully configurable via environment variables
- π§ Intelligent Error Diagnosis: Detailed error messages with specific troubleshooting steps
docker-b2-backup/
βββ src/
β βββ backup.sh # Main backup script with cross-host retention
β βββ restore.sh # Intelligent restore script (no nested directories)
β βββ scheduler.sh # Simple scheduler (replaces cron)
β βββ entrypoint.sh # Container initialization
β βββ healthcheck.sh # Comprehensive health monitoring
β βββ fix_nested_backup.sh # Migration tool for old backup formats
βββ Dockerfile # Alpine-based container definition
βββ docker-compose.yml # Example compose configuration
βββ example.env # Complete configuration template
βββ README.md # This file
This container uses a simple scheduler script instead of cron for maximum reliability and visibility.
Important: The container backs up the contents of the mounted directory, not the directory itself. This ensures that when you restore:
- If you have
/backup/a,/backup/b,/backup/c - They restore as
/backup/a,/backup/b,/backup/c - Not as
/backup/backup/a,/backup/backup/b,/backup/backup/c
-
On Container Start:
- Checks if backup directory is empty β restores latest snapshot if needed
- Optionally runs initial backup (controlled by
RUN_BACKUP_ON_STARTUP) - Starts scheduler loop
-
During Operation:
- Checks every 60 seconds if current time matches schedule
- Runs backup if schedule matches
- Runs restore check if schedule matches (only if directory is empty)
- Applies retention policy after each successful backup
-
Retention Management:
- Automatically cleans up old snapshots after each backup
- Groups snapshots by paths (not hostname) for consistent cleanup across container restarts
- Respects configured retention policy (daily, weekly, monthly, yearly)
-
Health Monitoring:
- Detects backup/restore failures within 60 seconds
- Marks container UNHEALTHY automatically
- Enables automatic restart with autoheal
- β Simpler - No crontab configuration
- β More reliable - No permission issues
- β Better visibility - All logs in stdout
- β Easier debugging - See exactly what's happening
- β Same flexibility - Supports cron syntax
# Create a directory for your data to backup
mkdir -p /path/to/your/data
# Clone the repository (optional - can build directly from GitHub)
git clone https://github.com/dynacylabs/docker-b2-backup.git
cd docker-b2-backupCreate a .env file or b2-backup.env with your settings:
# Required: Backblaze B2 Configuration
RESTIC_REPOSITORY=b2:your-bucket-name:backup-path
RESTIC_PASSWORD=your-secure-restic-password
B2_ACCOUNT_ID=your-b2-account-id
B2_ACCOUNT_KEY=your-b2-application-key
# Required: Backup Source
BACKUP_SOURCE_DIR=/backup
# Optional: Customize schedules (cron format)
BACKUP_SCHEDULE=0 0 * * * # Daily at midnight
RESTORE_CHECK_SCHEDULE=0 0 * * * # Daily at midnight
RUN_BACKUP_ON_STARTUP=false # Don't backup on container start
# Optional: Retention Policy
RESTIC_KEEP_DAILY=7
RESTIC_KEEP_WEEKLY=4
RESTIC_KEEP_MONTHLY=6
RESTIC_KEEP_YEARLY=2services:
b2-backup:
build:
context: https://github.com/dynacylabs/docker-b2-backup.git
dockerfile: Dockerfile
container_name: b2-backup
volumes:
- /path/to/your/data:/backup
env_file:
- ./b2-backup.env
restart: unless-stopped
healthcheck:
test: ["CMD", "/src/healthcheck.sh"]
interval: 60s
timeout: 60s
retries: 3
start_period: 60sdocker compose up -d# Check container status and health
docker compose ps
# View logs in real-time
docker compose logs -f b2-backup
# View snapshots in repository
docker exec b2-backup restic snapshots
# Check last backup status
docker exec b2-backup cat /tmp/last_backup_success| Variable | Description | Example |
|---|---|---|
RESTIC_REPOSITORY |
Backblaze B2 repository path | b2:my-bucket:backups/server1 |
RESTIC_PASSWORD |
Encryption password for backups | your-secure-password |
B2_ACCOUNT_ID |
Backblaze B2 account ID | 005fb80d1ad8fa50000000002 |
B2_ACCOUNT_KEY |
Backblaze B2 application key | K005xJm6AX6NmZgZHKWExjGq25XUftA |
| Variable | Default | Description |
|---|---|---|
BACKUP_SOURCE_DIR |
/backup |
Directory to backup (contents, not directory itself) |
BACKUP_TEMP_DIR |
/tmp/backup |
Temporary staging directory (legacy, not used in current version) |
BACKUP_SCHEDULE |
0 2 * * * |
Schedule for backups (cron syntax) |
RESTORE_CHECK_SCHEDULE |
0 1 * * * |
Schedule for restore checks (cron syntax) |
RUN_BACKUP_ON_STARTUP |
true |
Run backup when container starts |
RESTIC_KEEP_DAILY |
7 |
Daily snapshots to retain |
RESTIC_KEEP_WEEKLY |
4 |
Weekly snapshots to retain |
RESTIC_KEEP_MONTHLY |
6 |
Monthly snapshots to retain |
RESTIC_KEEP_YEARLY |
2 |
Yearly snapshots to retain |
HEALTHCHECK_FULL_INTERVAL |
3600 |
Seconds between full B2 health checks |
PUID |
1000 |
User ID for file ownership (optional) |
PGID |
1000 |
Group ID for file ownership (optional) |
The scheduler uses familiar cron-like syntax:
# Format: minute hour day-of-month month day-of-week
# Daily at midnight
BACKUP_SCHEDULE="0 0 * * *"
# Daily at 2:30 AM
BACKUP_SCHEDULE="30 2 * * *"
# Every 6 hours
BACKUP_SCHEDULE="0 */6 * * *"
# Twice daily (midnight and noon)
BACKUP_SCHEDULE="0 0,12 * * *"
# Weekly on Sunday at 2 AM
BACKUP_SCHEDULE="0 2 * * 0"
# Monthly on the 1st at 3 AM
BACKUP_SCHEDULE="0 3 1 * *"
# Every 15 minutes (for testing)
BACKUP_SCHEDULE="*/15 * * * *"Format: minute hour day-of-month month day-of-week
- Use
*for "any value" - Use
*/nfor "every n units" - Use
1-5for ranges - Use
1,3,5for lists - Day of week: 0-7 (0 and 7 are Sunday)
The container backs up the contents of your mounted directory, not the directory itself.
services:
b2-backup:
volumes:
- /path/to/your/data:/backup # Your data to backupImportant:
- Mount your data directory to
/backup(or configureBACKUP_SOURCE_DIR) - The backup will store the contents directly (no nested
/backup/backup/structure) - On restore, files are placed directly in the mount point
Example:
If your host has /home/user/mydata containing file1.txt and folder1/:
volumes:
- /home/user/mydata:/backupThe backup will contain:
file1.txtfolder1/
On restore, they appear at:
/home/user/mydata/file1.txt/home/user/mydata/folder1/
Not at /home/user/mydata/backup/file1.txt β
The container runs as the backup user (UID 1000, GID 1000 by default). Files in your backup directory should be readable by this user.
# Make your data readable by the backup user
sudo chown -R 1000:1000 /path/to/your/data
# Or make it world-readable (less secure)
sudo chmod -R 755 /path/to/your/dataIf you need different permissions, set PUID and PGID:
# In your .env file
PUID=1003 # Your user ID (from: id -u)
PGID=1003 # Your group ID (from: id -g)Then in your docker-compose.yml:
services:
b2-backup:
user: "${PUID}:${PGID}" # Or directly: user: "1003:1003"The container includes comprehensive health monitoring that automatically detects failures and marks the container as UNHEALTHY.
| Check | Frequency | Action on Failure |
|---|---|---|
| Scheduler process running | Every 60s | UNHEALTHY |
| Backup status markers | Every 60s | UNHEALTHY |
| Restore status markers | Every 60s | UNHEALTHY |
| Recent log errors | Every 60s | UNHEALTHY |
| Environment variables | Every 60s | UNHEALTHY |
| Required scripts | Every 60s | UNHEALTHY |
| Last successful backup | Every 60s | WARNING (48h+) |
| Disk space | Every 60s | WARNING (85%+) |
| B2 connectivity | Every 1 hour | UNHEALTHY |
| Repository integrity | Every 1 hour | WARNING |
# Check container health status
docker ps
# View detailed health output
docker inspect b2-backup --format='{{.State.Health.Status}}'
# Run manual health check
docker exec b2-backup /src/healthcheck.sh
# View last health check output
docker inspect b2-backup --format='{{range .State.Health.Log}}{{.Output}}{{end}}' | tail -1HEALTHY:
2025-10-23 15:30:00 - HEALTHY: Basic check passed (next full B2 check in 3540s)
UNHEALTHY:
2025-10-23 15:30:00 - UNHEALTHY: Backup failed 2h ago - Reason: Network timeout
The container uses status files to track operations:
/tmp/backup_status # Written on backup failure
/tmp/last_backup_success # Timestamp of last successful backup
/tmp/restore_status # Written on restore failure
/tmp/last_restore_success # Timestamp of last successful restoreCombine with autoheal to automatically restart unhealthy containers:
services:
autoheal:
image: willfarrell/autoheal
environment:
- AUTOHEAL_CONTAINER_LABEL=all
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
b2-backup:
# ... your backup config
labels:
autoheal: true # Optional: explicit labelWhen the backup container goes UNHEALTHY, autoheal restarts it automatically.
The container handles most operations automatically:
- Backup: Runs on configured schedule (and optionally on startup)
- Restore: Automatically restores if backup directory is empty
- Retention Cleanup: Removes old snapshots after each backup
- Health Checks: Continuous monitoring every 60 seconds
# Run backup manually
docker exec b2-backup /src/backup.sh
# The script will:
# - Change to /backup directory
# - Backup current directory contents (.)
# - Apply retention policy (cleanup old snapshots)
# - Group snapshots by paths (not hostname)# Restore latest snapshot
docker exec b2-backup /src/restore.sh
# Or restore specific snapshot
docker exec b2-backup restic restore <snapshot-id> --target /backup# List all snapshots
docker exec b2-backup restic snapshots
# View snapshot details
docker exec b2-backup restic snapshots --compact
# Repository statistics
docker exec b2-backup restic stats
# Check repository integrity
docker exec b2-backup restic check
# Check 10% of repository data
docker exec b2-backup restic check --read-data-subset=10%# Clean up snapshots (already runs automatically after backup)
docker exec b2-backup restic forget \
--group-by paths \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 2 \
--prune
# Dry run (see what would be deleted)
docker exec b2-backup restic forget \
--group-by paths \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 2 \
--dry-runIf you're upgrading from a version that created nested /backup/backup/ directories:
# Run the fix script
docker exec b2-backup /src/fix_nested_backup.sh
# This will:
# - Detect nested backup directory
# - Move contents up one level
# - Run a fresh backup with correct structureThe retention policy controls how many snapshots are kept. Old snapshots are automatically deleted after each successful backup.
- 7 daily snapshots (last week)
- 4 weekly snapshots (last ~month)
- 6 monthly snapshots (last 6 months)
- 2 yearly snapshots (last 2 years)
Approximate total: ~19 snapshots maximum
- After each backup, the cleanup process runs automatically
- Snapshots are grouped by paths (not by hostname)
- This means retention works correctly even if you:
- Restart the container multiple times
- Change the container hostname
- Rebuild with a new container ID
Example: If you restart your container 10 times in one day, you'll still only keep 7 daily snapshots total (not 70).
Adjust the values in your env file:
# Keep more history
RESTIC_KEEP_DAILY=14 # 2 weeks of daily backups
RESTIC_KEEP_WEEKLY=8 # 2 months of weekly backups
RESTIC_KEEP_MONTHLY=12 # 1 year of monthly backups
RESTIC_KEEP_YEARLY=5 # 5 years of yearly backups
# Keep less (save storage costs)
RESTIC_KEEP_DAILY=3 # Only 3 days
RESTIC_KEEP_WEEKLY=2 # Only 2 weeks
RESTIC_KEEP_MONTHLY=3 # Only 3 months
RESTIC_KEEP_YEARLY=1 # Only 1 year- Retention is enforced after every successful backup
- Uses
--group-by pathsto avoid per-hostname retention groups - Snapshots are permanently deleted with
--pruneflag - Failed cleanup doesn't fail the backup (warning only)
Symptom: You see multiple snapshots with the same date in restic snapshots
Cause: The container runs a backup on startup by default (RUN_BACKUP_ON_STARTUP=true)
Solution:
# Disable startup backup
echo "RUN_BACKUP_ON_STARTUP=false" >> b2-backup.env
# Rebuild container
docker compose up -d --force-recreate b2-backupThe retention policy will clean up excess snapshots on the next scheduled backup.
Symptom: You have dozens of snapshots instead of ~19
Cause: Old version didn't use --group-by paths, so each container restart created a new retention group
Solution:
# Manual cleanup with correct grouping
docker exec b2-backup restic forget \
--group-by paths \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 2 \
--prune
# Update container to latest version
docker compose pull b2-backup # Or rebuild from GitHub
docker compose up -d b2-backupSymptom: Files restore to /backup/backup/ instead of /backup/
Cause: Using old backup format that backed up the directory instead of its contents
Solution:
# Run the migration script
docker exec b2-backup /src/fix_nested_backup.sh
# Or manually:
docker exec b2-backup bash -c "mv /backup/backup/* /backup/ && rmdir /backup/backup"
docker exec b2-backup /src/backup.sh # Create new backup with correct structureSymptom: Backup fails with "permission denied" errors
Solution:
# Option 1: Fix ownership
sudo chown -R 1000:1000 /path/to/your/data
# Option 2: Make readable
sudo chmod -R 755 /path/to/your/data
# Option 3: Use your own UID/GID
# In docker-compose.yml:
user: "$(id -u):$(id -g)"Symptom: Errors about authentication, 401, or 403
Solution:
# Verify credentials in env file
docker exec b2-backup env | grep B2_
# Check B2 account:
# 1. Login to Backblaze B2 console
# 2. Verify application key hasn't expired
# 3. Check key has proper permissions (read/write)
# 4. Verify bucket exists
# Test B2 connectivity
docker exec b2-backup restic snapshotsSymptom: docker ps shows container as "unhealthy"
Solution:
# Check why unhealthy
docker inspect b2-backup --format='{{range .State.Health.Log}}{{.Output}}{{end}}' | tail -1
# View detailed status
docker exec b2-backup /src/healthcheck.sh
# Check for failure markers
docker exec b2-backup cat /tmp/backup_status
docker exec b2-backup cat /tmp/restore_status
# Clear failure status and let it retry
docker exec b2-backup rm -f /tmp/backup_status /tmp/restore_status
# Or restart container
docker compose restart b2-backupSymptom: No backups at scheduled time
Solution:
# Check scheduler is running
docker exec b2-backup ps aux | grep scheduler
# Verify schedule format
docker exec b2-backup env | grep SCHEDULE
# Check container time
docker exec b2-backup date
# Test with frequent schedule (every 2 minutes)
# In b2-backup.env:
BACKUP_SCHEDULE=*/2 * * * *
# Watch logs
docker logs -f b2-backup# View all logs
docker logs b2-backup --tail 100
# Interactive shell
docker exec -it b2-backup /bin/bash
# Test backup manually
docker exec b2-backup /src/backup.sh
# Test restore manually
docker exec b2-backup /src/restore.sh
# Check environment variables
docker exec b2-backup env
# List snapshots
docker exec b2-backup restic snapshots
# Repository info
docker exec b2-backup restic stats
# Health status
docker exec b2-backup /src/healthcheck.sh
echo $? # 0 = healthy, 1 = unhealthyIf you encounter issues:
- Check this troubleshooting section
- Enable debug logging:
docker logs -f b2-backup - Run manual health check:
docker exec b2-backup /src/healthcheck.sh - Check GitHub Issues
- Open a new issue with:
- Container logs
- Health check output
- Your docker-compose.yml (redact credentials)
- Steps to reproduce
-
Test Your Backups Regularly
# Restore to a test directory periodically docker exec b2-backup restic restore latest --target /tmp/restore-test
-
Monitor Health Status
# Set up monitoring alerts for unhealthy containers docker ps --filter "health=unhealthy"
-
Secure Your Credentials
- Use application keys with minimal B2 permissions (read/write to specific bucket)
- Store
.envfiles securely (never commit to git) - Rotate B2 keys periodically
-
Optimize Retention Policy
- Balance recovery needs vs. storage costs
- Daily backups are for recent recovery (7 days)
- Weekly/monthly for longer-term recovery
- Yearly for compliance/archival
-
Configure Startup Backup
- Set
RUN_BACKUP_ON_STARTUP=falsefor production (avoids duplicate backups) - Keep
truefor testing or critical systems
- Set
-
Schedule Wisely
- Run backups during low-activity periods
- Avoid overlapping backup and restore schedules
- Common:
BACKUP_SCHEDULE=0 0 * * *(midnight daily)
-
Monitor Disk Space
- The healthcheck warns at 85% disk usage
- Monitor backup source directory size
- Prune old files before they're backed up
-
Understand Retention Grouping
- Retention is by paths, not hostname
- Container restarts don't create separate retention groups
- All snapshots are treated as one logical backup set
-
Use Autoheal for Recovery
# Add to docker-compose.yml autoheal: image: willfarrell/autoheal volumes: - /var/run/docker.sock:/var/run/docker.sock restart: unless-stopped
-
Keep Container Updated
# Pull latest changes docker compose pull b2-backup # Or rebuild from GitHub docker compose up -d --build b2-backup
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Test thoroughly
- Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- β Fixed nested backup directory issue - Now backs up directory contents, not the directory itself
- β
Fixed retention policy - Uses
--group-by pathsto cleanup across all container restarts - β
Added configurable startup backup -
RUN_BACKUP_ON_STARTUPenvironment variable - β Improved error diagnostics - Detailed error messages with specific troubleshooting
- β
Added migration tool -
fix_nested_backup.shfor upgrading from old format
This project is licensed under the MIT License - see the LICENSE file for details.
- Restic - Fast, secure, efficient backup program
- Backblaze B2 - Affordable, reliable cloud storage
- Alpine Linux - Lightweight, secure container base
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: This README and inline code comments
Made with β€οΈ by Dynacylabs