A CLI to manage devcontainer templates from one place. Add a template once, run it in any project with a single command.
cd ~/projects/myapp
capsule run python # start the stored "python" template, open a shell insideNeed project-specific tweaks? Fork the template into the project in one step:
capsule init python # copy "python" into ./.devcontainer/
capsule run # local .devcontainer/ takes precedenceLinux and macOS (auto-detects OS and architecture, installs to ~/.local/bin):
curl -fsSL https://raw.githubusercontent.com/rachartier/capsule/main/install.sh | bashOr download manually from the releases page.
Then install the devcontainer CLI:
npm install -g @devcontainers/cliuv tool install --editable /path/to/capsule
npm install -g @devcontainers/cliRequires Python 3.13+, uv, and the devcontainer CLI.
Pull all shipped templates:
capsule add gh:rachartier/capsule/templatesThen run one from any project directory (in this example a python project):
capsule run pythonPoint at a single template directory (local, shorthand, or full URL):
capsule add gh:rachartier/capsule/templates/python
capsule add https://github.com/rachartier/capsule/tree/main/templates/rust
capsule add templates/python # local path
capsule add ~/projects/myapp/.devcontainer --name myapp # local, custom nameFor generic git remotes use --subpath and --ref:
capsule add git@mygitlab.com:team/devcontainers.git --subpath python --ref stableAuthentication uses your existing git setup (SSH keys, credential helpers, netrc). No token configuration needed.
If a .devcontainer/devcontainer.json exists in the current directory, it is used. Otherwise the named template is used directly from the store, with no copy step.
capsule run python # use stored template
capsule run # use local .devcontainer/
capsule run --rebuild # destroy and recreate the container after editsUnder the hood Capsule calls devcontainer up to start the container and run its lifecycle hooks, then devcontainer exec to drop you into an interactive shell. Mounts and env from config.toml are passed through as flags. Subsequent runs reuse the existing container.
Capsule reads ~/.config/capsule/config.toml (or $XDG_CONFIG_HOME/capsule/config.toml). Generate the file with defaults:
capsule config initThe file is optional; all settings have defaults.
$VAR references in [env] values and mount source paths are expanded from the host shell at runtime. ~ in source paths is also expanded.
Templates are stored in ~/.config/capsule/templates/. Logs go to ~/.config/capsule/capsule.log.
[dotfiles]
# Personal config files mounted into every container.
# Format: "host_path:container_path[:options]"
# ~ and $VAR in host_path are expanded at runtime.
# Microsoft devcontainer images use /home/vscode; root-based images use /root.
mounts = [
# "~/.bashrc:/root/.bashrc:ro",
# "~/.gitconfig:/root/.gitconfig:ro",
# "~/.ssh:/root/.ssh:ro",
]
[volumes]
# Additional bind mounts applied to every container.
# Format: "host_path:container_path[:options]"
mounts = []
[env]
# Environment variables injected into every container.
# $VAR values are expanded from the host shell at runtime.
# TERM = "xterm-256color"
# DISPLAY = "$DISPLAY"
[run]
# Shell launched inside the container by `capsule run`.
shell = "/bin/bash"
# Suppress devcontainer output while starting (spinner shown instead).
# Output is always printed on failure regardless of this setting.
quiet = false
# uid = 1000 # passed as UID env var; defaults to host uid
# gid = 1000 # passed as GID env var; defaults to host gidMounts applied to every container for personal config files. Kept separate from [volumes] so they are not confused with project data.
[dotfiles]
mounts = [
"~/.bashrc:/root/.bashrc:ro",
"~/.gitconfig:/root/.gitconfig:ro",
"~/.ssh:/root/.ssh:ro",
]Format: "host_path:container_path[:options]". The container path must match the container user's home directory. Microsoft devcontainer images use /home/vscode; root-based images use /root.
Default: []
Additional bind mounts applied to every container, separate from dotfiles.
[volumes]
mounts = [
"/data:/data:ro",
]Same format as [dotfiles]. Both lists are merged and passed to devcontainer up as bind mounts.
Default: []
Environment variables injected into every container. Accepts any key-value pairs.
[env]
TERM = "xterm-256color"
DISPLAY = "$DISPLAY"Values are shell-expanded at runtime, so "$DISPLAY" forwards whatever the launching shell carries.
Default: none
[run]
shell = "/bin/bash"
quiet = false| Key | Type | Default | Description |
|---|---|---|---|
shell |
string | /bin/bash |
Shell launched inside the container by capsule run. |
quiet |
bool | false |
Suppress devcontainer up output while starting. A spinner is shown instead. If the command fails, the captured output is printed so the error is always visible. |
uid |
integer | host UID | Passed as UID env var. Defaults to os.getuid(). |
gid |
integer | host GID | Passed as GID env var. Defaults to os.getgid(). |
Capsule passes UID and GID as --remote-env to every container, defaulting to the host user's IDs so bind-mounted files have correct ownership.
Global override (applies to all runs):
[run]
uid = 1000
gid = 1000Per-template override (stored in the template's capsule.toml):
capsule meta python --uid 1000 --gid 1000 # set
capsule meta python # view
capsule meta python --uid -1 --gid -1 # unset; reverts to global / hostPer-template values take precedence over config.toml. The image's postCreateCommand is responsible for consuming these vars.
| Command | What it does |
|---|---|
capsule list |
List stored templates with description and last modified date. |
capsule add <source> [--name <n>] [--ref <ref>] [--subpath <dir>] |
Store a template from a local directory, gh:owner/repo[/subpath], or any git URL. --ref overrides branch/tag, --subpath selects a subdirectory. |
capsule view <template> |
Pretty-print a template's devcontainer.json. |
capsule edit <template> |
Open a template's devcontainer.json in $EDITOR. |
capsule meta <template> [--description <d>] [--author <a>] [--uid <n>] [--gid <n>] |
View or set metadata (description, author, uid, gid) for a template. Pass -1 to --uid/--gid to unset a per-template override. |
capsule search <keyword> |
Case-insensitive search across all templates' devcontainer.json. |
capsule update <path> [--name <n>] |
Replace the devcontainer.json in a stored template from a folder. |
capsule rename <old> <new> |
Rename a stored template. |
capsule delete <template> [--force] |
Delete a stored template. |
capsule export <template> [--output <dir>] |
Export a template as a .zip archive. |
capsule pull <template> |
Re-fetch a template from its recorded git source and replace it in the store. |
| Command | What it does |
|---|---|
capsule init <template> [--output <dir>] [--force] |
Copy a template into the current project as .devcontainer/. |
capsule run [<template>] [--shell <sh>] [--rebuild] [--dry-run] |
Start the devcontainer and open a shell. --dry-run prints the commands without executing. |
capsule exec [<template>] <command...> [--rebuild] |
Run a one-shot command in the devcontainer. Uses local .devcontainer/ if present, otherwise the first positional is the template name. |
capsule ps |
List all capsule devcontainers (running and stopped). |
capsule stop [<workspace>] [--force] [--rm] |
Stop the devcontainer for the current directory or given workspace path. --force skips confirmation, --rm removes the container. |
| Command | What it does |
|---|---|
capsule config |
Show resolved config from config.toml. |
capsule config init [--force] |
Generate a default config.toml in the capsule config directory. |
capsule doctor |
Check that the environment is healthy: devcontainer CLI, container runtime, all stored templates, and config.toml validity. |
Annotate stored templates with a description and author:
capsule meta python --description "Python 3.12 with uv" --author "Alice"
capsule meta python # view current metadata
capsule list # description column shown in the listingMetadata is stored in capsule.toml inside the template directory alongside git provenance.
Preview the devcontainer commands that capsule run would execute without starting anything:
capsule run python --dry-runAdd the X11 socket mount and set DISPLAY in ~/.config/capsule/config.toml.
WSLg (Windows 11, built-in X server, DISPLAY is always :0):
[volumes]
mounts = ["/tmp/.X11-unix:/tmp/.X11-unix"]
[env]
DISPLAY = ":0"
WAYLAND_DISPLAY = "wayland-0"
XDG_RUNTIME_DIR = "/mnt/wslg/runtime-dir"Legacy WSL2 (VcXsrv, X410, DISPLAY is a dynamic Windows host IP):
[volumes]
mounts = ["/tmp/.X11-unix:/tmp/.X11-unix"]
[env]
DISPLAY = "$DISPLAY"
