{"protocol":"clank-doc/1","frameworkVersion":"0.9.4","slug":"provider-docker-runtime","title":"Provider Docker runtime","description":"@clank.run/framework/provider docker is Clank's zero dependency reference launcher for isolated remote application runtimes. It consumes only provider data that has already been verified, extracted, and migrated by @clank.run/framework/prov","group":{"id":"deploy","title":"Deploy and operate"},"url":"https://docs.clank.run/docs/provider-docker-runtime","source":"docs/provider-docker-runtime.md","headings":["Open the launcher","Reconcile data, runtime, and ingress","Secrets do not become Docker configuration","Container boundary","Restart and shutdown behavior"],"tableOfContents":[{"id":"open-the-launcher","title":"Open the launcher","level":2},{"id":"reconcile-data-runtime-and-ingress","title":"Reconcile data, runtime, and ingress","level":2},{"id":"secrets-do-not-become-docker-configuration","title":"Secrets do not become Docker configuration","level":2},{"id":"container-boundary","title":"Container boundary","level":2},{"id":"restart-and-shutdown-behavior","title":"Restart and shutdown behavior","level":2}],"markdown":"# Provider Docker runtime\n\n`@clank.run/framework/provider-docker` is Clank's zero-dependency reference launcher for isolated\nremote application runtimes. It consumes only provider data that has already been verified,\nextracted, and migrated by `@clank.run/framework/provider-data`. It starts the exact web, worker,\nand scheduler entries from that release, health-checks the web process over loopback, and returns\na non-secret candidate for generation-bound ingress.\n\nThe launcher uses the Docker CLI directly and adds no runtime package. Docker is a useful minimum\nboundary for mutually untrusted application accounts; it is not equivalent to a VM. Use dedicated\nVMs or microVMs when the host, kernel, or Docker daemon must not be shared between tenants.\n\n## Open the launcher\n\nCreate the provider data store first so its owner-only root exists. Pre-pull an application image,\nresolve its immutable repository digest, and run the provider as a dedicated non-root operating\nsystem user:\n\n```ts\nimport {\n  openDockerDeploymentRuntimeLauncher,\n} from \"@clank.run/framework/provider-docker\";\nimport {\n  openDeploymentProviderDataStore,\n} from \"@clank.run/framework/provider-data\";\n\nconst rootDirectory = \"/var/lib/clank-provider\";\n\nconst data = await openDeploymentProviderDataStore({ rootDirectory });\nconst runtimes = await openDockerDeploymentRuntimeLauncher({\n  rootDirectory,\n  owner: \"runner-west-01\",\n  image: \"registry.example/clank-node@sha256:<64-hex-digest>\",\n  memory: \"512m\",\n  cpus: \"1\",\n  pidsLimit: 128,\n  maxRuntimes: 100,\n  maxContainers: 400,\n  portStart: 46_000,\n  portEnd: 49_999,\n  onError(error) {\n    privateProviderLog(error);\n  },\n});\n```\n\nMutable image tags are rejected unless `allowMutableImage: true` is explicit. The default\ncontainer user is the provider process's uid and gid; uid 0 is rejected unless\n`allowContainerRoot: true` is also explicit. This keeps owner-only releases and SQLite files\nusable without changing their ownership while making a non-root provider service the safe\ndefault.\n\n`owner` is an exact Docker label and must identify one launcher process. On open, Clank lists\ncontainers carrying both `run.clank.managed=provider-runtime` and that exact owner, forcibly\nremoves them, and verifies cleanup before returning. Every later stop also enumerates the exact\nowner/project/release/generation labels, so a container whose create result was ambiguous cannot\ndisappear from tracking before removal is proven. It never adopts a process after restart. Choose\na stable node-specific owner and never run two live launchers with the same owner.\n\n## Reconcile data, runtime, and ingress\n\nPrefer the complete\n[`openDockerDeploymentProviderService()` composition](provider-service.md), which persists the\ndesired generation/fence and implements this ordering. If building a custom provider, SQLite\nmigrations and restore require a stopped writer. Revoke and drain traffic, stop the current\nruntime, apply provider data, and launch only the web candidate inside the store's private\nvalidation callback:\n\n```ts\nlet candidate: Awaited<ReturnType<typeof runtimes.launch>> | undefined;\nlet preparedRuntime: PreparedDeploymentRuntimeData | undefined;\n\nawait runtimeIngress.deactivate(\n  request.operation.projectId,\n  currentGeneration,\n);\nawait runtimes.stop(request.operation.projectId);\n\ntry {\n  const committed = await data.apply({\n    operation: request.operation,\n    desired: request.desired,\n    runtime: request.runtime!,\n    signal: request.signal,\n  }, async (prepared) => {\n    preparedRuntime = prepared;\n    candidate = await runtimes.launch({\n      prepared,\n      signal: request.signal,\n      deferBackground: true,\n    });\n  }, async () => {\n    if (candidate) {\n      await runtimes.stop(candidate.projectId, candidate.generation);\n    }\n  });\n\n  if (!candidate || !preparedRuntime) {\n    throw new Error(\"The private candidate was not launched.\");\n  }\n  const active = await runtimes.activate(candidate, request.signal);\n\n  await runtimeIngress.activate({\n    protocol: \"clank-runtime/1\",\n    projectId: committed.projectId,\n    releaseId: committed.releaseId,\n    generation: committed.generation,\n    path: preparedRuntime.ingress.route,\n    token: preparedRuntime.ingress.token,\n    upstream: active.upstream,\n  });\n} catch (error) {\n  await runtimes.stop(request.operation.projectId).catch(privateProviderLog);\n  // Reconcile the last committed generation before restoring its route.\n  throw error;\n}\n```\n\n`PreparedDeploymentRuntimeData.config` is the exact normalized deployment config decoded from the\nverified capsule. The launcher does not accept a second caller-supplied config, so an adapter\ncannot accidentally start a different entry or job topology from the one that was migrated.\n\nOrdinary `launch()` starts the web container, workers, and scheduler, then confirms the complete\ntopology. `deferBackground: true` starts and health-checks only the private web candidate while\nretaining a memory-only activation plan. After `data.apply()` commits, `activate()` starts every\nworker/scheduler, confirms they remain running, erases that plan, and atomically marks the runtime\nactive. `commit()` remains available only for a non-deferred candidate. If apply, abort, health,\nbackground startup, or activation fails, Clank stops and removes the candidate containers and\nreleases their port.\n\nAlways pass the optional data-store discard callback when validation can start a process. It\nproves the candidate stopped before an uncommitted SQLite change is restored. A cleanup failure\nleaves the journal for restart recovery rather than rolling data back beneath a possibly live\nruntime.\n\nAn exact live launch retry is idempotent. The exact last committed generation can also be\nrelaunched after a deliberate stop, which is required to recover the prior generation after a\nfailed migration or ingress switch. A different release or capsule for that generation, a lower\ngeneration, a second project beyond capacity, an unavailable port, or a second generation while\nthe project's current runtime remains present fails closed.\n\n## Secrets do not become Docker configuration\n\nClank does not pass application-named variables to the host Docker process and does not use\n`docker create --env` for the runtime envelope. It:\n\n1. creates an interactive container with no application environment in its arguments or config;\n2. starts and attaches to that exact container without a shell;\n3. writes one bounded base64url JSON envelope through container stdin;\n4. decodes and validates it inside the already-running Node process;\n5. destroys stdin, sets the application environment, and imports the verified relative entry; and\n6. retains it only in memory while background activation is deliberately deferred, then releases\n   it after activation or cleanup without exposing it through inspection.\n\nNames such as `DOCKER_HOST`, `LD_PRELOAD`, `NODE_OPTIONS`, proxy controls, and TLS controls can\ntherefore neither redirect the host Docker client nor affect the container's Node loader before\nthe bootstrap is running. Secret values are absent from Docker CLI arguments, host application\nvariables, labels, and persisted container environment metadata. They remain available to the\napplication process, its memory, `/proc` peers permitted by the host, a privileged Docker\nadministrator, and any child process it deliberately starts. Treat the provider host and Docker\ndaemon as trusted application compute.\n\nThe host Docker CLI inherits only a small operator allowlist for Docker connection/context/TLS,\nproxy, rootless runtime, locale, `PATH`, and `HOME` configuration. Unusual installations can add\nexplicit trusted values through `dockerEnvironment`; never populate that option from a runtime\ncapsule or project secret.\n\nThe launcher never records application stdout or stderr in Clank state. Docker's bounded `local`\nlog driver retains three 10 MiB files per container for private operator diagnostics. Applications\nmust still avoid logging secrets, and operators must protect and expire Docker logs.\n\n## Container boundary\n\nEvery runtime has:\n\n- an immutable image selected with `--pull never`;\n- a read-only root filesystem and read-only exact release mount;\n- one read-write mount for only that project's provider data directory;\n- all capabilities dropped, `no-new-privileges`, a non-root uid/gid, and Docker init;\n- explicit memory, equal memory-plus-swap, CPU, and PID ceilings;\n- bounded no-exec, no-suid, nodev `/tmp` and `/run` tmpfs mounts;\n- `--restart no`, bounded stop time, and bounded local logs; and\n- only the web port published on a provider-selected `127.0.0.1` port.\n\nWorkers and schedulers receive the same release and database but publish no port.\n`CLANK_PROCESS_ROLE`, worker concurrency, worker queues, database aliases, managed-ingress\nsettings, host, and port are provider-controlled and overwrite any capsule value.\n\nThe default Docker `bridge` network preserves ordinary application egress but is not a\ntenant-grade network policy. Configure a provider-controlled network, firewall application\negress, protect the Docker socket, enable rootless Docker or user namespaces where practical,\napply seccomp/AppArmor/SELinux, enforce disk quotas outside the container, and separate hostile\ntiers onto stronger compute boundaries.\n\n## Restart and shutdown behavior\n\nThe ingress registry and launcher registry are intentionally memory-only. After a provider\nrestart:\n\n1. managed ingress has no active route and returns unavailable;\n2. launcher open removes exact-owner orphan containers;\n3. the coordinator reissues the current desired capsule under a new operation fence;\n4. provider data recognizes the already-committed generation and re-verifies its release;\n5. the launcher starts and health-checks a fresh candidate; and\n6. only then does the provider restore the exact generation-bound ingress route.\n\nThis is restart reconciliation, not process adoption. A Docker container that happened to survive\ncannot receive traffic merely because old control-plane observation said it was healthy.\n\n`stop(projectId, generation?)` gracefully stops and removes every web/job container in that exact\nruntime, then verifies none of its owned container IDs remain. `close()` rejects new work,\naborts in-progress launch and health work, stops all tracked runtimes, removes any remaining\nexact-owner containers, verifies convergence, and rejects if Docker cleanup cannot be proven.\nAfter confirmed provider-data deletion, call `forget(projectId, generation)` to release the\ninactive generation high-water mark.\n\nContinue with [Provider data lifecycle](provider-data-lifecycle.md),\n[Provider runtime ingress](provider-runtime-ingress.md),\n[Complete deployment provider service](provider-service.md),\n[Deployment provider adapters](provider-adapters.md), and\n[Remote runtime placement](runtime-placement.md).\n"}