Skip to content

Helm chart

Helm chart

charts/devcontainer-builder deploys a single-replica Deployment + ClusterIP Service — no autoscaling or Ingress by design; this is an in-cluster-only caller service, not a public one. Every value below is the chart's own real values.yaml, validated by its values.schema.json: an unknown key, a wrong type, or a value outside an enum (build.mode, sshHostKeyPolicy, ...) fails helm install/helm template up front.

helm lint charts/devcontainer-builder
helm template charts/devcontainer-builder -f my-values.yaml

Upgrading with --reuse-values

helm upgrade --reuse-values keeps only the previous release's values. The chart fills in values added by newer versions from its own defaults, so that works. Helm 3.14's --reset-then-reuse-values is the more explicit way to do it.

image

Key Default Notes
image.repository ghcr.io/deepspacecartel/devcontainer-builder The real, published image — see .github/workflows/release.yaml.
image.tag "" Empty means the chart's own appVersion, so every chart release deploys its matching image. Set it only to deliberately pin a different image.
image.pullPolicy IfNotPresent
imagePullSecrets [] e.g. [{name: my-pull-secret}], for a private copy of the image.

service

Key Default Notes
service.port 8080 Also becomes the container's PORT env var.

environment

Key Default Notes
environment "" Deployment environment name (e.g. dev/staging/production), passed through as the container's DEPLOYMENT_ENVIRONMENT env var — see Configuration. Empty means unset, and the service falls back to its own development default. There's no equivalent chart value for the service's own name — the chart always sets SERVICE_NAME unconditionally from .Chart.Name, the same value the app.kubernetes.io/name label already uses everywhere else in this chart, so the two can never drift apart.

buildkit

Key Default Notes
buildkit.endpoint "" e.g. tcp://buildkit-buildkit-service.buildkit.svc.cluster.local:1234. Unset means readiness never passes — see /health/ready. Ignored when buildkit.deploy.enabled is true.
buildkit.deploy.enabled false Deploys a bundled BuildKit instance (andrcuns/buildkit-service, the buildkitBundled dependency in Chart.yaml) alongside devcontainer-builder itself, and computes buildkit.endpoint automatically from it — zero pre-existing BuildKit infra needed for a first install.
buildkit:
  deploy:
    enabled: true

| buildkit.manageNamespace | false | Renders the release Namespace itself, labeled pod-security.kubernetes.io/enforce: privileged — the behavior of chart versions up to 0.5.0. Only takes effect with buildkit.deploy.enabled. See the namespace below. |

buildkitBundled

Values passed straight through to the bundled buildkit-service chart (any of its own values work here). The chart relies on these:

Key Default Notes
buildkitBundled.fullnameOverride buildkit Pins the BuildKit Service's name, which the computed buildkit.endpoint (tcp://<fullnameOverride>.<namespace>.svc.cluster.local:<port>) is built from.
buildkitBundled.service.type ClusterIP
buildkitBundled.service.port 1234 Also the port in the computed endpoint.

The namespace for bundled BuildKit

BuildKit's default mode is genuinely privileged (inherent to how it does OCI builds), and the bundled chart installs into the release namespace — so that namespace must allow privileged pods. Create and label it once, before installing:

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 \
  --set buildkit.deploy.enabled=true

"Privileged" is the most permissive Pod Security level — it only widens what the namespace allows, so devcontainer-builder's own pod (non-root, read-only root filesystem, see below) is unaffected. The chart doesn't create the namespace: a chart-owned Namespace can't be installed into a namespace that already exists, and conflicts with --create-namespace.

Upgrading an install from chart 0.5.0 or earlier with buildkit.deploy.enabled

Those versions rendered the release Namespace themselves. Helm deletes a resource that disappears from a release on upgrade, which here would be the namespace and the release with it — so the chart keeps rendering the Namespace for an install whose live namespace is owned by the same release (detected with lookup), now annotated helm.sh/resource-policy: keep. A plain helm upgrade is safe, and a later helm uninstall leaves the namespace in place. If you upgrade with something that can't lookup (helm template | kubectl apply, Argo CD, client-side --dry-run renders), set buildkit.manageNamespace=true instead, or first run kubectl annotate namespace <ns> helm.sh/resource-policy=keep.

build

Server-wide defaults for platform selection and BuildKit build options, used whenever a /build request doesn't specify its own platforms or buildOptions — see API for the per-request fields.

Key Default Notes
build.platforms [] e.g. ["linux/amd64", "linux/arm64"]. Empty means today's behavior: no --platform flag, builder's native platform.
build.noCache false Passed as --no-cache when true.
build.cacheFrom "" Passed as --cache-from <value>, e.g. type=registry,ref=ghcr.io/example/app:buildcache.
build.cacheTo "" Passed as --cache-to <value>, e.g. type=registry,ref=ghcr.io/example/app:buildcache,mode=max. Points BuildKit's own layer cache at the same registry the image is pushed to — persistent and shared across replicas, unlike a per-pod cache.
build.mode auto auto or never — passed as --buildkit <value>, controlling whether BuildKit is used at all.
build.fallbackImage mcr.microsoft.com/devcontainers/base:ubuntu The image a repository without any devcontainer.json is built from, as if it had {"image": "<fallbackImage>"} (ADR-0013). Empty: such builds fail, as with the Dev Containers CLI.
build.cloneTimeoutSeconds null Seconds before a clone is killed. null keeps the service's default, 600.
build.timeoutSeconds null Seconds before a Dev Container build is killed. null keeps the service's default, 3600.
build.maxConcurrent null Builds running at once; past it, POST /build answers 429 with Retry-After. 0 means no limit; null keeps the service's default, 4.
git.allowInsecureProtocols false Allow git:// and http:// repository URLs. They're unencrypted and can reach in-cluster hosts; for test git servers only.
build:
  platforms: ["linux/amd64", "linux/arm64"]
  cacheFrom: "type=registry,ref=ghcr.io/example/app:buildcache"
  cacheTo: "type=registry,ref=ghcr.io/example/app:buildcache,mode=max"

registryAuth

Ambient registry push credentials — used whenever a /build request doesn't supply its own registryCredentials.

Key Default Notes
registryAuth.existingSecret "" Name of an existing kubernetes.io/dockerconfigjson-shaped Secret. If set, registries below is ignored.
registryAuth.registries [] List of {registry, username, password} entries. The chart builds the real docker-config-JSON ({"auths": {"<registry>": {"auth": "<base64 user:pass>"}}}) and renders it into a chart-managed Secret if existingSecret is unset — nobody hand-builds or pre-base64-encodes JSON themselves.
registryAuth:
  registries:
    - registry: https://index.docker.io/v1/ # Docker Hub's real registry host
      username: svc-bot
      password: hunter2

Unconfigured (empty registries, no existingSecret) means an empty {"auths":{}} — pushes rely entirely on whatever the target registry allows anonymously, or on a per-request registryCredentials override.

gitCredentials

Server-side default git credentials — see Configuration for the entry shape and Credential handling for how they're used.

Key Default Notes
gitCredentials.enabled true false omits GIT_CREDENTIALS_CONFIG_PATH and its volume entirely, rather than pointing at an empty list.
gitCredentials.existingSecret "" Name of an existing Secret containing a git-credentials.json key (same array shape as entries).
gitCredentials.entries [] Inline entries, rendered into a chart-managed Secret if existingSecret is unset.

sshHostKeyPolicy

Key Default Notes
sshHostKeyPolicy tofu tofu or pinned — see Configuration.

insecureRegistries

Key Default Notes
insecureRegistries [] Registry hosts the service's own registry calls reach over plain HTTP — see Configuration. Rendered into the settings file only when non-empty.

registryMapping

Server-side repo → registry routing rules, used to resolve image.registry when a request omits it.

Key Default Notes
registryMapping.enabled true false omits registryMapping from the rendered settings file entirely.
registryMapping.existingConfigMap "" Name of an existing ConfigMap containing a registry-mapping.json key, mounted separately with its own dedicated REGISTRY_MAPPING_CONFIG_PATH env var. If set, rules below is ignored.
registryMapping.rules [] Inline rules. Unlike existingConfigMap, these don't get a dedicated ConfigMap — they're folded straight into the chart-rendered settings file below.

The settings file

SERVICE_CONFIG_PATH and its ConfigMap are entirely automatic — there's no settingsFile.* value to set. The chart renders settings.json from values that already exist elsewhere in this page: buildkit.endpoint, build.* (only the sub-fields that differ from their zero-value default), sshHostKeyPolicy, insecureRegistries (only when non-empty), and registryMapping.rules (only when registryMapping.existingConfigMap is unset). gitCredentials is deliberately never included — GIT_CREDENTIALS_CONFIG_PATH always takes precedence over a settings-file copy of the same field (see Configuration for config.ts's real precedence rules), so a copy here would be both inert and a needless duplicate of sensitive material in a less-guarded ConfigMap.

Escape hatches: extraArgs, extraEnv, extraVolumes, extraVolumeMounts

Key Default Notes
extraArgs [] Appended to the container's entrypoint (devcontainer-builder, i.e. node dist/index.js) as args, e.g. ["--ssh-host-key-policy", "pinned"].
extraEnv [] Appended after the chart's own env entries — a plain [{name: ..., value: ...}] list.
extraVolumes / extraVolumeMounts [] Appended after the chart's own volumes/mounts — e.g. mounting a caller-provided ConfigMap holding a CA cert, paired with extraEnv pointing GIT_SSL_CAINFO at it.

extraEnv is also how the service's remaining optional observability integrations get configured — no dedicated chart values for these, since they're just environment variables the service already reads directly (unlike SERVICE_NAME/DEPLOYMENT_ENVIRONMENT above, which are first-class chart values, since every deployment should set an environment name, not just the ones opting into Sentry/OTel):

extraEnv:
  - name: SENTRY_DSN # GlitchTip (Sentry-protocol-compatible) works too
    value: "https://<key>@errors.example.com/1"
  - name: OTEL_EXPORTER_OTLP_ENDPOINT # this cluster's Alloy OTLP/HTTP receiver
    value: "http://alloy.observability.svc.cluster.local:4318"

See Architecture for what each actually does.

replicaCount, updateStrategy, terminationGracePeriodSeconds

Key Default Notes
replicaCount 1 0 or 1 — the schema rejects more. Captured build output (GET /logs/{id}) lives on the pod's own /tmp emptyDir, so a second replica would answer 404 for log ids the other one returned.
updateStrategy {} Empty means Kubernetes' own default (RollingUpdate).
terminationGracePeriodSeconds 300 On SIGTERM the service stops accepting requests and lets in-flight ones finish; a /build request lasts as long as its build, so this is how long an in-flight build gets during a rollout or node drain.

Security and scheduling

The image runs as uid/gid 2000 (USER 2000:2000, numeric so runAsNonRoot can verify it), with tini as PID 1 to reap the clone/ssh/buildx subprocesses. The defaults below fit the Pod Security restricted level.

Key Default Notes
podSecurityContext runAsNonRoot: true, runAsUser: 2000, runAsGroup: 2000, fsGroup: 2000, seccompProfile: {type: RuntimeDefault} Rendered as the pod's securityContext. The ids must match the image's user, so group-readable mounted Secret/ConfigMap files (mode 0440) and the emptyDirs are readable/writable by it.
securityContext runAsNonRoot: true, runAsUser: 2000, runAsGroup: 2000, allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities: {drop: [ALL]}, seccompProfile: {type: RuntimeDefault} Rendered as the container's securityContext. The root filesystem can be read-only because everything the service writes goes to the two emptyDirs below.
serviceAccount.automountServiceAccountToken false The service never calls the Kubernetes API, so the pod gets no token.
podAnnotations {} Merged with the chart's own checksum/* annotations.
nodeSelector / tolerations / affinity {} / [] / {} Standard pod scheduling fields.

resources, scratchVolume, dockerConfigVolume

Key Default Notes
resources.requests cpu: 250m, memory: 256Mi
resources.limits cpu: "1", memory: 1Gi
scratchVolume.sizeLimit 5Gi An emptyDir mounted at /tmp — every per-request scratch clone/credential directory (see Credential handling), the captured output behind GET /logs/{id}, and the Dev Containers CLI's own cache live here.
dockerConfigVolume.sizeLimit 64Mi An emptyDir mounted at DOCKER_CONFIG (/home/builder/.docker) for buildx's builder state; the registry-auth config.json is mounted into it.