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 differentregistry/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):
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:
{
"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.