Production-ready cloud-native development container, provisioned entirely with mise bootstrap.
Published image: ghcr.io/wagov-dtt/devcontainer-base
| Category | Tools |
|---|---|
| Languages | Go, Node.js, Python, Rust, uv, pnpm, aube |
| Cloud / platform | AWS CLI, Terraform, kubectl, k9s, k3d, helm, kustomize |
| Developer UX | Docker-outside-of-Docker, OpenCode, oy, git, just, mise, direnv, starship, zellij, neovim, lazygit, delta, difftastic |
| Security | Semgrep, cosign, SLSA verifier, lychee, Trivy, Syft, sops, age |
| Linting / formatting | ShellCheck, shfmt, actionlint, taplo, typos, hadolint, yamlfmt |
| Utilities | ripgrep, fzf, jq, yq, httpie, hurl, btop, restic, rclone |
Complete source of truth:
mise.toml— mise-managed tools, dotfiles, tasksmise.apt.toml— Debian/Ubuntu system packagesmise.brew.toml— Homebrew system packages
Create .devcontainer/devcontainer.json:
{
"name": "My Project",
"image": "ghcr.io/wagov-dtt/devcontainer-base",
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
"moby": false,
"dockerDashComposeVersion": "none"
}
},
"remoteEnv": {
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
},
"remoteUser": "vscode"
}Open in VS Code: Cmd/Ctrl+Shift+P → Dev Containers: Reopen in Container.
Why these settings?
docker-outside-of-docker- Reuses the host Docker socket without privileged mode and handles socket permissions/rootless setups more robustly than a manual bind mount.moby: false- Uses the Docker CLI already baked into this Debian stable-backports image. The feature's default Moby packages are not available on Debian Trixie.dockerDashComposeVersion: "none"- Avoids installing an extradocker-composebinary becausedocker composeis already included viadocker-compose-plugin.LOCAL_WORKSPACE_FOLDER- Makes the host workspace path available for Docker bind mounts from inside the container.remoteUser: vscode- Correct user permissions
If you need compatibility with an older Docker daemon, set DOCKER_API_VERSION in remoteEnv as a project-specific workaround rather than by default.
For rootless Docker, override the feature's default socket mount to point at your user socket:
{
"name": "My Project",
"image": "ghcr.io/wagov-dtt/devcontainer-base",
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
"moby": false,
"dockerDashComposeVersion": "none"
}
},
"mounts": [
{
"source": "/run/user/1000/docker.sock",
"target": "/var/run/docker-host.sock",
"type": "bind"
}
],
"remoteUser": "vscode"
}Replace 1000 with id -u from your host.
Docker commands run against the host daemon, so bind-mount source paths must exist on the host. Use LOCAL_WORKSPACE_FOLDER when invoking Docker:
docker run --rm -v "${LOCAL_WORKSPACE_FOLDER}:/workspace" debian:stable-slim pwdFor projects with Docker Compose files that assume container paths match host paths, mount the workspace at the same absolute path:
{
"workspaceFolder": "${localWorkspaceFolder}",
"workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind"
}This is not available when using VS Code's Clone Repository in Container Volume flow, because ${localWorkspaceFolder} does not exist there.
The image still includes Docker CLI/buildx/compose for direct docker run usage outside VS Code Dev Containers:
# Basic usage (mount host Docker socket)
docker run -it --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
--group-add $(stat -c '%g' /var/run/docker.sock) \
ghcr.io/wagov-dtt/devcontainer-base
# With your projects mounted
docker run -it --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
--group-add $(stat -c '%g' /var/run/docker.sock) \
-v ~/projects:/workspaces \
ghcr.io/wagov-dtt/devcontainer-baseWorks on Debian/Ubuntu (including Ubuntu 26.04) and brew-based hosts (macOS, atomic Linux). Installs via mise bootstrap — no Python, pip, uv, or pyinfra required.
# One-liner: auto-detects APT or brew
curl -sSL https://raw.githubusercontent.com/wagov-dtt/devcontainer-base/main/install.sh | sh
# Install for a specific user (requires root/sudo)
curl -sSL https://raw.githubusercontent.com/wagov-dtt/devcontainer-base/main/install.sh | SETUP_USER=myuser sh
# Clone and run locally
git clone https://github.com/wagov-dtt/devcontainer-base && cd devcontainer-base
sudo ./install.shWhat it does:
- Installs mise if missing
- Detects platform (APT for Debian/Ubuntu, brew for macOS/atomic)
- Fetches
mise.toml+ platform-specific config (mise.apt.tomlormise.brew.toml) - Runs
mise bootstrap --yes -E <platform>:- Installs system packages (Docker CLI, git, neovim, ripgrep, etc.)
- Installs 60+ development tools (Go, Node, Python, k9s, Terraform, etc.)
- Applies shell dotfiles (bashrc enhancements)
Set GITHUB_TOKEN to avoid API rate limits. The script auto-exports it from gh auth token when available.
To skip system packages and only install user-level tools, run manually:
mise trust --yes .
mise install --yes # tools only, no system packages
mise dotfiles apply --yes # shell config only- GitHub: Click "Use this template" to create your own repository
- Codespaces: Works immediately — click Code → Create codespace
- Local: Clone and customize as needed
Consume this project's mise bootstrap config to layer the same toolchain into your own Dockerfile.
For Debian/Ubuntu images, copy the config and run the APT variant:
FROM debian:stable-backports
ARG DEBIAN_FRONTEND=noninteractive
COPY mise.toml mise.apt.toml ./
RUN apt-get update -y \
&& apt-get install -y --no-install-recommends curl ca-certificates extrepo gnupg locales sudo \
&& curl --proto '=https' --tlsv1.2 -sSf https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh \
&& sed -i 's/^# - contrib/- contrib/' /etc/extrepo/config.yaml \
&& sed -i 's/^# - non-free/- non-free/' /etc/extrepo/config.yaml \
&& for repo in docker-ce github-cli kubernetes google_cloud ddev mise hashicorp; do \
extrepo enable "$repo" || echo "extrepo: $repo skipped or already enabled"; \
done \
&& mise trust --yes . \
&& mise bootstrap packages install --yes --update -E apt \
&& mise bootstrap --yesFor brew-based images or base OSes, copy mise.toml + mise.brew.toml and run the brew variant:
COPY mise.toml mise.brew.toml ./
RUN curl --proto '=https' --tlsv1.2 -sSf https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh \
&& mise trust --yes . \
&& mise bootstrap --yes -E brewThe -E apt flag loads mise.apt.toml; -E brew loads mise.brew.toml. If you want only mise-managed tools and dotfiles (no system packages), omit -E ... and run mise bootstrap --yes.
See the project Dockerfile for the full build pipeline including extrepo setup, user creation, and Docker socket integration.
Run tests in the devcontainer image for guaranteed consistency:
- name: Build and smoke test image
run: |
docker buildx bake test
docker run --rm devcontainer-base:test -c 'mise --version && https ipinfo.io'See .github/workflows/build.yml for the complete multi-arch build, push, and smoke-test workflow.
- Base: Debian stable-backports (currently Trixie/13)
- Package Management: APT for system tools, mise for development tools
- Provisioning: mise bootstrap handles all installation and configuration during Docker build or local install
- Docker-outside-of-Docker: Host socket reuse via the upstream Dev Containers feature; Docker CLI/buildx/compose are also pre-installed for plain
docker runusage
Tools are installed from two package backends, with separate config files for each platform:
- APT via extrepo — Signed packages from official repos (Debian/Ubuntu only)
- Docker, GitHub CLI, Terraform, kubectl, mise
- Configured in
mise.apt.tomlunder[bootstrap.packages]
- Homebrew — User-space packages for macOS and atomic Linux hosts
- ripgrep, gh, starship, neovim, kubectl, terraform
- Configured in
mise.brew.tomlunder[bootstrap.packages]
- mise — Cross-platform development tools (all platforms)
- Languages (Go, Node, Python), cargo tools, npm packages
- Configured in
mise.tomlunder[tools]
mise selects the right backend at runtime via environment configs: -E apt loads mise.apt.toml, -E brew loads mise.brew.toml. The Dockerfile always uses -E apt; install.sh auto-detects.
- Security: SBOM, signed images, Semgrep in-container
- Performance: Multi-platform builds (amd64/arm64), layer caching
- Flexibility: mise auto-switches tool versions per project
- Supply Chain: Verified packages via extrepo
Edit the appropriate config file and add your tool:
# Development tools → mise.toml
[tools]
"pipx:your-tool" = "latest" # pipx backend
"npm:your-tool" = "latest" # npm/aube backend
"cargo:your-tool" = "latest" # cargo-binstall backend
"github:user/repo" = "latest" # GitHub release binary
your-tool = "latest" # mise default registry
# System packages (APT) → mise.apt.toml
[bootstrap.packages]
"apt:your-package" = "latest"
# System packages (brew) → mise.brew.toml
[bootstrap.packages]
"brew:your-formula" = "latest"For provisioning hooks (extrepo setup, Docker daemon), see the [bootstrap.hooks] section in mise.apt.toml.
GCP CLI and Azure CLI are not installed by default (saves ~1 GB). Install them when needed:
# GCP CLI (repo already enabled via extrepo)
sudo apt-get update && sudo apt-get install -y google-cloud-cli
# Azure CLI (repo not available for Trixie, use pipx)
pipx install azure-clijust # List all commands
just build # Build test image
just test # Test Docker-outside-of-Docker
just dev # Interactive shell
just lint # Lint project files
just fmt # Format project files
just clean # Clean up imagesFor maintainers:
just release 2026.7 # Create and push release tag (CI publishes images)
just shell # Run published image interactivelyThis repository now publishes container images only. There is no Python package, PyPI release, pyproject.toml, or package version to bump.
Release versions are Git tags of the form vYYYY.N (for example, v2026.7). Pushing a tag triggers .github/workflows/build.yml, which builds and publishes:
ghcr.io/wagov-dtt/devcontainer-base:v2026.7-amd64ghcr.io/wagov-dtt/devcontainer-base:v2026.7-arm64ghcr.io/wagov-dtt/devcontainer-base:v2026.7(multi-arch manifest)
Recommended release flow:
git checkout main
git pull --ff-only
# Confirm CI is green before tagging.
gh run list --branch main --limit 5
version=2026.7
git tag -a "v${version}" -m "Release v${version}"
git push origin "v${version}"
# Optional GitHub release notes.
gh release create "v${version}" \
--title "v${version}" \
--notes "Container image release v${version}"Do not reintroduce Python packaging files for releases; all provisioning and release state is driven by mise config, Docker metadata, and git tags.
| Issue | Solution |
|---|---|
| Docker not working | Ensure Docker is running and the host socket is available. For rootless Docker, override the socket mount as shown above. |
| Tool missing | Check mise.toml |
| Build fails | Run just clean then just build |
| Docker permission errors | Rebuild the devcontainer so the docker-outside-of-docker feature can refresh socket access. For direct docker run, pass --group-add $(stat -c '%g' /var/run/docker.sock). |
| mise issues | Run mise doctor inside container |
- Fork and clone the repo
- Make changes to
mise.toml,Dockerfile, or docs - Test:
just build && just test && just dev - Submit PR with test results
What to contribute:
- New tools or tool updates
- Documentation improvements
- Bug fixes
- Performance optimisations
See CONTRIBUTING.md for contributor guidance and project philosophy.