Skip to content


Repository files navigation

ZFS TimeMachine

TimeMachine style backups for ZFS users. ZFS-Timemachine creates incremental backups of zfs datasets on one host to datasets on another disk or host. This is done via sending snapshots, deleting old ones in time machine style. It works with FreeBSD and Macs (with TensCompliments ZFS implementation), but should work with other ZFS implementations as well.

How it works

  • the script creates a snapshot on the source dataset every time it is called.
  • then it figures out the last snapshot on the destination dataset that matches to one on the source dataset.
  • it sends the snapshot from the source to the destination.
  • removes old snapshots on the source - it keeps just n-snapshots.
  • removes old snapshots on the destination - time machine fashion : 5min/last day, 1 hour last week, 1 day last 3 months, 1 week thereafter


It requires perl and the Time::Local and Date::Parse libraries. If you are on a Mac you can install them by using the command line:

$export PERL_MM_USE_DEFAULT=1 ; perl -MCPAN -e 'install Date::Parse' 'install Time::Local'

If you are on a different OS (like linux or bsd) everything should work.

How to use

The simplest use of the script requires just two options, --sourcedataset and --destinationdataset options. Like this:

$ sudo zfstimemachinebackup.perl --sourcedataset=tank --destinationdataset=root/backup

Usually you want the script to create a snapshot on the sourcedataset when it is called, so add the --createsnapshotonsource option. To see all options use the --help commandline option.

As there are quite a few options, let's go through them in detail:

Source host:

  • --sourcehost (string): hostname where the source dataset is on.
  • --sourcehostoptions (string): options given to ssh (default: -c blowfish -C -l root).
  • --sourcedataset (string): the source dataset that is to be backed up.

Destination host:

  • --destinationhost (string): hostname where the destination dataset is on.
  • --destinationhostoptions (string): options given to ssh (default: -c blowfish -C -l root)
  • --destinationdataset (string): the destination dataset where backups should be stored.

Source options:

  • --createsnapshotonsource (flag): When set the script will create a new snapshot on the source dataset everytime it is called.
  • --snapshotstokeeponsource (number): How many snapshots we should keep on the source dataset. More datasets on source will be deleted (oldest beeing deleted first). If set to 0 no snapshots will be removed on the source. See also --minimumtimetokeepsnapshotsonsource option.
  • --minimumtimetokeepsnapshotsonsource (string): Minimum time how long snapshots should exist on the source. With this set snapshots on the source will be kept at least that long even if there are more than the number of snapshots given in the --snapshotstokeeponsource option. (Eg: 1week, 1month or something like that).
  • --replicate (flag): Only needed for the very first backup. It will replicate all snapshots from the source to the destination.
  • --recursive (flag): Should we backup all decendent datasets on the source to the destination.
  • --raw (flag): Backup the data in raw mode, this sends the encrypted version of a dataset, if using ZFS encryption
  • --datasetstoignoreonsource (string): If you are recursivly backing up, you can disable backing up datasets that match this comma seperated list of datasets.

Destination snapshots:

  • --deletesnapshotsondestination (flag): Should old snapshots on the destination be deleted.

  • --keepbackupshash (string): A comma seperated list of value pairs that define the granularity of how many snapshots are kept on the destination when they are getting older. The default is 24h=>5min,7d=>1h,90d=>1d,1y=>1w,10y=>1month which means:

      24h=>5mi	for snapshots younger than 24hours: keep not more than one per 5 minutes
      7d=>1h		for snapshots younger than 7 days: keep not more than one snapshot per 1 hour
  • --maximumtimeperfilesystemhash (string) default: A comma seperated list of value pairs that define the granularity of how old snapshots can get on the destination. Special datasets might not be as important as others. Default of .*=>3months,.+/(Dropbox|Downloads|Caches|Mail Downloads|Saved Application State|Logs)$=>1month means:

      .*=>10yrs	keep everything 10 years by default - after that snapshots are removed
      .+/(Dropbox|Downloads|Caches|Mail Downloads|Saved Application State|Logs)$=>1month
      			remove snapshots older than one month for datasets ending with the regex.


  • --configurationfilename (string): config.ini filename the defaults are read from. Only works if you Config::Inifiles installed.
  • --debug (number): debugging level of the script itself. When set will also enable verbose.
  • --verbose (flag): showing more verbosely what is going on.
  • --help (flag): Shows all options of the script and all values without starting the script.


My current setup looks like this:

$ zfs list
puddle                                                           207Gi   214Gi   864Ki  /Volumes/puddle
puddle/Local                                                     207Gi   214Gi  2.50Gi  /Local
puddle/Local/Users                                               204Gi   214Gi   891Mi  /Local/Users
puddle/Local/Users/jolly                                         204Gi   214Gi  50.4Gi  /Local/Users/jolly
puddle/Local/Users/jolly/Disks                                  22.4Gi   214Gi  22.3Gi  /Local/Users/jolly/Disks
puddle/Local/Users/jolly/Downloads                              1.62Gi   214Gi  1.62Gi  /Local/Users/jolly/Downloads
puddle/Local/Users/jolly/Dropbox                                3.53Gi   214Gi  3.53Gi  /Local/Users/jolly/Dropbox
puddle/Local/Users/jolly/Library                                44.6Gi   214Gi  28.3Gi  /Local/Users/jolly/Library
puddle/Local/Users/jolly/Library/Caches                         2.05Gi   214Gi  2.04Gi  /Local/Users/jolly/Library/Caches
puddle/Local/Users/jolly/Library/Logs                           72.2Mi   214Gi  70.4Mi  /Local/Users/jolly/Library/Logs
puddle/Local/Users/jolly/Library/Mail                           13.8Gi   214Gi  13.7Gi  /Local/Users/jolly/Library/Mail
puddle/Local/Users/jolly/Library/Mail Downloads                  868Ki   214Gi   868Ki  /Local/Users/jolly/Library/Mail Downloads
puddle/Local/Users/jolly/Library/Saved Application State        50.8Mi   214Gi  9.38Mi  /Local/Users/jolly/Library/Saved Application State
puddle/Local/Users/jolly/Pictures                               80.4Gi   214Gi  80.4Gi  /Local/Users/jolly/Pictures
ocean                                                           1.24Ti   567Gi   266Ki  /Volumes/ocean
ocean/puddle                                                     635Gi   567Gi   187Ki  /Volumes/ocean/puddle
ocean/puddle/Local                                               635Gi   567Gi  1.76Gi  /Volumes/ocean/puddle/Local
ocean/puddle/Local/Users                                         632Gi   567Gi   539Mi  /Volumes/ocean/puddle/Local/Users
ocean/puddle/Local/Users/jolly                                   631Gi   567Gi  49.8Gi  /Volumes/ocean/puddle/Local/Users/jolly
ocean/puddle/Local/Users/jolly/Disks                            48.0Gi   567Gi  22.1Gi  /Volumes/ocean/puddle/Local/Users/jolly/Disks
ocean/puddle/Local/Users/jolly/Downloads                        1.62Gi   567Gi  1.62Gi  /Volumes/ocean/puddle/Local/Users/jolly/Downloads
ocean/puddle/Local/Users/jolly/Dropbox                          4.42Gi   567Gi  3.47Gi  /Volumes/ocean/puddle/Local/Users/jolly/Dropbox
ocean/puddle/Local/Users/jolly/Library                          93.7Gi   567Gi  22.6Gi  /Volumes/ocean/puddle/Local/Users/jolly/Library
ocean/puddle/Local/Users/jolly/Library/Caches                   1.90Gi   567Gi  1.90Gi  /Volumes/ocean/puddle/Local/Users/jolly/Library/Caches
ocean/puddle/Local/Users/jolly/Library/Logs                     65.2Mi   567Gi  65.1Mi  /Volumes/ocean/puddle/Local/Users/jolly/Library/Logs
ocean/puddle/Local/Users/jolly/Library/Mail                     18.6Gi   567Gi  11.2Gi  /Volumes/ocean/puddle/Local/Users/jolly/Library/Mail
ocean/puddle/Local/Users/jolly/Library/Mail Downloads            210Ki   567Gi   208Ki  /Volumes/ocean/puddle/Local/Users/jolly/Library/Mail Downloads
ocean/puddle/Local/Users/jolly/Library/Saved Application State  12.4Mi   567Gi  5.83Mi  /Volumes/ocean/puddle/Local/Users/jolly/Library/Saved Application State
ocean/puddle/Local/Users/jolly/Pictures                         85.7Gi   567Gi  73.8Gi  /Volumes/ocean/puddle/Local/Users/jolly/Pictures

/Local is where my home directory lives. The script is called as follows

$ sudo ./zfstimemachinebackup.perl  --sourcedataset=puddle --destinationdataset=ocean/puddle --snapshotstokeeponsource=100 --createsnapshotonsource --recursive

So puddle is set as source, ocean/puddle will receive the snapshots from puddle and 100 snapshots are kept on puddle itself.

I'm also sending backups from the backupdisk to a remote machine with less space, so I keep backups only for 3 months:

$ sudo ./zfstimemachinebackup.perl  --sourcedataset=ocean/puddle --destinationdataset=backups/puddle --recursive --maximumtimeperfilesystemhash='.*=>3months,.+/(Dropbox|Downloads|Caches|Mail Downloads|Saved Application State|Logs)$=>1month'

Autoscrub script

My backupserver usually sleeps and so it might take a day or two before finishing a download. The autoscrub script will scrub the given pool after the time given.

usage: sudo ./autoscrub.perl --scrubinterval=14

This will scrub your pools every 14 days. If you cancel a scrub that will be recognized but also it will be scrubed after the scrubinterval passed, in case you forgot that you canceled it.

You can start it for different pools as well.

I'm using it in a crontab entry:

1 * * * * cd ~jolly/Binaries/ZFSTimeMachine;./autoscrub.perl >/dev/null 2>&1

Mac OS X only stuff

The following is only relevant to those who use Macs.

CheckBackup Script

The checkbackup.perl script checks if your backupscript is working correctly. As I do sleep my machine it will check if the snapshots are beeing done within the last 2*snapshotinterval+snapshottime seconds since the last wake or reboot. Exit code is correct depending if the snapshot is there or not. If the checkbackupscript can't find out the last sleep and boot time it will bug you about backups beeing too old when the machine has beeing powerd off for some time.

It has three options :

--datasets which dataset(s) to use comma separated list
--snaphotinterval how often do you create snapshots
--snapshotime how long it usually take for a snapshot to complete

$[checkbackup.perl] module options are :
--configurationfilename (string) default: config.ini
								 current: not used as Config:IniFiles module not present	
--debug (number)                 default: 0	
--help (option)                  default: 
								 current: 1	
--datasets (string)              default: puddle	
--snapshotinterval (number)      default: 300	
--snapshottime (number)          default: 10

I'm currently using a script at crontab (executing as root) to tell me when things go wrong:


./checkbackup.perl --datasets="puddle/Local,puddle/Local/Users,puddle/Local/Users/jolly,puddle/Local/Users/jolly/Library,puddle/Local/Users/jolly/Disks,puddle/Local/Users/jolly/Pictures" --snapshotinterval=7200 || say -v alex "dataset snapshot on local host is too old"
./checkbackup.perl --datasets=",,,,," --snapshotinterval=7200 || say -v alex "dataset pond snapshots on are too old"

TimeMachine backups to ZFS Volumes

For those of you that want to use MacOS X's TimeMachine to backup to a ZFS volume you can create the needed sparsebundle with the following commands. I now have moved everything except for my boot partitions to ZFS. To have some backup of the root drive I'm backing that of with Apples provided TimeMachine and here is how I do it:

Create a zfs filesystem for the TimeMachine backups for several machines:

sudo zfs create ocean/TimeMachine

Create a 100Gb sparsebundle for TimeMachine (my root is rather small, your mileage may vary):

hdiutil create -size 100g -library SPUD -fs JHFSX -type SPARSEBUNDLE -volname "tmmachinename" /Volumes/ocean/TimeMachine/tmmachinename.sparsebundle

Set up crontab to mount the sparsebundle every 20 minutes if it's not mounted yet. This is needed as TimeMachine will unmount the backup disk if it's a sparsebundle after backing up.

*/20 * * * *	if [ ! -d /Volumes/tmtinkerbell ] ;then hdiutil attach /Volumes/ocean/TimeMachine/tmmachinename.sparsebundle; fi </dev/null >/dev/null 2>&1

Set up TimeMachine to use the sparsebundle:

tmutil setdestination -p /Volumes/tmmachinename


TimeMachine style backup for ZFS







No releases published


No packages published