Skip to content

Git sync

Git sync is optional and disabled by default.

When enabled, SimpleDeploy treats your apps_dir as a git working tree and commits every config change to a remote repository. Each deploy, env-var edit, or sidecar update triggers a commit and push within seconds. You can also pull remote changes back in, making it possible to manage deployments through git rather than (or alongside) the UI.

Git sync complements config sidecars, which write app config to local YAML files. Think of sidecars as the local source of truth and git sync as the transport layer that makes that truth portable. Sidecars are what get committed; git sync is what moves them to a remote.

Git sync is not a replacement for database backup. It does not capture metrics, deploy history, audit logs, or any file outside apps_dir. Secrets stay local: config.yml (which holds password hashes and encrypted registry credentials) and the SQLite database are never committed.

  • {apps_dir}/{slug}/docker-compose.yml
  • {apps_dir}/{slug}/.env
  • {apps_dir}/{slug}/simpledeploy.yml (per-app sidecar: alert rules, backup configs, access)
  • {apps_dir}/_global.yml (redacted global config: users, registries, webhooks without secrets)
  • .gitignore (auto-generated allowlist that restricts commits to the above files)

Never committed:

  • {data_dir}/config.yml (password hashes, encrypted credentials)
  • {data_dir}/simpledeploy.db (SQLite database)
  • Metrics, logs, and anything outside apps_dir

Super admins can enable and edit git sync configuration directly on the Git Sync page without a server restart. Changes saved through the UI are written to the database and take effect immediately. DB values override the config.yml YAML block, and the API response includes a source field ("db" or "yaml") so you can tell which value is active. To reset a field back to the YAML default, remove it from the DB via the UI.

Add a git_sync: block to your server config.yml:

git_sync:
enabled: true
remote: git@github.com:owner/infra.git
branch: main
author_name: SimpleDeploy
author_email: bot@example.com
ssh_key_path: /etc/simpledeploy/gitsync_id_ed25519
poll_interval: 60s
webhook_secret: "your-webhook-secret"
poll_enabled: true
auto_push_enabled: true
auto_apply_enabled: true
webhook_enabled: true
FieldRequiredDescription
enabledyesSet to true to start the sync worker.
remoteyesGit remote URL (SSH or HTTPS).
branchyesBranch to push to and pull from.
author_namenoCommit author name. Defaults to SimpleDeploy.
author_emailnoCommit author email.
ssh_key_pathone ofPath to an SSH private key. Mutually exclusive with https_token.
https_usernameone ofHTTPS username. Used together with https_token.
https_tokenone ofHTTPS token or password. Mutually exclusive with ssh_key_path.
poll_intervalnoHow often to pull from remote. Default 60s.
webhook_secretnoHMAC secret for verifying GitHub-compatible webhook pushes.
poll_enablednoWhether to run the poll loop. Default true.
auto_push_enablednoWhether to auto-commit and push local changes. Default true.
auto_apply_enablednoWhether to auto-apply fetched remote changes. Default true.
webhook_enablednoWhether the webhook endpoint is active. Default true.

Four behaviour toggles let you adjust the sync mode without disabling git sync entirely. All default to true. They can be set in config.yml or changed at runtime from the UI (DB overrides YAML).

Controls whether the background poll loop runs on poll_interval. Set to false if you rely exclusively on webhooks and want no background polling. The sync worker still starts; it just never fires the timer. Useful when you want tight control over when fetches happen.

Controls whether local config changes (deploys, env edits, sidecar updates) are automatically committed and pushed to the remote. Set to false for a pull-only setup where the remote is the source of truth and local changes are never pushed back. This is useful when you manage config entirely through the git repo and want to prevent the server from writing back.

Controls whether fetched remote commits are automatically rebased onto the local working tree. Set to false to enter fetch-only mode: the server fetches on each poll or webhook trigger and tracks how many commits the remote is ahead, but it does not apply those commits. A banner appears in the UI showing the number of commits behind, and you can apply them manually with the Apply button (or via POST /api/git/apply-pending). Useful when you want to review remote changes before they affect running deployments.

Controls whether POST /api/git/webhook accepts and processes incoming webhook payloads. Set to false to temporarily block webhook-triggered syncs, for example during maintenance windows. When disabled, the endpoint returns 404 with an empty body. The HMAC secret is not exposed in the response.

When auto_apply_enabled is false, each fetch (poll or webhook) updates an internal counter of how many commits the remote is ahead of the local HEAD. The Git Sync page shows a banner:

3 commits behind remote. Review the changes in your git repository, then click Apply to update running deployments.

Clicking Apply now (or calling POST /api/git/apply-pending) runs the full fetch, rebase with server-wins conflict resolution, sidecar import, and reconcile cycle. After a successful apply, the counter resets to zero and the banner disappears.

If the remote is already up-to-date when apply is triggered, the operation completes immediately with no changes.

SSH: Set ssh_key_path to an Ed25519 or RSA private key on the server. Add the corresponding public key as a deploy key on the remote (GitHub: Settings > Deploy keys, with write access).

HTTPS: Set https_username and https_token. For GitHub, create a fine-grained personal access token with Contents: Read and write permission on the target repository.

If the remote repository is empty, SimpleDeploy initializes a repo in apps_dir, commits current state, and pushes. This is the recommended starting point. The Git Sync config form detects this case: testing the connection shows an info banner, and the save button changes to Save & push initial commit, making it explicit that saving will push your current SimpleDeploy configs as the initial commit.

If the remote already has commits, SimpleDeploy refuses to push and surfaces an error in git status and on the Git Sync page in admin nav. You then have two options:

  • Adopt local state: from an admin shell, git push --force from apps_dir to overwrite the remote with current local state.
  • Adopt remote state: manually clone the remote, move the files into apps_dir, and restart the server so sidecars are imported.

Start with an empty remote whenever possible to avoid this decision.

A webhook lets the remote trigger an immediate pull instead of waiting for the next poll.

GitHub:

  1. Repository Settings > Webhooks > Add webhook.
  2. Payload URL: https://<your-server>/api/git/webhook
  3. Content type: application/json
  4. Secret: the value of webhook_secret in your config.
  5. Event: “Just the push event.”

SimpleDeploy verifies the X-Hub-Signature-256 header using your secret. Gitea and GitLab use the same header format and are also supported.

The poll worker runs on poll_interval (default 60s) regardless of webhook configuration. When a webhook arrives, an immediate sync runs; the poll continues as a safety net. There is no harm in running both.

Local state wins on conflict. If a remote change conflicts with a local change, SimpleDeploy logs the conflict to alert_history and surfaces it on the Git Sync page. The remote change is not applied.

Conflicts usually mean two operators edited the same file at the same time. To apply the remote change, re-enter it through the UI after reviewing what was lost.

Terminal window
simpledeploy git status # print worker status and last sync time
simpledeploy git sync-now # one-shot pull-and-apply against current config

sync-now is useful after a credentials change or for a manual bootstrap without restarting the server.

Set enabled: false (or remove the block). The sync worker stops. The .git directory and all local history remain in apps_dir untouched. Re-enabling picks up where it left off.