devcontainers/cli: Run a devcontainer.json Without an Editor
A reference implementation for the specification that can create and configure a dev container from a devcontainer.json.
At a glance
- What is it?
- The reference implementation of the Dev Container specification builds and starts a container straight from devcontainer.json, so the same environment works in CI and on a laptop. Here is how it installs, what it does well, and where it stops.
- Who is it for?
- Adopt devcontainers/cli if your team already keeps a devcontainer.json and you need that environment outside an editor, in CI or on a workstation without VS Code. Do not adopt it as a container lifecycle manager: the README's status list shows stop and down are still unchecked, so cleanup stays with Docker.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What devcontainers/cli solves, and who ends up using it
A devcontainer.json describes an editor-independent development environment: an image or Dockerfile, features, lifecycle commands, a remote user, environment variables. Editors read that file. The CLI reads the same file and produces the container itself, which is the part that matters when there is no editor in the loop.
The people who feel this most are the ones running the same setup in two places. A developer opens the workspace in VS Code with the Dev Containers extension. A CI job needs the identical toolchain, the same postCreateCommand, the same remoteUser. Without the CLI, that second environment is a hand-written docker run with the settings copied by hand, and the two drift. The README frames the project as the reference implementation for the specification, not as a wrapper around one editor, and the command list backs that up: build, up, run-user-commands, read-configuration, exec.
It is also useful for pre-building. The README notes that devcontainer build enables building and pre-building images, which is how you move the slow image layer out of the critical path of every container start.
It is not aimed at people who just want a container. If your project has a working Dockerfile and no devcontainer.json, the CLI adds a specification layer you are not using.
How the CLI turns devcontainer.json into a running container
The flow is visible in the README's own transcript of devcontainer up. The CLI first resolves the configuration for the workspace folder, then runs docker build against the Dockerfile in the .devcontainer directory, tagging the result with a name derived from the workspace path. In the example the tag is vsc-vscode-remote-try-rust-89420ad7399ba74f55921e49cc3ecfd2, and the build is passed --build-arg VARIANT=bullseye.
It then runs docker run with the workspace bind-mounted at /workspaces/<name>, a label devcontainer.local_folder pointing at the host path, --cap-add=SYS_PTRACE and --security-opt seccomp=unconfined, and an entrypoint of /bin/sh that echoes Container started. When that succeeds, the CLI prints a JSON result on stdout:
{"outcome":"success","containerId":"f0a055ff056c1c1bb99cc09930efbf3a0437c54d9b4644695aa23c1d57b4bd11","remoteUser":"vscode","remoteWorkspaceFolder":"/workspaces/vscode-remote-try-rust"}
That JSON is the interface. containerId, remoteUser and remoteWorkspaceFolder are what a script needs to do anything next, and devcontainer exec consumes the same workspace-folder argument to run a command inside with userEnvProbe, remoteUser and remoteEnv applied. read-configuration exposes the resolved configuration on its own, which is the piece to reach for when you want to inspect what the CLI decided before it builds anything.
A detail worth noticing: the CLI writes .devcontainer-lock.json by default on build and up, pinning feature versions. --no-lockfile opts out, --frozen-lockfile enforces an existing one. That default is a deliberate choice in favour of reproducibility over freshness, and it means the first run of a new project changes the repository unless you opt out.
Install devcontainer CLI: install script, npm, or from source
The README gives three routes. The install script downloads a bundled Node.js runtime, so no pre-installed Node.js is needed, and it works on Linux and macOS on x64 and arm64:
curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | shAfter it finishes, the install location has to be on PATH:
export PATH="$HOME/.devcontainers/bin:$PATH"The script also takes --version, --prefix, --update and --uninstall. The npm route installs the package globally but has a build requirement the README states plainly: Python and C/C++ have to be present to compile one of the dependencies.
npm install -g @devcontainers/cliEither way, verify with the help text. The README shows the command list it prints, including up, build, run-user-commands, read-configuration, features, templates and exec:
devcontainer --helpA first real use, taken from the README: clone the Rust sample and start its container. The CLI pulls the image from a registry and starts it, then prints the JSON result described above.
git clone https://github.com/microsoft/vscode-remote-try-rust
devcontainer up --workspace-folder <path-to-vscode-remote-try-rust>Then run something inside it. The README uses cargo run on the same sample, which compiles and runs the Rust binary in the container:
devcontainer exec --workspace-folder <path-to-vscode-remote-try-rust> cargo runTwo things the README does not spell out. It does not document a Windows install path for the script, and the example-usage folder is described only as simple shell scripts, so treat it as a starting point rather than a supported interface.
The lifecycle gap: no stop, no down, and what that costs you
The README's status checklist is unusually honest. devcontainer stop and devcontainer down are both unchecked. The CLI creates containers and executes in them, but it does not own their removal. In practice that means a CI job that runs devcontainer up and then finishes leaves the container behind unless the script calls Docker itself, and any cleanup logic you write is Docker-specific rather than CLI-specific.
This is a real limitation, not a documentation gap. It shapes how you write the surrounding script: capture containerId from the JSON output and hand it to docker rm, or run the CLI inside a job whose runner is discarded afterwards.
The second constraint is the lockfile default. Because build and up generate .devcontainer-lock.json unless you pass --no-lockfile, running the CLI against a repository that has never used it will produce a file change. In a CI job that checks for a clean working tree, that fails the build the first time. --frozen-lockfile is the opposite posture: it enforces the existing lockfile and will refuse to move. Neither flag is wrong; picking one by accident is.
The third is the platform surface. The install script covers Linux and macOS. The npm package needs a compiler toolchain. Windows users appear in search data often enough that it is worth saying directly: the README does not describe a Windows install route for the standalone script, so the npm package or building from source is the path that is documented.
devcontainers/cli against a plain docker run script
The obvious alternative is a shell script that calls docker build and docker run. It has no dependencies beyond Docker, no Node runtime, no lockfile, and it does exactly what it says. For a single project with a stable Dockerfile, that script is shorter than the CLI's install step and easier to debug, because every line is yours.
The difference in approach is where the configuration lives. A docker run script encodes the environment in the script: the mount path, the user, the capabilities, the environment variables. The CLI encodes it in devcontainer.json, which editors also read, and then derives the docker command. That is the whole trade. You accept a specification, a Node runtime and a lockfile in exchange for one file that editors and CI both understand.
If nothing else in your toolchain reads devcontainer.json, the CLI is the more expensive option. If the same developers already open the workspace in an editor that does, the script is the thing that will drift.
A second alternative is to keep using the editor's own container management and never touch the CLI. That works until the environment has to run somewhere an editor is not installed, which is the case the CLI exists for.
Maintenance, licence and what upgrading actually involves
The repository is not archived. The last push was on 2026-09-17, and package.json carries version 0.89.0 with an engines field of node >=20.0.0, so the npm route requires Node 20 or newer while the install script sidesteps that by bundling its own runtime. The project is published by Microsoft Corporation under the MIT licence, with a ThirdPartyNotices.txt at the repository root for the bundled dependencies. MIT is permissive; it does not impose obligations on your own source, and this is a description of the licence file, not legal advice.
The upgrade cost is mostly the lockfile. Because build and up write .devcontainer-lock.json by default, upgrading the CLI can change which feature versions resolve, and the README documents --no-lockfile and --frozen-lockfile as the two controls but does not document rollback for a lockfile you have already committed. If reproducibility matters to you, commit the lockfile and use --frozen-lockfile in CI so an upgrade cannot silently move feature versions underneath a build.
Version pinning is available on the install script through --version, which the README demonstrates with 0.82.0. Pinning that version in CI is the cheapest way to keep the CLI's behaviour stable while you decide when to move.
The README notes the CLI is in active development and lists which commands are complete. The unchecked entries are the honest signal about what is still moving.
Editorial conclusion
Adopt devcontainers/cli if your team already keeps a devcontainer.json and you need that environment outside an editor, in CI or on a workstation without VS Code. Do not adopt it as a container lifecycle manager: the README's status list shows stop and down are still unchecked, so cleanup stays with Docker. Before rolling it out, pin the version and confirm whether the default lockfile behaviour fits your build, because the README does not document rollback for a lockfile that pins a feature you later need to move.
Frequently asked questions
What is the devcontainer CLI?
It is the reference implementation of the Dev Container specification: a command line tool that takes a devcontainer.json and creates and configures a dev container from it. The README lists up, build, run-user-commands, read-configuration, exec, outdated, upgrade, features and templates as complete.
What is a devcontainer used for?
The README describes a development container as a full-featured development environment that can run an application, separate the tools, libraries and runtimes needed for a codebase, and aid continuous integration and testing. Dev containers can run locally or remotely, in a private or public cloud.
How do I get a command line inside the container devcontainers/cli started?
Use devcontainer exec with the workspace folder, which the README shows running cargo run in a started container. The README states that exec applies userEnvProbe, remoteUser, remoteEnv and other properties to the command it runs.
How do I run a dev container in VS Code?
The README does not describe the VS Code workflow itself; it covers the CLI, which reads the same devcontainer.json. The CLI's devcontainer up command starts the container from a workspace folder without an editor.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/devcontainers-cli)