#50 Cloud-Based Agentic Dev Container: Claude Code, Codex, and OpenCode in One
/ 20 min read
Updated:Building a Cloud-Based AI Development Environment: Claude Code, Codex, and OpenCode in a Single Docker Container
The Problem: Too Many Tools, Too Little Integration
As a developer working with AI coding assistants in 2026, I found myself juggling multiple tools across different terminals, each with their own configuration, environment requirements, and quirks. Claude Code from Anthropic, OpenAI’s Codex CLI, and the open-source OpenCode—all powerful tools, but managing them separately was becoming a productivity drain.
Then came the mobility problem: I wanted to code from my MacBook at the office, my iPad with Termius while traveling, and occasionally from my phone when inspiration struck. But each AI tool had local configurations, different API keys scattered across machines, and no consistent environment.
I needed a solution that was:
- Portable: Access the same environment from any device
- Persistent: Keep my configurations, history, and projects intact
- Isolated: Don’t pollute my local machine with conflicting dependencies
- Remote-ready: Run on a cloud server for always-on access
The answer? A Docker container running on Hetzner Cloud, accessible via SSH from anywhere.
The Solution: A Unified AI Development Container
Here’s what I built: a single Docker container that bundles Claude Code, OpenAI Codex CLI, and OpenCode, running on a remote server with persistent storage for configs and projects. The entire environment can be deployed with a single command and accessed from any device.
Architecture Overview
┌─────────────────────────────────────────────┐│ Your Devices ││ ┌──────┐ ┌──────┐ ┌──────┐ ││ │ Mac │ │ iPad │ │Phone │ ││ └──┬───┘ └──┬───┘ └──┬───┘ ││ └─────────┼─────────┘ ││ │ SSH (port 2222) │└───────────────┼─────────────────────────────┘ │ ▼┌─────────────────────────────────────────────┐│ Hetzner Cloud Server ││ ┌───────────────────────────────────────┐ ││ │ Docker Container │ ││ │ ┌─────────────────────────────────┐ │ ││ │ │ AI Tools │ │ ││ │ │ • Claude Code (@anthropic) │ │ ││ │ │ • Codex (@openai/codex) │ │ ││ │ │ • OpenCode (opencode-ai) │ │ ││ │ └─────────────────────────────────┘ │ ││ │ ┌─────────────────────────────────┐ │ ││ │ │ Persistent Volumes │ │ ││ │ │ • /workspace (projects) │ │ ││ │ │ • ~/.claude (config) │ │ ││ │ │ • ~/.codex (config) │ │ ││ │ │ • ~/.zsh_history │ │ ││ │ └─────────────────────────────────┘ │ ││ └───────────────────────────────────────┘ │└─────────────────────────────────────────────┘Part 1: Building the Docker Container
Base Image and Development Tools
I started with Ubuntu 24.04 as the base image and added all the essential development tools. The container needed to support multiple languages since AI assistants work with polyglot codebases:
FROM ubuntu:24.04
# Prevent interactive prompts during installationENV DEBIAN_FRONTEND=noninteractiveENV TZ=UTC
# Install system dependenciesRUN apt-get update && apt-get install -y \ curl \ wget \ git \ vim \ nano \ zsh \ tmux \ htop \ build-essential \ python3 \ python3-pip \ python3-venv \ openssh-server \ ca-certificates \ gnupg \ && rm -rf /var/lib/apt/lists/*The key tools here:
- zsh + oh-my-zsh: Modern shell with better autocomplete and history
- tmux: Terminal multiplexing for managing multiple sessions
- openssh-server: Critical for remote access
- Build tools: gcc, make, etc. for compiling dependencies
Installing Node.js, Go, and Rust
AI coding assistants often work with multiple languages, so I included the major ecosystems:
# Node.js 20.x (for Claude Code and Codex)RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ apt-get install -y nodejs && \ npm install -g npm@latest
# Go 1.22RUN wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz && \ tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz && \ rm go1.22.0.linux-amd64.tar.gz
# RustRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -yThe AI Tools Installation
Here’s where it gets interesting. Each AI tool has its own quirks:
# Claude Code (Anthropic's official CLI)RUN npm install -g @anthropic-ai/claude-code
# OpenAI Codex CLIRUN npm install -g @openai/codex
# OpenCode (open-source alternative)RUN npm install -g opencode-aiImportant detail: I initially tried installing Python packages globally, but ran into a pyparsing version conflict. The solution was using --ignore-installed to bypass the system package:
RUN pip3 install --break-system-packages --ignore-installed pyparsing opencode-aiSSH Server Configuration
This is critical for remote access. The container runs SSH on port 2222 (not 22, to avoid conflicts):
# Configure SSHRUN mkdir -p /var/run/sshd && \ sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \ sed -i 's/#Port 22/Port 2222/' /etc/ssh/sshd_config && \ sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config
# Create .ssh directory with proper permissionsRUN mkdir -p /root/.ssh && chmod 700 /root/.sshKey security settings:
- Port 2222: Separates container SSH from host SSH
- PubkeyAuthentication: Only allow SSH key access, no passwords
- PermitRootLogin yes: We’re running as root inside the container (isolated environment)
Shell Customization
I added oh-my-zsh for a better development experience:
# Install oh-my-zshRUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
# Copy custom .zshrc with aliasesCOPY .zshrc /root/.zshrcThe .zshrc includes helpful aliases:
# AI tool shortcutsalias cc='claude' # Quick access to Claude Codealias ai='aider' # Quick access to Aider
# Git shortcutsalias gs='git status'alias gp='git pull'alias gc='git commit'alias gd='git diff'
# Navigationalias ll='ls -lah'alias la='ls -A'alias ..='cd ..'alias ...='cd ../..'
# Systemalias reload='source ~/.zshrc'The Entrypoint Script
The container startup needs special handling for SSH keys. Docker mounts files as read-only by default, but SSH requires authorized_keys to have 600 permissions owned by root. The solution is a two-step dance:
#!/bin/bash
# Copy authorized_keys from mounted location with correct permissionsif [ -f /tmp/authorized_keys ]; then cp /tmp/authorized_keys /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys chown root:root /root/.ssh/authorized_keys echo "✓ SSH keys configured"fi
# Start SSH server/usr/sbin/sshd -DWe mount authorized_keys to /tmp/ (read-only is fine), then copy it to /root/.ssh/ with the right permissions. This happens on every container start.
Part 2: Docker Compose Configuration
Local Development Setup
For local development, I created a simple docker-compose.yml:
version: '3.8'
services: ai-dev: build: . container_name: ai-dev-local ports: - '2222:2222' # SSH access volumes: - ./ssh_keys:/root/.ssh/git_keys:ro - ~/.ssh:/root/.ssh/host_keys:ro - ai-dev-workspace:/workspace - ai-dev-history:/root/.zsh_history environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - OPENAI_API_KEY=${OPENAI_API_KEY} stdin_open: true tty: true
volumes: ai-dev-workspace: ai-dev-history:Volume strategy explained:
- Git SSH keys (
./ssh_keys): Your GitHub/GitLab keys for the container to clone repos - Host SSH keys (
~/.ssh): Read-only access to your local SSH config (optional) - Workspace (named volume): Persistent storage for projects
- History (named volume): Persist command history across rebuilds
Production Configuration for Hetzner
The production setup adds persistent volumes for AI tool configurations:
version: '3.8'
services: ai-dev: build: . container_name: ai-dev-environment ports: - '2222:2222' volumes: # SSH authorization - ./authorized_keys:/tmp/authorized_keys:ro
# Git SSH keys for cloning repos - ./ssh_keys:/root/.ssh/git_keys:ro
# Persistent workspace and configs - ai-dev-workspace:/workspace - ai-dev-claude-config:/root/.claude - ai-dev-codex-config:/root/.codex - ai-dev-history:/root/.zsh_history
environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - OPENAI_API_KEY=${OPENAI_API_KEY} restart: unless-stopped stdin_open: true tty: true
volumes: ai-dev-workspace: driver: local ai-dev-claude-config: driver: local ai-dev-codex-config: driver: local ai-dev-history: driver: localCritical addition: Persistent volumes for ~/.claude and ~/.codex. Without these, you’d lose your AI tool configurations (conversation history, preferences, cached models) on every rebuild.
Environment Variables
Create a .env file (never commit this!):
ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxOPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGet your keys from:
- Anthropic: https://console.anthropic.com/
- OpenAI: https://platform.openai.com/api-keys
Part 3: SSH Key Management
This was the trickiest part. The setup uses two different SSH keys:
Your Mac ──(hetzner_ai_dev)──▶ Container ──(id_ed25519)──▶ GitHub SSH access git operationsKey 1: Container Access Key
Generate a key for accessing the container:
ssh-keygen -t ed25519 -f ~/.ssh/hetzner_ai_dev -C "hetzner-ai-dev"Add the public key to authorized_keys:
cat ~/.ssh/hetzner_ai_dev.pub >> authorized_keysKey 2: GitHub Access Key
This key lives inside the container and authenticates git operations:
Add ssh_keys/id_ed25519.pub to your GitHub account.
Multi-Device Access
To access from your phone (Termius):
- In Termius: Create a new ED25519 key
- Export the public key
- Add it to
authorized_keys:
echo "ssh-ed25519 AAAA...your-phone-key... phone-termius" >> authorized_keys- Redeploy the container
Now both your Mac and phone can SSH in using their respective private keys.
Part 4: Deploying to Hetzner Cloud
Initial Server Setup
First, create a server on Hetzner:
- Image: Ubuntu 24.04
- Type: CPX11 (2 vCPU, 2GB RAM) - $5/month is enough
- Location: Choose closest to you
- SSH Key: Upload your
hetzner_ai_dev.pub
Once the server is running, install Docker and security tools:
#!/bin/bashset -e
echo "🔧 Updating system..."apt-get update && apt-get upgrade -y
echo "🐳 Installing Docker..."curl -fsSL https://get.docker.com -o get-docker.shsh get-docker.shrm get-docker.sh
echo "🐳 Installing Docker Compose..."apt-get install -y docker-compose-plugin
echo "🔒 Setting up UFW firewall..."ufw --force enableufw default deny incomingufw default allow outgoingufw allow 22/tcp # Standard SSHufw allow 2222/tcp # Container SSHufw allow 80/tcp # HTTP (future use)ufw allow 443/tcp # HTTPS (future use)
echo "🛡️ Installing fail2ban..."apt-get install -y fail2bansystemctl enable fail2bansystemctl start fail2ban
echo "✅ Server setup complete!"Run it once:
ssh -i ~/.ssh/hetzner_ai_dev root@YOUR_SERVER_IP 'bash -s' < scripts/hetzner-setup.shThe Deployment Script
I automated deployment with a single-command script:
#!/bin/bashset -e
# Color codes for pretty outputGREEN='\033[0;32m'YELLOW='\033[1;33m'RED='\033[0;31m'NC='\033[0m'
# ConfigurationHETZNER_IP="${HETZNER_IP}"HETZNER_USER="${HETZNER_USER:-root}"HETZNER_SSH_KEY="${HETZNER_SSH_KEY:-$HOME/.ssh/hetzner_ai_dev}"REMOTE_DIR="${REMOTE_DIR:-/root/agent-container}"
# Validate inputsif [ -z "$HETZNER_IP" ]; then echo -e "${RED}Error: HETZNER_IP not set${NC}" echo "Usage: HETZNER_IP=<ip> ./scripts/deploy.sh" exit 1fi
if [ ! -f "$HETZNER_SSH_KEY" ]; then echo -e "${RED}Error: SSH key not found at $HETZNER_SSH_KEY${NC}" exit 1fi
# Check for .env fileif [ ! -f ".env" ]; then echo -e "${RED}Error: .env file not found${NC}" echo "Create one from .env.example and add your API keys" exit 1fi
SSH_OPTS="-i $HETZNER_SSH_KEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
echo -e "${GREEN}=========================================="echo "🚀 Deploying AI Dev Environment"echo "=========================================="echo "Server: $HETZNER_USER@$HETZNER_IP"echo "SSH Key: $HETZNER_SSH_KEY"echo "Remote Dir: $REMOTE_DIR"echo -e "==========================================${NC}"
# Create remote directoryecho -e "${GREEN}📁 Creating remote directory...${NC}"ssh ${SSH_OPTS} "${HETZNER_USER}@${HETZNER_IP}" "mkdir -p ${REMOTE_DIR}"
# Sync filesecho -e "${GREEN}📦 Syncing files...${NC}"rsync -avz --progress \ -e "ssh ${SSH_OPTS}" \ --exclude '.git' \ --exclude 'node_modules' \ --exclude '.DS_Store' \ ./ "${HETZNER_USER}@${HETZNER_IP}:${REMOTE_DIR}/"
# Set SSH key permissionsecho -e "${GREEN}🔧 Setting permissions...${NC}"ssh ${SSH_OPTS} "${HETZNER_USER}@${HETZNER_IP}" "chmod 600 ${REMOTE_DIR}/ssh_keys/* 2>/dev/null || true"
# Check for --no-cache flagBUILD_FLAGS="--build"if [ "$1" == "--no-cache" ] || [ "$NO_CACHE" == "1" ]; then echo -e "${YELLOW}🔄 Building with --no-cache (full rebuild)...${NC}" BUILD_FLAGS="--build --no-cache"fi
# Build and start containerecho -e "${GREEN}🐳 Building and starting container...${NC}"ssh ${SSH_OPTS} "${HETZNER_USER}@${HETZNER_IP}" "cd ${REMOTE_DIR} && docker compose -f docker-compose.prod.yml up -d ${BUILD_FLAGS}"
echo ""echo -e "${GREEN}=============================================="echo "✅ Deployment complete!"echo "=============================================="echo "Connect: ssh -i $HETZNER_SSH_KEY -p 2222 root@${HETZNER_IP}"echo -e "==============================================\n${NC}"Deploy with one command:
HETZNER_IP=123.45.67.89 ./scripts/deploy.shFor a fresh build (no cache):
HETZNER_IP=123.45.67.89 ./scripts/deploy.sh --no-cache# orNO_CACHE=1 HETZNER_IP=123.45.67.89 ./scripts/deploy.shThe script:
- Validates that you have your
.envfile - Creates the remote directory
- Syncs all files via rsync (excludes .git, node_modules)
- Sets proper permissions on SSH keys
- Builds and starts the Docker container
- Shows connection command
Part 5: SSH Configuration for Easy Access
Typing ssh -i ~/.ssh/hetzner_ai_dev -p 2222 [email protected] gets old fast. Create an SSH config:
Host hetzner HostName 123.45.67.89 User root IdentityFile ~/.ssh/hetzner_ai_dev StrictHostKeyChecking no UserKnownHostsFile /dev/null
Host ai-dev HostName 123.45.67.89 Port 2222 User root IdentityFile ~/.ssh/hetzner_ai_dev StrictHostKeyChecking no UserKnownHostsFile /dev/null
Host ai-dev-local HostName localhost Port 2222 User root IdentityFile ~/.ssh/hetzner_ai_dev StrictHostKeyChecking no UserKnownHostsFile /dev/nullNow you can simply:
ssh hetzner # Connect to host serverssh ai-dev # Connect to remote containerssh ai-dev-local # Connect to local containerPart 6: Daily Usage and Workflow
Connecting and Starting Work
# Connect to the containerssh ai-dev
# You'll land in /root - navigate to workspacecd /workspace
# Clone a project (this is persistent!)cd your-projectImportant filesystem concept: When you SSH in, you land in /root (the root user’s home directory). Running ls shows what’s in that directory:
/ ← filesystem root├── root/ ← where you land (home directory)│ ├── .claude/ ← Claude config (persistent volume)│ ├── .codex/ ← Codex config (persistent volume)│ └── .zshrc ← shell config├── workspace/ ← YOUR PROJECTS GO HERE├── home/├── etc/└── ...To see all directories at the filesystem root:
ls /Using Claude Code
cd /workspace/your-project
# Start Claude Codeclaude
# Or use the aliasccClaude Code will:
- Read your codebase
- Understand context across files
- Make multi-file edits
- Run tests and iterate
- Commit changes
Example session:
You: Refactor the authentication module to use JWT tokens instead of sessions
Claude: I'll help refactor the authentication to use JWT. Let me first examine the current implementation...
[Claude reads auth.js, user.js, middleware/auth.js]
Claude: I've identified the changes needed. I'll:1. Install jsonwebtoken package2. Update the login endpoint to issue JWT tokens3. Replace session middleware with JWT verification4. Update user model to store refresh tokens
Shall I proceed?
You: Yes
[Claude makes the changes, runs tests, fixes issues, commits]
Claude: ✓ Refactoring complete. All 24 tests passing.Using OpenAI Codex
# Start Codex in your projectcodex
# Natural language commands> Create a React component for a user profile card> Add TypeScript types for the API responses> Write unit tests for the validator functionsUsing OpenCode
# Start OpenCodeopencode
# Or specific modelopencode --model gpt-4Listing Services and Processes
To see what’s running inside the container:
# View all processesps aux
# Interactive process viewerhtop
# Check if AI tools are availablewhich claude codex opencodeFrom your Mac, check the container status:
# Check if container is runningssh hetzner "docker ps"
# View container logsssh hetzner "docker logs ai-dev-environment"
# Check processes inside containerssh ai-dev "ps aux"Working with Hidden Files
When listing files, use:
ls # Regular filesls -a # Show hidden files (starting with .)ls -la # Detailed list with hidden filesls -lah # Human-readable sizes
# Common hidden files you'll see:# .git - Git repository# .env - Environment variables# .gitignore - Git ignore rules# .claude - Claude configurationPart 7: Persistence and Data Management
What Persists Across Rebuilds?
Persistent (Docker volumes):
/workspace- All your projects and code/root/.claude- Claude Code configuration and history/root/.codex- Codex configuration/root/.zsh_history- Your command history
Ephemeral (lost on rebuild):
- Files created in
/root(except those above) - System packages installed with
apt-get(unless added to Dockerfile) - Temporary files in
/tmp
Backing Up Your Work
The volumes live on the Hetzner server. To back up:
# From your Macssh hetzner "docker run --rm -v ai-dev-workspace:/data -v /root/backups:/backup ubuntu tar czf /backup/workspace-$(date +%Y%m%d).tar.gz -C /data ."
# Download the backupOr use git for your projects:
# Inside containercd /workspace/your-projectgit add .git commit -m "Progress checkpoint"git pushUpdating the Container
When you modify the Dockerfile or add new tools:
# Deploy with no-cache to rebuild everythingNO_CACHE=1 HETZNER_IP=123.45.67.89 ./scripts/deploy.shYour volumes (workspace, configs) remain intact!
Part 8: Advanced Tips and Tricks
1. Using tmux for Multiple Sessions
tmux is pre-installed. Use it to run multiple AI tools simultaneously:
# Start tmuxtmux
# Create new pane: Ctrl+b then "# Switch panes: Ctrl+b then arrow keys# New window: Ctrl+b then c# Switch windows: Ctrl+b then window number
# Example: Run Claude in one pane, Codex in another# Pane 1: claude# Pane 2 (Ctrl+b "): codex2. Git Configuration
Set your git identity inside the container:
git config --global user.name "Your Name"git config --global core.editor "vim"Or mount a .gitconfig in the Dockerfile:
COPY .gitconfig /root/.gitconfig3. Custom Aliases
Add more aliases to .zshrc:
# Project shortcutsalias work='cd /workspace'alias proj='cd /workspace/my-main-project'
# Git workflowsalias gpo='git push origin'alias gpl='git pull origin'alias gco='git checkout'alias gcb='git checkout -b'
# Docker (from host)alias dps='docker ps'alias dlogs='docker logs -f ai-dev-environment'4. Monitoring Resource Usage
Inside the container:
# Memory usagefree -h
# Disk usagedf -h
# Top processeshtopFrom the host:
ssh hetzner "docker stats ai-dev-environment"5. Setting Resource Limits
If running multiple containers or large workloads, add to docker-compose.prod.yml:
services: ai-dev: # ... other config ... deploy: resources: limits: memory: 4G cpus: '2' reservations: memory: 2G cpus: '1'6. Automatic Workspace Switching
Add to .zshrc to always start in your workspace:
# Auto-navigate to workspace on loginif [[ $PWD == $HOME ]]; then cd /workspacefi7. Port Forwarding for Web Projects
If your AI tool generates a web app, forward the port:
services: ai-dev: ports: - '2222:2222' - '3000:3000' # React/Next.js - '8080:8080' # Common dev serverThen access at http://123.45.67.89:3000
8. Environment-Specific Configurations
Use different .env files for local vs production:
# Localcp .env.local .envdocker compose up -d
# Productioncp .env.prod .envHETZNER_IP=123.45.67.89 ./scripts/deploy.shPart 9: Troubleshooting Common Issues
Issue 1: “Permission denied (publickey)”
Symptoms: Can’t SSH into container
Causes:
- Wrong SSH key
authorized_keyshas wrong permissions- Key not in
authorized_keys
Solutions:
# Verify key is in authorized_keyscat authorized_keys | grep "$(cat ~/.ssh/hetzner_ai_dev.pub)"
# Check from host serverssh hetzner "docker exec ai-dev-environment cat /root/.ssh/authorized_keys"
# Check permissionsssh hetzner "docker exec ai-dev-environment ls -la /root/.ssh/authorized_keys"# Should show: -rw------- 1 root root (600 permissions)
# Force redeployHETZNER_IP=123.45.67.89 ./scripts/deploy.sh --no-cacheIssue 2: API Keys Not Working
Symptoms: AI tools can’t authenticate
Solutions:
# Check if env vars are set inside containerssh ai-dev "echo \$ANTHROPIC_API_KEY"
# Verify .env file existsls -la .env
# Check for trailing spaces in .envcat -A .env # Should not show extra spaces
# Rebuild to reload env varsHETZNER_IP=123.45.67.89 ./scripts/deploy.shIssue 3: Container Won’t Start
Symptoms: Container exits immediately
Solutions:
# Check logsssh hetzner "docker logs ai-dev-environment"
# Common issues:# - Port 2222 already in use# - Missing .env file# - Syntax error in docker-compose.yml
# Verify compose filedocker compose -f docker-compose.prod.yml config
# Try running interactivelyssh hetzner "docker run -it --rm $(docker build -q .)"Issue 4: Lost Work After Rebuild
Symptoms: Files disappeared after rebuilding container
Cause: Files were stored outside /workspace
Prevention:
# ALWAYS work in /workspacecd /workspace
# Check what's in volumesssh hetzner "docker volume ls"ssh hetzner "docker volume inspect ai-dev-workspace"Issue 5: Slow Performance
Symptoms: AI tools running slowly
Solutions:
# Check system resourcesssh ai-dev "free -h && df -h"
# Check Docker statsssh hetzner "docker stats ai-dev-environment --no-stream"
# Upgrade Hetzner instance# CPX11 (2GB RAM) → CPX21 (4GB RAM) → CPX31 (8GB RAM)
# Clean up Dockerssh hetzner "docker system prune -a"Issue 6: Git Clone Fails
Symptoms: “Permission denied” when cloning private repos
Cause: Git SSH key not configured
Solutions:
# Verify git SSH key is mountedssh ai-dev "ls -la /root/.ssh/git_keys/"
# Test GitHub connection
# Add GitHub key to ssh agentssh ai-deveval "$(ssh-agent -s)"ssh-add /root/.ssh/git_keys/id_ed25519
# Or create ~/.ssh/configcat > ~/.ssh/config << EOFHost github.com IdentityFile /root/.ssh/git_keys/id_ed25519 StrictHostKeyChecking noEOFPart 10: Real-World Usage Examples
Example 1: Building a Full-Stack App
# Connect to containerssh ai-devcd /workspace
# Start Claude Codeclaude
# Natural language promptYou: Create a full-stack todo app with:- Next.js 14 frontend- Prisma + SQLite backend- shadcn/ui components- CRUD operations- TypeScript throughout
[Claude creates the project structure, installs dependencies, generates components, sets up database, writes API routes]
# Test locally (if you forwarded port 3000)cd todo-appnpm run dev
# Visit http://123.45.67.89:3000Example 2: Refactoring Legacy Code
# Clone existing projectcd /workspacecd legacy-app
# Start Codexcodex
You: Analyze this codebase and identify code smells
Codex: I've found:- 15 functions over 100 lines- Duplicate code in user auth (3 places)- No error handling in API calls- Missing TypeScript types
You: Refactor the authentication module
[Codex extracts auth logic, adds proper error handling, adds TypeScript types, writes tests]
# Commit changesgit checkout -b refactor/authgit add .git commit -m "Refactor: Extract and type auth module"git push origin refactor/authExample 3: Multi-AI Workflow
Use tmux to run multiple AI tools:
ssh ai-devtmux
# Pane 1: Claude for architectureclaudeYou: Design a microservices architecture for an e-commerce platform
# Split pane (Ctrl+b ")# Pane 2: Codex for implementationcodexYou: Implement the product service API
# Split pane again (Ctrl+b %)# Pane 3: OpenCode for testsopencodeYou: Generate integration tests for the product service
# Switch between panes with Ctrl+b arrow keysExample 4: Documentation Generation
cd /workspace/my-libraryclaude
You: Generate comprehensive documentation for this library:- README with examples- API documentation- Contributing guide- JSDoc comments for all functions
[Claude analyzes code, generates docs, adds examples]
# Review and commitgit add .git commit -m "docs: Add comprehensive documentation"git pushPart 11: Cost Analysis
Infrastructure Costs
Hetzner Cloud (CPX11):
- 2 vCPUs, 2GB RAM, 40GB SSD
- €4.51/month (~$5/month)
- 20TB traffic included
Hetzner Cloud (CPX21 - recommended for heavy use):
- 3 vCPUs, 4GB RAM, 80GB SSD
- €8.21/month (~$9/month)
Hetzner Cloud (CPX31 - for large projects):
- 4 vCPUs, 8GB RAM, 160GB SSD
- €15.40/month (~$17/month)
API Costs
Anthropic Claude Code:
- Sonnet: 15/M tokens (output)
- Opus: 75/M tokens (output)
- Typical session: 2.00
OpenAI Codex:
- GPT-4: 0.06/1K tokens (output)
- GPT-3.5: 0.002/1K tokens (output)
- Typical session: 1.00
Total monthly estimate:
- Server: $9/month (CPX21)
- AI usage (moderate): $50-100/month
- Total: ~$60-110/month
Much cheaper than a GitHub Copilot subscription + separate AI tool subscriptions + local resource costs!
Part 12: Security Considerations
SSH Security
✅ What we did:
- Key-based authentication only (no passwords)
- Non-standard SSH port (2222)
- fail2ban for brute-force protection
- UFW firewall
❌ Additional hardening (optional):
# Disable root login (after creating non-root user)sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
# Allow only specific IPsufw delete allow 2222ufw allow from YOUR_HOME_IP to any port 2222ufw allow from YOUR_OFFICE_IP to any port 2222API Key Security
✅ What we did:
.envfile (gitignored)- Environment variables (not hardcoded)
❌ Additional security:
# Use Docker secrets (production)docker secret create anthropic_key ./anthropic_key.txtContainer Isolation
The container runs as root, but it’s isolated from the host:
- Separate network namespace
- Separate filesystem
- No privileged access to host
For even more isolation:
services: ai-dev: security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICERegular Updates
# Update container base image# Edit Dockerfile: FROM ubuntu:24.04 -> ubuntu:24.10NO_CACHE=1 HETZNER_IP=123.45.67.89 ./scripts/deploy.sh
# Update AI tools# They're npm packages, so they update automatically when rebuildingPart 13: Future Enhancements
Ideas to Extend This Setup
1. Multiple Environments
# docker-compose.staging.yml# docker-compose.prod.yml2. Code Server (VS Code in Browser)
Add to Dockerfile:
RUN curl -fsSL https://code-server.dev/install.sh | shAccess VS Code at http://123.45.67.89:8080
3. Database Containers
# Add to docker-compose.prod.ymlservices: ai-dev: # ... existing config ...
postgres: image: postgres:16 volumes: - postgres-data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes: postgres-data:4. Monitoring and Metrics
services: prometheus: image: prom/prometheus ports: - '9090:9090'
grafana: image: grafana/grafana ports: - '3001:3000'5. Automated Backups
# Add to crontab on Hetzner server0 2 * * * docker run --rm -v ai-dev-workspace:/data -v /root/backups:/backup ubuntu tar czf /backup/workspace-$(date +\%Y\%m\%d).tar.gz -C /data .6. CI/CD Integration
name: Deploy to Hetzner
on: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Deploy env: HETZNER_IP: ${{ secrets.HETZNER_IP }} HETZNER_SSH_KEY: ${{ secrets.HETZNER_SSH_KEY }} run: ./scripts/deploy.shConclusion: The Power of Containerized AI Development
After several weeks using this setup, here’s what I’ve gained:
Productivity wins:
- 🚀 Access my dev environment from any device
- 💾 Never lose configurations or project state
- 🔄 Consistent environment (no “works on my machine”)
- 🤝 Easy collaboration (share SSH access)
Cost savings:
- 💰 $9/month server vs expensive local GPU
- ⚡ Offload AI computation to cloud
- 📦 No local resource consumption
Workflow improvements:
- 🎯 All AI tools in one place
- 📱 Code from phone during commute
- 🌍 Same environment at office, home, travel
- 🔐 Secure, isolated, backed up
The bottom line: This setup transformed how I work with AI coding assistants. Instead of juggling tools across machines, I have a single, always-available, persistent environment that follows me everywhere.
The initial setup takes a few hours, but the daily workflow is seamless. One SSH command and you’re in your fully-configured AI development environment, with all your projects, history, and tools exactly as you left them.
Complete File Listing
For reference, here’s the final project structure:
agent-container/├── Dockerfile├── docker-compose.yml├── docker-compose.prod.yml├── .env.example├── .env├── .gitignore├── .zshrc├── .gitconfig├── authorized_keys├── ssh_config.example├── README.md├── HETZNER.md├── blogpost.md (this file)└── scripts/ ├── deploy.sh ├── entrypoint.sh ├── hetzner-setup.sh └── start.sh└── ssh_keys/ ├── config ├── id_ed25519 ├── id_ed25519.pub └── known_hostsQuick Start Command Summary
# One-time setupgit clone https://github.com/your-username/agent-container.gitcd agent-containercp .env.example .env# Edit .env with your API keysssh-keygen -t ed25519 -f ~/.ssh/hetzner_ai_devcat ~/.ssh/hetzner_ai_dev.pub >> authorized_keys
# Deploy to Hetzner (first time)ssh -i ~/.ssh/hetzner_ai_dev root@YOUR_IP 'bash -s' < scripts/hetzner-setup.shHETZNER_IP=YOUR_IP ./scripts/deploy.sh
# Daily usagessh ai-devcd /workspaceclaude # or codex, or opencode
# Update deploymentHETZNER_IP=YOUR_IP ./scripts/deploy.sh
# Force rebuildNO_CACHE=1 HETZNER_IP=YOUR_IP ./scripts/deploy.shConclusion
The dev container provides natural guardrails to keep your AI-assisted coding efficient, secure, and consistent. With everything set up, you can focus on building great software with the help of powerful AI tools, no matter where you are or what device you’re using. Happy coding!
Any Questions?
Contact me on any of my communication channels: