Reusable Dev Container Features, a host-side Taskfile for driving the devcontainer CLI from Windows without installing Node.js, and a one-line bootstrap that wires both into any repo.
| Feature | What it installs |
|---|---|
go-task |
Task (task), sha256-verified from GitHub releases, with bash/zsh completions. |
devcontainer-cli |
@devcontainers/cli as devcontainer, on a private Node.js under /opt/devcontainer-cli that is never put on PATH. |
claude-code |
Claude Code as its native release binary (no Node.js), auto-updater off, version pinnable. |
coder-cli |
Coder CLI from GitHub releases, verified against the release checksums. |
openbao |
OpenBao CLI (bao), verified against the release checksums. |
velero |
Velero CLI, verified against the release CHECKSUM. |
kustomize |
Standalone kustomize, verified against the release checksums. |
kubeconform |
kubeconform manifest validator, verified against the release CHECKSUMS. |
argocd |
Argo CD CLI, verified against the release cli_checksums.txt. |
dsc |
Microsoft DSC v3 in /opt/dsc, verified against the GitHub-published asset digest (or a pinned sha256). |
Every Feature takes a version option (latest or a pinned release) and
verifies what it downloads before installing it.
Each Feature also ships a Taskfile for its tool (dsc:get, velero:backups,
kustomize:build:all, argocd:sync, ...). With the default
installTaskfile: true it is registered at build time under
/usr/local/share/go-task/includes.d/, and the go-task Feature keeps
/usr/local/share/go-task/features.yml including all of them. One include in
the root Taskfile, never edited again:
includes:
features:
taskfile: /usr/local/share/go-task/features.yml
optional: true
flatten: trueAdd a Feature and rebuild: its tasks appear. Remove it: they disappear.
bootstrap/bootstrap.ps1 / bootstrap.sh write that include into the starter
Taskfile.yml.
"features": {
"ghcr.io/nabil-becker/devcontainer-features/go-task:1": {},
"ghcr.io/nabil-becker/devcontainer-features/devcontainer-cli:1": {},
"ghcr.io/nabil-becker/devcontainer-features/claude-code:1": {},
"ghcr.io/nabil-becker/devcontainer-features/kustomize:1": { "version": "5.8.3" }
}Not in this collection on purpose: the GitHub Copilot CLI (now an official
ghcr.io/devcontainers/features/copilot-cli), code-server (Coder publishes
ghcr.io/coder/devcontainer-features/code-server), and shellcheck/yamllint
(ghcr.io/devcontainers-extra/features/*).
To try an unpublished change, reference the Feature folder locally
("./features/<id>": {}); the CLI only accepts local Features that live
under the .devcontainer/ folder, so copy or link it there.
Windows is the primary host (PowerShell 7 + Task + Docker Desktop or
wslc.exe); Linux and macOS are supported through a bash twin of every
host-side script. From the repo root:
# Windows
irm https://raw.githubusercontent.com/nabil-becker/devcontainer-features/main/bootstrap/bootstrap.ps1 | iex# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/nabil-becker/devcontainer-features/main/bootstrap/bootstrap.sh | bashbootstrap/bootstrap.ps1 / bootstrap/bootstrap.sh:
- vendor
bootstrap/devcontainer.yml,bootstrap/host/*(the PowerShell flavour) and thedevcontainer-cliFeature'sdevcontainer-cli.sh(the bash flavour) into the repo'sbootstrap/folder - always refreshed, so re-running is the update path; - write
Taskfile.yml,.devcontainer/devcontainer.json(go-task, devcontainer-cli, docker-outside-of-docker, PowerShell),.devcontainer/env_mnt/.gitignoreand.devcontainer/.env.examplefrombootstrap/- only if they do not exist yet - and gitignore.devcontainer/.env; - run
task devcontainer:init(caches the portable Node.js + CLI, reports the engine) andtask devcontainer:up.
That breaks the circular dependency: the repo never needs a checkout of this
one, and from then on every devcontainer operation goes through
task devcontainer:*.
Every setting of the bootstrap and of the tasks can be given three ways, in
this precedence: explicit parameter / flag, environment variable, .env
file (./.env or ./.devcontainer/.env, loaded by the bootstrap scripts and
by Task's dotenv: in the generated Taskfile). That is how a piped
irm | iex or curl | bash run is configured:
| Setting | Env var | Parameter |
|---|---|---|
Docker-compatible CLI for --docker-path (docker, wslc, podman, nerdctl, finch, or a path). Unset: docker on PATH, else wslc.exe on Windows. |
DEVCONTAINER_DOCKER_PATH |
-DockerPath / --docker-path |
| Portable Node.js version | DEVCONTAINER_NODE_VERSION |
- |
@devcontainers/cli version (default latest) |
DEVCONTAINER_CLI_VERSION |
- |
Git ref to vendor from (default main) |
DEVCONTAINER_BOOTSTRAP_REF |
-Ref / --ref |
| Source URL base or local checkout | DEVCONTAINER_BOOTSTRAP_SOURCE |
-Source / --source |
| Repo root (default cwd) | DEVCONTAINER_BOOTSTRAP_PATH |
-Path / --path |
Stop before up |
DEVCONTAINER_BOOTSTRAP_NO_UP=true |
-NoUp / --no-up |
$env:DEVCONTAINER_DOCKER_PATH = 'wslc'; irm https://raw.githubusercontent.com/nabil-becker/devcontainer-features/main/bootstrap/bootstrap.ps1 | iex
# or with parameters
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/nabil-becker/devcontainer-features/main/bootstrap/bootstrap.ps1))) -DockerPath wslc -NoUpSee bootstrap/env.example for a commented .env.
| Task | Does |
|---|---|
devcontainer:init |
Cache portable Node.js + @devcontainers/cli, report engine. FORCE=true reinstalls the CLI. |
devcontainer:up / rebuild / build |
devcontainer up (extra flags after --), from-scratch rebuild, image build only. |
devcontainer:exec CMD='...' / shell |
Run a command (base64-safe) or an interactive bash in the container. |
devcontainer:config |
Resolved configuration. |
devcontainer:<anything> |
Passthrough to that CLI subcommand, args after --. |
devcontainer:reset-storage |
wslc.exe only: reset WSL Containers' storage after a read-only BuildKit error. |
Each task dispatches per platform (Task's per-command platforms):
- Windows runs
bootstrap/host/Invoke-DevcontainerCli.ps1: a pinned portable Node.js is downloaded once (sha256-verified) into%LOCALAPPDATA%\devcontainer-features\devcontainer-node, the CLI is npm-installed next to it, nothing lands on PATH. Needs PowerShell 7. - Linux/macOS run
devcontainer-cli.sh, the very script that installs thedevcontainer-cliFeature (vendored asbootstrap/host/devcontainer-cli.sh):devcontaineron PATH if present, else the same portable Node.js trick under~/.cache/devcontainer-features. Needs bash and curl.
Both honour the DEVCONTAINER_* settings above. The include must be a
local include - the tasks run the scripts next to the Taskfile, which
remote includes cannot reach.
task test # devcontainer features test, all Features
task test FEATURE=go-task # one Feature
task test:global # combined scenario in test/_global
task lint # shellcheck + PSScriptAnalyzer
task docs # regenerate src/<id>/README.md (commit the result)devcontainer features test does not run on a Windows host (the CLI shells
out to chmod/bash), so task test is gated to Linux/macOS. The easiest
place is this repository's own devcontainer (.devcontainer/: docker-in-docker,
shellcheck, PowerShell, and the collection's go-task and devcontainer-cli
Features); otherwise let CI do it.
Layout follows devcontainers/feature-starter:
src/<id>/devcontainer-feature.json + install.sh + NOTES.md, with
README.md generated from those two by task docs and checked by CI, and
test/<id>/test.sh + scenarios.json.
Run the Release dev container features workflow (workflow_dispatch on
main, release environment). It publishes every src/<id> to
ghcr.io/nabil-becker/devcontainer-features/<id>; it has no write access to
the repository, since READMEs are committed with each change rather than
generated at release time. Bump version in the Feature's
devcontainer-feature.json before releasing a change.
After the first release, GHCR packages are private by default: open each package's settings on GitHub, change visibility to Public, and link it to this repository. Otherwise consumers (and the containers.dev crawler) cannot pull it.
The collection is meant to be listed in the
containers.dev index. Once published,
open a PR against
_data/collection-index.yml
in devcontainers/devcontainers.github.io adding:
- name: Dev Container Features by nabil-becker
maintainer: Nabil Becker
contact: https://github.com/nabil-becker/devcontainer-features/issues
repository: https://github.com/nabil-becker/devcontainer-features
ociReference: ghcr.io/nabil-becker/devcontainer-featuresListed collections are crawled for liveness and surface in VS Code and Codespaces, so the repo is run as a public project: see CONTRIBUTING.md, SECURITY.md and CODE_OF_CONDUCT.md.
mainis protected by theprotect-mainruleset: pull requests only, one approval including a code owner, conversations resolved, required checksvalidate/lint/tests/DCO, squash merges, no force-push. Repo admins may bypass only through a PR.- Every commit is signed off under the Developer Certificate of Origin;
task repo:setupmakes that automatic per clone (see CONTRIBUTING). - Workflows run with a read-only
GITHUB_TOKEN; the release job alone getspackages: write, inside thereleaseenvironment restricted tomain. Actions are SHA-pinned and limited to GitHub-owned ones plusdevcontainers/action; Dependabot bumps them weekly. - Secret scanning with push protection, Dependabot alerts and private vulnerability reporting are on.
All of this is applied by task repo:govern
(.github/scripts/Set-RepoGovernance.ps1),
which is idempotent, so settings drift can be corrected by re-running it.