Skip to content

Latest commit

 

History

History
239 lines (193 loc) · 12.4 KB

README.md

File metadata and controls

239 lines (193 loc) · 12.4 KB

Dev Container CLI (devcontainer-cli)

Installs @devcontainers/cli as /usr/local/bin/devcontainer, backed by a private, sha256-verified Node.js under /opt/devcontainer-cli that is NOT added to PATH - so devcontainer up/build/exec/features test work inside a container without a system-wide Node.js. Pair with docker-in-docker (needed for features test and nested up on Windows/macOS hosts) or docker-outside-of-docker (Linux hosts, same paths).

Example Usage

"features": {
    "ghcr.io/nabil-becker/devcontainer-features/devcontainer-cli:1": {}
}

Options

Options Id Description Type Default Value
installTaskfile Also install this Feature's Taskfile into /usr/local/share/go-task/includes.d so its tasks appear under the : namespace of a root Taskfile that includes /usr/local/share/go-task/features.yml (see the go-task Feature). boolean true
version @devcontainers/cli npm version to install (e.g. '0.89.0'), or 'latest'. string latest
nodeVersion Node.js release used privately by the CLI. Downloaded from nodejs.org and verified against its SHASUMS256.txt. string 24.21.0

What it installs

The @devcontainers/cli npm package, run on a private Node.js build, so devcontainer up, build, exec and features test work inside a container with no system-wide Node.js. install.sh runs devcontainer-cli.sh install, which:

  1. Downloads node-v<nodeVersion>-linux-<arch>.tar.gz and SHASUMS256.txt from https://nodejs.org/dist/v<nodeVersion>/, checks the tarball's SHA-256 against that file, and unpacks it to /opt/devcontainer-cli/node.
  2. Runs npm install -g --prefix /opt/devcontainer-cli @devcontainers/cli (with @<version> unless version is latest), using that Node.
  3. Writes a two-line wrapper at /usr/local/bin/devcontainer that runs the CLI with the private node, then runs devcontainer --version so the version is in the build log.

node and npm are deliberately not on PATH afterwards. Only the devcontainer wrapper knows about them, so the Feature cannot collide with the Node.js toolchain your project pins. It installs no Docker engine (see Composition). The directory /opt/devcontainer-cli is removed and rebuilt on every install, and npm's cache goes to a temporary directory that is deleted, so it does not stay in the image layer.

What is verified, and what is not:

  • The private Node.js is verified: a missing line in SHASUMS256.txt or a mismatch fails the build.
  • The CLI itself is NOT checked against a checksum. It comes from npm install -g and is protected only by npm's own integrity metadata for the package and its dependencies. This Feature adds no checksum, no signature check and no lockfile for it. The root README names this as the one exception to "verifies what it downloads".

curl, ca-certificates and tar are installed with apt-get only when missing. The install must run as root.

Options

  • version (default latest): the @devcontainers/cli npm version, for example 0.89.0; no leading v. latest is whatever npm calls latest at image build time (not the releases/latest redirect the other installers use), so two builds a month apart can differ. Pin it when you want devcontainer features test or up to behave the same in CI and on every laptop, or when a new CLI release breaks something.
  • nodeVersion (default 24.21.0): the Node.js release the CLI runs on, as x.y.z without v. Change it only when a pinned CLI version needs a newer or older Node than the default (the CLI states its Node range), or when nodejs.org no longer serves the default. It does not change the Node.js your project uses; that one is separate.
  • installTaskfile (default true): set it to false when you do not use the go-task registry and do not want /usr/local/share/go-task/includes.d/devcontainer-cli.yml in the image.

There is no run-time override; both versions are fixed at build.

State

No state: installs binaries only. No volume, no containerEnv, no post-create hook.

Minimal devcontainer.json

"features": {
  "ghcr.io/devcontainers/features/docker-in-docker:2": {},
  "ghcr.io/nabil-becker/devcontainer-features/devcontainer-cli:1": {
    "version": "0.89.0"
  }
}

Drop version to follow npm's latest at each image build.

Verify it works

devcontainer --version     # prints the CLI version, for example 0.89.0
task devcontainer-cli:version

The build log ends the install with the same devcontainer --version line. command -v node finds nothing, on purpose. There is no post-create line: nothing runs after the build.

Composition

  • installsAfter orders it behind this collection's go-task Feature (so the Taskfile registry exists and is refreshed), the official common-utils, and the two official Docker Features (docker-in-docker, docker-outside-of-docker) so a Docker engine is in place before it when you use one.
  • It needs a Docker daemon for anything that builds or runs containers (up, build, features test); read-configuration and --version do not. It does not install one. Pair it with one of the official Features:
Host Use Why
Windows / macOS (Docker Desktop) docker-in-docker features test and nested devcontainer up bind-mount paths from inside this container; only a daemon that shares its filesystem can resolve them.
Linux (same paths on host and container) docker-outside-of-docker Lighter, shares the host's image cache; works while the workspace is mounted at the same absolute path.
  • It augments no official Feature and replaces none. It is independent of kube-state, git and the other Features here.

Tasks

With installTaskfile on, and a root Taskfile that includes the go-task registry, these appear under devcontainer-cli::

  • devcontainer-cli:version: show the installed CLI version.
  • devcontainer-cli:config: print the resolved and merged configuration of WORKSPACE (default .) with devcontainer read-configuration.
  • devcontainer-cli:features:test: run devcontainer features test for the Features repository at PROJECT (default .); extra flags go after -- (for example -f <id>). Needs a Docker daemon.

These are for using the CLI inside the container. To drive a devcontainer from a host, use the devcontainer:* tasks (next section).

Limits and troubleshooting

  • Linux only in the container. The architectures are x86_64/amd64, aarch64/arm64 and armv7l; any other uname -m fails with unsupported architecture.
  • ... not found in SHASUMS256.txt or SHA-256 mismatch: the nodeVersion does not exist for your architecture, or the download was corrupted. Fix the version or retry. The Node.js is never unpacked unchecked.
  • A failing npm install (no network, or a version that npm does not have) fails the build. Check the version with npm view @devcontainers/cli versions on a machine that has npm.
  • features test and nested up fail with a Docker error when no daemon is reachable; add docker-in-docker (or docker-outside-of-docker on Linux). With docker-outside-of-docker, a workspace mounted at a different path on the host than inside the container cannot be resolved.
  • The CLI's own dependencies are resolved by npm at build time and are not locked here, so supply-chain trust in them is npm's, not this Feature's.

Host-side counterpart

The same devcontainer-cli.sh that installs this Feature also has init and run modes for a Linux/macOS host, where it caches a portable Node.js under ~/.cache/devcontainer-features (or just uses a devcontainer already on PATH when the version is latest). Together with the PowerShell flavour for Windows it backs the task devcontainer:* tasks that bootstrap/bootstrap.ps1 / bootstrap.sh vendor into a repo; nothing is installed globally on any host. Its host settings are DEVCONTAINER_DOCKER_PATH, DEVCONTAINER_NODE_VERSION and DEVCONTAINER_CLI_VERSION.

task devcontainer:up (alias start) and task devcontainer:stop (alias down) are idempotent, so other tasks can require them. up starts a stopped container and leaves a running one alone. exec, shell, pwsh and claude run up first; rebuild runs stop first (shutdown check included; FORCE=1 and STOP_CHECK pass through) and then recreates the container; a stop that fails (the check refuses, or any error) prints one line and the rebuild continues. build and config touch no container.

One build at a time. up (so rebuild too) and build hold a host-wide lock while they run: the named mutex Global\devcontainer-features-build on Windows, an flock on ~/.cache/devcontainer-features/build.lock on Linux/macOS (none on a host without flock, such as stock macOS). Why: when the engine has no BuildKit build context (wslc), @devcontainers/cli tags the Feature content image with the fixed name dev_container_feature_content_temp for every repository, so two builds at once overwrite each other's image and the first fails with "/tmp/build-features/<feature>_N": not found. A second build prints waiting for another devcontainer build to finish... and then runs; the lock is released when the build ends or its process dies (an abandoned lock counts as free). It applies on every engine, harmless with Docker Desktop; exec, run-user-commands, set-up and the other subcommands take no lock, and devcontainer features test takes it only with DEVCONTAINER_BUILD_LOCK_FEATURES_TEST=true (default false, so a long test run does not hold up every rebuild). DEVCONTAINER_BUILD_LOCK=0 turns the lock off.

stop is the one host task that is not a CLI subcommand (the CLI has none), and it is one host-script command, the same in run stop and Stop-Devcontainer (running? check? stop?), not nested tasks. The workspace's container is found by the devcontainer.local_folder label that devcontainer up sets, through the configured engine (DEVCONTAINER_DOCKER_PATH). The engine's label filter is case-sensitive and the CLI writes a lowercase drive letter (c:\...) where the task has C:\..., so the lookup tries the folder as given, then with the drive letter lowercased, then uppercased (only for an X:\ path). With no running container stop prints "already stopped" and succeeds, without running the check and without starting anything. Otherwise it runs the STOP_CHECK command inside the container (task var, environment variable or .devcontainer/.env; default devhost-shutdown-check of the devhost Feature, ADR 0015) through the same exec path, without up, and when the check fails it prints "Not stopping: ..." and exits with the check's code, before stopping anything; FORCE=1 skips the check. The engine's ps --filter label=... and stop are assumed to behave as Docker's; this is not tested against wslc or Podman. Stopping the container any other way is not guarded.

run running (and Get-RunningDevcontainerId) is the same lookup as a silent test: exit 0 when a container runs, 1 when none does, 2 when the engine cannot be asked. Task reports a failed command as 201 itself; task --exit-code devcontainer:running passes the script's 0/1/2 through.

A failing exec (what exec, shell, pwsh and claude run) asks the same lookup which of three things failed and says so, instead of one guess: the container runs, so the command failed (The command exited with code <n> inside the devcontainer: <command>, exit with that code); none runs (No running devcontainer for <folder>. Start it with: task dc:start, exit 1); or the engine could not be reached (its own error plus the container engine (<path>) could not be reached, exit 2). Other subcommands keep their generic failure message.

Why this one exists

The Dev Container CLI is published by the Dev Containers project as an npm package, and its documented install is npm install -g @devcontainers/cli, which needs a Node.js on PATH. This repository knows of no published Dev Container Feature that installs it, so there is none to compare. This Feature exists so that a container can run the CLI without a system-wide Node.js, and so the same script serves the container and the host-side tasks.


Note: This file was auto-generated from the devcontainer-feature.json. Add additional notes to a NOTES.md.