This is the recommended deployment. A background worker backs up all configured GitHub App installations to a private Spaces bucket. A small protected config service provides automated GitHub App registration through an HTTPS domain. The existing CLI and filesystem backends remain available.
Protect Coolify and Spaces with credentials independent of the GitHub accounts being backed up. Disable automatic deployments from GitHub for this backup worker. Deploy reviewed source commits manually, or use a separately trusted image pinned by digest when available. A compromised GitHub account must not be able to push replacement backup-worker code into Coolify and steal its Spaces credentials. The worker never checks out or executes code from backed-up repositories. For protection against compromise of the backup host/Spaces credentials as well, maintain an independent offline or immutable copy; this deployment has delete permission for retention.
Deploy on Ubuntu 24.04 LTS or a newer supported Ubuntu server, x86_64 / AMD64 with
Docker Engine, the Docker Compose plugin, and Coolify configured on that destination.
The Compose service fixes both its build target and runtime platform to linux/amd64.
CI builds/scans that platform on Ubuntu 24.04 runners; releases publish the same x86_64 image.
Raspberry Pi OS and ARM64 are not the deployment targets of this stack.
Confirm the destination server before deployment:
uname -m # expected: x86_64
cat /etc/os-release # Ubuntu 24.04 or a newer supported Ubuntu release
docker version
docker compose versionThe container intentionally uses a patched, digest-pinned Python 3.13 Debian Bookworm slim userspace. It runs on the Ubuntu host through Docker; the image distribution does not need to match the host distribution. No Python, Git, or OpenSSL installation is needed directly on the host for the backup worker. Select this Ubuntu x86_64 server as the Coolify destination. Do not remove the platform setting to build accidentally for an ARM development machine.
References: Docker Engine on Ubuntu, Compose platform/build selection.
| Setting | Default | Purpose |
|---|---|---|
| Schedule | After GitHub setup, then every hour | Begins within 30 seconds of first account enablement |
| Recovery points | 3 latest + 2 weekly + 2 monthly + oldest baseline (up to 8 distinct) | Recent code and older safety checkpoints |
| Missing/deleted repositories | Keep indefinitely; report unavailable IDs | Upstream deletion never propagates |
| Artifact budget | 100 GiB per prefix | Refuse new artifacts before crossing budget |
| Run timeout | 12 hours | Stop hung backups and report failure |
| Workers | 1 | Process repositories sequentially |
| Worker CPU / memory limits | 0.5 CPU / 512 MiB | Prevent host resource exhaustion |
| Config CPU / memory limits | 0.25 CPU / 128 MiB | One web worker with two threads; usually idle |
| Manifest history | 14 recent manifests; independent per-repository catalogs | Bound diagnostic history |
| Extra service data | Disabled | Predictable scope and size; Git refs/history only |
| Cache | Persistent named volume | Incremental Git fetches between runs |
The scheduler measures intervals from each run's start, rather than a wall-clock cron. Restarting the service triggers a new run. It never overlaps scheduled jobs; missed intervals are skipped. Set BACKUP_INTERVAL_HOURS to a number of hours (minimum 1). Do not add a second Coolify scheduled job for this service.
The README installation steps are the primary walkthrough.
Use Docker Compose from Git, Base Directory /, Compose Location /compose.yaml, one replica
of each service and the shared persistent backup-cache volume. Assign an HTTPS domain to
github-config, forwarding to container port 8080, with no published host port. If Coolify
uses a target-port suffix in its domain field, enter https://github-backup.example.com:8080;
the public URL remains https://github-backup.example.com. The worker has no domain.
Only the config HTTPS URL is required in the environment for a new installation:
GITHUB_CONFIG_URL=https://github-backup.example.comThe URL is the exact HTTPS origin without a path or internal target port. Enable Buildtime and Runtime on supplied variables for Compose interpolation. Save and deploy. Do not add a Coolify scheduled task: the worker includes scheduling.
The first visit shows registration. Retrieve the one-time code from the github-config
service Terminal in Coolify:
python -m github_backup.config_auth registration-codeChoose your own username (3-64 letters, digits, dots, underscores or hyphens) and a unique 16-128 character password/passphrase, confirm it and store it securely. Register once, then log in through the form. Registration is permanently disabled after that first owner is created. No reset route or extra users are available. Forgotten credentials require a fresh deployment with a new persistent volume; do not delete remote backups. Damaged/deleted auth state blocks setup instead of reopening registration. Read account recovery. The registration code is never displayed in the web page or ordinary container logs.
After login, fill out DigitalOcean Spaces in the form. Enter the
bucket name, region slug, root folder (default github-backup), Spaces access key ID and
matching secret. These values are no longer required in environment variables. Find the
bucket under DigitalOcean Spaces Object Storage → Buckets, and create a key under
Spaces Object Storage → Access Keys. This is not a DigitalOcean personal API token.
For company-files.ams3.digitaloceanspaces.com, enter bucket company-files, region ams3.
fra1 is only the initial form default; choose the actual bucket region.
Use an unversioned bucket, a private exclusive prefix, and a key with object read/write/delete,
bucket listing and versioning-status read access. Shared buckets are supported: other apps'
prefixes are not counted, listed or deleted. The save action tests one disposable private
object under PREFIX/_system/setup-check/, including read-back and deletion. Failed tests
preserve the previous saved configuration. An interrupted probe or a key without delete
permission may leave a tiny object under that setup-check prefix; remove it after fixing
permissions. The application never changes bucket policies.
Key inputs are never prefilled or echoed. Leave them blank to keep saved credentials when editing other fields. Changing bucket/region/root requires an explicit confirmation; this starts a separate archive and budget, leaving the old location intact. The current backup finishes with its original configuration; subsequent runs load the new saved configuration. Do not reuse or overlap another worker's prefix. The new destination may be empty until the next scheduled run; verify its first successful manifest and restore before retiring the old one.
Create one or more GitHub Apps in setup and connect their personal/organization installations. Choose each account's repository scope: all, selected, all except selected, or pause. GitHub may grant the App access to all repositories or only selected ones. The backup selection can narrow that access; it cannot widen it. The all/all-except modes require GitHub's All repositories grant so future repositories are covered. New connections start paused. See the GitHub setup guide for multiple Apps and accounts.
Settings are atomically stored in /cache/github-app/credentials.json, permissions 0600,
on the shared volume. Both containers need access to this secret file, which contains App
keys, account selections and Spaces credentials. Back it up securely and retain ownership
UID 10001. A single immutable snapshot is used for each backup run; UI edits apply next run.
Do not run parallel deployments against the same archive.
While initial setup is incomplete, a live scheduler emits waiting_for_setup and reports
down to Kuma. Docker health confirms that the waiting scheduler is alive, not that backups
exist. After setup, check backup_finished, zero failure/partial counts, the selected repo
inventory and a restore drill. An all-paused or empty selection backs up no repositories.
A failing App connection is recorded in discovery_errors and makes the run unhealthy;
other working connections continue backing up. Deliberate exclusions appear separately in
excluded_resources, with their existing archives preserved.
.env.example contains all optional schedule/retention/budget/monitoring variables with defaults. The default is hourly checks, one transfer worker, a 100 GiB prefix budget and 3 recent + 2 weekly + 2 monthly + oldest baseline retention. At most 8 distinct versions remain per active repository with the default zero-day age floor. Paused/excluded and missing repositories preserve their existing versions indefinitely and remain in the budget.
Existing deployments may keep AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, SPACES_BUCKET,
SPACES_REGION (default fra1) and SPACES_PREFIX (default github-backup). The AWS_ names
are boto3's standard credential names and take DigitalOcean Spaces values; no AWS account
is required. Saved browser Spaces settings take precedence. Remove legacy values after
verifying the saved configuration to avoid ambiguity. If none are configured, the worker
waits for browser configuration; a partially filled legacy configuration fails clearly.
Manual single-App configuration remains available through GITHUB_APP_ID,
GITHUB_APP_OWNERS and GITHUB_APP_PRIVATE_KEY_BASE64. Those values override browser App
connections. Clear all three to use multiple Apps and repository scopes through setup.
Manual mode continues requiring All repositories on GitHub. See manual setup.
The config URL has no universal default. The account password is chosen in the registration form and is never stored in an environment variable. Optional operational variables have defaults; keep secrets out of Git/Dockerfiles. Coolify's Developer environment view can remove omitted entries when saving; Normal view lets you edit individual masked values. Preserve environment log redaction. The compose file builds the checked-out source; use a reviewed revision, not automatic GitHub-triggered updates.
Logs go to stdout/stderr and are available in Coolify Logs or docker compose logs.
Scheduler/run events are flushed JSON on stdout; application progress/errors are JSON on stderr.
Startup failures may also write standard Python tracebacks to stderr. The Docker log driver
rotates at 3 × 10 MB; no application log files or extra log volume are required.
For optional external monitoring, create a Kuma Push monitor with 180-second heartbeat
interval, 0 retries, and a tested notification channel. In setup, enter its generated
secret URL under Uptime Kuma, enable monitoring and save. It takes effect within 30 seconds
without redeployment. Leave the URL input blank to retain the saved secret; uncheck Enable
and save to disable monitoring. Saved monitoring choices override legacy UPTIME_KUMA_*
environment values. For private HTTP Kuma, explicitly check the trusted-private-network opt-in.
The scheduler sends every 60 seconds plus status changes and reports failures and overdue
successes, rather than sending unconditional up pings. A monitoring-server failure never stops
backups. The monitoring guide explains all settings, startup behavior,
private HTTP connections, secrets, freshness thresholds and an alert/recovery test.
Compose owns the health check. Failed or partial runs become unhealthy until a successful run. A dead scheduler heartbeat or an overdue completed backup also becomes unhealthy. Configure Coolify's notifications/external monitoring to alert on unhealthy status and repeated restarts; Docker does not restart containers just because they are unhealthy. Container logs rotate at 3 × 10 MB. The stored credentials remain accessible to the container administrator; protect Coolify, host access, and the underlying secret store.
The volume is initialized by Docker for UID 10001. If replacing it with a host bind mount, set the correct owner and private permissions. Size local storage for the Git mirrors plus one temporary bundle by default (more if increasing worker count). Local free-space checks run before backups. Git pack operations use one thread and modest cache/window settings; transfers also run single-threaded. The 512 MiB limit is a containment limit, not a guarantee that any-size repository will fit. Very large repositories or blobs may need BACKUP_MEMORY_LIMIT raised and a longer timeout. Throttled transfers retry after two minutes below 1 KiB/s. Persistent cache growth still needs host disk monitoring. If a cache is corrupt, stop the worker and remove only its local cache mirrors; remote catalogs remain authoritative.
The recommended profile uses versioned mode. It fetches and verifies Git mirrors on every
poll but creates a self-contained bundle only when the digest of the advertised Git refs
changes relative to the last remotely committed version. This detects commits, force-pushes,
branch/tag creation, and deleted refs; it does not trust API pushed_at timestamps. If refs
are identical, no new code artifact is uploaded. Small run manifests are still written to
prove the worker checked. Polling can miss a push and deletion/rewrite between polls; it
cannot recover code that GitHub no longer exposes before it has been backed up.
company-files/ # existing shared bucket
├── invoices/ # another application: untouched
└── github-backup/ # SPACES_PREFIX (configurable)
├── alice/
│ └── website/
│ ├── 20261107153000/
│ │ └── repository-123-<unique-id>.bundle
│ └── 20261109104520/
│ └── repository-123-<unique-id>.bundle
├── example-team/
│ └── api/
│ └── 20261107154500/
│ └── repository-456-<unique-id>.bundle
└── _system/ # internal indexes and run reports
├── catalog/repository/123.json
└── manifests/latest.json
Folder timestamps are UTC YYYYMMDDHHMMSS, captured when the local artifact is ready
for upload. The file includes the stable repository ID and a unique suffix so multiple
backups in one second or deleted-and-recreated repositories cannot overwrite each other.
Empty repositories use .empty.tar.gz instead of .bundle. Unchanged polls do not create
a timestamp folder. S3 folders are key prefixes, so deleting a folder's final object removes
that folder from the listing.
Each repository's _system/catalog/repository/GITHUB_ID.json records its name, owner,
artifact keys, timestamps, ref digests, sizes and SHA-256 checksums. Renaming or transferring
a repository sends future backups to the new owner/name folders. Existing backups stay at
their original paths and remain referenced in the same ID-based catalog. Deleting and recreating
a repository with the same name uses a new catalog and filenames, preserving the old backup.
Catalogs and code can be downloaded from Spaces without access to GitHub.
Existing legacy catalog/ and versions/ data are read and migrated lazily: new catalogs use
_system/, and new artifacts use the human-readable layout. Existing files are not moved or
mass-deleted; they remain in the budget. Use the current catalog when restoring. Do not add
bucket-wide lifecycle expiration rules to implement application retention.
Each upload is bundled and locally verified, hashed in a streaming pass, uploaded with one transfer stream, then checked using remote size and checksum metadata before its catalog is updated. HEAD metadata verification is not a full remote read-back or restore test. Missing remote artifacts are repaired even if no Git refs changed. Cache loss does not create duplicate remote versions. If upload or catalog publication fails, the previous catalog stays valid; no existing artifact is overwritten. An orphan upload is removed after 48 hours on a later inventory pass only when an existing valid catalog proves it is unreferenced and its repository is currently visible. Failure to read a catalog is fatal, never interpreted as an empty catalog.
For visible repositories, retention takes the union of these selections:
- The latest 3 changed versions.
- The newest version in each of the previous 2 completed UTC calendar weeks (Monday–Sunday).
- The newest version in each of the previous 2 completed UTC calendar months.
- The first successful version, pinned as an older baseline.
A version selected more than once is stored only once: the default maximum is 8 distinct
versions per active repository, often fewer. No duplicate backups are synthesized for weeks
or months without changes. The oldest baseline remains even when all recent pushes happen
within a short interval. Calendar representatives rotate as time passes; this is a compact
policy choice, not a guarantee of recovery from every incident or detection delay.
BACKUP_MIN_AGE_DAYS=0 replaces the previous 30-day age floor to avoid retaining hundreds
of hourly changes. Setting it above zero adds an explicit age floor and removes the 8-copy bound.
Retention runs only for repositories whose current version was successfully verified; it never prunes an unavailable repository. Catalog updates precede artifact deletion, so an interruption leaves extra objects instead of dangling references. Failed deletes surface as backup failures. Budget limits still apply when retention selects fewer versions.
A missing repository is reported as missing or inaccessible, not asserted to be deleted:
revoked permissions, ownership changes, and deletion may look identical. All its versions
are retained indefinitely. Health becomes unhealthy until access is restored or its numeric
repository ID is added to ACKNOWLEDGED_MISSING_REPOSITORIES (comma-separated). Acknowledging
only silences this warning; it never deletes the catalog or code. After an intentional deletion,
verify an offline restore and keep the archived versions. Purging them requires a separate,
explicit administrative action after reviewing the catalog and recovery requirements.
The 100 GiB budget is an admission guard, not a billing quota. Upload workers synchronize
full-size reservations against the current prefix inventory and leave 16 MiB for control data.
If another version cannot fit, the run fails while existing versions remain intact. Raise the
reviewed budget or explicitly remove reviewed old data to recover capacity. Count retention,
tiered retention and indefinite deleted-repository preservation cannot together
provide an absolute storage-size guarantee: stopping new uploads is the safe response.
At least one new version must fit before old versions are pruned. Versioned mode never
silently deletes data previously written under legacy snapshots/ or latest/ prefixes;
review migration storage before switching an existing deployment.
Use one exclusive prefix inside the shared bucket and exactly one writer. The Compose profile uses a local volume lock, because Spaces' documented API does not establish the conditional PUT semantics required by the CLI's cross-host ETag lease. Do not run another host/deployment against this prefix. For other S3 deployments the distributed lease stays enabled by default.
Versioned or formerly versioned buckets are rejected when budgeting is enabled: deleting
current keys does not remove historical versions. Existing noncurrent data, incomplete
multipart uploads, other prefixes, and bucket overhead do not belong to the artifact budget.
Set provider billing alerts and a bucket lifecycle rule to abort incomplete multipart
uploads after one day. A sample rule is in spaces-lifecycle.json.
The sample filters only github-backup/; change that filter to match a custom prefix.
Merge it with existing lifecycle rules using an administrative credential; do not overwrite
unrelated policies. The application never changes bucket-wide policies automatically.
Avoid age expiration of snapshots by default: it can delete the last good recovery point
if the backup worker has been failing or offline for a long period.
Check the first manifest at SPACES_PREFIX/_system/manifests/latest.json: all expected accounts
and repositories must be present, missing_resources must be empty, and failure/partial counts
must be zero. Download a
retained .bundle and its repository catalog using a Spaces-compatible client, then restore:
git clone /path/to/downloaded-repository.git.bundle restored-repository
git -C restored-repository fsck --full
git -C restored-repository log -1 --onelineRepeat restore drills periodically. The CLI's verify command verifies filesystem backups;
it does not download or verify remote Spaces objects. The Spaces profile does not send an
AWS-specific AES256 SSE request (Spaces S3 compatibility).
Configure host encryption and independent storage/access controls for your recovery requirements.
cp .env.example .env
# Fill required variables with your own values; keep .env private.
chmod 600 .env
docker compose config --quiet
docker compose up -d --build
docker compose logs -f github-backupPoint the config domain at a local HTTPS reverse proxy targeting the config service on port 8080; the stack intentionally does not publish host ports. The environment file and private-key files are excluded from the build context.
References: Coolify Compose from Git, Coolify application environment variables, DigitalOcean Spaces API.
The default backs up Git history and refs exposed by GitHub, not Git LFS payloads, release
assets, Actions data, issues, secrets, or submodule contents from other repositories. Those
need separate coverage; the legacy CLI supports optional exports, while versioned mode
currently rejects additional_data rather than misrepresenting its retention.
For recovery when GitHub is unavailable: list the SPACES_PREFIX/_system/catalog/ prefix, download the desired
catalog and version, compare the downloaded file's SHA-256 with sha256 in that catalog,
then clone the bundle and run git fsck --full. Older versions are essential after malicious
force-pushes; latest does not necessarily mean trustworthy code. Empty repositories use
.empty.tar.gz instead of Git bundles. Validate the archive and extract it into an isolated
restore directory. Run periodic restore drills independent of the backup worker.
CISA's ransomware guidance recommends offline backups and regular restoration testing. The storage budget is deliberately fail-closed: it does not sacrifice the last good copies to keep a green status.
The page uses the neutral shadcn-style visual language with locally served CSS and a tiny script for appearance/time formatting. It has no third-party scripts, fonts or trackers. Light/dark/system appearance works on desktop and iPhone-sized screens; forms use labeled controls, keyboard focus indicators and touch targets at least 44 pixels high.
Follow Storage → GitHub connections → Repository scope, then verify a completed check in Activity. Reopen configured sections to edit them. Account changes require your current password, invalidate every session and keep registration closed. Forgotten credentials still require a fresh persistent deployment or a complete known-good authentication restore.
The worker stores at most 250 completed repository results in the private local file
/cache/backup-history/history.sqlite3. The config service reads it without needing S3 access
for Activity. History includes successes and failures, excludes raw error payloads/credentials,
and is independent of remote manifests and retention. History failures do not stop backups.
Deleting the local cache loses this activity history; it never deletes remote backup versions.
The current service name is github-config and the canonical URL variable is GITHUB_CONFIG_URL.
The old GITHUB_SETUP_URL remains a compatibility fallback; new installations should use the
new name. After upgrading, reassign the HTTPS domain to github-config port 8080
and remove any leftover route/container for github-setup without deleting volumes.
Reuse the existing backup-cache volume. Owners may need to log in again after the cookie
name changes; their credentials are retained. Existing private setup-auth storage paths and the deployment seal are intentionally
retained to preserve registered accounts. Do not rename/delete these files during upgrades.
Security headers prevent framing, caching of private pages, external resources, inline scripts and browser referrer leakage. Values are contextually escaped; forms enforce field allowlists, length limits, single-value parameters, CSRF tokens and typed validation. Passwords and secrets are never inserted into the DOM as saved values. Password characters are not globally forbidden: a password is hashed, not interpreted as HTML. Protect the host, persistent volume, proxy and browser as well; application protections cannot secure a compromised administrator device/host. Keep HTTPS, disable request-body/query logging, retain independent credentials and disk encryption.