Deploying from Git
Deploy keys, webhooks that ship a push, host-key pinning, and the optional GitHub App.
Set a repository and a branch on an application, and the platform clones it and deploys it. Add a webhook and every push to that branch deploys itself.
Deploy keys
The platform generates an Ed25519 deploy key per application and shows you its public half. Add that to the repository — read access is enough — and it can clone.
This works against GitHub, GitLab, Bitbucket, and any self-hosted host, with nothing registered anywhere.
The private half is stored encrypted under the master key and is returned by
no endpoint. GET /applications/:id/git-source returns the public half and
never the private one.
The key is written in the openssh-key-v1 container rather than PKCS#8, because ssh will
not load PKCS#8 for Ed25519. During a clone it is written to disk at mode 0600 for the
length of that one clone and removed in a finally.
Cloning
Clones are --depth 1 --single-branch --branch <ref> and run through the driver, so a clone
onto a remote server happens on that server rather than being copied there. The wall-clock
ceiling is GIT_CLONE_TIMEOUT_SECONDS (default 600).
The commit SHA, message, and author are read back from the clone, not from whatever asked for the deployment. The deployment history therefore shows the commit that actually produced each release.
Host keys
Host keys are trusted on first use (StrictHostKeyChecking=accept-new) and pinned afterwards
in <DEPLOY_DATA_ROOT>/known_hosts.
A Git host whose key changes will make the next clone fail loudly rather than
quietly trusting a substituted key. If you rebuilt that host on purpose,
remove its line from /var/lib/sthora/known_hosts.
Webhooks
Generate a webhook secret on the application. The portal shows the URL and the secret once — the secret is stored encrypted and no endpoint will show it again.
Add that URL to the repository's webhooks as a JSON push event. The platform accepts
github, gitlab, bitbucket, and generic at /webhooks/:provider.
| Signature | HMAC, verified before anything else happens. |
| Rate limit | Its own, tighter than the global one. |
| Guard | Public — and the only public route that can start work. |
A push to a branch other than the deployed one is accepted and ignored.
PUBLIC_BASE_URL has to be set for the portal to show you a complete webhook
URL. Without it you get the path only.
One repository, several applications
By default every push to the deployed branch deploys the application. In a monorepo that means a change to one application rebuilds and restarts all of the others configured for the same repository.
Set watched paths on the application to stop that. They are paths relative to the repository root, and a push deploys only when it touched one of them or a file inside one:
| Application | Watched paths |
|---|---|
| API | apps/backend, packages/shared |
| Website | apps/website |
| Docs | apps/docs |
Matching is on path segments, not on string prefixes — apps/docs matches
apps/docs/index.mdx but never apps/docsite/index.mdx. Leave the field empty to go back
to deploying on every push.
A watched path that names nothing real is a silent no-deploy: pushes keep arriving, keep verifying, and never deploy. The webhook response says which paths were expected, and the delivery log on the Git host shows it.
Two cases deliberately deploy anyway rather than risk skipping a release: a host that does not report which files changed (Bitbucket does not), and a push so large the host truncated its own file list.
The GitHub App (optional)
Deploy keys are the recommended path and need no registration anywhere. A GitHub App adds the two things a deploy key cannot do:
- a repository picker in the portal, instead of pasting a URL, and
- deployment status reported back onto the commit.
Register the App at https://github.com/settings/apps, install it on the account holding your
repositories, and set GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY_FILE, and
GITHUB_APP_WEBHOOK_SECRET. See configuration.
Repository URLs
Repository URLs are validated before anything is attempted with them. A URL that is not a recognised Git remote is refused at the API rather than becoming a shell argument.