Skip to content

wagov-dtt/devcontainer-base

Repository files navigation

Cloud Native Devcontainer

Build and Push DevContainer CodeQL

Production-ready cloud-native development container, provisioned entirely with mise bootstrap.

Published image: ghcr.io/wagov-dtt/devcontainer-base

What's inside

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:

Quick Start

VS Code Devcontainer (Recommended)

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+PDev 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 extra docker-compose binary because docker compose is already included via docker-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.

Rootless Docker

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 bind mounts from inside the devcontainer

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 pwd

For 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.

Docker CLI

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-base

Install on Existing System

Works 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.sh

What it does:

  1. Installs mise if missing
  2. Detects platform (APT for Debian/Ubuntu, brew for macOS/atomic)
  3. Fetches mise.toml + platform-specific config (mise.apt.toml or mise.brew.toml)
  4. 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

Use as Template

  1. GitHub: Click "Use this template" to create your own repository
  2. Codespaces: Works immediately — click CodeCreate codespace
  3. Local: Clone and customize as needed

Use in Custom Docker Images

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 --yes

For 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 brew

The -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.

CI/CD 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.

How It Works

Architecture

  • 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 run usage

Tool Sources

Tools are installed from two package backends, with separate config files for each platform:

  1. APT via extrepo — Signed packages from official repos (Debian/Ubuntu only)
    • Docker, GitHub CLI, Terraform, kubectl, mise
    • Configured in mise.apt.toml under [bootstrap.packages]
  2. Homebrew — User-space packages for macOS and atomic Linux hosts
    • ripgrep, gh, starship, neovim, kubectl, terraform
    • Configured in mise.brew.toml under [bootstrap.packages]
  3. mise — Cross-platform development tools (all platforms)
    • Languages (Go, Node, Python), cargo tools, npm packages
    • Configured in mise.toml under [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.

Key Features

  • 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

Adding Tools

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.

Optional Cloud CLIs

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-cli

Development Commands

just              # 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 images

For maintainers:

just release 2026.7 # Create and push release tag (CI publishes images)
just shell        # Run published image interactively

Releases

This 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-amd64
  • ghcr.io/wagov-dtt/devcontainer-base:v2026.7-arm64
  • ghcr.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.

Troubleshooting

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

Contributing

  1. Fork and clone the repo
  2. Make changes to mise.toml, Dockerfile, or docs
  3. Test: just build && just test && just dev
  4. 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.