Skip to content

Quickstart

Quickstart: zero pre-existing infra, build a public repo, push to GHCR

Deploy devcontainer-builder with Helm — bundling BuildKit too, so there's nothing else to stand up first — then make one real /build request against a public git repository, pushing the result to GHCR. Every command below is real — nothing here is pseudocode.

Prerequisites

  • A Kubernetes cluster and helm (v3) pointed at it.
  • A registry to push to, and credentials for it — a build always needs somewhere to push, and (unless the registry allows anonymous pushes, which essentially none do) real credentials for it. This walkthrough uses GHCR and a PAT scoped to write:packages — a Docker Hub access token works the same way, just a different registry/host.

No git credentials are needed for this walkthrough — a public repository clones anonymously; see Credential handling.

Never expose this Service outside the cluster

The API has no authentication of its own — see the HTTP API reference. It's designed to sit behind ordinary cluster-internal networking only (the chart's Service is ClusterIP, no Ingress — see Helm chart). Anyone who can reach POST /build can make this service clone arbitrary repositories and push arbitrary images using whatever credentials it's configured with — don't put an Ingress/LoadBalancer in front of it, and restrict which pods can reach it with a NetworkPolicy if your cluster is multi-tenant.

1. Write a values file

GHCR_USERNAME=<your-github-username>

The chart's registryAuth.registries value is a plain list of {registry, username, password} entries — the chart builds the real docker-config-JSON itself, so there's no manual base64-encoding or hand-built JSON here. buildkit.deploy.enabled: true bundles a real BuildKit instance alongside the service — no separate BuildKit of your own to source or configure first (see the Helm chart reference for the one real prerequisite this doesn't remove, and when to point buildkit.endpoint at an existing instance instead):

quickstart-values.yaml
buildkit:
  deploy:
    enabled: true

registryAuth:
  registries:
    - registry: ghcr.io
      username: <your-github-username> # same value as $GHCR_USERNAME above
      password: <your-write:packages-scoped GitHub PAT>

With registryAuth configured this way, every /build request pushes using these ambient credentials unless it supplies its own registryCredentials.

2. Install the chart

Install into its own namespace, never default. The bundled BuildKit runs privileged (inherent to how it does OCI builds), so the namespace needs the privileged Pod Security level. Create and label it first, so the BuildKit pod is admitted on the first try:

kubectl create namespace devcontainer-builder
kubectl label namespace devcontainer-builder \
  pod-security.kubernetes.io/enforce=privileged

helm install devcontainer-builder oci://ghcr.io/deepspacecartel/charts/devcontainer-builder \
  --namespace devcontainer-builder --create-namespace \
  -f quickstart-values.yaml

--create-namespace is harmless here (the namespace already exists) and keeps the command copy-pasteable on its own. The privileged level only widens what the namespace allows; devcontainer-builder's own pod stays unprivileged. See Security model and the Helm chart reference.

Confirm it's actually ready — not just that the Pod is Running, but that BuildKit was picked up (see /health/ready):

kubectl -n devcontainer-builder port-forward svc/devcontainer-builder 8080:8080 &
curl -s http://localhost:8080/health/ready
# {"status":"ready"}

3. Build a public repo and push it to GHCR

Any public repo with a .devcontainer/devcontainer.json (or a root .devcontainer.json) works — devcontainer-builder-examples is a stable, real one, with a real example for exactly this:

payload.json
{
  "repository": "https://github.com/deepspacecartel/devcontainer-builder-examples.git",
  "branch": "node",
  "image": { "registry": "ghcr.io/deepspacecartel" }
}
curl -s -X POST http://localhost:8080/build \
  -H 'Content-Type: application/json' \
  -d @payload.json
{
  "image": "ghcr.io/deepspacecartel/devcontainer-builder-examples:sha-a1b2c3d",
  "registry": "ghcr.io/deepspacecartel",
  "name": "devcontainer-builder-examples",
  "tag": "sha-a1b2c3d",
  "commit": "a1b2c3d…",
  "gitCloneLogId": "…",
  "imageBuildLogId": "…"
}

image.name defaulted to the repo's own last path segment (devcontainer-builder-examples), and image.tag defaulted to sha-<short HEAD sha> — see the POST /build reference for every field and its default. Pull it back to confirm the push really happened:

docker pull ghcr.io/deepspacecartel/devcontainer-builder-examples:sha-a1b2c3d

Skipping image.registry on every request

Passing image.registry on every single request is fine for a one-off, but a deployment that always pushes to the same registry can configure a registry mapping rule instead, so callers can omit image entirely:

registryMapping:
  rules:
    - registry: "ghcr.io/deepspacecartel" # no hostMatch/pathPrefix = universal fallback

See ADR-0003 for why this resolves server-side instead of being required on every request.

Using this from an actual Coder Workspace Template

Everything above talks to devcontainer-builder directly, over its raw HTTP API. For the real end-to-end shape — a Coder user types in a git repo URL when creating a workspace, and a template turns that into a running pod, no separate CI/CD pipeline of your own to build and track every project's own image variant — see the Set up the platform guide.

If something goes wrong

A failed build returns 500 with the failing command, its exit code and a logId — not the underlying error text:

{"error": "devcontainer build ... exited with code 1", "logId": "…"}

Fetch the command's full captured output with that id (see GET /logs/{id}):

curl -s http://localhost:8080/logs/<logId>

That's where a registry's real 401, git's "Permission denied" or a devcontainer.json error shows up — often a typo'd registryAuth.registries entry from step 1 rather than a devcontainer-builder bug. A successful build returns gitCloneLogId and imageBuildLogId the same way. More in Troubleshooting.

If /health/ready never reports ready, check that the BuildKit pod exists:

kubectl -n devcontainer-builder get pods
kubectl -n devcontainer-builder get events --sort-by=.lastTimestamp

An event like violates PodSecurity "baseline" means the namespace label from step 2 is missing.