This CLI is designed to replace the official CLI in supported Linux and Docker
workflows without changing devcontainer.json.
Confirm that the workflow uses:
- Linux on amd64 or arm64.
- Docker Engine API v1.44 or newer.
- Docker Compose v2, if it uses a Compose configuration.
- OCI-based Dev Container Features rather than the legacy GitHub Releases fallback.
Windows, macOS, Podman, and Compose v1 are not migration targets.
Install a pinned release and replace invocations such as:
devcontainer up --workspace-folder .The executable name and the main command and flag surface match the official CLI; the migration normally changes how the executable is installed, not the command line or project configuration.
If existing automation invokes a JavaScript entry point directly, replace:
node /path/to/devcontainer.js up --workspace-folder .with:
/usr/local/bin/devcontainer up --workspace-folder .- Run
devcontainer checkon the target host. - Run
read-configurationagainst representative projects and compare the output consumed by automation. - Test
buildandupagainst image-, Dockerfile-, and Compose-based projects that the organization uses. - Test private base images, push targets, cache exporters, proxies, and custom CA certificates where applicable.
- Pin the CLI version before rolling it out to every runner or host.
The repository validates the core command families against a pinned official CLI, but an organization's registry, network, and host policies remain part of its own acceptance test.
Read Divergences, decisions, and accepted limitations before a production rollout. Differences most likely to affect automation include:
--override-configdeep-merges a partial override instead of replacing the base configuration wholesale.--terminal-log-fileand--log-filecapture the same combined, non-ANSI stream.- Legacy Feature fallback through GitHub Releases is not implemented.
- Platform and runtime support is intentionally limited to Linux and Docker.
The Go-only commands and flags are additive. Existing workflows do not need to use them, but Go-only features describes the operational benefits available after migration.
Releases that close a divergence from the official CLI can change behavior that automation relied on. These changes match the official CLI, so a workflow that also runs the official CLI is unaffected:
features package,features publish, andtemplates publishrequire the<target>folder argument. Earlier releases defaulted it to the current folder; without it the command now exits 1 withNot enough non-option arguments: got 0, need at least 1. Pass the folder explicitly, for exampledevcontainer features package . --output-folder output.- A configuration that references a legacy GitHub Releases Feature fails, in
read-configurationtoo, instead of silently leaving the Feature out. upwith--id-labeland no workspace folder or configuration fails withNo dev container config and no workspace found.instead of attaching to the matching container.- Features are staged under
<tmp>/devcontainercli-<user>/container-features/, one folder per build, and that folder is kept as the official CLI keeps it. - When the workspace is a Git worktree and the configuration sets both
workspaceMountandworkspaceFolder,upno longer mounts the main repository's Git common directory, andread-configurationno longer reportsadditionalMountString, as in the official CLI v0.89.0. With onlyworkspaceMountset, the common directory is mounted under/workspaces/<main repository>/.gitrather than under the custom mount's target. Earlier releases carried an unreleased upstream change (#1261) instead. A configuration that needs Git inside the container this way should add the common directory tomountsitself. - A
${containerEnv}reference without a variable name failsup,set-up,run-user-commands,execandread-configurationwith exit 1 and'${containerEnv}' in <file> can not be resolved because no environment variable name is given., as the official CLI does. Earlier releases reported success for most of them. Name the variable, as in${containerEnv:PATH}. execreads the configuration--configor--override-confignames, also with--container-idand no--workspace-folder, and exits 1 when it is missing (Dev container config (<path>) not found.) or cannot be loaded. Earlier releases ignored those flags without a workspace folder and ran the command despite a configuration error.read-configurationlooks up the workspace's container (or the one--id-labelnames) as the official CLI does, and exits 1 with nothing on stdout when that lookup fails, including when no Docker engine is reachable. Earlier releases ignored the failure and printed the configuration. Run it where the Docker engine answers.- A local Feature (
./x) must live inside the workspace's.devcontainer/folder, as in the official CLI.build,up,read-configurationandupgradenow exit 1 withERR: Feature './x' could not be processed. ...for../x, an absolute path, or./xnext to a.devcontainer.jsonat the workspace root, where earlier releases resolved the folder next to the configuration. Move the Feature's folder under.devcontainer/. buildon a Docker Compose configuration without--image-namereports"imageName"as the image's name, a string, as the official CLI does. Earlier releases wrapped it in an array. With--image-name, and for image and Dockerfile configurations, it stays an array.- The text log on stderr follows the official CLI's two formats. With stdout
redirected or piped, each line starts with its UTC time
(
[2026-01-02T03:04:05.678Z] text) and Stop lines appear at--log-level debugandtrace. With stdout on a terminal, lines keep the elapsed[N ms]prefix, are colored and end in\r\n, and Stop lines appear only attrace. Earlier releases always wrote[N ms] textwithout color. A script that parses the log should read the timestamped form, or use--log-format json. buildandupwrite the Feature lockfile by default whenever the configuration uses Features, as the official CLI does:devcontainer-lock.jsonnext todevcontainer.json(.devcontainer-lock.jsonnext to.devcontainer.json) is created when missing and rewritten when the resolved Features change. Earlier releases wrote it only with--experimental-lockfileor--frozen-lockfile, so the file now appears in projects that never had one. Commit it to pin the Features' versions and digests, or pass--no-lockfileto neither read nor write it.--experimental-lockfileis accepted but does nothing more and printsWarning: --experimental-lockfile is deprecated. Lockfiles are now enabled by default.on stderr;--experimental-frozen-lockfileprints a warning to use--frozen-lockfileinstead.upgradealways rewrites the lockfile in its canonical form, even when the existing one differs only in formatting.upgrade --log-levelaccepts onlyerror,info,debug, andtrace. Any other value exits 1 withInvalid value "<value>" for --log-levelinstead of running at the default level.upgrade --confignaming a file that does not exist reportsDev container config (<path>) not found.instead of the operating system's read error. The exit code stays 1.upgradeno longer logsLockfile updated.on stderr after writing the lockfile, and starts its log with the version header once the configuration loads, as the official CLI does. Check the exit code to tell whether the lockfile was written.upgradeon a configuration without Features exits 1 withFailed to update lockfileand writes no lockfile, with or without--dry-run. Earlier releases wrote or printed{"features":{}}and exited 0. Skipupgradefor configurations that use no Features.upgraderesolves Features the waybuilddoes. A Feature whose manifest or content cannot be fetched fails the command (exit 1,ERR: Feature '<id>' could not be processed. ...) instead of being left out of the lockfile. The lockfile also lists the Features that other Features depend on, with each entry'sdependsOn, so it matches the onebuildand the official CLI write. Make every Feature reachable, with registry credentials where needed, before runningupgrade.upgrade --feature <id> --target-version <version>rewrites the Feature's key indevcontainer.jsonto<id without version>:<version>, even with--dry-run, before generating the lockfile. Earlier releases validated the two flags but otherwise ignored them. Run it on a clean working tree, or without the flags, when the configuration must stay unchanged.outdated --output-format jsonindents its result only when stdout is a terminal. Redirected or piped, as in a script, it is one unindented line ({"features":{...}}) followed by a newline; earlier releases always indented it with two spaces. A script that parses the JSON is unaffected; one that matches lines or greps the indented text should parse the JSON instead (withjq, say).outdatedon a configuration without Features prints its table header alone (Feature Current Wanted Latest) instead ofNo features configured..- Only a registry whose host is
localhostis reached over plain HTTP. Earlier releases also used plain HTTP for127.0.0.1, which is now addressed over HTTPS like any other host. Refer to a local plain-HTTP registry aslocalhost:<port>.
Keep the previous official CLI version available during rollout. Because project configuration is unchanged, rollback consists of restoring the executable used by automation. Do not begin relying on Go-only flags until the migration has been accepted; the official CLI will not recognize them.