Download OpenAPI specification:
Builds a container image from a git repository's .devcontainer.json using a remote BuildKit builder, and pushes it to a registry.
No authentication of its own - the service is meant to sit behind cluster-internal networking (see the Helm chart's Service).
Every response is application/json except GET /metrics (text/plain). Any other method/path returns 404 {"error":"not found"}.
400 always means the request itself was unusable - nothing was ever attempted, or a required piece of configuration to even attempt it (a registry, a host key pin) was missing:
{"error": "invalid JSON body"} - the request body isn't valid JSON at all.{"error": "missing or invalid fields: repository (required); branch, image.{registry,name,tag}, gitCredentials.{username,token}, registryCredentials.{registry,username,password}, platforms, buildOptions.{noCache,cacheFrom,cacheTo,mode} (all optional)"} - the parsed body isn't a JSON object, or fails the shape check.{"error": "unable to parse git repository URL: <repository>"} - repository doesn't match any accepted URL form.{"error": "repository URL scheme \"<scheme>://\" is not allowed: ..."} - repository uses a scheme other than https:///ssh:///SCP-style (git:///http:// need the server's allowInsecureGitProtocols).{"error": "repository URL must not contain credentials (user:token@...) - pass them in gitCredentials instead"}.{"error": "buildOptions.cacheTo: cache type \"<type>\" is not allowed (allowed: registry, gha, inline)"} - likewise for cacheFrom.{"error": "unable to derive an image name from repository path ...: provide image.name"}.{"error": "no registry resolved for repository <repository>: provide image.registry or configure a matching registry mapping rule"} - no image.registry given and no registry mapping rule matched.{"error": "SSH host key policy is \"pinned\" but no pinned key configured for host <host>"} - the resolved SSH credential has no pinnedHostKey under sshHostKeyPolicy: pinned.429 means the instance is already running build.maxConcurrent builds; retry after Retry-After seconds.
500 always means a real external command was actually run and failed - the request was well-formed, but cloning or building it didn't work: {"error": "<command> <args...> exited with code <n>", "logId": "<id>"}. The body is the real command and exit code, not the real error text (a registry's real 401 body, git's real "Permission denied") - fetch that via GET /logs/{logId} (also returned on success, as gitCloneLogId/imageBuildLogId).
image/gitCredentials/registryCredentials/buildOptions may each be omitted entirely; once present, their own required sub-fields apply. Unknown top-level fields are tolerated, not rejected.
| repository required | string non-empty
|
null or string Branch (or tag) to build. Omitted or null: the repository's default branch. Non-empty if given. | |
null or object | |
object Per-request git credentials for the clone. Both fields required together. | |
object Per-request registry push credentials. All three fields required together. Omit entirely to rely on the server's own ambient registryAuth for the target registry instead. | |
| platforms | Array of strings[ items non-empty ^[a-z0-9]+/[a-z0-9_]+(?:/[a-z0-9]+)?$ ] Overrides the chart's build.platforms for this request, e.g. ["linux/amd64", "linux/arm64"]. Omitted or empty means today's behavior (no --platform flag). |
object | |
| property name* additional property | any |
{- "branch": "node",
- "image": {
- "registry": "ghcr.io/deepspacecartel"
}
}{- "image": "string",
- "registry": "string",
- "name": "string",
- "tag": "string",
- "branch": "string",
- "commit": "string",
- "gitCloneLogId": "string",
- "imageBuildLogId": "string"
}Checks whether a previously-built image still exists in its registry, by talking to the registry's own HTTP API V2 directly (bearer-token challenge/exchange, then a manifest lookup).
| registry required | string non-empty Non-empty. |
| name required | string non-empty Non-empty. |
| tag required | string non-empty Non-empty. |
| x-registry-username | string Together with X-Registry-Password, explicit credentials for this call. Falls back to the service's ambient DOCKER_CONFIG auth for |
| x-registry-password | string See X-Registry-Username. |
{- "image": "string",
- "exists": true
}Best-effort deletion of a previously-built image's manifest. Not all registries support this via the standard API (Docker Hub notably doesn't) - see 0008 for why that's treated as a normal outcome, not an error.
| registry required | string non-empty Non-empty. |
| name required | string non-empty Non-empty. |
| tag required | string non-empty Non-empty. |
| x-registry-username | string Together with X-Registry-Password, explicit credentials for this call. Falls back to the service's ambient DOCKER_CONFIG auth for |
| x-registry-password | string See X-Registry-Username. |
{- "image": "string",
- "deleted": true,
- "reason": "string"
}Read a built image's merged Dev Container configuration (lifecycle commands, VS Code customizations, ...) from its registry.
Reads a built image's devcontainer.metadata label straight from its registry (no pull, no Docker daemon) and returns it merged the way the Dev Containers CLI merges it, plus ready-to-run lifecycle scripts and merged VS Code customizations - what a Coder template needs to run postCreateCommand & co. and pre-install extensions. Pass registry/name/tag exactly as POST /build returned them. See 0011.
| registry required | string non-empty Non-empty. As |
| name required | string non-empty Non-empty. |
| tag required | string non-empty Non-empty. |
| platform | string^[^/]+/[^/]+(/[^/]+)?$
|
| x-registry-username | string Together with X-Registry-Password, explicit credentials for this call. Falls back to the service's ambient DOCKER_CONFIG auth for |
| x-registry-password | string See X-Registry-Username. |
{- "image": "ghcr.io/deepspacecartel/devcontainer-builder-examples:sha-a1b2c3d",
- "digest": "sha256:3f1c…",
- "configuration": {
- "init": false,
- "privileged": false,
- "remoteUser": "node",
- "onCreateCommands": [ ],
- "updateContentCommands": [ ],
- "postCreateCommands": [
- "/usr/local/share/pull-git-lfs-artifacts.sh",
- "bash .devcontainer/postCreateCommand.sh"
], - "postStartCommands": [ ],
- "postAttachCommands": [ ],
- "customizations": {
- "vscode": [
- {
- "extensions": [
- "dbaeumer.vscode-eslint"
]
}, - {
- "extensions": [
- "hashicorp.terraform"
]
}
]
}, - "remoteEnv": { },
- "containerEnv": { },
- "portsAttributes": { }
}, - "lifecycleScripts": {
- "onCreateCommand": null,
- "updateContentCommand": null,
- "postCreateCommand": "#!/bin/sh\necho 'devcontainer: postCreateCommand from ghcr.io/devcontainers/features/git-lfs:1'\n/bin/sh -c '/usr/local/share/pull-git-lfs-artifacts.sh' || { dc_rc=$?; echo 'devcontainer: postCreateCommand from ghcr.io/devcontainers/features/git-lfs:1 failed with exit code '\"$dc_rc\" >&2; exit \"$dc_rc\"; }\n…",
- "postStartCommand": null,
- "postAttachCommand": null
}, - "vscode": {
- "extensions": [
- "dbaeumer.vscode-eslint",
- "hashicorp.terraform"
], - "settings": { }
}, - "warnings": [ ],
- "metadata": [
- {
- "id": "ghcr.io/devcontainers/features/git-lfs:1",
- "postCreateCommand": "/usr/local/share/pull-git-lfs-artifacts.sh"
}, - {
- "remoteUser": "node",
- "postCreateCommand": "bash .devcontainer/postCreateCommand.sh"
}
]
}Kubernetes startupProbe. Always 200 {"status":"started"} once the process is listening - checks nothing beyond that. A distinct route from GET /health/live even though the check is identical today (this service has no separate async startup phase), so a startupProbe (a generous total budget, to tolerate slow pod scheduling/image pulls) can be tuned independently of livenessProbe (tight, once actually started).
{- "status": "started"
}Prometheus text-format metrics (Content-Type: text/plain), for a ServiceMonitor/node-exporter-style scrape.
Node.js process/runtime defaults (prom-client's collectDefaultMetrics) plus:
devcontainer_builder_builds_total (counter, label status: success|failure|invalid_request|busy) - every POST /build attempt.devcontainer_builder_build_duration_seconds (histogram, label status) - real build wall-clock time, bucketed toward minutes (not the sub-second defaults), since a build is a clone + image build + push.devcontainer_builder_image_checks_total (counter, label result: exists|absent|error) - every GET /image call.devcontainer_builder_image_deletes_total (counter, label result: deleted|unsupported|error) - every DELETE /image call.devcontainer_builder_devcontainer_lookups_total (counter, label result: found|absent|no_metadata|error) - every GET /devcontainer call.{- "buildkitConfigured": true,
- "buildxBuilderName": "devcontainer-builder-remote",
- "sshHostKeyPolicy": "tofu",
- "defaultPlatforms": [ ],
- "defaultBuildOptions": {
- "noCache": false,
- "mode": "auto"
}, - "gitCredentials": [
- {
- "host": "github.com",
- "kind": "https"
}
], - "registryMappingRules": [
- {
- "registry": "ghcr.io/deepspacecartel"
}
], - "insecureRegistries": [ ],
- "allowInsecureGitProtocols": false,
- "cloneTimeoutSeconds": 600,
- "buildTimeoutSeconds": 3600,
- "maxConcurrentBuilds": 4,
- "registryAuth": [
- {
- "registry": "ghcr.io"
}
]
}Full captured stdout+stderr of one git clone or one devcontainer build --push (including remote builder setup) - the id comes from POST /build's response (gitCloneLogId/imageBuildLogId on success, logId on a 500).
Not persisted beyond this pod's own lifetime (backed by the same ephemeral scratch space as an in-progress build, not a volume) and rotated: only the most recent commandLogRetention (default 10) files are kept per kind (git/docker) - an id from an old build may already be gone.
| id required | string^(git|docker)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f... A log id returned by |
{- "error": "string",
- "logId": "string"
}Deletes one captured command-output log ahead of its normal rotation. Idempotent - deleting an already-gone id also 404s.
| id required | string^(git|docker)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f... A log id returned by |
{- "error": "string",
- "logId": "string"
}