Docker Development Environment¶
Pre-installed AI agents in a secure, isolated container environment.
This document covers the Docker image architecture, security model, and how everything works together.
Table of Contents¶
- Overview
- Image Contents
- What's NOT in the Image
- How Secrets Are Injected
- Security Model
- Image Versioning
- Building Custom Images
- Resource Limits
- Docker Hub Workflow
Overview¶
What Is It?¶
The Docker development environment is a pre-built container image (my-dev-env:latest) that includes:
- Ubuntu 24.04 base
- Claude Code 2.0.76 (pre-installed)
- Gemini CLI 0.22.2 (pre-installed)
- Development tools (Git, Python, Node.js, etc.)
- Infisical CLI (tool only, no tokens)
Why a Docker image?
- ✅ Consistency: Same environment on every machine (Mac, servers, colleagues)
- ✅ Isolation: Work doesn't affect host system
- ✅ Instant startup: No waiting for npm install of AI tools
- ✅ Offline capable: Works without internet (tools pre-installed)
- ✅ Version-locked: Claude 2.0.76, Gemini 0.22.2 (no unexpected updates)
The Architecture¶
┌────────────────────────────────────────────────────┐
│ HOST (Hetzner/Kimsufi/MacBook) │
│ ┌──────────────────────────────────────────────┐ │
│ │ ~/.config/infisical/ (auth token) │ │
│ │ ~/infisical/.env (ENCRYPTION_KEY) │ │
│ │ ~/.gemini/oauth-edu1.json (Gemini session) │ │
│ │ ~/.claude/session-personal.json (Claude) │ │
│ │ ~/.ssh/id_ed25519_macbook (SSH key) │ │
│ │ ~/Coding/ (your code) │ │
│ └────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ dev() FUNCTION │ │
│ │ 1. Fetches secrets from Infisical │ │
│ │ 2. Saves to /tmp/env-$$.list │ │
│ └────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ docker run --rm -it │ │
│ │ --env-file /tmp/env-$$.list │ │
│ │ -v ~/Coding:/app │ │
│ │ -v ~/.gemini/oauth-edu1.json:... (ro) │ │
│ │ -v ~/.claude/session-personal.json:... (ro) │ │
│ │ -v ~/.ssh:/root/.ssh:ro │ │
│ │ my-dev-env:latest │ │
│ └────────────┬─────────────────────────────────┘ │
└───────────────┼──────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────┐
│ CONTAINER (Ephemeral, isolated) │
│ ┌──────────────────────────────────────────────┐ │
│ │ my-dev-env:latest │ │
│ │ │ │
│ │ Pre-installed (in image): │ │
│ │ ✅ Claude Code 2.0.76 │ │
│ │ ✅ Gemini CLI 0.22.2 │ │
│ │ ✅ Git, Python, Node.js │ │
│ │ ✅ Infisical CLI (tool only) │ │
│ │ │ │
│ │ Runtime-injected (from host): │ │
│ │ ✅ Secrets (ANTHROPIC_API_KEY, etc.) │ │
│ │ ✅ Gemini OAuth (read-only) │ │
│ │ ✅ Claude session (read-only) │ │
│ │ ✅ SSH keys (read-only) │ │
│ │ ✅ Code (/app → ~/Coding) │ │
│ │ │ │
│ │ User works here: │ │
│ │ $ claude │ │
│ │ $ gemini │ │
│ │ $ git commit && git push │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ On exit: │
│ - Container deleted (--rm) │
│ - Temp secrets file deleted │
│ - Code changes preserved (mounted volume) │
└────────────────────────────────────────────────────┘
Image Contents¶
Base System¶
FROM ubuntu:24.04
# System packages
RUN apt-get update && apt-get install -y \
git \
vim \
curl \
wget \
build-essential \
ca-certificates
# Runtime environments
RUN apt-get install -y \
python3 \
python3-pip \
nodejs \
npm
Pre-installed AI Agents¶
# Claude Code (pre-installed, no installation delay)
RUN npm install -g @anthropic-ai/claude-code@2.0.76
# Gemini CLI (pre-installed)
RUN npm install -g @google/gemini-cli@0.22.2
Why pre-installed?
- ❌ Before: 10-30 second wait for npm install on every container start
- ✅ Now: Instant startup, works offline
Infisical CLI¶
# Infisical CLI (tool only, no authentication)
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash
RUN apt-get install -y infisical
Important: The CLI tool is in the image, but authentication tokens are NOT.
Development Tools¶
# Additional tools
RUN apt-get install -y \
jq \ # JSON processing
htop \ # Process viewer
tree \ # Directory tree
ripgrep \ # Fast grep (rg)
fzf \ # Fuzzy finder
tmux # Terminal multiplexer
Total Image Size¶
Approximate: 3.5GB - Base Ubuntu: 100MB - System packages: 500MB - Node.js + npm: 200MB - Claude Code: 85MB - Gemini CLI: 494MB - Python: 500MB - Development tools: ~1.6GB
Is this too big? No: - Downloads once, cached locally - Much faster than installing tools on every container start - Bandwidth savings over time (no repeated npm installs)
What's NOT in the Image¶
Credentials (NEVER Included)¶
❌ Never in Docker image:
# ❌ NEVER DO THIS
COPY ~/.config/infisical/ /root/.config/infisical/
COPY ~/.gemini/oauth-edu1.json /root/.gemini/
COPY ~/.claude/session-personal.json /root/.config/claude/
COPY ~/.ssh/id_ed25519_macbook /root/.ssh/
ENV ANTHROPIC_API_KEY=sk-ant-...
ENV ENCRYPTION_KEY=def456...
Why? - Anyone with the image = access to your accounts - Can't be removed from Docker layers (permanent) - If pushed to Docker Hub = public exposure - Violates every security best practice
The Correct Approach¶
✅ Credentials stay on host, injected at runtime:
- Infisical token: ~/.config/infisical/ (on host only)
- ENCRYPTION_KEY: ~/infisical/.env (on host only)
- Gemini OAuth: ~/.gemini/oauth-edu1.json (mounted read-only)
- Claude session: ~/.claude/session-personal.json (mounted read-only)
- SSH keys: ~/.ssh/ (mounted read-only)
- Secrets: Fetched from Infisical → /tmp/env-$$.list → --env-file
How Secrets Are Injected¶
The Flow¶
1. HOST: dev() function runs
↓
2. HOST: infisical export → /tmp/env-$$.list
↓
3. HOST: docker run --env-file /tmp/env-$$.list
↓
4. DOCKER: Receives secrets as environment variables
↓
5. DOCKER: AI agents read from environment
↓
6. EXIT: Trap deletes /tmp/env-$$.list
Detailed Example¶
Step 1: Fetch secrets (on host)
Step 2: Temp file contains
ANTHROPIC_API_KEY=sk-ant-api03-abc123...
GOOGLE_API_KEY=AIzaSyDEF456...
DATABASE_URL=postgresql://localhost:5432/mydb
AWS_ACCESS_KEY_ID=AKIAEXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCY
Step 3: Pass to Docker
Step 4: Inside container
# Secrets available as environment variables
$ env | grep API_KEY
ANTHROPIC_API_KEY=sk-ant-api03-abc123...
GOOGLE_API_KEY=AIzaSyDEF456...
# AI agents use them
$ claude
# Claude automatically reads ANTHROPIC_API_KEY from environment
$ gemini
# Gemini uses GOOGLE_API_KEY from environment
Step 5: On exit
Security Features¶
- ✅ Temp file: Unique per session (
$$= process ID) - ✅ Automatic cleanup: Trap handles EXIT, INT, TERM
- ✅ No persistence: Secrets never written to image layers
- ✅ Isolated: Each container has separate environment
Security Model¶
Principle: Defense in Depth¶
┌────────────────────────────────────────────┐
│ Security Layer 1: HOST │
│ - SSH keys require passphrase │
│ - Infisical requires authentication │
│ - Temp files auto-deleted │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ Security Layer 2: DOCKER ISOLATION │
│ - Container filesystem isolated │
│ - Process namespace isolated │
│ - Network isolated (unless exposed) │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ Security Layer 3: RESOURCE LIMITS │
│ - Memory: 4GB max │
│ - CPU: 2 cores max │
│ - Processes: 100 max │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ Security Layer 4: READ-ONLY MOUNTS │
│ - SSH keys: Read-only │
│ - Gemini OAuth: Read-only │
│ - Claude session: Read-only │
│ - Git config: Read-only │
└────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ Security Layer 5: NO-NEW-PRIVILEGES │
│ - Can't escalate privileges │
│ - Can't gain root inside container │
│ - Prevents privilege escalation attacks │
└────────────────────────────────────────────┘
Security Options Explained¶
| Option | Purpose | What It Prevents |
|---|---|---|
--rm |
Remove on exit | Persistent containers with secrets |
-it |
Interactive terminal | Background containers |
--memory="4g" |
Memory limit | Memory exhaustion attacks |
--cpus="2.0" |
CPU limit | CPU exhaustion, crypto mining |
--pids-limit=100 |
Process limit | Fork bomb attacks |
--security-opt=no-new-privileges |
No privilege escalation | Gaining root, exploits |
--env-file (temp) |
Secrets via file | Secrets in command line (visible in ps) |
-v ... :ro |
Read-only mount | Modifying SSH keys, credentials |
What Can't Happen¶
❌ Container can't: - Access host filesystem beyond mounts - Modify SSH keys (read-only mount) - Modify Gemini/Claude credentials (read-only) - Exceed 4GB RAM - Exceed 2 CPU cores - Fork more than 100 processes - Escalate to root (even if exploit found) - Leave secrets on disk after exit (temp file deleted)
✅ Container can (intentionally): - Read/write to ~/Coding (mounted as /app) - Use SSH keys for git operations (read-only access) - Use AI agent credentials (read-only sessions) - Access secrets from environment variables - Install packages inside container (ephemeral, lost on exit)
Image Versioning¶
Tagging Strategy¶
my-dev-env:latest # Always points to newest stable
my-dev-env:v1.0.0 # Specific version (locked)
my-dev-env:v1.0.1 # Bug fixes
my-dev-env:v1.1.0 # New features (e.g., new tool)
When to Create New Version¶
Patch version (v1.0.0 → v1.0.1): - Update AI agent versions (Claude 2.0.76 → 2.0.77) - Fix bugs in entrypoint script - Security patches
Minor version (v1.0.0 → v1.1.0):
- Add new tool (e.g., terraform)
- Add new language runtime (e.g., Go, Rust)
- Non-breaking changes
Major version (v1.0.0 → v2.0.0): - Breaking changes (e.g., Ubuntu 24.04 → 26.04) - Incompatible architecture changes - Require dev() function updates
Updating Agents¶
When Claude Code v3.0 releases:
cd ~/Coding/.docker-image
# Edit Dockerfile
nano Dockerfile
# Change:
# RUN npm install -g @anthropic-ai/claude-code@2.0.76
# To:
# RUN npm install -g @anthropic-ai/claude-code@3.0.0
# Rebuild
./build.sh v1.1.0
# Test locally
dev
# Push to Docker Hub
docker tag my-dev-env:v1.1.0 my-dev-env:latest
docker push kuatecno/my-dev-env:v1.1.0
docker push kuatecno/my-dev-env:latest
# On other machines
docker pull kuatecno/my-dev-env:latest
Building Custom Images¶
When to Customize¶
Good reasons: - Project needs specific tool (e.g., Terraform, Ansible) - Need specific language version (e.g., Python 3.12) - Add project-specific dependencies
Bad reasons: - Want to include credentials (NEVER) - Want to save 5 seconds (pre-installation saves more time) - Don't understand what's in base image
Build Script¶
File: ~/Coding/.docker-image/build.sh
What it does:
1. Builds Docker image from Dockerfile
2. Tags with version: my-dev-env:v1.0.0
3. Tags as latest: my-dev-env:latest
4. Verifies build (checks Claude & Gemini versions)
Custom Dockerfile Example¶
FROM my-dev-env:latest
# Add Terraform
RUN curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add -
RUN apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
RUN apt-get update && apt-get install -y terraform
# Add Go
RUN wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
RUN tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
ENV PATH="/usr/local/go/bin:$PATH"
# Verify installations
RUN terraform --version
RUN go version
Build:
Resource Limits¶
Why Limits Matter¶
Without limits: - Container can consume all host RAM (crashes server) - Container can use all CPU (slows everything) - Fork bomb can spawn millions of processes (kernel panic)
With limits: - Container gets 4GB RAM (enough for most work) - Container gets 2 CPU cores (plenty for development) - Container gets 100 processes (prevents fork bombs)
Current Limits¶
| Resource | Limit | Rationale |
|---|---|---|
| Memory | 4GB | Enough for AI agents + application |
| CPU | 2 cores | Balanced performance without hogging |
| Processes | 100 | Prevents fork bombs, enough for tools |
Adjusting Limits¶
Edit dev() function:
nano ~/.zshrc
# Find:
--memory="4g" \
--cpus="2.0" \
--pids-limit=100 \
# Change to:
--memory="8g" \ # If running large models locally
--cpus="4.0" \ # If compiling large projects
--pids-limit=200 \ # If running complex builds
When to increase: - Large builds (e.g., compiling Rust/C++ projects) - Running local LLMs - Multi-threaded workloads - Heavy parallel testing
When NOT to increase: - "Just in case" (wastes resources) - Because you can (degrades multi-user server performance)
Docker Hub Workflow¶
Why Docker Hub?¶
- ✅ One build, many machines: Build once on Mac, pull on servers
- ✅ Version control: Tag specific versions
- ✅ Rollback: Can pull older versions if needed
- ✅ Team sharing: Colleagues can use same image
Push to Docker Hub¶
# Login (one time)
docker login
# Username: kuatecno
# Password: [Docker Hub access token]
# Tag for Docker Hub
docker tag my-dev-env:latest kuatecno/my-dev-env:latest
docker tag my-dev-env:v1.0.0 kuatecno/my-dev-env:v1.0.0
# Push
docker push kuatecno/my-dev-env:latest
docker push kuatecno/my-dev-env:v1.0.0
Pull on Other Machines¶
# On Hetzner
ssh hetzner
docker pull kuatecno/my-dev-env:latest
docker tag kuatecno/my-dev-env:latest my-dev-env:latest
# On Kimsufi
ssh kimsufi
docker pull kuatecno/my-dev-env:latest
docker tag kuatecno/my-dev-env:latest my-dev-env:latest
# On colleague's Mac
docker pull kuatecno/my-dev-env:latest
docker tag kuatecno/my-dev-env:latest my-dev-env:latest
Image Visibility¶
Private repository (recommended): - Only you (and collaborators) can pull - Costs $5/month for Docker Hub Pro
Public repository (if image has no secrets): - Anyone can pull - Free - Safe if image contains ONLY tools (no credentials)
Current setup: Private (contains no secrets, but prefer private for control)
Troubleshooting¶
Container fails to start¶
Check Docker is running:
Check image exists:
If missing, pull:
AI agents not found¶
Verify they're in image:
docker run --rm my-dev-env:latest claude --version
docker run --rm my-dev-env:latest gemini --version
If missing: Image is outdated, pull latest
Secrets not loading¶
Check temp file was created:
Check file has content:
If empty: Infisical export failed (check authentication)
Git operations fail¶
Check SSH keys mounted:
Check SSH agent:
Out of memory¶
Check memory usage:
If hitting 4GB limit: Increase memory in dev() function
Quick Reference¶
Image Info¶
# Check image exists
docker images my-dev-env
# Check what's in image
docker run --rm my-dev-env:latest dpkg -l # Installed packages
docker run --rm my-dev-env:latest npm list -g # Global npm packages
# Check versions
docker run --rm my-dev-env:latest claude --version
docker run --rm my-dev-env:latest gemini --version
Build Commands¶
# Build image
./build.sh v1.0.0
# Test image
docker run --rm -it my-dev-env:latest /bin/bash
# Push to Docker Hub
docker tag my-dev-env:latest kuatecno/my-dev-env:latest
docker push kuatecno/my-dev-env:latest
Cleanup¶
# Remove stopped containers
docker container prune
# Remove unused images
docker image prune
# Remove everything unused
docker system prune -a
Related Documentation¶
- dev() Function Guide - How dev() uses this image
- AI Agents Integration - Using Claude & Gemini
- Infisical Secrets - How secrets are managed
- Security Guide (old) - Deep dive into security model