keech-dev-container
A multi-container VS Code dev environment for .NET development with a default-DROP network firewall, Claude Code sandboxing, PostgreSQL 17, and Grafana 11.5.
Most dev containers trust the network by default. This one does not. Every outbound connection is blocked unless it is explicitly allowed. The firewall drops traffic to the entire internet, then punches narrow holes for exactly the services a .NET developer needs: GitHub, NuGet, npm, Anthropic, Google Fonts, and VS Code marketplace. Nothing else gets through.
I built this because AI coding tools change the threat model of a development environment. Claude Code can execute arbitrary shell commands, install packages, and make network requests. That is what makes it powerful. It is also what makes an unrestricted container a liability. This dev container gives AI tools full autonomy inside a network sandbox, so they can do their job without having access to anything they should not. The current version pairs the sandboxed app container with PostgreSQL and Grafana via Docker Compose, so the full development stack runs inside the firewall boundary.
The Firewall Sandbox
The firewall uses iptables with a default DROP policy on all chains: INPUT, FORWARD, and OUTPUT. If a destination is not on the allowlist, the connection is rejected. It uses ipset with hash:net for efficient CIDR matching, so the kernel does a single hash lookup against the allowlist instead of walking a rule chain.
The firewall is configured declaratively as a devcontainer feature rather than a custom shell script. The entire network policy lives in devcontainer.json:
"ghcr.io/w3cj/devcontainer-features/firewall:0": {
"verbose": true,
"githubIps": true,
"claudeCode": true,
"hosts": "volta.sh,get.volta.sh,nodejs.org,api.nuget.org,builds.dotnet.microsoft.com,download.visualstudio.microsoft.com,dotnet.microsoft.com,dot.net,aka.ms,vscode.blob.core.windows.net,update.code.visualstudio.com,fonts.googleapis.com,fonts.gstatic.com"
}githubIps fetches GitHub's published CIDR ranges from api.github.com/meta at every container start, pipes them through aggregate to consolidate overlapping ranges, and loads them into the ipset. claudeCode enables a curated set of domains for the AI toolchain: Anthropic's API and telemetry endpoints, Sentry, the npm registry, and the VS Code marketplace. hosts is the explicit allowlist for everything else, resolved via DNS at startup.
An earlier version of this container used a custom init-firewall.sh script that did the same thing: ipset creation, GitHub IP aggregation, DNS resolution for individual domains, and self-verification. The community feature handles all of that with a config block instead of ~80 lines of bash. The feature provides the same default-DROP, allowlist-only approach with ipset-backed matching. What changed is that the iptables plumbing is no longer something I maintain.
The feature verifies itself on every startup. After all rules are applied, it confirms that example.com is unreachable (the DROP policy works) and that api.github.com is reachable (the allowlist works). If either check fails, the container startup fails. A broken sandbox is worse than no sandbox because it creates a false sense of security.
verbose mode surfaces blocked connections in the terminal. The feature installs a NFLOG iptables rule and a background watcher that correlates blocked IPs with DNS queries. When a connection is rejected, a notification appears at the next shell prompt showing the blocked domain and destination. This makes it easy to diagnose missing allowlist entries without tailing log files.
AI Tools as First-Class Citizens
Claude Code is installed at the system level during the Docker build, not left for the user to configure after the container starts:
# Install Claude Code using the native installer (npm method is deprecated)
RUN curl -fsSL https://claude.ai/install.sh | bash
# Install GSD (Get Shit Done) for Claude Code
RUN CLAUDE_CONFIG_DIR="/home/$USERNAME/.claude" npx get-shit-done-cc@latest --claude --globalThe entire home directory is persisted as a named Docker volume. This means Claude Code config, API keys, shell history, Volta toolchain, Zsh configuration, and any other dotfiles survive container rebuilds:
volumes:
- claude-code-home:/home/vscodeThis is broader than the previous approach, which mounted only the .claude directory. Persisting the whole home directory eliminates the need for separate volume mounts for shell history and tool configs. The volume is shared across rebuilds of the same project, so nothing resets when the container image changes.
GSD (Get Shit Done) is also pre-installed as a workflow layer on top of Claude Code. It provides structured project planning, phase execution, and verification patterns for AI-assisted development. Having it baked into the image means the full AI toolchain is ready the moment the container starts.
Claude Code Permission Allow List
The container ships a settings.json that controls what Claude Code can do without prompting for approval. 75 commands are auto-approved across several categories: shell utilities (cat, ls, grep, find, jq, etc.), core .NET CLI commands (dotnet build, test, restore, clean, format, sln), common git operations, GitHub CLI commands for PR and issue workflows, and Docker inspection and cleanup (images, ps, logs, stop, rm).
Six deny patterns block Claude Code from ever reading sensitive files: .env, .env.*, **/secrets/*, **/*credential*, *.pem, and *.key.
The auto-approval list is deliberately conservative. Commands that can exfiltrate data or escalate privileges require explicit approval: curl and gh api can transmit data to arbitrary endpoints, docker run and docker exec can spawn processes outside the container's restrictions, and npm install and npm run can execute arbitrary scripts from the registry. Claude Code can still use all of these, but it has to ask first.
This creates a layered security model. The iptables firewall controls where network traffic can go. The permission allow list controls what commands AI tools can run autonomously. Even if a command is auto-approved, it still cannot reach a destination outside the firewall allowlist. And even if a destination is allowed by the firewall, a sensitive command like curl still requires explicit approval.
Multi-Container Architecture
The environment runs three services orchestrated by Docker Compose on a shared bridge network:
services:
app:
build:
context: .
dockerfile: Dockerfile
cap_add:
- NET_ADMIN
- NET_RAW
volumes:
- claude-code-home:/home/vscode
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:17
grafana:
image: grafana/grafana:11.5The app container is the development environment, built from the .NET 10 SDK on Ubuntu 24.04. NET_ADMIN and NET_RAW capabilities are required for the firewall feature to manipulate iptables and run its verification checks.
PostgreSQL 17 runs with a healthcheck (pg_isready every 5 seconds), and the app container will not start until Postgres is confirmed healthy. Connection variables (PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE) are pre-configured in the app container's environment, so psql connects to the devdb database with no arguments.
Grafana 11.5 runs on port 3030 (shifted from the default 3000 to avoid colliding with .NET's default development port). Both Postgres and Grafana ports are forwarded to the host through devcontainer.json. Default credentials are postgres/postgres and admin/admin respectively.
Three named volumes persist state across rebuilds: claude-code-home for the entire home directory, postgres-data for the database, and grafana-data for dashboards and configuration.
Docker-in-Docker is enabled through the official dev containers feature, so you can build and run Docker containers from inside the dev container. The firewall runs as a devcontainer feature during container creation, so the network sandbox is in place before any user code executes.
Developer Experience
The terminal environment is opinionated. Zsh with Powerlevel10k is the default shell, configured during the Docker build so there is no manual setup step:
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \
-p git \
-p dotnet \
-a "export PROMPT_COMMAND='history -a' && export HISTFILE=~/.zsh_history" \
-xShell history persists across rebuilds because the entire home directory is a named volume. Git Delta is installed for readable, syntax-highlighted diffs. Volta manages the Node.js toolchain with pinned versions that stay consistent across rebuilds. The container also includes fzf for fuzzy file and history searching, postgresql-client for direct database access, and both nano and vim as available editors.
The locale is set to en_US.UTF-8 for consistent character encoding. The default editor is nano (EDITOR=nano, VISUAL=nano), and VS Code is configured with formatOnSave enabled and Zsh as the default terminal profile. .NET telemetry and the startup banner are suppressed via DOTNET_CLI_TELEMETRY_OPTOUT and DOTNET_NOLOGO. POWERLEVEL9K_DISABLE_GITSTATUS is set to prevent the Powerlevel10k prompt from running gitstatus queries, which avoids slow prompt rendering in large repositories.
The vscode user has passwordless sudo. This is intentional for a development container where the firewall feature needs root privileges to manipulate iptables:
RUN echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAMESeven VS Code extensions are pre-installed: Claude Code, C# Dev Kit, Docker, ESLint, Prettier, PostgreSQL, and Markdown All in One. These are declared in the container configuration rather than in a user profile, so every team member gets the same tooling without manual setup.