No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-05-18 08:56:35 +02:00
.gitignore first commit 2026-05-18 08:56:35 +02:00
backup.conf first commit 2026-05-18 08:56:35 +02:00
README.md first commit 2026-05-18 08:56:35 +02:00
setup-zfs-dataset.sh first commit 2026-05-18 08:56:35 +02:00
zfs-docker-backup.sh first commit 2026-05-18 08:56:35 +02:00

ZFS Docker Backup

Automated backup solution for Docker Compose projects using ZFS snapshots with TrueNAS replication.

Features

  • Atomic ZFS snapshots — Instant, consistent backups with zero data corruption risk
  • Parallel container stop/start — Minimized downtime (typically 15-60 seconds)
  • TrueNAS replication — Pull-based backup via SSH with incremental transfers
  • Restricted SSH access — Dedicated user with limited ZFS commands only
  • Pre/post backup hooks — Run custom commands before/after backup
  • Dual notifications — UptimeKuma for monitoring + Gotify for failure alerts
  • Comprehensive logging — Detailed logs with terminal output when run interactively
  • Dry-run mode — Test without making changes
  • Automatic recovery — Containers are automatically restarted if backup fails

For optimal backup efficiency, organize your Docker services as follows:

    /data/                          # ZFS dataset (mounted, snapshotted)
    ├── nextcloud/
    │   ├── docker-compose.yaml     # Service definition
    │   ├── config/                 # Configuration files
    │   │   └── config.php
    │   └── data/                   # Persistent application data
    │       └── nextcloud/
    ├── gitea/
    │   ├── docker-compose.yaml
    │   ├── config/
    │   └── data/
    └── vaultwarden/
        ├── docker-compose.yaml
        └── data/

Key principles:

  1. One subdirectory per service — Each Docker Compose project lives in its own folder under /data/

  2. Include all persistent data — Mount application data directories into the service folder:

    volumes:
      - ./data/nextcloud:/var/www/html          # Persistent (backed up)
      - ./config/config.php:/var/www/html/config/config.php
    
  3. Exclude non-persistent data — Use Docker volumes for cache, temp, and other non-essential data:

    volumes:
      - nextcloud-tmp:/tmp                      # Non-persistent (not backed up)
      - nextcloud-cache:/var/cache              # Non-persistent (not backed up)
    
    # Named volumes are NOT stored on ZFS, so they won't consume backup space
    

This approach ensures:

  • Only meaningful data is backed up
  • Backup size remains minimal
  • Fast restore (just restore snapshot and restart containers)

Requirements

  • Ubuntu Server (or any Linux with ZFS support)
  • ZFS filesystem mounted at /data (or your preferred path)
  • Docker and Docker Compose
  • OpenSSH server (for TrueNAS replication)
  • UptimeKuma instance (optional, for monitoring)
  • Gotify instance (optional, for push notifications)

Installation

1. Set up ZFS Dataset (Wizard Mode)

Run the setup script without arguments to start the interactive wizard:

sudo ./setup-zfs-dataset.sh

The wizard will guide you through:

  1. Pool creation — Choose storage method:
    • LVM logical volume (recommended for existing systems)
    • Physical disk device
  2. Dataset configuration — Set name and mount point
  3. SSH user setup — Configure TrueNAS replication user (optional)

Alternatively, use command-line options:

# Create new pool on /dev/sdb
sudo ./setup-zfs-dataset.sh --disk /dev/sdb

# Use existing pool
sudo ./setup-zfs-dataset.sh --pool tank --dataset data --mount /data

# Setup SSH user only (after initial setup)
sudo ./setup-zfs-dataset.sh --setup-ssh

2. Install Scripts

# Create directory
sudo mkdir -p /opt/zfs-backup

# Make executable
sudo chmod +x /opt/zfs-backup/zfs-docker-backup.sh
sudo chmod +x /opt/zfs-backup/setup-zfs-dataset.sh

# Create log directory
sudo mkdir -p /var/log/zfs-backup

3. Edit Configuration

sudo nano /opt/zfs-backup/backup.conf

4. Test with Dry-Run

sudo /opt/zfs-backup/zfs-docker-backup.sh --dry-run --verbose

5. Set up Cron

sudo crontab -e

Add:

# Daily backup at 03:30 (TrueNAS replication at 04:30)
30 3 * * * /opt/zfs-backup/zfs-docker-backup.sh >> /var/log/zfs-backup/cron.log 2>&1

TrueNAS Core Replication Setup

Step 1: Generate SSH Key

  1. Navigate to Account > Users
  2. Edit the root user
  3. Under SSH Public Key, click Generate SSH Key Pair
  4. Copy the public key for next step

Step 2: Configure SSH User on VPS

Run the setup wizard:

sudo ./setup-zfs-dataset.sh --setup-ssh

Paste the SSH public key when prompted.

Or provide the key directly:

sudo ./setup-zfs-dataset.sh --ssh-key "ssh-ed25519 AAAA..."

Step 3: Test Connection

From TrueNAS shell:

ssh zfs-repl@<vps-ip> 'zfs list -t snapshot'

Expected: List of snapshots from the backup dataset.

Step 4: Create Replication Task

  1. Navigate to Tasks > Replication Tasks
  2. Click Add

Basic Settings

Setting Value
Name vps-backup
Direction Pull

SSH Connection

Setting Value
Remote Host <vps-ip-address>
Port 22
Username zfs-repl
Private Key Select key from Step 1

Dataset Selection

Setting Value
Source Dataset tank/data
Destination Dataset tank/backups/vps-data

Options

Setting Value
Recursive Yes
Compressed Transfer Yes

Schedule

Setting Value
Schedule Custom
Run automatically Yes
Cron 30 4 * * * (daily at 04:30)

Retention

Setting Value
Snapshot Lifetime 4 weeks
  1. Click Submit

Step 5: Run Initial Replication

  1. Click the play button on the replication task
  2. Monitor progress in Tasks > Running Tasks

First run transfers full dataset. Subsequent runs are incremental.

Verify

On TrueNAS:

zfs list -t snapshot -r tank/backups/vps-data

Usage

Command Line

# Normal backup
sudo /opt/zfs-backup/zfs-docker-backup.sh

# Dry-run (test without changes)
sudo /opt/zfs-backup/zfs-docker-backup.sh --dry-run

# Verbose output (show DEBUG logs)
sudo /opt/zfs-backup/zfs-docker-backup.sh --verbose

# Skip notifications
sudo /opt/zfs-backup/zfs-docker-backup.sh --no-push

# Use alternative config
sudo /opt/zfs-backup/zfs-docker-backup.sh --config /etc/my-backup.conf

# Show help
sudo /opt/zfs-backup/zfs-docker-backup.sh --help

Exit Codes

Code Meaning
0 Success
1 Lock acquisition failed (another instance running)
2 Docker Compose stop failed
3 ZFS snapshot failed
4 Docker Compose start failed
5 Snapshot cleanup failed
10 Unknown error

Configuration Reference

Required Variables

Variable Description Example
ZFS_DATASET ZFS dataset name tank/data
DATA_DIR Mount point of ZFS dataset /data
UPTIMEKUMA_PUSH_URL UptimeKuma push URL https://uptime-kuma.example.com/api/push/XXXXX
GOTIFY_URL Gotify server URL https://gotify.example.com
GOTIFY_TOKEN Gotify application token XXXXX

Optional Variables

Variable Default Description
LOCAL_SNAPSHOT_RETENTION 7 Number of local snapshots to keep
STOP_TIMEOUT 300 Max seconds to wait for container stop
LOG_RETENTION_DAYS 30 Days to keep log files
GOTIFY_PRIORITY 8 Gotify message priority (1-10)
LOCK_FILE /var/run/zfs-backup.lock Lock file path

Projects Format

PROJECTS=(
    "project_name:/path/to/docker-compose/directory"
    "nextcloud:/data/nextcloud"
    "gitea:/data/gitea"
)

Hooks Format

# Pre-backup hooks (run before stopping containers)
PRE_HOOKS=(
    "nextcloud:sudo -u www-data php /data/nextcloud/html/occ maintenance:mode --on"
)

# Post-backup hooks (run after starting containers)
POST_HOOKS=(
    "nextcloud:sudo -u www-data php /data/nextcloud/html/occ maintenance:mode --off"
)

How It Works

Backup Flow

1. Acquire lock (prevent concurrent runs)
2. Run pre-backup hooks
3. Stop all Docker Compose projects in parallel
4. Create ZFS snapshot (instant, atomic)
5. Start all Docker Compose projects in parallel
6. Run post-backup hooks
7. Clean old snapshots (keep last N)
8. Send success notifications

Replication Architecture

┌────────────────────────────────────────────────────────────────┐
│                         VPS (Source)                           │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  ┌─────────────────┐     ┌─────────────────┐                   │
│  │ Docker Services │     │ zfs-docker-     │                   │
│  │                 │───▶│ backup.sh       │                   │
│  │ - Nextcloud     │     │                 │                   │
│  │ - Gitea         │     │ 1. Stop         │                   │
│  │ - Vaultwarden   │     │ 2. Snapshot     │                   │
│  └─────────────────┘     │ 3. Start        │                   │
│                          │ 4. Cleanup      │                   │
│                          └────────┬────────┘                   │
│                                   │                            │
│                                   ▼                            │
│                          ┌─────────────────┐                   │
│                          │ ZFS Dataset     │                   │
│                          │ tank/data       │                   │
│                          │                 │                   │
│                          │ Snapshots:      │                   │
│                          │ @backup-2024... │                   │
│                          │ @backup-2024... │                   │
│                          └────────┬────────┘                   │
│                                   │                            │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ SSH Server (zfs-repl user)                              │   │
│  │                                                         │   │
│  │ Restricted to:                                          │   │
│  │ - zfs send                                              │   │
│  │ - zfs list                                              │   │
│  │ - zfs get                                               │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                   │                            │
└───────────────────────────────────┼────────────────────────────┘
                                    │
                                    │ SSH (pull)
                                    │ zfs send | zfs receive
                                    │
┌───────────────────────────────────┼────────────────────────────┐
│                         TrueNAS (Destination)                  │
├───────────────────────────────────┼────────────────────────────┤
│                                   │                            │
│  ┌────────────────────────────────────────────────────────┐    │
│  │ Replication Task                                       │    │
│  │                                                        │    │
│  │ Schedule: Daily at 04:30                               │    │
│  │ Action: Pull new snapshots from VPS                    │    │
│  │ Retention: 4 weeks                                     │    │
│  └────────────────────────────────────────────────────────┘    │
│                                   │                            │
│                                   ▼                            │
│                          ┌─────────────────┐                   │
│                          │ ZFS Dataset     │                   │
│                          │ tank/backups/   │                   │
│                          │   vps-data      │                   │
│                          │                 │                   │
│                          │ Snapshots:      │                   │
│                          │ @backup-2024... │                   │
│                          │ @backup-2024... │                   │
│                          └─────────────────┘                   │
│                                                                │
└────────────────────────────────────────────────────────────────┘

Failure Recovery

If any step fails:

  1. All containers are automatically restarted (if they were stopped)
  2. Failure notification is sent to both Gotify and UptimeKuma
  3. Script exits with appropriate error code

Snapshot Naming

Snapshots are named with full timestamp:

tank/data@backup-20260414-033000    # Cron run at 03:30
tank/data@backup-20260414-150327    # Manual test at 15:03:27

Accessing Snapshots

# List snapshots
zfs list -t snapshot -r tank/data

# Browse snapshot files
ls /data/.zfs/snapshot/backup-20260414-033000/

# Restore specific file
cp /data/.zfs/snapshot/backup-20260414-033000/path/to/file /data/path/to/file

Security Model

SSH User Restrictions

The zfs-repl user is restricted via SSH forced command:

Allowed Blocked
zfs send Shell access
zfs list File access
zfs get Arbitrary commands
Other datasets

Configuration Files

File Purpose
/home/zfs-repl/.ssh/authorized_keys SSH public key
/etc/ssh/sshd_config.d/zfs-repl.conf SSH restrictions
/etc/sudoers.d/zfs-repl ZFS sudo permissions
/usr/local/bin/zfs-repl-shell Restricted shell
/var/log/zfs-repl-shell.log Command audit log

Additional Hardening

Restrict SSH access to TrueNAS IP only:

# Edit SSH config
sudo nano /etc/ssh/sshd_config.d/zfs-repl.conf

# Add:
AllowUsers zfs-repl@<truenas-ip>

Log Files

Logs are stored in /var/log/zfs-backup/:

/var/log/zfs-backup/
├── backup-20260414-033000.log    # Cron run
├── backup-20260414-150327.log    # Manual test
└── cron.log                       # Cron output redirect

Notifications

UptimeKuma

  • Sends up status on success
  • Sends down status on failure
  • Short message format for monitoring dashboard
  • Always notified (success and failure)

Gotify

  • Sends push notification on failure only
  • More detailed error information
  • Priority configurable (default: 8)
  • Not sent on successful backups — only alerts when something goes wrong
  • Notification title includes hostname: [hostname] Backup Failed

Troubleshooting

"Lock acquisition failed"

Another instance is running. Check:

# Check for running process
ps aux | grep zfs-docker-backup.sh

# Remove stale lock if needed
sudo rm /var/run/zfs-backup.lock

"Failed to stop" timeout

Increase STOP_TIMEOUT in config:

STOP_TIMEOUT=600  # 10 minutes

If a container doesn't stop gracefully within the timeout, Docker will force-kill it. The script logs this as a warning.

Snapshot creation fails

Check ZFS status:

zpool status
zfs list

SSH connection issues

# Check SSH service
sudo systemctl status ssh

# Check firewall
sudo ufw status

# Test connection from TrueNAS
ssh zfs-repl@<vps-ip> 'zfs list -t snapshot'

Replication not working

  1. Check TrueNAS replication task logs
  2. Check VPS audit log: tail -f /var/log/zfs-repl-shell.log
  3. Verify snapshots exist: zfs list -t snapshot -r tank/data

Permission denied

Ensure script runs as root:

sudo /opt/zfs-backup/zfs-docker-backup.sh

Security Considerations

  • Store backup.conf with restricted permissions: chmod 600
  • Gotify token should be kept secret
  • SSH user is restricted to ZFS commands only
  • Consider firewall rules to limit SSH access to TrueNAS IP only
File Description
zfs-docker-backup.sh Main backup script
setup-zfs-dataset.sh ZFS and SSH user setup
backup.conf Configuration file

License

MIT License