Keep the server in sync
The server is a reader. It clones your repository once, then pulls it — it never commits, never pushes, and never writes anything back to the remote. Everything on this page is about getting new commits into the server.
Three ways changes arrive
| Way | When it runs | Turn it off with |
|---|---|---|
| Startup sync | Once, as the server finishes booting | DOCUCOMMIT_SYNC_ON_STARTUP=false |
| Scheduled pull | Every DOCUCOMMIT_SYNC_INTERVAL, default 5m, measured from the end of the previous pull | DOCUCOMMIT_SYNC_SCHEDULED=false |
| Webhook | On demand, when your Git host or CI posts to /webhook/sync/{repoId} | Clear DOCUCOMMIT_WEBHOOK_SECRET |
All three run the same code path, so they behave identically — the webhook is only about latency. Each variable is described in the configuration reference.
Leave the scheduled pull on even when you use the webhook. It is the safety net for a webhook that never arrived, and five minutes is the worst case rather than the normal case.
What a sync actually does
For each configured repository, in its own directory under <workdir>/<id>:
- If there is no checkout yet, initialise one, fetch, and check out the tracked branch. If no branch is configured, the remote’s default branch is used.
- Otherwise fetch
+refs/heads/*:refs/remotes/origin/*(deleted remote branches are pruned) and fast-forward only onto the remote tip.
Every sync ends in one of four outcomes: CLONED, UPDATED, UP_TO_DATE, or FAILED. A
repository is locked for the duration of its own sync, so a webhook firing during a scheduled
pull waits rather than colliding with it.
After a sync that brings in new commits, the server rebuilds the rendered library and the search
index, reconciles comments and notification subscriptions against the new file set, and queues
change notifications for followers. UP_TO_DATE does no work beyond the fetch.
There is no merge and no rebase. The server has no local commits of its own, so a fast-forward is always enough — until the remote history changes underneath it, which is the one failure worth planning for.
The webhook
POST /webhook/sync/{repoId}
X-DocuCommit-Token: <DOCUCOMMIT_WEBHOOK_SECRET>
{repoId} is the repository’s configured id — the same value as DOCUCOMMIT_SYNC_REPOS_0_ID.
The body is ignored; the header is the whole authentication story, and the secret is compared in
constant time.
| Response | Meaning |
|---|---|
202 {"status":"accepted","repoId":"docs"} | Queued. The pull happens after the debounce window. |
401 {"status":"unauthorized"} | Wrong token, missing header, or DOCUCOMMIT_WEBHOOK_SECRET is blank. |
404 {"status":"unknown_repo","repoId":"…"} | No repository with that id is configured and licensed. |
Because a blank secret returns 401 for everything, the endpoint is safe to expose before you
have configured it. It is also exempt from the licence gate, so it answers even on a server whose
licence key is not yet valid.
This is not a stock GitHub webhook. GitHub signs the body with an HMAC; DocuCommit wants the
secret as a literal header value. Anything that can set a request header will do — a CI step, a
Gerrit webhooks.config entry, or plain curl.
From any CI system
curl -fsS --retry 3 --retry-delay 2 -X POST \
-H "X-DocuCommit-Token: $DOCUCOMMIT_WEBHOOK_SECRET" \
https://docs.example.com/webhook/sync/docs
From GitHub Actions
Add this as .github/workflows/notify-docucommit.yml in the documentation repository, and store
the secret as a repository secret named DOCUCOMMIT_WEBHOOK_SECRET:
name: Notify DocuCommit on push
on:
push:
branches: [main]
# No tokens or permissions needed — we don't touch the GitHub API.
permissions: {}
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: POST to DocuCommit webhook
env:
DOCUCOMMIT_WEBHOOK_SECRET: ${{ secrets.DOCUCOMMIT_WEBHOOK_SECRET }}
run: |
curl -fsS --retry 3 --retry-delay 2 -X POST \
-H "X-DocuCommit-Token: $DOCUCOMMIT_WEBHOOK_SECRET" \
-H "Content-Type: application/json" \
https://docs.example.com/webhook/sync/docs
Change the branch, the hostname, and the docs at the end of the URL to your repository id. The
job needs no GITHUB_TOKEN and no permissions, because it never calls the GitHub API.
This is the same workflow that keeps the public DocuCommit demo server current.
Debounce
DOCUCOMMIT_WEBHOOK_DEBOUNCE (default 3s) is a trailing debounce, kept per repository. Each
call cancels the pending sync and schedules a new one three seconds out. A branch push of twelve
commits, or a merge that fires several hook deliveries in a row, produces exactly one pull —
three seconds after the last one lands, not after the first.
The trade-off is that a steady drip of pushes closer together than the debounce window keeps
deferring the pull. Keep the window short; 3s is enough to collapse a burst and short enough
that nothing waits noticeably.
When a sync fails
A failed sync is logged and nothing else changes: the previously rendered library stays up and keeps serving. The server does not go down because a pull failed.
| Logged message | Cause |
|---|---|
auth/network failure — … | The fetch could not authenticate or could not reach the remote. Bad token, wrong SSH key, missing known_hosts entry, DNS or firewall. |
remote branch not found: origin/<branch> | The configured branch was renamed or deleted on the remote. |
local directory exists but is not a Git repository: … | Something else is occupying <workdir>/<id>. |
non-fast-forward update for branch '<branch>' (upstream rebased or force-pushed); manual intervention required | The remote history was rewritten. |
A rewritten upstream
The server only ever fast-forwards. If someone force-pushes, rebases, or amends a commit that the server has already fetched, the remote tip is no longer a descendant of the local tip and the merge is refused. Every subsequent sync fails the same way until an operator acts. There is no automatic reset — deliberately, because silently discarding a checkout is not something the server should decide on its own.
Two supported fixes:
Restore the history on the remote. If the force-push was a mistake, push the original commits back. The next sync fast-forwards as usual and nothing else is needed.
Delete the checkout and let it re-clone. The directory under the working directory holds nothing you cannot recreate — it is a clone of your remote, and comments, accounts, and subscriptions live elsewhere in the volume. Remove it and restart; the startup sync clones fresh.
The server image is distroless and has no shell, so docker compose exec is not available. Reach
the volume with a throwaway container instead:
docker compose stop docucommit-server
docker run --rm -v docucommit-repos:/var/repos alpine rm -rf /var/repos/docs
docker compose start docucommit-server
docucommit-repos is the volume name from the compose file and docs is the repository id.
Nothing else in /var/repos should be touched — .auth/, .comments/, and
.docucommit-license/ are the server state described in
Backup and restore.
Watching sync state
There is no sync-status page and no status API. Monitoring is the container logs.
docker compose logs -f docucommit-server
A successful sync logs one line per repository:
sync docs: UPDATED (9f2c1ab…)
sync docs: UP_TO_DATE (9f2c1ab…)
A failure logs at error level:
sync docs FAILED: auth/network failure — TransportException: …
Grep for sync and alert on FAILED in whatever log pipeline you already run. The commit id in
the success line is the exact commit the site is serving, which is the quickest way to confirm
that a push actually landed.