perch gc and perch backup are one-shot commands and are safe to run while perch serve is running: SQLite WAL mode allows concurrent readers and writers.

perch reconcile does not need a timer. serve reconciles S3 at startup and every storage.s3.reconcile_every (default 5m).

Backup timer

perch backup refuses to overwrite an existing destination. Use a wrapper to add timestamps and prune old copies. Write /usr/local/bin/perch-backup:

sudo tee /usr/local/bin/perch-backup >/dev/null <<'EOF'
#!/bin/sh
set -eu
stamp=$(date +%Y%m%d-%H%M%S)
/usr/local/bin/perch backup --config /etc/perch/perch.yaml "/var/lib/perch/backups/perch-$stamp.db"
find /var/lib/perch/backups -type f -name 'perch-*.db' -mtime +14 -delete
EOF
sudo chmod 0755 /usr/local/bin/perch-backup

/etc/systemd/system/perch-backup.service:

[Unit]
Description=perch SQLite backup
After=perch.service

[Service]
Type=oneshot
User=perch
Group=perch
ExecStart=/usr/local/bin/perch-backup
EnvironmentFile=-/etc/perch/perch.env
UMask=0077
Nice=10
IOSchedulingClass=idle

/etc/systemd/system/perch-backup.timer:

[Unit]
Description=Daily perch backup

[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=15m
Persistent=true
Unit=perch-backup.service

[Install]
WantedBy=timers.target

Garbage collection timer

perch gc removes orphaned local blobs, dead letters older than 30 days (--dead-letter-age), and enforces the S3 spool limit, then runs an incremental vacuum. Add --s3-delete to also delete S3 objects belonging to deleted messages; without it, those tombstones accumulate.

/etc/systemd/system/perch-gc.service:

[Unit]
Description=perch garbage collection
After=perch.service

[Service]
Type=oneshot
User=perch
Group=perch
ExecStart=/usr/local/bin/perch gc --config /etc/perch/perch.yaml
EnvironmentFile=-/etc/perch/perch.env
UMask=0077
Nice=10
IOSchedulingClass=idle

/etc/systemd/system/perch-gc.timer:

[Unit]
Description=Daily perch garbage collection

[Timer]
OnCalendar=*-*-* 04:30:00
RandomizedDelaySec=15m
Persistent=true
Unit=perch-gc.service

[Install]
WantedBy=timers.target

Enable both timers:

sudo systemctl daemon-reload
sudo systemctl enable --now perch-backup.timer perch-gc.timer
systemctl list-timers --all 'perch-*'

Run a job immediately with sudo systemctl start perch-backup.service or sudo systemctl start perch-gc.service; inspect results with journalctl -u perch-backup.service -u perch-gc.service.

The timers run as perch and only need the same config and environment as the main service. In SQLite mode, --s3-delete and the spool steps are ignored.