Recently I spent some time learning Docker and ZFS. I was already using ZFS snapshots on OS-level projects, automating them with Sanoid and replicating them to another machine using Syncoid. In this post I will show how to apply the same ZFS snapshot strategy on top of Docker containers — from setting up a Dockerized Drupal + MySQL stack all the way to automated snapshots and replication to a backup machine.


Setting Up the Environment

I started by spinning up two Ubuntu VMs using QEMU/KVM — one as the main host and one as the backup machine. On both machines I ran the usual setup:

sudo apt update
sudo apt full-upgrade
sudo apt autoremove
sudo apt install zfsutils-linux sanoid

For Docker installation, I always follow the official Docker documentation.


Docker Compose Setup

For this I chose a simple Drupal website with MySQL as the database.

Here's the docker-compose.yml I ended up with:

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql

  drupal:
    image: drupal:10-apache
    ports:
      - "8080:80"
    volumes:
      - drupal_data:/var/www/html
    depends_on:
      - db

volumes:
  db_data:
  drupal_data:

Sensitive values like passwords are kept out of the compose file using a .env file:

MYSQL_ROOT_PASSWORD=your_password
MYSQL_DATABASE=drupal
MYSQL_USER=drupal
MYSQL_PASSWORD=your_password

Setting Up ZFS

First, I added a second disk to the VM and created a ZFS pool called tank on it. A ZFS pool is the foundation — it sits on top of physical disks and contains datasets.

sudo zpool create tank /dev/vdb

Then I created a dataset inside the pool to store Docker data. This dataset is what Sanoid will snapshot later.

sudo zfs create tank/docker

Next, I applied some recommended settings to improve performance and snapshot efficiency:

sudo zpool set ashift=12 tank
sudo zfs set atime=off tank
sudo zfs set compression=lz4 tank
sudo zfs set aclmode=passthrough tank
sudo zfs set logbias=throughput tank

Finally, I told Docker to store all its data — volumes, images, containers — inside the ZFS dataset by modifying /etc/docker/daemon.json:

{
    "data-root": "/tank/docker"
}

Then restarted Docker to apply the change:

sudo systemctl restart docker

Verify Docker is now using the ZFS dataset:

docker info | grep "Docker Root Dir"
# Output: Docker Root Dir: /tank/docker

From this point on, every Docker volume is automatically stored on ZFS — no extra configuration needed per project.


Automating Snapshots with Sanoid

Sanoid automates ZFS snapshots — you define a policy once and it handles creating and cleaning up snapshots automatically.

I created the config directory and file:

sudo mkdir /etc/sanoid
sudo nano /etc/sanoid/sanoid.conf
[template_drupal]
    hourly = 12
    daily = 7
    monthly = 3
    autosnap = yes
    autoprune = yes

[tank/docker]
    use_template = drupal

This policy keeps 12 hourly, 7 daily, and 3 monthly snapshots. autoprune automatically deletes old snapshots beyond the retention limits — so storage doesn't fill up over time.

Then I enabled the built-in Sanoid systemd timer to run every 15 minutes:

sudo systemctl enable --now sanoid.timer

Verify snapshots are being taken:

zfs list -t snapshot

Replicating Snapshots with Syncoid

Syncoid automates replicating ZFS snapshots from one machine to another — in this case from the host VM to the backup VM.

First, I generated an SSH key on the host machine:

sudo ssh-keygen -t ed25519 -f /root/.ssh/syncoid_key -C "syncoid@host"

On the backup machine, I created a dedicated syncoid user with limited privileges — following the least privilege principle, this user can only run ZFS commands:

sudo useradd -m -s /bin/bash syncoid
sudo mkdir /home/syncoid/.ssh
sudo visudo

Add this line in visudo:

syncoid ALL=(ALL) NOPASSWD: /usr/sbin/zfs

Then I copied the public key from the host machine manually into /home/syncoid/.ssh/authorized_keys on the backup machine.

Next, I created a systemd service file at /etc/systemd/system/syncoid.service:

[Unit]
Description=Syncoid to Automate Copies to Backup

[Service]
Type=oneshot
ExecStart=/usr/sbin/syncoid --sshkey=/root/.ssh/syncoid_key tank/docker syncoid@<backup_ip>:tank/docker

And a timer at /etc/systemd/system/syncoid.timer to trigger it every 4 hours:

[Unit]
Description=Syncoid Timer Triggered Every 4 Hours

[Timer]
OnBootSec=5min
OnUnitActiveSec=4h
Persistent=true

[Install]
WantedBy=timers.target

Then enabled and started the timer:

sudo systemctl daemon-reload
sudo systemctl enable --now syncoid.timer

Verify replication is working on the backup machine:

zfs list -t snapshot

Recovery Drill

A backup you've never tested is not a backup you can trust. So I did a full recovery drill — simulating a complete host machine failure and bringing Drupal back up on the backup machine from a ZFS snapshot.

First, I found the latest Sanoid snapshot on the backup machine:

zfs list -t snapshot | grep autosnap | tail -1

Instead of running Docker directly on the live dataset, I cloned the snapshot into a separate dataset — this keeps the original snapshots untouched

sudo zfs clone tank/docker@autosnap_<timestamp>_hourly tank/drupal

Then I pointed Docker at the cloned dataset by modifying /etc/docker/daemon.json on the backup machine:

{
    "data-root": "/tank/drupal"
}

Restarted Docker:

sudo systemctl restart docker

Then brought the stack up:

docker compose up -d

The Drupal site came back up perfectly — with all content intact from the last snapshot.

Note: docker-compose.yml files live outside of Docker volumes, so they won't be included in ZFS snapshots. It's a good practice to store them in a Git repository so they're always accessible — even if your host machine dies completely.


Conclusion

The beauty of this setup is its simplicity. You configure ZFS once, point Docker at it, and every project you run is automatically protected — no per-project backup configuration needed. Sanoid handles snapshot retention, Syncoid handles replication, and in a disaster scenario you can clone a snapshot and have your entire stack running on a different machine in minutes.

The full stack we built:

  • Docker + Compose for running containerized applications
  • ZFS as the storage foundation
  • Sanoid for automated snapshot management
  • Syncoid for replication to a backup machine