Skip to main content

Devcontainer Configuration

This page is the flat reference for every field, env var, and CLI flag that governs Crewship’s devcontainer runtime. For the step-by-step narrative, see Guides → Devcontainers & Runtime Images.

Per-crew fields

Stored on the crews table, settable via UI wizard, REST PATCH, or crewship crew config.

devcontainer_config schema

Parser: internal/devcontainer/config.go:Config.

Allowed feature registries

Feature IDs are validated against an allowlist in internal/devcontainer/features.go to avoid arbitrary OCI artifact execution. Accepted prefixes:
  • ghcr.io/devcontainers/features/* — official
  • ghcr.io/devcontainers-extra/features/* — extras (claude-code, opencode, codex, …)
  • ghcr.io/devcontainers-community/features/*
  • ghcr.io/crewship-ai/features/* — reserved for first-party features
Dependencies declared in a feature’s devcontainer-feature.json installsAfter (either spec form ["common-utils"] or legacy wild form [{"id":"common-utils"}]) are honoured by the topological sort in SortFeatures.

Canonical feature set for every Crewship crew

These two are considered the minimum to turn a bare Debian/Ubuntu image into a working Crewship crew container:
  • common-utils creates the agent user at UID 1001 and installs curl, git, ca-certificates, sudo. Replaces the deleted EnsureAgentUser Go helper.
  • claude-code installs Node.js 22 (NodeSource) + @anthropic-ai/claude-code globally. Replaces the deleted EnsureClaudeCode Go helper.

mise_config schema

Provisioner installs mise as the agent user, then mise install per tool. Versions follow mise semantics (latest, 22, 22.11.0, lts).

Environment variables

Extend the main environment reference. These govern the bind-mount pipeline.

Autodetect order

internal/config/config.go:autodetectSidecarPaths walks these candidates and picks the first hit: Sidecar binary
  1. {dir of executable}/crewship-sidecar — tar.gz / installer layout
  2. {dir of executable}/../libexec/crewship-sidecar — Homebrew layout
  3. {dir of executable}/../libexec/crewship/crewship-sidecar — deb/rpm FHS layout (/usr/libexec/crewship/)
  4. /usr/local/bin/crewship-sidecar
Entrypoint script
  1. {dir of executable}/entrypoint.sh — tar.gz / installer layout
  2. {dir of executable}/../libexec/entrypoint.sh — Homebrew layout
  3. {dir of executable}/../libexec/crewship/entrypoint.sh — deb/rpm FHS layout (/usr/libexec/crewship/)
  4. {cwd}/scripts/entrypoint.sh
  5. {cwd}/entrypoint.sh
  6. /usr/local/share/crewship/entrypoint.sh
The executable directory is resolved through symlinks first, so a Homebrew-symlinked crewship on your PATH resolves to its real Cellar directory (where the libexec sibling lives). If either remains empty and CREWSHIP_SKIP_SIDECAR != 1, config.Load returns a descriptive error and the server refuses to start.

REST endpoints

REST contract

All routes in this section require an authenticated request. Crew routes also require workspace context and resolve crewId within that workspace.

GET /api/v1/crews//provision

Auth: Authenticated workspace request. Request: crewId path parameter; no body. Response: 200 OK with the current status, cached image tag, and config hash. Statuses: 200 OK or 404 Not Found when the crew is not in the caller’s workspace.

POST /api/v1/crews//provision

Auth: Authenticated workspace caller with the provisioning write permission. Request: crewId path parameter; no body. Response: 202 Accepted when the asynchronous job is queued. Statuses: 202 Accepted, or 400, 403, 404, 409, 429, 500, or 503 for the validation, permission, workspace, duplicate-job, rate-limit, server, or Docker-configuration conditions described in the provisioning reference.

POST /api/v1/crews//rebuild

Auth: Authenticated workspace caller with the provisioning write permission. Request: crewId path parameter; no body. Response: 202 Accepted when the cache marker is cleared and re-provisioning is queued. Statuses: 202 Accepted, or 403, 404, 409, 429, 500, or 503 for the corresponding conditions.

GET /api/v1/features/catalog

Auth: Authenticated request. Request: optional search query parameter; no body. Response: 200 OK with a features array. Statuses: 200 OK; the handler uses its catalog fallback when the dynamic fetch fails.

GET /api/v1/runtimes/catalog

Auth: Authenticated request. Request: optional search query parameter; no body. Response: 200 OK with a runtimes array. Statuses: 200 OK; the handler uses its catalog fallback when the dynamic fetch fails. All endpoints require a workspace-authenticated user. ProvisionTrigger returns 503 Service Unavailable if the Docker client is not configured on the server.

CLI flags

Flags are mutually exclusive; combining them errors out before hitting the API.

Provisioning states

Emitted by ProvisionStatus + WebSocket channel provision:{crewId}:

Validation rules

Enforced in internal/api/crews.go write paths and devcontainer.Config.Validate:
  • image / runtime_image: non-empty, no whitespace/control chars, ≤ 512 chars.
  • features: keys match the allowlisted registries above.
  • postCreateCommand: ≤ 4096 chars per entry, no null bytes.
  • containerEnv: ≤ 32 keys, each key [A-Z_][A-Z0-9_]*.
  • mise_config: well-formed JSON, tools map only, ≤ 32 entries.
  • Total devcontainer_config blob ≤ 100 KB.
  • Total mise_config blob ≤ 10 KB.

Cache image naming

Successful provisions commit a reusable layer tagged crewship-cache:{hash[:12]}, where hash is the SHA-256 of (runtime_image, devcontainer_config, mise_config). The full 64-char hash is written to crews.config_hash; the truncated 12-char form is used as the image tag so docker images stays legible.
  • Cache hit — provisioner sees the crewship-cache:{hash[:12]} image already exists and the crew row is in sync. No work is done; cached_image is kept.
  • Cache miss — hash differs from crews.config_hash, or the tagged image was pruned. Full feature + mise install runs and the new image is committed under the new hash.
Two crews with identical configs share the same cache image (deduped on first provision). Pushing a custom containerEnv key or tweaking postCreateCommand changes the hash and creates a fresh cache image; the old one becomes eligible for GC after its last crew migrates off.

Runtime base-image digest check

internal/dockerutil/imagedigest.go keeps a HEAD-manifest cache of remote base-image digests (DefaultDigestTTL = 24 * time.Hour). On a provision request, the resolver checks whether the caller’s runtime_image reference still resolves to the digest baked into the existing cache image. If the registry has moved the tag (security update, rolled release), the provisioner treats the cache as stale even when the config_hash is unchanged and rebuilds.
  • TTL: 24 hours. Empty/negative results are also cached so a missing upstream does not trigger a HEAD storm.
  • Bypass: pin runtime_image to an explicit digest (ghcr.io/…@sha256:…). Digest-pinned references skip the HEAD round-trip.
  • Shared: the same resolver backs GET /api/v1/cache/images so the admin UI and the provisioner do not double-pay for the registry check.

Background GC

ProvisioningHandler runs a sweeper every 30 minutes (and once at startup) to remove crash leaks:
  • Temp containers — those named crewship-provision-* and labelled crewship.temp=provision — are force-removed once older than 1 hour. Both conditions are required, and the name is the one that authorises deletion.
    The label alone is not proof of provenance. Provisioning ends in a docker commit of the scratch container, and Docker copies that container’s labels into the resulting crewship-cache:* image — so a crew container started from it inherited crewship.temp=provision without asking. A label-only sweeper therefore force-removes healthy crews an hour into their lives; this happened on a dev host on 2026-07-20. Container names are never copied into images, which is why the name is the marker that holds.
    Crew containers now set crewship.temp to the empty string at create, which stops the inheritance: the daemon merges an image label only for a key the create request leaves unset, so the explicit empty value wins and the container drops out of --filter label=crewship.temp=provision. Consequence for docker ps: an exact-value filter (label=crewship.temp=provision) now lists only scratch containers plus any crew container created before this release. A key-only filter (label=crewship.temp) still lists every crew, because the key is present with an empty value. Filter by name=crewship-provision- when you want only genuine scratch containers — that is what the sweeper itself decides on. Scratch containers leaked before this release carry a Docker-assigned random name and are no longer swept automatically. Remove them by hand if they bother you — docker ps -a --filter label=crewship.temp=provision, then discount anything named like a crew.
  • Cache images (crewship-cache:*) with no referencing crew row across any workspace are flagged. A 5-minute age floor protects images that Provision() has just committed but not yet linked to a crew, eliminating the obvious race window.
  • Intermediate feature images (crewship-feat:*) — the per-feature layers the BuildKit path produces — are regenerable and never referenced by a crew row, so the sweeper treats any unreferenced one beyond the age floor the same way it treats orphaned cache images.

Orphan deletion policy

Cache-image deletion is opt-in via the CREWSHIP_CACHE_GC_AUTODELETE environment variable (also described in Environment → Devcontainer cache GC): The sweeper logs a single line per pass with removed, total_orphans, and skipped_too_young so dashboards can alert on cache growth regardless of mode.

Image-list cache

Docker’s ImageList is O(n) over every image on the host. The sweeper and GET /api/v1/cache/images share a 10-second image-list cache so admin UI polling does not pay that cost on every request. The cache is invalidated on any commit/delete performed by the provisioner itself, so the caller always sees its own writes.

Code of record