Skip to content

DevContainer Spec Reference


Spec: Dev Containers

Tools search for devcontainer.json in this order:

  1. .devcontainer/devcontainer.json
  2. .devcontainer.json
  3. .devcontainer/<folder>/devcontainer.json (one level deep only)

Multiple files may coexist. When more than one is found, the tool should let the user choose. The search starts at the project workspace folder, typically the git repository root.


Spec: Dev Containers

ScenarioRequired FieldsNotes
Image-basedimagePulls a container image directly
Dockerfile-basedbuild.dockerfileBuilds from a Dockerfile
Docker Compose-baseddockerComposeFile, serviceUses Compose to orchestrate one or more services

Compose configurations handle images and Dockerfiles natively, so image and build.dockerfile are not used in that scenario.


JSON Reference

PropertyTypeDefaultDescription
namestring-Display name for the dev container
featuresobject-Feature IDs mapped to their options
overrideFeatureInstallOrderstring[]-Override automatic Feature install ordering
forwardPorts(number|string)[][]Ports to forward from container to host
portsAttributesobject-Per-port configuration (see Port Attributes)
otherPortsAttributesobject-Defaults for ports not listed in portsAttributes
containerEnvobject-Env vars set on the container itself
remoteEnvobject-Env vars injected by the tool post-ENTRYPOINT
containerUserstringroot or last Dockerfile USERUser for all container operations
remoteUserstringsame as containerUserUser for lifecycle hooks and tool processes
updateRemoteUserUIDbooleantrueSync UID/GID to local user (Linux only)
userEnvProbeenumloginInteractiveShellShell type for probing env vars (none, interactiveShell, loginShell, loginInteractiveShell)
overrideCommandbooleantrue (image/Dockerfile), false (Compose)Replace default command with a sleep loop
shutdownActionenumstopContainer or stopComposeAction on close (none, stopContainer, stopCompose)
initbooleanfalseUse tini init process
privilegedbooleanfalseRun in privileged mode
capAddstring[][]Linux capabilities to add
securityOptstring[][]Security options
mounts(string|object)[]-Additional mounts (Docker --mount syntax)
customizationsobject-Tool-specific properties (namespaced by tool)
hostRequirementsobject-See Host Requirements
PropertyTypeDefaultDescription
imagestringrequired (image scenario)Container registry image
build.dockerfilestringrequired (Dockerfile scenario)Path to Dockerfile, relative to devcontainer.json
build.contextstring"."Docker build context directory
build.argsobject-Docker build arguments
build.optionsstring[][]Extra Docker build CLI flags
build.targetstring-Multi-stage build target
build.cacheFromstring|string[]-Cache source images
workspaceMountstring-Custom mount for source code
workspaceFolderstring-Default path opened inside the container
runArgsstring[][]Extra docker run CLI flags
appPortint|string|array[]Legacy, use forwardPorts instead
PropertyTypeDefaultDescription
dockerComposeFilestring|string[]requiredPath(s) to Compose file(s)
servicestringrequiredPrimary service to connect to
runServicesstring[]all servicesSubset of services to start. The primary service is always started regardless.
workspaceFolderstring"/"Default path opened inside the container
PropertyTypeRunsWhen
initializeCommandstring|string[]|objectOn hostInitialization (may run multiple times)
onCreateCommandstring|string[]|objectIn containerFirst creation only
updateContentCommandstring|string[]|objectIn containerAfter content is available (creation only)
postCreateCommandstring|string[]|objectIn containerAfter user setup (creation only, background by default)
postStartCommandstring|string[]|objectIn containerEvery container start
postAttachCommandstring|string[]|objectIn containerEvery tool attachment
waitForenum-Block until this stage completes. Default: updateContentCommand. Values: initializeCommand, onCreateCommand, updateContentCommand, postCreateCommand, postStartCommand

Command type details:

  • string: Executed via the default shell (/bin/sh).
  • string[]: Direct exec, no shell interpretation.
  • object: Keys are unique names, values are string or string[]. All entries run in parallel. Every entry must succeed for the stage to succeed.
PropertyTypeDescription
hostRequirements.cpusintegerMinimum CPU count
hostRequirements.memorystringMinimum RAM (suffix: tb, gb, mb, kb)
hostRequirements.storagestringMinimum storage (same suffixes)
hostRequirements.gpuboolean|string|objecttrue = required, "optional" = preferred, or {"cores": N, "memory": "Xgb"}

Properties inside portsAttributes entries:

PropertyTypeDefaultDescription
labelstring-Display name
protocolenum-http or https
onAutoForwardenumnotifynotify, openBrowser, openBrowserOnce, openPreview, silent, ignore
requireLocalPortbooleanfalseRequire the same port number locally
elevateIfNeededbooleanfalseAuto-elevate permissions for low ports

JSON Reference: Variables

VariableUsable InDescription
${localEnv:VAR}Any propertyHost env var. Supports default: ${localEnv:VAR:fallback}
${containerEnv:VAR}remoteEnv onlyContainer env var. Supports default: ${containerEnv:VAR:fallback}
${localWorkspaceFolder}Any propertyFull path to the opened local folder
${containerWorkspaceFolder}Any propertyFull path inside the container
${localWorkspaceFolderBasename}Any propertyLocal folder basename only
${containerWorkspaceFolderBasename}Any propertyContainer folder basename only
${devcontainerId}name, lifecycle hooks, mounts, env vars, user properties, customizationsStable unique ID across rebuilds

Substitution happens at the time the value is applied (runtime, not build time). ${containerEnv} is restricted to remoteEnv because container env vars only exist after the container is running.


Spec: Lifecycle

initializeCommand (host, may run multiple times)
|
v
Image build / pull (Features applied as Dockerfile layers)
|
v
Container creation (mounts, containerEnv, containerUser applied)
| (remoteUser/remoteEnv NOT applied yet)
v
onCreateCommand (in container, with remoteUser/remoteEnv)
|
v
updateContentCommand (in container)
|
v
postCreateCommand (in container, background by default)
|
v
[implementation-specific] (tool-specific init, e.g. extension install)
|
v
postStartCommand (in container)
|
v
postAttachCommand (in container)

The waitFor property controls at which point the tool reports the environment as ready and proceeds to implementation-specific steps. Default is updateContentCommand.

Restart containers
|
v
[implementation-specific steps]
|
v
postStartCommand
|
v
postAttachCommand

Remote env vars and user configuration apply during resume.

  • Commands within an object run in parallel. All must succeed.
  • Feature-provided lifecycle hooks run before user-defined hooks, in Feature installation order.
  • onCreateCommand, updateContentCommand, and postCreateCommand run only on first creation.
  • postStartCommand runs on every start (creation and resume).
  • postAttachCommand runs on every tool attachment.
  • Remote env vars and userEnvProbe results are available for all post-creation hooks.

Spec: Users

Two distinct user concepts:

  • Container User (containerUser for image/Dockerfile, user in Compose): Runs all container operations including the ENTRYPOINT.
  • Remote User (remoteUser): Runs lifecycle hooks and tool processes. Defaults to containerUser if not set. This separation allows different permissions for container operations vs. developer activity.
  • Linux only, default true.
  • When enabled and a remote/container user is specified, the tool updates the image user’s UID/GID to match the local user before container creation.
  • Prevents permission mismatches on bind mounts.
  • Implementations may skip this when not using bind mounts or when the container engine provides automatic UID translation.

Spec: Environment Variables

Two classes:

TypePropertySet WhenAvailable
ContainercontainerEnvAt container build/create timeEntire container lifecycle
RemoteremoteEnvPost-ENTRYPOINT by the toolLifecycle hooks and tool processes

Remote env vars support ${containerEnv:VAR} substitution since the container is already running when they are applied.

Tools probe the user’s environment using the configured shell type and merge the resulting variables with remoteEnv for injected processes. This emulates the behavior developers expect from their profile/rc files.


Spec: Workspace

  • workspaceMount (image/Dockerfile only): Defines the source mount for the workspace. The default is a bind mount of the local folder into /workspaces/<folder-name>.
  • workspaceFolder: The default working directory inside the container.
  • Both should reference the repository root (where .git lives) for proper source control.
  • For monorepos, workspaceFolder can point to a subfolder while workspaceMount targets the repo root.

Spec: Image Metadata

Configuration can be baked into images via the devcontainer.metadata label. The label value is a JSON string containing either:

  • An array of metadata objects (one per Feature, plus one for the devcontainer.json config).
  • A single top-level object.

The array format is preferred because subsequent builds can simply append entries.

These properties can appear in image metadata: forwardPorts, portsAttributes, otherPortsAttributes, containerEnv, remoteEnv, remoteUser, containerUser, updateRemoteUserUID, userEnvProbe, overrideCommand, shutdownAction, init, privileged, capAdd, securityOpt, mounts, customizations, hostRequirements, all lifecycle hooks (onCreateCommand through postAttachCommand), and waitFor.

When merging image metadata with devcontainer.json, the local file is considered last (highest priority where order matters).

StrategyProperties
Boolean OR (true if any is true)init, privileged
Union without duplicatescapAdd, securityOpt
Union, last wins on conflictsforwardPorts
Collected list (append in order)All lifecycle hooks, entrypoint
Collected list, last wins on conflictsmounts
Last value wins (scalar)waitFor, containerUser, remoteUser, userEnvProbe, shutdownAction, updateRemoteUserUID, overrideCommand
Per-variable, last winsremoteEnv, containerEnv
Per-port, last winsportsAttributes
Last value winsotherPortsAttributes
Maximum value winshostRequirements (cpus, memory, storage, gpu)
Tool-specific logiccustomizations

Features Specification | Features Overview

Features are modular, self-contained units that add tools, runtimes, and capabilities to dev containers without writing complex Dockerfiles.

A Feature is a directory containing:

  • devcontainer-feature.json (metadata, required)
  • install.sh (entrypoint script, required)
  • Additional supporting files (optional)
PropertyTypeDescription
idstringUnique within the repository, must match directory name
versionstringSemver (e.g., 1.0.0)
namestringHuman-friendly display name
PropertyTypeDescription
descriptionstringFeature overview
documentationURLstringDocs link
licenseURLstringLicense link
keywordsstring[]Search terms
deprecatedbooleanDeprecation flag
legacyIdsstring[]Previous IDs (for renaming)
PropertyTypeDescription
optionsobjectUser-configurable parameters (see Feature Options)
containerEnvobjectEnv vars added as Dockerfile ENV before install.sh
privilegedbooleanRequires privileged mode
initbooleanRequires tini init
capAddstring[]Linux capabilities
securityOptstring[]Security options
entrypointstringCustom startup script path
mountsobjectAdditional mounts
PropertyTypeDescription
dependsOnobjectHard dependencies (recursive). Feature fails if unresolvable. Values include options and version.
installsAfterstring[]Soft ordering (non-recursive). Only affects ordering of already-queued Features. Ignored if the referenced Feature is not being installed.

Features can declare their own lifecycle hooks: onCreateCommand, updateContentCommand, postCreateCommand, postStartCommand, postAttachCommand. Same types as the main config.

PropertyTypeDescription
customizationsobjectTool-specific config. Arrays merge as union, objects merge values.
"options": {
"optionId": {
"type": "string|boolean",
"description": "What this option controls",
"proposals": ["val1", "val2"],
"enum": ["strict1", "strict2"],
"default": "val1"
}
}
  • proposals: Suggested values, free-form input allowed.
  • enum: Strict allowed values only.
  • default: Fallback when the user provides nothing.

Option IDs are converted to environment variables for install.sh:

replace non-word chars with _ -> strip leading digits/underscores -> UPPERCASE

Written to devcontainer-features.env and sourced before the script runs.

VariableDescription
_REMOTE_USERConfigured remote user
_CONTAINER_USERContainer user
_REMOTE_USER_HOMERemote user’s home directory
_CONTAINER_USER_HOMEContainer user’s home directory

Features always execute as root during image build.

In devcontainer.json:

"features": {
"ghcr.io/devcontainers/features/go:1": {},
"ghcr.io/user/repo/node:18": {"version": "18"},
"https://example.com/feature.tgz": {},
"./local-feature": {"optionA": "value"}
}

Three source types:

  1. OCI Registry: <registry>/<namespace>/<id>[:<semver>]
  2. HTTPS Tarball: Direct URL to .tgz
  3. Local Directory: ./path relative to devcontainer.json

Features Spec: Installation Order

Step 1 - Build dependency graph:

  • Traverse dependsOn recursively and installsAfter non-recursively.
  • Deduplicate using Feature Equality.

Step 2 - Assign round priority:

  • Default: 0.
  • overrideFeatureInstallOrder assigns priorities: array_length - index (first item = highest priority).

Step 3 - Round-based sorting:

  1. Identify Features whose dependencies are all satisfied.
  2. From those, commit only Features with the maximum roundPriority.
  3. Return lower-priority Features to the worklist for the next round.
  4. Within a round, stable sort lexicographically by: resource name, version tag, options count, option keys/values, canonical name.
  5. If a round makes no progress, fail (circular dependency).
SourceEqual When
OCI RegistryManifest digests match AND options are identical
HTTPS TarballContent hashes match AND options are identical
Local DirectoryAlways unique
  • Each Feature’s install.sh runs as its own Dockerfile layer (for caching).
  • containerEnv values become ENV instructions before install.sh runs.
  • install.sh is invoked as: chmod +x install.sh && ./install.sh
  • Default shell: /bin/sh.
  • privileged and init: required if any Feature needs them (boolean OR across all).
  • capAdd and securityOpt: union across all Features.
  • Feature hooks execute before user-defined hooks.
  • Hooks run in Feature installation order.
  • Object-syntax commands within a single Feature run in parallel.
  • All hooks run from the project workspace folder.

Features Distribution

Distributed as devcontainer-feature-<id>.tgz tarballs containing the Feature directory.

ArtifactMedia Type
Configapplication/vnd.devcontainers
Feature layerapplication/vnd.devcontainers.layer.v1+tar
Collection metadataapplication/vnd.devcontainers.collection.layer.v1+json

<registry>/<namespace>/<id>[:version]

Example: ghcr.io/devcontainers/features/go:1.2.3

Multiple tags are pushed for each release: 1, 1.2, 1.2.3, and latest.

An auto-generated devcontainer-collection.json aggregates all Feature metadata in a namespace. Published at <registry>/<namespace>:latest.

Published manifests include a dev.containers.metadata annotation containing the escaped JSON from devcontainer-feature.json.

  1. Docker config: $HOME/.docker/config.json
  2. Environment variable: DEVCONTAINERS_OCI_AUTH (format: service|user|token)

Templates Specification | Templates Distribution | Templates Overview

Templates are pre-configured dev environment blueprints. They are not directly relevant to crib’s runtime behavior but are useful context for understanding the ecosystem.

template/
devcontainer-template.json
.devcontainer.json (or .devcontainer/devcontainer.json)
(supporting files)

Required: id, version, name, description.

Optional: documentationURL, licenseURL, platforms, publisher, keywords, options, optionalPaths.

Template options use ${templateOption:optionId} syntax. The tool replaces these placeholders with user-selected values when applying a template.

Same OCI pattern as Features (tarballs with custom media types, semver tagging). The namespace for Templates must be different from the namespace for Features.


Spec: devcontainerId

Computed from container labels:

  1. Serialize labels as sorted JSON (keys alphabetically, no whitespace).
  2. Compute SHA-256 of the UTF-8 encoded string.
  3. Base-32 encode the hash.
  4. Left-pad to 52 characters with 0.

The ID is deterministic across rebuilds and unique per Docker host.


JSON Schema

  • Conforms to JSON Schema Draft 7.
  • Permits comments (JSONC), disallows trailing commas.
  • Base schema: devContainer.base.schema.json.
  • Main schema: devContainer.schema.json (references base + tool-specific schemas).
  • Source: devcontainers/spec on GitHub.

ResourceURL
Main Specificationhttps://containers.dev/implementors/spec/
JSON Referencehttps://containers.dev/implementors/json_reference/
JSON Schemahttps://containers.dev/implementors/json_schema/
Features Overviewhttps://containers.dev/features/
Features Spec (Implementors)https://containers.dev/implementors/features/
Features Distributionhttps://containers.dev/implementors/features-distribution/
Templates Overviewhttps://containers.dev/templates/
Templates Spec (Implementors)https://containers.dev/implementors/templates/
Templates Distributionhttps://containers.dev/implementors/templates-distribution/
Supporting Toolshttps://containers.dev/supporting
GitHub Repositoryhttps://github.com/devcontainers/spec