Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.crewship.ai/llms.txt

Use this file to discover all available pages before exploring further.

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.
ColumnTypeDefaultNotes
runtime_imagestringdebian:bookworm-slim (global default) / mcr.microsoft.com/devcontainers/base:bookworm (seeds)OCI reference. Glibc Linux only. Whitespace/control chars rejected.
devcontainer_configJSON stringNULLSubset of devcontainer.json. Max 100 KB. Validated at write time.
mise_configJSON stringNULL{"tools":{…}} map. Max 10 KB.
cached_imagestringNULLPopulated by the provisioner on success: crewship-cache:{hash[:12]}.
config_hashstringNULLSHA-256 of (runtime_image, devcontainer_config, mise_config).

devcontainer_config schema

Parser: internal/devcontainer/config.go:Config.
FieldTypeRequiredPurpose
imagestringBase image reference. Must equal runtime_image at the API boundary.
featuresmap[featureRef]optionsCommunity devcontainer features. Accepts tag form (ghcr.io/…/python:1) and SHA256 digest form (ghcr.io/…/python@sha256:abc…). Digest form pins the exact artifact — recommended for paid/shared templates where an upstream tag rewrite would silently ship new code to every customer.
postCreateCommandstring | []string | map[string]stringCommands run as agent user (UID 1001) once during provisioning. Baked into the cached image. Changes invalidate the cache.
postStartCommandstring | []string | map[string]stringCommands run as agent user (UID 1001) on every container start / restart. Not baked into the image; changes do not invalidate the cache. Use for “start local services” style init. Failures log a warning but do not block the container.
containerEnvmap[string]stringEnv vars baked into the cached image (via ENV on commit).
remoteUserstringDefault user for commands. Only "agent" / "1001" / empty are accepted — other values are rejected with HTTP 400. Crewship always runs as UID 1001.

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:
"features": {
  "ghcr.io/devcontainers/features/common-utils:2": {
    "username": "agent",
    "userUid": "1001",
    "userGid": "1001",
    "installZsh": false
  },
  "ghcr.io/devcontainers-extra/features/claude-code:2": {}
}
  • 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

{
  "tools": {
    "<tool>": "<version>"
  }
}
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.
VariableDefaultDescription
CREWSHIP_SIDECAR_PATHautodetectedAbsolute path to the crewship-sidecar Go binary on the host.
CREWSHIP_ENTRYPOINT_PATHautodetectedAbsolute path to entrypoint.sh on the host.
CREWSHIP_SKIP_SIDECAR(unset)Set to 1 to bypass the fail-fast check. Intended for unit tests; never set in production.
CREWSHIP_RUNTIME_IMAGEdebian:bookworm-slimGlobal default base image if a crew has no explicit runtime_image.

Autodetect order

internal/config/config.go:autodetectSidecarPaths walks these candidates and picks the first hit: Sidecar binary
  1. {dir of crewship executable}/crewship-sidecar
  2. /usr/local/bin/crewship-sidecar
Entrypoint script
  1. {dir of crewship executable}/entrypoint.sh
  2. {cwd}/scripts/entrypoint.sh
  3. {cwd}/entrypoint.sh
  4. /usr/local/share/crewship/entrypoint.sh
If either remains empty and CREWSHIP_SKIP_SIDECAR != 1, config.Load returns a descriptive error and the server refuses to start.

REST endpoints

MethodPathHandlerPurpose
GET/api/v1/crews/{crewId}/provisionProvisioningHandler.ProvisionStatusCurrent status + cached image tag + config hash.
POST/api/v1/crews/{crewId}/provisionProvisioningHandler.ProvisionTriggerEnqueue async provisioning; returns 202 Accepted.
POST/api/v1/crews/{crewId}/rebuildProvisioningHandler.ProvisionRebuildClear cache marker + re-provision.
GET/api/v1/features/catalogfeature/runtime catalog handlerDynamic devcontainer + mise catalog for UI (3-tier cache).
GET/api/v1/runtimes/catalogruntime catalog handlerCurated base-image + mise runtime list (the cmd_features.go / cmd_runtimes.go CLI commands hit these same endpoints).
All endpoints require a workspace-authenticated user. ProvisionTrigger returns 503 Service Unavailable if the Docker client is not configured on the server.

CLI flags

crewship crew config <slug>
  --show                    Print current config as YAML.
  --export                  Export runtime configuration as JSON to stdout.
  --clear                   Remove runtime_image + devcontainer_config + mise_config.
  --devcontainer <path>     Load JSON from path and PATCH devcontainer_config.
  --mise <path>             Load JSON from path and PATCH mise_config.
  --runtime-image <ref>     PATCH runtime_image only.

crewship crew provision <slug>                # trigger async
crewship crew provision status <slug>         # poll
crewship crew rebuild <slug>                  # invalidate + trigger
Flags are mutually exclusive; combining them errors out before hitting the API.

Provisioning states

Emitted by ProvisionStatus + WebSocket channel provision:{crewId}:
StateMeaning
idleNo config yet, or cached image in sync with hash.
pendingJob enqueued, goroutine not yet started.
runningActive — installing features / mise / running postCreate.
successCache image committed, cached_image + config_hash written.
failedCommit never happened; error recorded. Re-run with crewship crew rebuild.

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 labelled crewship.temp=provision and older than 1 hour are force-removed. Filter is label-scoped, so unrelated containers are never touched.
  • 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.

Orphan deletion policy

Cache-image deletion is opt-in via the CREWSHIP_CACHE_GC_AUTODELETE environment variable (also described in Environment → Devcontainer cache GC):
ValueBehaviour
unset / 0 / false (default)Log-only. Orphans are enumerated and logged; nothing is deleted. Operators see the list and can decide.
1 / trueAuto-delete. Orphans beyond the 5-minute age floor are force-removed each pass.
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

ConcernFile
Config parser / validationinternal/devcontainer/config.go
Feature downloader (OCI)internal/devcontainer/features.go
Feature installer (exec)internal/devcontainer/installer.go
Provisioner + cachinginternal/devcontainer/provisioner.go
REST handlers + GC sweepersinternal/api/crew_provisioning.go
Shared HEAD-digest cacheinternal/dockerutil/imagedigest.go
CLI crew configcmd/crewship/cmd_crew_config.go
CLI crew provisioncmd/crewship/cmd_crew_provision.go
Seed demo datacmd/crewship/seeddata/crews.go
Sidecar bind mountinternal/provider/docker/docker.go:buildMounts
Autodetect + fail-fastinternal/config/config.go:autodetectSidecarPaths