Skip to content

Latest commit

 

History

History

samba-timemachine

README.md

samba-timemachine-docker

This is a docker container based on Debian Trixie with SAMBA configured to provide Apple "Time Capsule" like backups.

The Docker Hub images support AMD64, Raspberry Pi 3/4 and other modern ARM64 based systems.

Networking

The container by default listens on port 10445, allowing this container to run alongside an existing SAMBA server and to remove the need for root access in the container

An example of how to use the container with raw docker

docker run -d -t \
    -v /backups/timemachine:/backups \
    -p 10445:10445 \
    --restart unless-stopped timjdfletcher/samba-timemachine:latest

The repo includes an example docker compose file that starts the container with a local volume and healthchecks enabled.

Discovery

The container only runs smbd on a nonstandard port, to enable discovery on your local network a tool such as avahi to announce the server over mDNS.

I do this by running avahi-daemon on the docker host system, for debian type systems install the package avahi-daemon:

apt install avahi-daemon

And copy the example service file to /etc/avahi/services/timemachine.service

Autostart with SystemD

Use the setup script to create a system user and install the systemd service:

sudo ./scripts/setup-system.sh --backup-dir /mnt/backups

The script:

  • Creates a timemachine system user (in the docker group)
  • Sets up the backup directory with correct permissions
  • Installs the docker-compose stack to /opt/samba-timemachine
  • Creates and enables the systemd unit

Options:

  • --uid UID — UID for timemachine user (default: 999)
  • --gid GID — GID for timemachine group (default: 999)
  • --backup-dir DIR — Backup storage directory (default: /data/timemachine)
  • --install-dir DIR — Installation directory (default: /opt/samba-timemachine)

After setup, edit /opt/samba-timemachine/.env to set your password, then start the service:

systemctl start timemachine

For manual installation, see the example unit file.

Settings

Variable Function Default
USER Time Machine Username timemachine
PASS Time Machine Password password
PUID Unix User ID for Time Machine user 999
PGID Unix Group ID for Time Machine user 999
LOG_LEVEL SAMBA logging level 1
METRICS_FILE Path to write backup metrics to unset (off)

The defaults are embedded in the Dockerfile

Security

The security design is simple and assumes that timemachine backups are encrypted before leaving the source macOS system.

The default configuration of the container creates a unix user called timemachine with uid and gid 999, and a matching SAMBA user called timemachine with a password of password.

A custom username can be passed to the container with the environment variable USER.

A custom password can be passed to the container with the environment variable PASS.

Quota

Quota management is now handled on the client side using the tmutil command. This is more reliable and consistent with modern macOS versions.

To set a quota, you first need to identify the destination_id for your Time Machine backup. You can do this by running:

tmutil destinationinfo

This will output information about your backup destination(s), including the ID. Once you have the ID, you can set the quota.

For example, to set a quota of 500GB on a destination with the ID A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6, you would run:

sudo tmutil setquota A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6 500

Building the Docker image

To build the image you need to have docker and docker buildx available, this is included by default in docker desktop but for colime buildx needs to be installed.

Testing

Goss tests are included, to execute the tests use the run script: ./run test

Trivy is configured as well to test the container for known vulnerabilities.

Debugging

The container can be started with SAMBA debugging flags for example: --debuglevel=4

There is a utility function in the run script that will print out macOS timemachine logs and then follow them to use it call: ./run timemachineLogs

Storage notes

Generally speaking timemachine backups are heavy metadata workloads. I have had some performance problems using ZFS as a backing store for the container in Catalina. I'm not sure if this because of the slow SMR drive I was using or by ZFS's copy on write design interacting badly with APFS. I have changed the backend storage that I use to ext4 which has been working well.

Backup Monitoring

The repository includes a backup-check.sh script in the scripts/ directory. This script is designed to monitor the health of your Time Machine backups.

It works by scanning for .sparsebundle directories and checking the modification times of the files within them. If no files have been modified within a configurable number of days (default is 14), it is considered "stale," and a notification can be sent.

Features

  • Stale Backup Detection: Identifies backups that haven't been updated recently.
  • Webhook Integration: Can trigger a webhook (e.g., to a service like Healthchecks.io or a custom notification system) by setting the WEBHOOK_URL environment variable. If the variable is not set, it performs a dry run.
  • Cron Job Ready: The script can be scheduled to run automatically. An example cron file, scripts/backup-check.cron, is provided to show how to execute it periodically.

Backup Metrics

Set METRICS_FILE to a path inside the container and it writes the state of the backups to that file in Prometheus text format, for a collector such as node-exporter's textfile collector to read. Leave it unset and the feature is off: no file is written and Samba's config has no postexec line.

The file is written:

  • once at container start, so it exists before the first backup;
  • each time a client disconnects from the share (Samba's postexec). A Mac connects, backs up and disconnects, so this is the end of each backup session. Connections from inside the container, such as the healthcheck, are ignored.

There is no schedule inside the container.

Each run is cheap however large the backups are. The bundle values are read from the small property lists the Mac keeps at the top of each sparsebundle and from a listing of its bands directory; the band files themselves are never opened or measured.

Metric Labels Meaning
timemachine_bundle_last_backup_timestamp_seconds bundle Completion time of the newest snapshot, from the bundle's com.apple.TimeMachine.SnapshotHistory.plist
timemachine_bundle_size_bytes bundle Size of the sparsebundle, estimated as its band count times the band-size in its Info.plist
timemachine_bundles Number of sparsebundles found
timemachine_volume_available_bytes Space available to the backups on the filesystem holding /backups (as df reports it)
timemachine_volume_size_bytes Size of that filesystem
timemachine_metrics_success 1 if the last run could read the backup directory, else 0
timemachine_metrics_last_success_timestamp_seconds Time of the last successful run

bundle is the sparsebundle's name without .sparsebundle. Alert on free space with e.g. timemachine_volume_available_bytes / timemachine_volume_size_bytes < 0.1. A failed run writes only the last two metrics, keeping the previous last-success time, so a consumer can tell a failed run from an old one.

A bundle value that can't be read is left out rather than reported as zero: a sparsebundle with no completed backup has no last-backup time, and one without a bands directory or a band-size has no size. It still counts towards timemachine_bundles.

Mounting the output directory

The script runs as the Time Machine user (PUID/PGID), both at start and on disconnect, because Samba runs postexec as the connecting user. Mount a directory for the file and make it writable by that user, e.g. for a node-exporter textfile directory on the host:

sudo chown 999:999 /var/lib/node_exporter/textfile
docker run -d -t \
    -v /backups/timemachine:/backups \
    -v /var/lib/node_exporter/textfile:/metrics \
    -e METRICS_FILE=/metrics/timemachine.prom \
    -p 10445:10445 \
    --restart unless-stopped timjdfletcher/samba-timemachine:latest

With compose, docker-compose.metrics.yml adds a samba-timemachine_metrics volume at /metrics. Create it and hand it to the user first:

docker volume create samba-timemachine_metrics
docker run --rm -v samba-timemachine_metrics:/metrics debian:stable-slim chown 999:999 /metrics
METRICS_FILE=/metrics/timemachine.prom docker compose -f docker-compose.yml -f docker-compose.metrics.yml up -d

If the directory isn't writable the container logs a warning at start and runs without metrics. The path must not contain ' or %.

The file is written to a temporary file in the same directory and renamed into place, so readers never see a partial file and clients disconnecting together can't corrupt it. node-exporter's textfile collector only reads *.prom files, so it ignores the temporary files.

Grafana dashboard

grafana/timemachine-dashboard.json is a simple dashboard for these metrics. Import it in Grafana (Dashboards → New → Import) or provision it from a file, and pick your Prometheus data source. It shows:

  • the oldest backup, free space (bytes and %), the number of backups, and whether the metrics run is working;
  • time since the last backup and backup size for each Mac;
  • free space and backup size over time.

Time since backup turns amber after 3 days and red after 14; free space turns amber below 20% and red below 10%. The instance selector filters by the Prometheus instance label if you run more than one server.

Time Machine backups dashboard

Caveats

  • Don't alert on the file's age. The file is only rewritten when a client disconnects, so between backups it is as old as the last backup. Its values are timestamps and stay correct: alert on time() - timemachine_bundle_last_backup_timestamp_seconds, and on timemachine_metrics_success == 0.
  • A client that sleeps or drops off mid-backup only triggers the rewrite when smbd notices the dead connection, so the file can lag by that timeout.
  • Free space is sampled at the same moments, at start and on disconnect. Backups are what fill the volume, so that catches the growth that matters; if other things share the filesystem, watch it from the host as well (node-exporter's node_filesystem_avail_bytes).
  • Bundle size is an estimate. Bands are counted, not measured, so a partly filled band counts as a full one and the value is an upper bound on the disk space used.
  • The last-backup time comes from a file the Mac writes. Apple doesn't document its format. If the history is there but can't be parsed, the time it was last written is reported instead.
  • A failed or interrupted backup doesn't move the last-backup time. Only completed snapshots count, so a Mac that connects every hour but never finishes still shows as stale.

Known Bugs

I have had some macOS kernel watchdogd crashes in smbfs that I think might be related to this container, I've done the following things to fix them:

  • Switch to using trixie backports for a newer version of SAMBA
  • Applied this fix to my MacBook

Software Used