Systemd Timer

Run github-backup automatically on a schedule using a systemd service + timer pair.

Setup

1. Create a dedicated user

sudo useradd -r -m -d /var/backup/github -s /sbin/nologin github-backup
sudo mkdir -p /var/backup/github
sudo chown github-backup:github-backup /var/backup/github

2. Store the token securely

sudo mkdir -p /etc/github-backup
sudo tee /etc/github-backup/secrets.env > /dev/null <<'EOF'
GITHUB_TOKEN=ghp_your_token_here
EOF
sudo chmod 600 /etc/github-backup/secrets.env
sudo chown root:github-backup /etc/github-backup/secrets.env

3. Create a config file

sudo tee /etc/github-backup/config.toml > /dev/null <<'EOF'
owner = "octocat"
output = "/var/backup/github"
concurrency = 8
repositories = true
issues = true
pulls = true
releases = true
wikis = true
gists = true
EOF
sudo chmod 644 /etc/github-backup/config.toml

4. Create the service unit

sudo tee /etc/systemd/system/github-backup.service > /dev/null <<'EOF'
[Unit]
Description=GitHub Backup
After=network-online.target
Wants=network-online.target
OnFailure=notify-failure@%n.service

[Service]
Type=oneshot
User=github-backup
Group=github-backup
EnvironmentFile=/etc/github-backup/secrets.env
ExecStart=/usr/local/bin/github-backup --config /etc/github-backup/config.toml --report /var/lib/github-backup/report.json
StateDirectory=github-backup
StandardOutput=journal
StandardError=journal
SyslogIdentifier=github-backup

# Hardening
ProtectSystem=strict
ReadWritePaths=/var/backup/github
PrivateTmp=true
NoNewPrivileges=true
EOF

5. Create the timer unit

sudo tee /etc/systemd/system/github-backup.timer > /dev/null <<'EOF'
[Unit]
Description=Run GitHub Backup daily at 02:00
Requires=github-backup.service

[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=1800
Persistent=true

[Install]
WantedBy=timers.target
EOF

6. Enable and start

sudo systemctl daemon-reload
sudo systemctl enable --now github-backup.timer

7. Verify

# Check timer status
systemctl status github-backup.timer

# Run immediately to test
sudo systemctl start github-backup.service

# Follow logs
journalctl -u github-backup.service -f

Exit Status and Failure Handling

The unit fails whenever the exit status is not 0: 1 (the run could not run), 3 (the run finished but something could not be backed up) and 130 / 143 (stopped). OnFailure= therefore fires for an incomplete backup too, and systemctl status github-backup.service shows the status. Do not add SuccessExitStatus=3: that would hide incomplete backups. journalctl shows the summary banner with the list of failures. See Monitoring for the report file, metrics and webhook.

A failure unit that sends mail:

# /etc/systemd/system/notify-failure@.service
[Unit]
Description=Notify on failure of %i

[Service]
Type=oneshot
ExecStart=/usr/bin/mail -s "%i failed on %H" ops@example.com

StateDirectory=github-backup creates /var/lib/github-backup (owned by the service user) for the --report file; with ProtectSystem=strict the report path must be writable, which StateDirectory= provides.

Stopping and Timeouts

systemctl stop sends SIGTERM. The backup stops its git processes, releases its lock and exits 143 within a few seconds; the next run resumes (within 6 hours) from the checkpoint. No cleanup of lock files is ever needed.

Multiple Owners

Create separate service/timer pairs per owner, or one script that does not stop at the first failure:

#!/bin/bash
# /usr/local/bin/github-backup-all.sh
status=0
for owner in octocat myorg another-org; do
  github-backup "$owner" --config /etc/github-backup/config.toml \
    --output /var/backup/github || status=$?
done
exit "$status"

(Each owner needs its own --report and --prometheus-metrics file if you use them: both hold one run. If the config file has org = true it applies to every owner; keep organisations and users in separate configs.) The last non-zero status is the one systemd sees.

Monitoring

systemctl list-timers github-backup.timer
journalctl -u github-backup.service --since "yesterday"