English | Русский
Painless + fast + simple (that's the whole point) regular backup of all your GitHub repos to any VPS.
Requires github.com/gabrie30/ghorg in /usr/local/bin, plus git, jq and curl.
Each account needs a classic personal access token with the repo and read:org scopes: repo for private repos, read:org to list organizations.
cp accounts.env.example accounts.env
chmod 600 /root/github-backup/accounts.env
chmod 700 /root/github-backup/github-backup.sh
/etc/systemd/system/github-backup.service:
[Unit]
Description=Mirror GitHub accounts and their organizations
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/root/github-backup/github-backup.sh
/etc/systemd/system/github-backup.timer:
[Unit]
Description=Run github-backup daily
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now github-backup.timer
journalctl -u github-backup.service -f
Repos deleted on GitHub are moved to /data/_trash/... with a timestamp:
- user:
/data/<account>/repos/<repo>→/data/_trash/<account>/repos/<repo>__<UTC> - org:
/data/<account>/orgs/<org>/<repo>→/data/_trash/<account>/orgs/<org>/<repo>__<UTC>
Deletion is detected through the GitHub API (the source of truth), not through ghorg --prune.
If the API request fails or returns an empty list, the move is skipped (protection against false removal when a token or the network fails) – look for trash skipped lines in the logs.
Contents – what was last backed up and when (sorted by freshness):
./backup-status.sh # or: ./backup-status.sh /dataColumns: LAST FETCH (when the mirror was last synced),
LAST COMMIT (date of the newest commit in the mirror), REPO. The top rows are
the most recent. At the bottom: the number of mirrors in trash and the total size.
Runs – whether the timer fired and with what result (systemd):
systemctl list-timers github-backup.timer --no-pager # next / last run
systemctl status github-backup.service --no-pager # result of the last run
journalctl -u github-backup.service -n 100 --no-pager # full log of the last run
journalctl -u github-backup.service -f # follow liveA failed ghorg run or a failed GitHub API request is logged as FAIL:. The run still processes everything else, then exits non-zero, so systemctl status shows the service as failed. An empty remote list only logs trash skipped and is not a failure.
Optionally, report each run to an Uptime Kuma push monitor:
- Create a monitor of type Push with a heartbeat interval of
93600seconds (26 hours: the daily timer can drift by up to an hour). - Add its URL to
accounts.env:
KUMA_PUSH_URL="https://kuma.example.com/api/push/<token>"
A clean run sends up, a run with failures sends down. A missing heartbeat also turns the monitor down, which covers a dead timer or host.
banochkin.com DAO 🏴☠️