{"protocol":"clank-doc/1","frameworkVersion":"0.9.4","slug":"provider-service","title":"Complete deployment provider service","description":"@clank.run/framework/provider service composes Clank's provider data store, isolated Docker launcher, and generation bound private ingress into one restart safe DeploymentProvider . It is the recommended provider implementation when self ho","group":{"id":"deploy","title":"Deploy and operate"},"url":"https://docs.clank.run/docs/provider-service","source":"docs/provider-service.md","headings":["Run the packaged provider","Open the complete stack","What one reconcile does","Retry and crash behavior","Inspection, rollback, deletion, backups, and shutdown","Remote snapshot boundary","Trust and operating boundary"],"tableOfContents":[{"id":"run-the-packaged-provider","title":"Run the packaged provider","level":2},{"id":"open-the-complete-stack","title":"Open the complete stack","level":2},{"id":"what-one-reconcile-does","title":"What one reconcile does","level":2},{"id":"retry-and-crash-behavior","title":"Retry and crash behavior","level":2},{"id":"inspection-rollback-deletion-backups-and-shutdown","title":"Inspection, rollback, deletion, backups, and shutdown","level":2},{"id":"remote-snapshot-boundary","title":"Remote snapshot boundary","level":3},{"id":"trust-and-operating-boundary","title":"Trust and operating boundary","level":2}],"markdown":"# Complete deployment provider service\n\n`@clank.run/framework/provider-service` composes Clank's provider data store, isolated Docker\nlauncher, and generation-bound private ingress into one restart-safe `DeploymentProvider`. It is\nthe recommended provider implementation when self-hosting remote application compute.\n\nThe service remains a separate trusted-compute process, while the built-in control plane can now\nassign explicit provider projects to it through a statefully pinned `clank-runner`. The control\nplane publishes managed edge traffic only after this service and the runner report the exact\nrelease generation as observed.\n\n## Run the packaged provider\n\n`clank-provider` exposes both the authenticated lifecycle bridge and generation-bound private\nruntime ingress, so the reference Docker stack needs no application glue:\n\n```sh\nexport HOST=0.0.0.0\nexport PORT=4600\nexport ALLOWED_HOSTS=runtime.internal.example\nexport CLANK_PROVIDER_DATA=/var/lib/clank-provider\nexport CLANK_PROVIDER_OWNER=provider-us-central-01\nexport CLANK_PROVIDER_IMAGE='registry.example/clank-node@sha256:<64-hex-digest>'\nexport CLANK_PROVIDER_TOKEN=\"$(secret read clank-runtime-provider)\"\nexport CLANK_PROVIDER_MEMORY=512m\nexport CLANK_PROVIDER_CPUS=1\nexport CLANK_PROVIDER_PIDS=128\nclank-provider\n```\n\nThe provider process must use a private persistent volume and a private/TLS network path. Run it\nas a dedicated non-root Unix account so the default container uid/gid can write only the provider\nfiles it owns. `clank-provider --help` lists capacity, port, database, capsule, Docker, and drain\nbounds. Mutable application image tags are rejected unless the operator explicitly enables the\ndevelopment-only escape hatch.\n\nLarge capsule uploads have an independent request-receive deadline. Tune\n`CLANK_PROVIDER_HTTP_REQUEST_TIMEOUT_MS` for the slowest private link instead of weakening\napplication timeouts. `CLANK_PROVIDER_MAX_ARTIFACT_BYTES` bounds legacy artifacts,\n`CLANK_PROVIDER_MAX_RUNTIME_BYTES` bounds sensitive runtime capsules, and\n`CLANK_PROVIDER_INGRESS_TIMEOUT_MS` separately limits application responses.\n\nPoint the packaged runner at this origin with a different authority for control-plane enrollment:\n\n```sh\nexport CLANK_CONTROL_URL=https://deploy.example.com\nexport CLANK_RUNNER_NODE_ID=provider-us-central-01\nexport CLANK_RUNNER_ENDPOINT=https://runtime.internal.example\nexport CLANK_PROVIDER_URL=https://runtime.internal.example\nexport CLANK_PROVIDER_TOKEN=\"$(secret read clank-runtime-provider)\"\nclank-runner\n```\n\nThe same provider token is shared only by this runner and provider bridge. It is not a Clank\naccount, CLI, node, application, or platform-master credential.\n\n## Open the complete stack\n\nOne factory creates the provider data lifecycle, removes exact-owner Docker orphans, creates the\nprivate ingress registry, and opens durable desired-state fencing:\n\n```ts\nimport {\n  openDockerDeploymentProviderService,\n} from \"@clank.run/framework/provider-service\";\nimport {\n  createDeploymentProviderHandler,\n} from \"@clank.run/framework/provider\";\n\nconst service = await openDockerDeploymentProviderService({\n  rootDirectory: \"/var/lib/clank-provider\",\n  owner: \"runner-west-01\",\n  image: \"registry.example/clank-node@sha256:<64-hex-digest>\",\n  data: {\n    maxDatabaseBytes: 512 * 1024 * 1024,\n  },\n  docker: {\n    memory: \"512m\",\n    cpus: \"1\",\n    pidsLimit: 128,\n    maxRuntimes: 100,\n    maxContainers: 400,\n  },\n  ingress: {\n    timeoutMs: 30_000,\n    maxBodyBytes: 25 * 1024 * 1024,\n  },\n  onError(error) {\n    privateProviderLog(error);\n  },\n});\n\nconst reconciliation = createDeploymentProviderHandler(service, {\n  token: process.env.CLANK_PROVIDER_TOKEN!,\n  onError(error) {\n    privateProviderLog(error);\n  },\n});\n```\n\nMount `reconciliation.handle(request)` only at its fixed authenticated `POST`\n`/v1/clank/reconcile`, `/v1/clank/rollback`, and `/v1/clank/delete` paths. Route the\nprovider-private application path to `service.handle(request)`. Terminate TLS in front of both,\nrestrict network admission to the runner and managed edge respectively, and never expose\napplication loopback ports.\n\nPass the service directly to `openProviderDeploymentAgent()` for an in-process runner, or run the\npackaged `clank-runner` against the authenticated provider handler. No provider SDK, npm\ndependency, database service, or paid control-plane resource is required.\n\n## What one reconcile does\n\nFor a running `clank-runtime/1` desired generation, the service:\n\n1. independently hashes, bounds, decodes, and binds the capsule before persisting intent;\n2. atomically records the exact operation ID, monotonic fence, generation, release, and capsule\n   digest in owner-only provider state;\n3. revokes and drains any mismatched private route before stopping its writer;\n4. recovers an interrupted data journal only after the old runtime is proven quiescent;\n5. stages the immutable release, applies SQLite restore/migrations behind a safety snapshot, and\n   launches only the private web candidate for health validation;\n6. commits provider data metadata, then starts the deferred workers and scheduler;\n7. publishes the exact project/release/generation/path/token binding to private ingress; and\n8. records the generation as running only after the complete runtime is reachable.\n\nThe order is intentional. Workers cannot perform queue or cron work against an uncommitted\ndatabase. If metadata commit fails after web health, the data store invokes candidate cleanup\nbefore restoring SQLite. If cleanup cannot be proven, it leaves the recovery journal intact\ninstead of changing database bytes under a possibly live process.\n\nA stopped desired generation revokes new traffic, waits for assigned response streams, stops the\nruntime, verifies removal, and durably records the stopped high-water mark. It preserves provider\ndata for a later restart, backup, rollback, or explicit deletion.\n\n## Retry and crash behavior\n\nThe service persists no environment values, ingress tokens, application output, loopback origins,\nor Docker identifiers. Its state contains only:\n\n- project and operation IDs;\n- fence and desired generation;\n- running/stopped state and release ID;\n- capsule SHA-256;\n- reconciliation phase and timestamp.\n\nAn exact response-lost retry re-verifies the capsule and immutable release. If the matching data,\nruntime, and ingress are still active, launch and activation are idempotent and traffic is not\ndrained. A lower generation, lower fence, conflicting operation at the same fence, or different\nrelease/capsule at the same generation fails before infrastructure mutation.\n\nAfter process or host restart, the ingress registry starts empty and the Docker launcher removes\nonly exact-owner orphan containers. The durable service state may say `running`, but it never\ncauses process adoption. The coordinator must replay the current capsule; the service recognizes\nthe committed data, starts a new candidate, activates its complete process topology, and restores\nthe private route.\n\nIf data commits but background or ingress activation fails, the candidate is removed and the\ndurable phase remains `reconciling`. An exact retry uses the already-committed release without\nreplaying migrations and attempts activation again.\n\n## Inspection, rollback, deletion, backups, and shutdown\n\n`inspect(projectId)` returns the durable non-secret state above. `snapshot(projectId)` delegates\nto the provider data store's consistent SQLite backup and is intended to feed an independently\nencrypted recovery repository. Both calls serialize with lifecycle work for the project and reject\nnew work after the service starts closing.\n\n### Remote snapshot boundary\n\nA runtime capsule may carry a separate high-entropy `ingress.controlToken`. The complete service\nkeeps only its SHA-256 in memory for the active generation; it never places the token in provider\nstate, Docker metadata, application environment, public ingress, or logs. When present, the\nservice exposes the exact private path returned by\n`deploymentProviderSnapshotPath(projectId)`:\n\n```http\nGET /v1/clank/control/<project-id>/snapshot\nAuthorization: Bearer <generation-control-token>\n```\n\nThe request succeeds only while the token, durable service state, active release, and active\ngeneration all agree. It serializes with lifecycle work, asks the data store for a consistent\nSQLite snapshot, rechecks the generation and SHA-256, and returns\n`application/vnd.clank.provider-snapshot` with exact length, digest, release, and generation\nheaders. Missing, malformed, old, restarted-without-reconcile, stopped, or deleted bindings return\na fixed non-enumerating response. Query strings and non-`GET` methods are rejected.\n\nThis endpoint returns plaintext SQLite bytes and belongs only on the authenticated private TLS\nprovider origin. The control token is an authorization secret, not transport encryption. Never\nmount the control prefix through an application's public domain or managed app route. The\nreceiving control plane must enforce response deadlines and byte limits, refuse redirects, verify\nall identity headers and the body digest, and immediately import the bytes into an encrypted\nrecovery repository. The low-level service provides this boundary; whether a particular\ncontrol-plane release consumes it is documented separately in [Remote runtime\nplacement](runtime-placement.md#current-support-boundary).\n\nUse `rollback(request)` and `delete(request)` rather than reaching into the provider data store\nbeneath a live service:\n\n```ts\nconst signal = new AbortController().signal;\n\nawait service.rollback({\n  operation: {\n    id: \"op_rollback_43\",\n    projectId: \"orbit_tasks\",\n    fence: 87,\n    attempt: 1,\n    maxAttempts: 10,\n  },\n  generation: 42,\n  confirmation: \"rollback orbit_tasks 42\",\n  signal,\n});\n\nawait service.delete({\n  operation: {\n    id: \"op_delete_44\",\n    projectId: \"orbit_tasks\",\n    fence: 88,\n    attempt: 1,\n    maxAttempts: 10,\n  },\n  generation: 43,\n  confirmation: \"delete orbit_tasks\",\n  signal,\n});\n```\n\nBoth methods validate the exact current generation and a newer project-wide fence, revoke ingress,\nwait for assigned response streams, stop every web/worker/scheduler process, and only then mutate\nprovider data. Rollback carries its fence into the restored generation. Destructive intent is\nwritten before the data commit: an exact retry resumes `rolling-back` or `deleting`, while normal\nreconciliation remains blocked until rollback reaches `rolled-back` or deletion removes the\nproject state. This covers process exit after SQLite commit but before service metadata commit.\nWrong confirmations, stale generations/fences, uncertain drains, and conflicting retries fail\nbefore destructive mutation.\n\n`close()` first prevents new reconcile work, revokes and drains private routes, closes and verifies\nthe Docker runtime boundary, then waits for in-flight per-project reconciliation to settle. A\nfailed drain or unproven container cleanup rejects shutdown and is sent only to the private\ndiagnostic hook.\n\nThese service methods are the provider-local safety boundary. `openProviderDeploymentAgent()` and\nthe authenticated provider bridge can now deliver canonical rollback/delete operations to them\nwithout sharing coordinator credentials or accepting caller confirmation strings. The built-in\ncontrol plane must still preserve an encrypted recovery point, deactivate public managed ingress,\nallocate that lifecycle operation and fence, update desired/observed placement, and reconcile the\nselected release after rollback. Do not call raw provider-data `rollback()` or `delete()` beneath\na running service.\n\n## Trust and operating boundary\n\nThe factory inherits all controls from the component guides:\n\n- runtime capsules and durable service state are independently validated;\n- service metadata is bounded, exact-field, `0600`, no-follow, atomically replaced, and\n  current-user owned beneath a `0700` real directory;\n- application secrets remain in the verified capsule and transient stdin activation plan;\n- only the web container gets a loopback publish;\n- background work is deferred until data commit;\n- private ingress retains only the route-token digest; and\n- generation/fence conflicts fail closed.\n\nUse one service writer per provider root. Pin every stateful project to the node owning its durable\nvolume. Docker is a minimum multi-account boundary, not a VM: protect the daemon and host, apply\negress and disk policy, enable rootless/user namespaces and LSM/seccomp where practical, and use\ndedicated VMs or microVMs for hostile workloads.\n\nContinue with [Provider data lifecycle](provider-data-lifecycle.md),\n[Provider Docker runtime](provider-docker-runtime.md),\n[Provider runtime ingress](provider-runtime-ingress.md),\n[Deployment provider adapters](provider-adapters.md),\n[Remote runtime placement](runtime-placement.md), and\n[Managed ingress](data-plane.md).\n"}