Sthora
Deploying applications

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.

SignatureHMAC, verified before anything else happens.
Rate limitIts own, tighter than the global one.
GuardPublic — 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:

ApplicationWatched paths
APIapps/backend, packages/shared
Websiteapps/website
Docsapps/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.

On this page