{"protocol":"clank-doc/1","frameworkVersion":"0.8.0","slug":"self-hosting","title":"Self-hosting Clank Deploy","description":"Clank Deploy is one Node control plane process plus one supervised process or container per active project.","group":{"id":"deploy","title":"Deploy and operate"},"url":"https://docs.clank.run/docs/self-hosting","source":"docs/self-hosting.md","headings":["Requirements","Environment","Production start","Tailscale","Railway","Storage","Upgrades"],"tableOfContents":[{"id":"requirements","title":"Requirements","level":2},{"id":"environment","title":"Environment","level":2},{"id":"production-start","title":"Production start","level":2},{"id":"tailscale","title":"Tailscale","level":2},{"id":"railway","title":"Railway","level":2},{"id":"storage","title":"Storage","level":2},{"id":"upgrades","title":"Upgrades","level":2}],"markdown":"# Self-hosting Clank Deploy\n\nClank Deploy is one Node control-plane process plus one supervised process or container per active project.\n\n## Requirements\n\n- Node 22.16+;\n- persistent local storage;\n- HTTPS proxy outside loopback;\n- Docker for mutually untrusted deployers;\n- external master key and off-host backups for production.\n\n## Environment\n\n| Variable | Default | Purpose |\n| --- | --- | --- |\n| `PORT` | `4200` | Control-plane port |\n| `HOST` | `127.0.0.1` | Listener address |\n| `CLANK_PLATFORM_URL` | loopback URL | Exact public console origin |\n| `CLANK_PLATFORM_DATA` | `.clank-platform` | Persistent root |\n| `CLANK_PLATFORM_MASTER_KEY` | generated file | Base64/base64url 32-byte key |\n| `CLANK_SIGNUP` | `bootstrap` | `bootstrap`, `public`, or `disabled` |\n| `CLANK_RUNNER` | `process` | `process` or `docker` |\n| `CLANK_DOCKER_IMAGE` | Node image | Pin by digest in production |\n| `CLANK_APP_MEMORY` | `512m` | Container memory |\n| `CLANK_APP_CPUS` | `1` | Container CPUs |\n| `CLANK_APP_PIDS` | `128` | Container PID limit |\n| `CLANK_APP_PORT_START` | `4300` | Port-range start |\n| `CLANK_APP_PORT_END` | `4999` | Port-range end |\n| `CLANK_APP_URL_TEMPLATE` | loopback with `{port}` | Public app URL pattern |\n| `CLANK_MAX_ARTIFACT_BYTES` | `104857600` | Artifact limit |\n| `CLANK_INGRESS` | inferred from base domain | Enable managed exact-host ingress |\n| `CLANK_INGRESS_BASE_DOMAIN` | none | Built-in `slug.<base>` site namespace |\n| `CLANK_CUSTOM_DOMAIN_TARGET` | base domain | CNAME target displayed to customers |\n| `CLANK_CUSTOM_DOMAIN_ADDRESSES` | none | Comma-separated edge A/AAAA values accepted for apex routing |\n| `CLANK_TLS_ASK_TOKEN` | none | Secret for the private Caddy certificate permission check |\n| `CLANK_INGRESS_MAX_BODY_BYTES` | `26214400` | Per-request managed-ingress body limit |\n| `CLANK_DOMAIN_RECHECK_INTERVAL_MS` | `300000` | Background custom-domain routing interval; `0` disables it |\n| `CLANK_DOMAIN_RECHECK_BATCH_SIZE` | `25` | Maximum domains durably claimed by one routing pass |\n| `CLANK_DOMAIN_RECHECK_TIMEOUT_MS` | `10000` | Per-domain DNS lookup deadline |\n| `CLANK_BACKUP_INTERVAL_MS` | `86400000` | Verified encrypted-backup cadence; `0` disables automatic runs |\n| `CLANK_BACKUP_BATCH_SIZE` | `5` | Maximum projects durably claimed by one backup pass |\n| `CLANK_BACKUP_MAX_COUNT` | `30` | Maximum retained backups per project |\n| `CLANK_BACKUP_MAX_AGE_MS` | `7776000000` | Maximum retained backup age |\n| `CLANK_BACKUP_MAX_DATABASE_BYTES` | `10737418240` | Maximum source database size accepted by backup creation |\n| `CLANK_MAX_ORGANIZATIONS_PER_ACCOUNT` | `5` | Transactionally enforced account organization limit |\n| `CLANK_MAX_PROJECTS_PER_ACCOUNT` | `10` | Transactionally enforced account-wide site limit |\n| `CLANK_MAX_PROJECTS_PER_ORGANIZATION` | `10` | Transactionally enforced site limit |\n| `CLANK_MAX_DOMAINS_PER_PROJECT` | `5` | Transactionally enforced custom-domain limit |\n| `CLANK_METRICS_RETENTION_DAYS` | `30` | Ingress metric retention, 1–365 days |\n| `CLANK_MAX_RELEASES_PER_PROJECT` | `50` | Retained runtime-artifact count per site |\n| `CLANK_MAX_RELEASE_STORAGE_BYTES_PER_PROJECT` | `21474836480` | Uncompressed release files plus pre-deploy snapshots retained per site |\n| `CLANK_ALLOW_UNSAFE_MIGRATIONS` | `0` | Operator approval for unrestricted SQL |\n| `ALLOWED_HOSTS` | loopback | Exact host allowlist |\n| `TRUST_PROXY` | `0` | Trust forwarded client/protocol |\n\n`bootstrap` permits one initial account and then closes ordinary registration. Its SQLite claim is shared by control-plane processes using the same data directory. `disabled` blocks ordinary registration immediately. In both modes, an owner/admin-issued invitation can still create only its bound email account through **Use invitation**; revoke outstanding invitations before disabling all intended onboarding.\n\n## Production start\n\n```sh\nexport CLANK_PLATFORM_URL=https://deploy.example.com\nexport CLANK_PLATFORM_DATA=/var/lib/clank\nexport CLANK_PLATFORM_MASTER_KEY=\"$(your-secret-manager read clank-master-key)\"\nexport CLANK_RUNNER=docker\nexport CLANK_DOCKER_IMAGE=node@sha256:<approved-digest>\nexport CLANK_APP_URL_TEMPLATE='https://{slug}.apps.example.com'\nexport CLANK_INGRESS=1\nexport CLANK_INGRESS_BASE_DOMAIN=apps.example.com\nexport CLANK_CUSTOM_DOMAIN_TARGET=edge.apps.example.com\nexport CLANK_CUSTOM_DOMAIN_ADDRESSES=192.0.2.10,2001:db8::10\nexport CLANK_TLS_ASK_TOKEN=\"$(your-secret-manager read clank-tls-ask-token)\"\nexport HOST=127.0.0.1\nexport PORT=4200\nexport ALLOWED_HOSTS=deploy.example.com,127.0.0.1,localhost\nexport TRUST_PROXY=1\n\nclank-platform\n```\n\nProxy the console and application hosts to port 4200. Clank performs exact-host project routing; the edge performs public DNS, TLS, WAF/rate limiting, and DDoS controls. The recommended Caddy On-Demand TLS configuration and DNS records are in [Deployment dashboard, quotas, and domains](platform-dashboard.md).\n\nUse `/livez` only to determine whether the process can answer HTTP. Use `/healthz` or `/readyz` for\ncontrol-host readiness. Hosted load balancers that send a different `Host` header should use\n`/_clank/readyz`, which is evaluated before application-host ingress. These readiness endpoints\nexecute a control-database probe and return `503` when the platform cannot safely accept work.\n`SIGINT` and `SIGTERM` stop new HTTP work, drain an active scheduled backup, close supervised\napplications and platform storage, and fail the process if shutdown cannot finish within 30 seconds.\n\n## Tailscale\n\n```sh\nCLANK_PLATFORM_URL=https://host.tailnet-name.ts.net:8447 \\\nHOST=127.0.0.1 PORT=4200 TRUST_PROXY=1 \\\nALLOWED_HOSTS=host.tailnet-name.ts.net,localhost,127.0.0.1 \\\nclank-platform\n\ntailscale serve --https=8447 http://127.0.0.1:4200\n```\n\nExpose app ports separately or place a wildcard-capable proxy in front.\n\n## Railway\n\nThe checked-in production image, health/restart policy, persistent-volume topology, DNS setup, and\noperator runbook are in [Railway production deployment](railway.md).\n\n## Storage\n\n```text\ncontrol.sqlite\nmaster.key\nprojects/<id>/\n  data/app.sqlite\n  releases/<release-id>/\n  backups/<release-id>.sqlite\n  recovery/<backup-id>/\n    database.enc\n    manifest.json\n```\n\nUse a local filesystem with correct SQLite locking/rename semantics. The platform sets umask `0077`.\n\nClank automatically creates authenticated, AES-256-GCM encrypted recovery points under `recovery/`. Durable control-database claims prevent duplicate scheduled work when multiple control-plane processes share the store. Release count and byte ceilings separately bound retained extracted runtime files and pre-deploy rollback snapshots. Back up the control database, completed recovery directories, recoverable artifacts/source, and master key through separate paths. Pre-release snapshots remain rollback material, not part of the recovery retention policy. See [Backup and disaster recovery](recovery.md).\n\nPermanent site deletion removes the complete matching `projects/<id>/` tree after symlink-safe validation, then removes its control rows and revokes project tokens. It does not reach copied/off-host backups, external databases, artifact mirrors, or Caddy certificate storage. Include those systems in tenant-retention and erasure runbooks, and preserve the control database if deletion audit history must remain available.\n\nAudit rows live in `control.sqlite`, retain organization attribution after project removal, and upgrade in place from earlier schemas. The public API does not mutate them, but SQLite administrators can. Export them to a separate append-only audit system when control-plane operator tampering or longer retention is in scope.\n\nAuthentication and CLI device-start rate-limit windows also live in `control.sqlite`. Their client/account keys are HMAC pseudonyms under the platform master key, expire automatically, and remain effective when requests move between control-plane processes or the process restarts. The table is bounded, so keep the console behind production edge rate limiting and DDoS controls.\n\n## Upgrades\n\n1. Back up data and key.\n2. Stop new deploys and the platform.\n3. Install and verify the new Clank build.\n4. Start the selected active supervisor/worker topology.\n5. Verify browser login, CLI login, organization and scoped-token access, project status, ingress/domain state, app health, test deploy, backup verification, rollback, and a disposable-site deletion.\n\nDurable distributed locks, authenticated nodes, desired generations, operations/fencing, wildcard base-domain routing, ownership and routing verification, Caddy certificate eligibility, ingress metrics, enforced account/organization/site/domain limits, organization RBAC, scheduled encrypted backups, and external database drivers are implemented. The included child-process supervisor remains single-leader and artifacts/backups are local by default; a hosted multi-region service still needs leader/remote-runner integration, external object storage, globally transactional control storage, shared metric storage, and a multi-region edge service.\n"}