Skip to content

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

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)

infisical export --env=dev --projectId=personal-vault --format=dotenv > /tmp/env-123.list

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

docker run --env-file /tmp/env-123.list my-dev-env:latest

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

# Trap runs:
rm -f /tmp/env-123.list

# Secrets cleaned up

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

./build.sh v1.0.0

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:

docker build -t my-custom-dev-env:v1.0.0 .


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:

docker ps

Check image exists:

docker images | grep my-dev-env

If missing, pull:

docker pull kuatecno/my-dev-env:latest

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:

ls -la /tmp/env-*.list
# Should show recent file

Check file has content:

cat /tmp/env-123.list
# Should show KEY=value pairs

If empty: Infisical export failed (check authentication)

Git operations fail

Check SSH keys mounted:

# Inside container
ls -la /root/.ssh/
# Should show id_ed25519_macbook (read-only)

Check SSH agent:

ssh-add -l
# Should show key loaded

Out of memory

Check memory usage:

docker stats

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