Skip to content

Latest commit

 

History

History
197 lines (146 loc) · 7.52 KB

File metadata and controls

197 lines (146 loc) · 7.52 KB

Architecture

This document explains how the devtools container environment is assembled.

Overview

┌─────────────────────────────────────────────────────────────┐
│                         Host                                 │
│  ┌─────────────────┐  ┌─────────────────┐                   │
│  │   acl-proxy     │  │  run.sh         │                   │
│  │  (port 8881)    │  │  (nsenter)      │                   │
│  └────────┬────────┘  └────────┬────────┘                   │
│           │                    │ iptables via nsenter       │
│  ┌────────┴────────────────────┴────────────────────────┐   │
│  │                    Container                          │   │
│  │  ┌──────────────────────────────────────────────┐    │   │
│  │  │ Network Namespace                             │    │   │
│  │  │  - iptables redirects 80/443 → proxy         │    │   │
│  │  │  - Default deny outbound                     │    │   │
│  │  └──────────────────────────────────────────────┘    │   │
│  │                                                       │   │
│  │  /home/$USER ──────────── workspace/ (bind mount)     │   │
│  │  /usr, /lib, /etc ─────── host dirs (read-only)      │   │
│  │  ~/.bashrc, etc ───────── overlay/ (overrides)       │   │
│  └───────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Directory Layout

devtools/
├── container/
│   ├── Dockerfile        # Minimal image (just creates user)
│   ├── build.sh          # Build with current UID/GID
│   ├── run.sh            # Launch container with mounts
│   └── overlay/          # Files that override host mounts
│       ├── etc/          # System config overrides
│       └── home/user/    # User config overrides (placeholder)
├── workspace/            # Becomes $HOME inside container
│   └── worktrees/        # Git worktrees live here
├── proxy/
│   └── acl-proxy.toml    # URL allowlist config
└── host/
    ├── bashrc.additions  # Add to ~/.bashrc
    └── bash_completion.d/

Container Image

The Dockerfile is intentionally minimal:

  • Base: Rocky Linux 10
  • Creates user matching host UID/GID
  • No tools installed (uses host's via bind mounts)

This keeps the image small and ensures tools are always up-to-date with the host.

Mount Strategy

Workspace Mount

workspace/ is mounted as $HOME inside the container. This is where all your files live.

System Mounts (read-only)

/usr, /lib, /lib64, /etc are mounted from the host. This provides:

  • All host-installed tools (git, cargo, node, etc.)
  • System libraries
  • CA certificates (including acl-proxy CA)

Overlay Mounts

Files in overlay/ are mounted last, overriding anything from host or workspace:

  • Custom .bashrc for container-specific shell setup
  • SSH config (without copying private keys)
  • Tool configurations Rename overlay/home/user/ to your actual username so paths match /home/<username> inside the container.

State Mounts

Certain host directories are mounted 1:1 to preserve state:

  • ~/.cargo, ~/.rustup - Rust toolchain
  • ~/.claude, ~/.codex, ~/.pi - AI assistant state
  • ~/.local/bin - User binaries

Transparent Proxy

The transparent proxy is an optional add-on. See docs/PROXY.md for setup details.

When PROXY=1 is set:

  1. Container starts without network capabilities
  2. Host uses nsenter to enter container's network namespace
  3. iptables rules redirect 80→8881, 443→8889
  4. Default deny blocks any non-proxied traffic
  5. User inside container cannot modify rules

This provides secure URL allowlisting without giving the container elevated privileges.

Path Preservation

Git worktree stores absolute paths in .git metadata files. To ensure paths are consistent between host and container, we use a bind mount (not a symlink).

Why not a symlink? When you run wt switch --create <branch> on the host, git resolves symlinks and stores the real path. A symlink at ~/worktrees~/devtools/workspace/worktrees would result in git storing ~/devtools/workspace/worktrees/... in metadata, breaking consistency.

Solution: Bind mount makes both paths identical at the kernel level:

Host:      ~/worktrees ←bind mount→ ~/devtools/workspace/worktrees
Container: ~/worktrees ← workspace mounted as $HOME

Setup (in /etc/fstab):

/home/user/devtools/workspace/worktrees /home/user/worktrees none bind 0 0

Now /home/$USER/worktrees/project/main is the same path in both environments, and git metadata stores the correct path.

Worktree Workflow

The container is designed for a git-worktree workflow:

# Clone a repo (creates bare repo with worktrees)
wt clone git@github.com:user/project.git project

# Create a worktree for a feature
cd project
wt switch --create feature/my-work

# Files live at: workspace/worktrees/project/feature/my-work

The wt tool (worktrunk) manages worktrees. The overlay .bashrc integrates it and overrides wt clone to run git clone --bare.

Adapting for Docker

This setup uses Podman but can be adapted for Docker with these changes:

User namespace mapping

Podman:

podman run --userns=keep-id ...

Docker (rootless mode, or with userns-remap configured):

docker run --user $(id -u):$(id -g) ...

SELinux labels

The :Z suffix on volume mounts is for SELinux relabeling. On systems without SELinux, remove it:

# Podman on SELinux systems
-v $WORKSPACE:$HOME:Z

# Docker or non-SELinux
-v $WORKSPACE:$HOME

Host gateway address

Podman uses host.containers.internal (169.254.1.2) to reach the host.

Docker on Linux requires adding the host explicitly:

docker run --add-host=host.docker.internal:host-gateway ...

Update proxy-iptables.sh to use Docker's address:

PROXY_IP="host-gateway"  # or the actual host IP

On Docker Desktop (macOS/Windows), host.docker.internal works automatically.

nsenter for iptables

The nsenter approach works identically - get the container PID and enter its network namespace:

# Docker
PID=$(docker inspect --format '{{.State.Pid}}' <container>)
sudo nsenter -t $PID -n iptables ...

Summary of changes to run.sh

Podman Docker
podman run docker run
--userns=keep-id --user $(id -u):$(id -g)
:Z volume suffix remove or use :z
host.containers.internal --add-host=host.docker.internal:host-gateway
podman inspect docker inspect