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.
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:latestThe repo includes an example docker compose file that starts the container with a local volume and healthchecks enabled.
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-daemonAnd copy the example service file to /etc/avahi/services/timemachine.service
Use the setup script to create a system user and install the systemd service:
sudo ./scripts/setup-system.sh --backup-dir /mnt/backupsThe script:
- Creates a
timemachinesystem 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 timemachineFor manual installation, see the example unit file.
| 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
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 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 destinationinfoThis 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 500To 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.
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.
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
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.
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.
- 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_URLenvironment 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.
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.
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:latestWith 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 -dIf 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/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.
- 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 ontimemachine_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.
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
- Debian Trixie
- SAMBA
- Goss — built from source with patched dependencies (see
../gossproject)
