Self-hosting
Run Healkite on your own infrastructure: one container with the API, the dashboard and the update CDN. Data never leaves your servers, and there are no usage limits.
Requirements
- Docker (or Node 22.13+ without Docker)
- A public HTTPS URL for the server, for example
https://ota.example.com, behind your load balancer or a reverse proxy such as Caddy or nginx - 512 MB RAM and some disk for bundles (or an S3-compatible bucket)
Start the server
Self-hosting licenses come with access to the Healkite source repository. From a checkout:
cd healkite
PUBLIC_URL=https://ota.example.com ADMIN_TOKEN=$(openssl rand -hex 24) docker compose up -d --buildOn first start the server creates its manifest signing key and secret in the /data volume, next to the SQLite
database and bundles. Back up the whole volume; losing server-keys.json means apps with the old
manifest key need a store update.
Create your organization
curl -X POST https://ota.example.com/v1/bootstrap \
-H "x-admin-token: <ADMIN_TOKEN>" -H "content-type: application/json" \
-d '{"orgName":"Acme"}'
# → { "token": "hk_…" } an owner access keyThen remove ADMIN_TOKEN from the environment and restart. Use the token with the CLI:
npx healkite login --api-url https://ota.example.com --token hk_…Dashboard sign-in
Create a GitHub OAuth app with the callback URL https://ota.example.com/v1/auth/github/callback, set
GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET, restart, and invite people:
npx healkite members add <github-login> --role ownerOpen https://ota.example.com and sign in. For Okta, Microsoft Entra ID or Google Workspace, see SSO.
Point your apps at it
iOS Info.plist | Android strings.xml | |
|---|---|---|
| CDN URL | HealkiteCdnUrl = https://ota.example.com/cdn | healkite_cdn_url |
| Manifest key | HealkiteManifestKey = manifestPublicKey from GET /v1/config | healkite_manifest_key |
Configuration
| Variable | Default | |
|---|---|---|
API_BASE_URL | http://localhost:8787 | Public URL of the server |
PORT | 8787 | |
DATABASE_PATH | /data/healkite.db | SQLite database |
BLOB_DIR | /data/cdn | Bundle storage when S3 isn't set |
S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION | Store bundles in S3, R2, MinIO or GCS (S3 API) | |
CDN_BASE_URL | <API_BASE_URL>/cdn | A CDN in front of /cdn or the bucket |
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET | Dashboard sign-in | |
ADMIN_TOKEN | One-time bootstrap | |
MANIFEST_SIGNING_KEY, MANIFEST_PUBLIC_KEY, AUTH_SECRET | generated | Bring your own keys instead |
SIGNUP_MODE | invite | open lets anyone with GitHub create an org |
GUARD_INTERVAL_SEC | 60 | How often guarded rollouts and schedules are evaluated |
Scaling
The server is stateless apart from SQLite and the bundle directory. For heavy traffic, put a CDN in front of
/cdn (manifests are cacheable for 60 seconds, bundles forever) and set CDN_BASE_URL. The same code also runs
on Cloudflare Workers with D1 and R2, which is how Healkite Cloud runs.
Without Docker
pnpm install && node scripts/build-selfhost.mjs
cd dist/selfhost && API_BASE_URL=https://ota.example.com DASHBOARD_DIR=dashboard \
DATABASE_PATH=data/healkite.db BLOB_DIR=data/cdn node server.mjsUpdating
Pull the new version and run docker compose up -d --build. Database migrations run on start.