A comprehensive development container for DevOps and Infrastructure-as-Code workflows, built on Ubuntu 24.04 with essential tools for cloud infrastructure management, container orchestration, and automation.
This devcontainer includes pre-configured tools for:
- Infrastructure as Code: Terraform, Terragrunt, tflint, tf-summarize, checkov
- Cloud Management: Azure CLI (az), AzCopy
- Container Operations: Docker Engine, Helm, kubectl, kubelogin
- Configuration Management: Ansible with 9 popular collections
- Scripting & Automation: PowerShell 7 with modules, Python with DevOps tools, uv, Node.js, ZSH with Oh My Zsh
- .NET Development: .NET 10 SDK (LTS) with C# and C# Dev Kit extensions
- AI Tooling: Claude Code, Codex, cswap (Claude Code account switcher)
- Security: git-crypt, gitleaks, checkov, optional custom CA trust chain
- Development Utilities: Custom bash/zsh aliases, shell completions, pre-commit
- Data Processing: jq, yq
| Tool | Purpose |
|---|---|
| Terraform | Infrastructure provisioning |
| Terragrunt | Terraform wrapper for DRY configurations |
| tflint | Terraform linting |
| tf-summarize | Human-readable Terraform plan summaries |
| checkov | IaC security and compliance scanning |
| Azure CLI | Azure cloud management |
| AzCopy | Bulk transfer to/from Azure Storage |
| Docker | Container runtime and management |
| Helm | Kubernetes package manager |
| kubectl | Kubernetes cluster management |
| kubelogin | Azure AD authentication for kubectl |
| Ansible | Configuration management and automation |
| PowerShell | Cross-platform automation and scripting |
| Python | Scripting with DevOps-focused packages |
| uv | Fast Python package/project manager (uv, uvx) |
| Node.js | JavaScript runtime and npm tooling |
| .NET SDK | C# / .NET 10 application development |
| Claude Code | Anthropic coding agent (self-updating, user-tree install) |
| Codex | OpenAI coding agent (run codex-init to configure) |
| cswap | Switch between Claude Code accounts (claude-swap) |
| git-crypt | Transparent encryption of files in git |
| gitleaks | Secret scanning |
| pre-commit | Git hook framework |
| jq / yq | JSON and YAML processing |
jq,zshand the other base utilities come from the apt package list and the upstream devcontainer base image rather than a dedicatedinstall-*.shscript β which is why they have no entry in theinstall/tree above.
uvis available for interactive use, but no tool in this image is installed through it β the Python-based tools (checkov, claude-swap, ansible) still use system-widepip.
devcontainer-devops/
βββ .github/
β βββ workflows/
β βββ build.yml # Reusable build: lint, build, test, scan, publish
β βββ ci.yml # Pull requests and pushes to main
β βββ release.yml # Weekly CalVer release
βββ .devcontainer/
β βββ Dockerfile # Multi-stage container build
β βββ devcontainer.json # VS Code devcontainer configuration
β βββ files/
β βββ install/ # Installation scripts (each uses /tmp/install-<tool>)
β β βββ _arch.sh # Sourced helper: architecture detection
β β βββ install-ansible.sh
β β βββ install-azcopy.sh
β β βββ install-azure-cli.sh
β β βββ install-checkov.sh
β β βββ install-claude-code.sh
β β βββ install-codex.sh
β β βββ install-cswap.sh
β β βββ install-docker.sh
β β βββ install-dotnet.sh
β β βββ install-git-crypt.sh
β β βββ install-gitleaks.sh
β β βββ install-helm.sh
β β βββ install-kubectl.sh
β β βββ install-kubelogin.sh
β β βββ install-node.sh
β β βββ install-powershell.sh
β β βββ install-pre-commit.sh
β β βββ install-python-tools.sh
β β βββ install-terraform.sh
β β βββ install-terragrunt.sh
β β βββ install-tf-summarize.sh
β β βββ install-tflint.sh
β β βββ install-uv.sh
β β βββ install-yq.sh
β βββ certs/ # Drop-in dir for extra CA certificates
β β βββ README.md # How to add your own CA chain
β βββ codex/ # Codex bootstrap (rendered by `codex-init`)
β β βββ codex-init
β β βββ config.toml.tmpl
β βββ home/ # Home directory files
β β βββ .bash_aliases # Convenience aliases
β β βββ .environment # Shell-aware environment config
β β βββ .zshrc # ZSH configuration
β β βββ .claude/ # Claude Code defaults
β β βββ .config/ # PowerShell profile and theme
β βββ entrypoint.sh # Container entrypoint for home dir init
βββ tests/
β βββ integration-test.sh # Integration tests
β βββ run-all-tests.sh # Test runner
β βββ validate-tools.sh # Tool validation
βββ scripts/
β βββ check-latest-versions.sh
βββ .hadolint.yaml # Dockerfile lint rules, shared by CI and pre-commit
βββ ARCHITECTURE.md # System architecture documentation
βββ CHANGELOG.md # Version history
βββ CONTRIBUTING.md # Contribution guidelines
βββ QUICKSTART.md # Quick start guide
βββ README.md # This file
βββ SECURITY.md # Security policies
βββ VERSION_MANAGEMENT.md # Version management guide
-
Clone the repository:
git clone https://github.com/dbhq-uk/devcontainer-devops.git cd devcontainer-devops -
Open in VS Code:
code . -
Reopen in Container:
- Press
F1orCtrl+Shift+P - Select
Dev Containers: Reopen in Container - The pre-built image is pulled from GHCR, so there is no wait for a build
- Press
-
Start developing! The container will be ready with all tools pre-installed.
You do not need this repository to use the container. Point any
devcontainer.json at the published image, or pull it yourself:
docker pull ghcr.io/dbhq-uk/devcontainer-devops:latest| Tag | What it is |
|---|---|
latest |
The most recent weekly release |
2026.08.09 |
A specific weekly release |
2026.08 |
The most recent release in that month |
main |
Head of the default branch, rebuilt on every push |
sha-abc1234 |
One specific commit |
Images are published for linux/amd64 and linux/arm64; Docker picks the
right one automatically.
The devcontainer uses Docker volumes for persistent storage:
- Workspace Volume:
dev-workspace-<user>mounted at/workspace - Home Volume:
dev-home-<user>mounted at/home/vscode - Bind Mount: The local workspace folder mounted at
/workspace/devcontainer - Permissions: Automatically configured via
postCreateCommand - Home Init: Entrypoint script copies default configs on first run
This ensures your work and settings persist across container rebuilds.
Both volumes are per-user (suffixed with $USER), so several checkouts can run
side by side without sharing state. Note that /home/vscode is only seeded from
the image's /tmp-home template on first start β tools installed into the
home tree do not refresh on rebuild for an existing volume, which is why most
tooling installs system-wide.
If your environment terminates TLS with a private CA, drop the PEM-encoded chain
into .devcontainer/files/certs/ as a *.crt file before building. The
Dockerfile copies the directory to /usr/local/share/ca-certificates/extra/
and runs update-ca-certificates, merging it into the system bundle at
/etc/ssl/certs/ca-certificates.crt.
REQUESTS_CA_BUNDLE, SSL_CERT_FILE and NODE_EXTRA_CA_CERTS all point at that
bundle, which covers Python (az, ansible, checkov, β¦), curl, and Node.js β
Node ignores the system store, so it has to be told explicitly.
Certificates are git-ignored (*.crt, *.pem), so nothing is committed by
accident. Adding none is fine: the build succeeds and the container trusts the
public roots from the base image. See
.devcontainer/files/certs/README.md.
GitHub Actions builds, tests and publishes the image to the GitHub Container
Registry. There is nothing to configure - it runs on the repository's own
GITHUB_TOKEN, with no secrets and no external registry account.
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml |
Pull requests | Lints, builds both architectures, runs the test suite. Publishes nothing |
ci.yml |
Push to main |
The same, then publishes :main and :sha-<short> |
release.yml |
Sundays 03:00 UTC, or manually | A --no-cache rebuild, published as a dated release and :latest |
Both call build.yml, which holds the actual
build so the two entry points cannot drift apart.
Each architecture builds on its own native runner - ubuntu-24.04 and
ubuntu-24.04-arm - rather than under QEMU emulation, which would take hours
for an image this size. Each runner builds its platform, loads it locally, runs
tests/run-all-tests.sh against the real image, and
only then pushes by digest. A final job merges the digests into one
multi-architecture manifest.
Releases are calendar-versioned: v2026.08.09 is the image as it was built on
that date. Most entries in versions.json are "install latest", so a tag is a
point-in-time snapshot rather than a reproducible build - rebuilding the same
tag a week later would produce a different image. If you need one exact
image, pin the digest, which every release records.
Every published image carries an SBOM and SLSA build provenance, generated by BuildKit and signed with a short-lived Sigstore certificate. Verify that an image really came from this repository:
gh attestation verify oci://ghcr.io/dbhq-uk/devcontainer-devops:latest \
-R dbhq-uk/devcontainer-devopsRead the SBOM out of the image:
docker buildx imagetools inspect ghcr.io/dbhq-uk/devcontainer-devops:latest \
--format '{{ json .SBOM }}'Trivy scans each build for HIGH and CRITICAL vulnerabilities and reports them to the repository's Security tab. Scans report, they do not block: an image bundling the Azure CLI, Ansible and a .NET SDK always carries some upstream findings, and blocking on those would stop the weekly rebuild and leave the published image staler than the CVEs it was avoiding.
-
Create an installation script in
.devcontainer/files/install/. It should take the desired version as$1, resolve the latest when that argument is empty, and verify the install before exiting:.devcontainer/files/install/install-your-tool.sh
The whole directory is copied and
chmod +x'd in one step, so noCOPYline is needed per script.The image is built for
amd64andarm64, so never hardcode an architecture. Source the shared helper and use the spelling your upstream uses:. "$(dirname "$0")/_arch.sh" # ARCH_DEB amd64 / arm64 Debian and Go convention, most releases # ARCH_X64 x64 / arm64 e.g. gitleaks # ARCH_GNU x86_64 / aarch64 Rust target triples, e.g. uv
If the tool has no
arm64Linux build, say so in a comment and skip it on that architecture rather than failing the build. -
Add an
ARG YOUR_TOOL_VERSION=to both blocks at the top of theDockerfile(before and after theFROM), then invoke the script:RUN /tmp/install/install-your-tool.sh ${YOUR_TOOL_VERSION}Place the
RUNas late in the file as the dependencies allow β a version bump invalidates every layer after it. -
Record the version in
versions.json, add avalidate_toolline totests/validate-tools.sh, and note it inCHANGELOG.md. -
Rebuild the container
versions.json is the documented source of truth for tool versions; an empty
string means "install latest". See VERSION_MANAGEMENT.md
for the per-tool version sources.
The Dockerfile ARGs default to empty (latest). .devcontainer/devcontainer.json
builds from the local Dockerfile by default β to pin, add the versions to its
build.args block:
"args": {
"UBUNTU_VERSION": "24.04",
"TERRAFORM_VERSION": "1.13.5",
"POWERSHELL_VERSION": "7.5.4"
}CI does not pass these build args, so published images install the latest of everything left unpinned in the
Dockerfile. That is deliberate - see Versioning - and it is why a release tag is a snapshot rather than a reproducible build.
terraform init
terraform plan
terraform applyaz login
az account list
az group create --name myResourceGroup --location eastusdocker ps
docker build -t myimage .
docker run myimagekubectl get pods
helm install myrelease mychart/claude # start Claude Code
codex-init # one-off: configure Codex endpoint and deployment
cswap list # list managed Claude Code accounts
cswap status # show the account currently in use
cswap add # register the account you are signed in as
cswap switch # rotate to the next account- Create a feature branch
- Make your changes
- Test in the devcontainer
- Submit a pull request
MIT - see LICENSE.
- Ensure Docker Desktop is running
- Check Docker has sufficient resources (CPU/Memory)
- Try rebuilding without cache:
Dev Containers: Rebuild Container
- The
postCreateCommandshould handle this automatically - Manually run:
sudo chown -R vscode:vscode /workspace
- Verify the installation script exists in
.devcontainer/files/install/ - Check the
Dockerfilehas aRUN /tmp/install/install-<tool>.shstep - Confirm it appears in
tests/validate-tools.sh, then run that script - Rebuild the container
/home/vscode is a persistent per-user volume, seeded from the image only on
first start. Anything installed into the home tree (Claude Code, for example)
will not refresh for an existing volume. Remove the dev-home-<user> volume to
re-seed, or update the tool in place.
Open an issue. For
anything security-related, follow SECURITY.md instead of
opening a public issue.