This document provides a comprehensive overview of ContainAI's architecture, including the system container model, Sysbox runtime, SSH-based access, and security boundaries.
- System Container Overview
- Why Sysbox?
- Architecture Layers
- Container Lifecycle
- SSH Connection Flow
- Systemd Service Dependencies
- Docker-in-Docker Architecture
- Component Architecture
- Modular Library Structure
- Data Flow
- Volume Architecture
- Security Model
- Design Decisions
- References
ContainAI uses system containers - VM-like Docker containers that run systemd as PID 1 and can host multiple services. Unlike traditional application containers that run a single process, system containers provide:
| Capability | Application Container | System Container |
|---|---|---|
| Init system | None (process as PID 1) | systemd as PID 1 |
| Multiple services | No | Yes (sshd, dockerd, etc.) |
| Docker-in-Docker | Requires --privileged |
Works unprivileged via Sysbox |
| User namespace isolation | Manual configuration | Automatic via Sysbox |
| SSH access | Port mapping only | VS Code Remote-SSH compatible |
| Service management | Not available | systemctl commands work |
This makes system containers ideal for AI coding agents that need to:
- Build and run containers (Docker-in-Docker)
- Connect via SSH for VS Code Remote-SSH and agent forwarding
- Run background services
- Access a full Linux environment
Sysbox is a container runtime that enables system containers with enhanced isolation:
Sysbox automatically maps container root (UID 0) to an unprivileged host user. No manual /etc/subuid or /etc/subgid configuration required.
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#1a1a2e',
'primaryTextColor': '#ffffff',
'primaryBorderColor': '#16213e',
'secondaryColor': '#0f3460',
'tertiaryColor': '#1a1a2e',
'lineColor': '#a0a0a0',
'textColor': '#ffffff',
'background': '#0d1117'
}}}%%
flowchart LR
subgraph Container["System Container"]
CRoot["root (UID 0)"]
CAgent["agent (UID 1000)"]
end
subgraph Sysbox["Sysbox Runtime"]
Userns["User Namespace<br/>Mapping"]
end
subgraph Host["Host System"]
HRoot["UID 100000+"]
HAgent["UID 101000+"]
end
CRoot -->|"mapped by"| Userns
CAgent -->|"mapped by"| Userns
Userns -->|"unprivileged"| HRoot
Userns -->|"unprivileged"| HAgent
style Container fill:#1a1a2e,stroke:#16213e,color:#fff
style Sysbox fill:#0f3460,stroke:#16213e,color:#fff
style Host fill:#16213e,stroke:#0f3460,color:#fff
Sysbox virtualizes /proc and /sys so containers see only their own resources, not the host's. This enables:
systemctlcommands to work correctly- Accurate resource reporting inside containers
- Isolation from host process information
With Sysbox, containers can run Docker without --privileged:
- The inner Docker daemon runs with its own isolated filesystem
- No access to host Docker socket
- No capability escalation to host
ContainAI uses a dedicated Docker installation separate from Docker Desktop:
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#1a1a2e',
'primaryTextColor': '#ffffff',
'primaryBorderColor': '#16213e',
'secondaryColor': '#0f3460',
'tertiaryColor': '#1a1a2e',
'lineColor': '#a0a0a0',
'textColor': '#ffffff',
'background': '#0d1117'
}}}%%
flowchart TB
subgraph Host["Host System"]
DD["Docker Desktop<br/>(if present, NOT used)"]
CAI["ContainAI docker-ce<br/>Socket: /var/run/containai-docker.sock<br/>Runtime: sysbox-runc"]
end
subgraph SysContainer["System Container (sysbox-runc)"]
Systemd["PID 1: systemd"]
SSHD["ssh.service<br/>(port 22 -> 2300-2500)"]
Dockerd["docker.service<br/>(inner Docker)"]
Init["containai-init.service<br/>(workspace setup)"]
end
subgraph Inner["Inner Containers (DinD)"]
App1["Agent builds"]
App2["Agent runs"]
end
CAI -->|"--runtime=sysbox-runc"| SysContainer
Systemd --> SSHD
Systemd --> Dockerd
Systemd --> Init
Dockerd --> Inner
style Host fill:#1a1a2e,stroke:#16213e,color:#fff
style SysContainer fill:#0f3460,stroke:#16213e,color:#fff
style Inner fill:#16213e,stroke:#0f3460,color:#fff
- Docker Desktop does not support Sysbox - The
sysbox-runcruntime is not available in Docker Desktop - System containers need Sysbox - For systemd, DinD without
--privileged, and VM-like behavior - No conflicts - ContainAI uses its own socket (
/var/run/containai-docker.sock) and data directory
The dedicated docker-ce instance (/etc/containai/docker/daemon.json):
{
"runtimes": {
"sysbox-runc": {
"path": "/usr/bin/sysbox-runc"
}
},
"default-runtime": "sysbox-runc",
"hosts": ["unix:///var/run/containai-docker.sock"],
"data-root": "/var/lib/containai-docker"
}%%{init: {'theme': 'base', 'themeVariables': {
'background': '#f5f5f5',
'actorBkg': '#1a1a2e',
'actorTextColor': '#ffffff',
'actorBorder': '#16213e',
'actorLineColor': '#606060',
'signalColor': '#606060',
'signalTextColor': '#1a1a2e',
'labelBoxBkgColor': '#0f3460',
'labelBoxBorderColor': '#16213e',
'labelTextColor': '#ffffff',
'loopTextColor': '#1a1a2e',
'noteBkgColor': '#0f3460',
'noteTextColor': '#ffffff',
'noteBorderColor': '#16213e',
'activationBkgColor': '#16213e',
'activationBorderColor': '#0f3460',
'sequenceNumberColor': '#1a1a2e'
}}}%%
sequenceDiagram
participant User
participant CLI as cai CLI
participant Docker as ContainAI Docker
participant Container as System Container
participant Systemd as systemd (PID 1)
participant SSH as sshd
participant Agent as AI Agent
User->>CLI: cai run /workspace
CLI->>Docker: docker run --runtime=sysbox-runc
Docker->>Container: Create container
Container->>Systemd: Start systemd as PID 1
Note over Container: ssh-keygen.service<br/>generates host keys
Systemd->>Container: Start containai-init.service (oneshot)
Note over Container: Workspace setup complete
par Service Startup
Systemd->>SSH: Start ssh.service
Systemd->>Container: Start docker.service
end
CLI->>CLI: Allocate port (2300-2500)
CLI->>Container: Inject SSH public key
CLI->>SSH: Wait for sshd ready
CLI->>CLI: Update known_hosts
CLI->>SSH: SSH connect
SSH->>Agent: Run agent command
Agent-->>User: Interactive session
Note over User: Session ends or user runs cai stop
User->>CLI: cai stop
CLI->>Docker: docker stop (SIGRTMIN+3)
Docker->>Systemd: Graceful shutdown signal
Systemd->>SSH: Stop ssh.service
Systemd->>Container: Stop docker.service
Docker->>Container: Container stopped
Note over CLI: SSH config retained<br/>(cleaned on --restart/removal)
- Container Creation:
docker run --runtime=sysbox-runccreates the system container - Systemd Boot: systemd starts as PID 1, initializes the service manager
- SSH Key Generation:
ssh-keygen.servicegenerates host keys on first boot (not baked into image) - Workspace Setup:
containai-init.service(oneshot) runs first, creating symlinks and loading environment - Service Startup:
ssh.serviceanddocker.servicestart after init completes - SSH Connection: CLI allocates port, injects key, waits for sshd, then connects
Each container receives a short name in the format {repo}-{branch_leaf} (max 24 characters) and an RFC 1123 compliant hostname that matches a sanitized version of its name.
Hostname Sanitization Rules:
The _cai_sanitize_hostname() function ensures hostnames comply with RFC 1123:
| Rule | Example |
|---|---|
| Convert to lowercase | MyProject → myproject |
| Replace underscores with hyphens | my_workspace → my-workspace |
| Remove invalid characters | app@v2.0 → appv20 |
| Collapse multiple hyphens | app--test → app-test |
| Remove leading/trailing hyphens | -app- → app |
| Truncate to 63 characters | Long names are shortened |
| Fallback if empty | Empty result becomes container |
Why This Matters:
- For broad DNS and network compatibility, ContainAI enforces RFC 1123-style hostnames
- Container names can include underscores and special characters that aren't valid hostnames
- The hostname is set via Docker's
--hostnameflag during container creation - Inside the container,
hostnamereturns this sanitized value
Example Transformations:
| Container Name | Hostname |
|---|---|
my_workspace-main |
my-workspace-main |
MyProject-Feature |
myproject-feature |
test__app |
test-app |
All container access uses SSH instead of docker attach or direct execution. This enables:
- VS Code Remote-SSH integration
- SSH agent forwarding
- Port tunneling for development
- Standard SSH tooling (scp, rsync)
%%{init: {'theme': 'base', 'themeVariables': {
'background': '#f5f5f5',
'actorBkg': '#1a1a2e',
'actorTextColor': '#ffffff',
'actorBorder': '#16213e',
'actorLineColor': '#606060',
'signalColor': '#606060',
'signalTextColor': '#1a1a2e',
'labelBoxBkgColor': '#0f3460',
'labelBoxBorderColor': '#16213e',
'labelTextColor': '#ffffff',
'loopTextColor': '#1a1a2e',
'noteBkgColor': '#0f3460',
'noteTextColor': '#ffffff',
'noteBorderColor': '#16213e',
'activationBkgColor': '#16213e',
'activationBorderColor': '#0f3460',
'sequenceNumberColor': '#1a1a2e'
}}}%%
sequenceDiagram
participant User
participant CLI as cai shell
participant SSHConfig as ~/.ssh/containai.d/
participant Port as Port Allocator
participant Container as Container:22
participant SSHD as sshd
User->>CLI: cai shell /workspace
CLI->>Port: Find available port (2300-2500)
Port-->>CLI: Port 2342
CLI->>Container: Inject public key to authorized_keys
CLI->>Container: Wait for sshd ready (retry with backoff)
CLI->>SSHConfig: Write containai-myproject.conf
Note over SSHConfig: Host containai-myproject<br/> HostName localhost<br/> Port 2342<br/> User agent<br/> IdentityFile ~/.config/containai/id_containai
CLI->>SSHD: ssh -p 2342 agent@localhost
SSHD-->>User: Interactive shell
Note over User: VS Code can also connect:<br/>code --remote ssh-remote+containai-myproject
| Component | Path | Purpose |
|---|---|---|
| Private Key | ~/.config/containai/id_containai |
ed25519 key for authentication |
| Public Key | ~/.config/containai/id_containai.pub |
Injected into containers |
| Config Directory | ~/.ssh/containai.d/ |
Per-container SSH configs |
| Known Hosts | ~/.config/containai/known_hosts |
Container host key verification |
| Port Range | 2300-2500 (configurable) | SSH port allocation range |
- Key-only authentication: Password auth disabled in sshd
- Host key verification: Each container generates unique host keys on first boot
- Automatic cleanup: Stale known_hosts entries removed on
--freshrestart - Port isolation: Each container gets a unique port from the configured range
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#1a1a2e',
'primaryTextColor': '#ffffff',
'primaryBorderColor': '#16213e',
'secondaryColor': '#0f3460',
'tertiaryColor': '#1a1a2e',
'lineColor': '#a0a0a0',
'textColor': '#ffffff',
'background': '#0d1117'
}}}%%
flowchart TD
subgraph Targets["Systemd Targets"]
LocalFS["local-fs.target"]
Network["network.target"]
MultiUser["multi-user.target"]
end
subgraph Services["ContainAI Services"]
SSHKeygen["ssh-keygen.service<br/>(Type=oneshot)<br/>Generates host keys"]
SSH["ssh.service<br/>(Type=notify)<br/>SSH daemon"]
DockerSvc["docker.service<br/>(Type=notify)<br/>Docker daemon"]
Init["containai-init.service<br/>(Type=oneshot)<br/>Workspace setup"]
end
LocalFS --> Init
Network --> Init
Init --> SSH
Init --> DockerSvc
SSHKeygen --> SSH
SSH --> MultiUser
DockerSvc --> MultiUser
style Targets fill:#1a1a2e,stroke:#16213e,color:#fff
style Services fill:#0f3460,stroke:#16213e,color:#fff
| Service | Type | Purpose |
|---|---|---|
ssh-keygen.service |
oneshot | Generate SSH host keys if missing (security: not baked into image) |
ssh.service |
notify | OpenSSH daemon for remote access |
docker.service |
notify | Inner Docker daemon for DinD |
containai-init.service |
oneshot | Volume structure, workspace symlinks, git config |
- Image:
/etc/systemd/system/(installed fromsrc/services/) - Drop-ins:
/etc/systemd/system/<service>.service.d/
Sysbox enables secure Docker-in-Docker without --privileged:
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#1a1a2e',
'primaryTextColor': '#ffffff',
'primaryBorderColor': '#16213e',
'secondaryColor': '#0f3460',
'tertiaryColor': '#1a1a2e',
'lineColor': '#a0a0a0',
'textColor': '#ffffff',
'background': '#0d1117'
}}}%%
flowchart TB
subgraph Host["Host System"]
HostDocker["ContainAI docker-ce<br/>Runtime: sysbox-runc"]
HostSocket["containai-docker.sock"]
end
subgraph SysContainer["System Container"]
InnerDocker["docker.service<br/>Runtime: sysbox-runc"]
InnerSocket["/var/run/docker.sock"]
InnerStorage["/var/lib/docker"]
end
subgraph InnerContainers["Inner Containers"]
Build["docker build -t myapp ."]
Run["docker run myapp"]
Compose["docker compose up"]
end
HostDocker -->|"creates"| SysContainer
HostSocket -.->|"NOT mounted"| SysContainer
InnerDocker --> InnerContainers
style Host fill:#1a1a2e,stroke:#16213e,color:#fff
style SysContainer fill:#0f3460,stroke:#16213e,color:#fff
style InnerContainers fill:#16213e,stroke:#0f3460,color:#fff
- Isolated Docker Daemon: The inner Docker runs with its own socket and storage
- No Host Socket: The host Docker socket is NOT mounted into containers
- Sysbox Runtime: Both outer and inner Docker use sysbox-runc for consistent isolation
- Nested User Namespaces: Each layer has its own UID mapping
Inside the system container (/etc/docker/daemon.json):
{
"runtimes": {
"sysbox-runc": {
"path": "/usr/bin/sysbox-runc"
}
},
"default-runtime": "sysbox-runc"
}%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#1a1a2e',
'primaryTextColor': '#ffffff',
'primaryBorderColor': '#16213e',
'secondaryColor': '#0f3460',
'tertiaryColor': '#1a1a2e',
'lineColor': '#a0a0a0',
'textColor': '#ffffff',
'background': '#0d1117'
}}}%%
flowchart LR
subgraph CLI["CLI Layer"]
direction TB
Main["src/containai.sh<br/>(entry point)"]
Cmds["Commands<br/>(run, shell, doctor, import)"]
end
subgraph Lib["Library Layer"]
direction TB
Core["core.sh<br/>(logging)"]
SSH["ssh.sh<br/>(SSH infrastructure)"]
Container["container.sh<br/>(lifecycle)"]
Config["config.sh<br/>(TOML parsing)"]
Doctor["doctor.sh<br/>(health checks)"]
end
subgraph Runtime["Container Runtime"]
direction TB
Entry["entrypoint.sh"]
Services["systemd services"]
Dockerfile["Dockerfile layers"]
end
Main --> Lib
Container --> SSH
Container --> Entry
style CLI fill:#1a1a2e,stroke:#16213e,color:#fff
style Lib fill:#0f3460,stroke:#16213e,color:#fff
style Runtime fill:#16213e,stroke:#0f3460,color:#fff
The CLI sources modular shell libraries from src/lib/:
| Module | Purpose | Key Functions |
|---|---|---|
core.sh |
Logging utilities | _cai_info, _cai_error, _cai_warn, _cai_ok |
platform.sh |
OS/platform detection | _cai_detect_platform, _cai_is_wsl |
docker.sh |
Docker helpers | _cai_docker_available, _cai_docker_version |
doctor.sh |
Health checks | _cai_doctor, _cai_select_context |
config.sh |
TOML config parsing | _containai_parse_config, _containai_resolve_volume |
container.sh |
Container lifecycle | _containai_start_container, _containai_stop_all |
import.sh |
Dotfile sync | _containai_import |
export.sh |
Volume backup | _containai_export |
setup.sh |
Sysbox installation | _cai_setup |
ssh.sh |
SSH infrastructure | _cai_setup_ssh_key, _cai_allocate_ssh_port, _cai_ssh_run |
env.sh |
Environment handling | _containai_import_env |
version.sh |
Version management | _cai_version, _cai_check_update |
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#1a1a2e',
'primaryTextColor': '#ffffff',
'primaryBorderColor': '#16213e',
'secondaryColor': '#0f3460',
'tertiaryColor': '#1a1a2e',
'lineColor': '#a0a0a0',
'textColor': '#ffffff',
'background': '#0d1117'
}}}%%
flowchart TD
Main["containai.sh"] --> Core["core.sh"]
Core --> Platform["platform.sh"]
Platform --> Docker["docker.sh"]
Docker --> Doctor["doctor.sh"]
Doctor --> Config["config.sh"]
Config --> Container["container.sh"]
Container --> Import["import.sh"]
Import --> Export["export.sh"]
Export --> Setup["setup.sh"]
Setup --> SSH["ssh.sh"]
SSH --> Env["env.sh"]
Env --> Version["version.sh"]
style Main fill:#1a1a2e,stroke:#16213e,color:#fff
style Core fill:#e94560,stroke:#16213e,color:#fff
style SSH fill:#0f3460,stroke:#16213e,color:#fff
%%{init: {'theme': 'base', 'themeVariables': {
'background': '#f5f5f5',
'actorBkg': '#1a1a2e',
'actorTextColor': '#ffffff',
'actorBorder': '#16213e',
'actorLineColor': '#606060',
'signalColor': '#606060',
'signalTextColor': '#1a1a2e',
'labelBoxBkgColor': '#0f3460',
'labelBoxBorderColor': '#16213e',
'labelTextColor': '#ffffff',
'loopTextColor': '#1a1a2e',
'noteBkgColor': '#0f3460',
'noteTextColor': '#ffffff',
'noteBorderColor': '#16213e',
'activationBkgColor': '#16213e',
'activationBorderColor': '#0f3460',
'sequenceNumberColor': '#1a1a2e'
}}}%%
sequenceDiagram
participant User
participant CLI as containai.sh
participant Config as config.sh
participant Doctor as doctor.sh
participant SSH as ssh.sh
participant Container as container.sh
participant Docker as ContainAI Docker
participant SSHD as Container sshd
User->>CLI: cai run [options]
CLI->>Config: Parse config.toml
Config-->>CLI: Volume, agent, resources
CLI->>Doctor: Select context
Doctor->>Docker: docker info (verify sysbox)
Docker-->>Doctor: Sysbox available
CLI->>Container: Start/find container
Container->>Docker: docker run --runtime=sysbox-runc
Docker-->>Container: Container ID
Container->>SSH: Setup SSH (port, key, known_hosts)
SSH->>SSHD: Wait for ready
SSH->>SSHD: ssh agent@localhost -p PORT
SSHD-->>User: Agent session
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#1a1a2e',
'primaryTextColor': '#ffffff',
'primaryBorderColor': '#16213e',
'secondaryColor': '#0f3460',
'tertiaryColor': '#1a1a2e',
'lineColor': '#a0a0a0',
'textColor': '#ffffff',
'background': '#0d1117'
}}}%%
flowchart LR
subgraph Host["Host System"]
Workspace["Workspace<br/>(/path/to/project)"]
HostConfigs["Host Configs<br/>(~/.ssh, ~/.gitconfig)"]
end
subgraph Volumes["Docker Volumes"]
DataVol["Data Volume<br/>(containai-data)"]
end
subgraph Container["System Container"]
WorkMount["/home/agent/workspace<br/>(bind mount)"]
DataMount["/mnt/agent-data<br/>(volume mount)"]
subgraph DataStructure["Data Volume Contents"]
Claude["/claude<br/>(credentials, settings)"]
GH["/config/gh<br/>(GitHub CLI)"]
VSCode["/vscode-server"]
Shell["/shell<br/>(bash config)"]
end
end
Workspace -->|"bind mount"| WorkMount
DataVol --> DataMount
DataMount --> DataStructure
HostConfigs -.->|"cai import"| DataVol
style Host fill:#1a1a2e,stroke:#16213e,color:#fff
style Volumes fill:#16213e,stroke:#0f3460,color:#fff
style Container fill:#0f3460,stroke:#16213e,color:#fff
| Volume | Mount Point | Purpose | Lifecycle |
|---|---|---|---|
| Workspace | /home/agent/workspace |
Project files | Bind mount per session |
| Data Volume | /mnt/agent-data |
Agent configs, credentials | Persistent named volume |
/mnt/agent-data/
├── claude/ # Claude Code
│ ├── credentials.json
│ ├── settings.json
│ └── plugins/
├── config/
│ ├── gh/ # GitHub CLI
│ ├── git/ # Git config
│ └── tmux/
├── gemini/ # Gemini CLI
├── copilot/ # Copilot CLI
├── codex/ # Codex CLI
├── shell/
│ └── .bashrc.d/ # Shell extensions
└── vscode-server/ # VS Code Server state
Sysbox provides multiple isolation layers:
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#1a1a2e',
'primaryTextColor': '#ffffff',
'primaryBorderColor': '#16213e',
'secondaryColor': '#0f3460',
'tertiaryColor': '#1a1a2e',
'lineColor': '#a0a0a0',
'textColor': '#ffffff',
'background': '#0d1117'
}}}%%
flowchart TB
subgraph Host["Host (TRUSTED)"]
HostKernel["Kernel"]
HostDocker["Docker Daemon"]
SysboxMgr["sysbox-mgr"]
SysboxFS["sysbox-fs"]
end
IsolationLayer["Sysbox Isolation Layer<br/>User Namespace + Procfs/Sysfs Virtualization"]
subgraph Container["System Container (UNTRUSTED)"]
ContainerRoot["Container Root<br/>(unprivileged on host)"]
ContainerProc["/proc (virtualized)"]
ContainerSys["/sys (virtualized)"]
InnerDocker["Inner Docker"]
end
HostKernel --> SysboxMgr
SysboxMgr --> SysboxFS
HostDocker --> IsolationLayer
IsolationLayer --> Container
SysboxFS -->|"virtualizes"| ContainerProc
SysboxFS -->|"virtualizes"| ContainerSys
style Host fill:#16213e,stroke:#16213e,color:#fff
style IsolationLayer fill:#0f3460,stroke:#16213e,color:#fff
style Container fill:#e94560,stroke:#16213e,color:#fff
| Protection | Implementation |
|---|---|
| User namespace isolation | Sysbox auto-maps UIDs (container root = unprivileged host user) |
| Procfs virtualization | Container sees only its own processes |
| Sysfs virtualization | Container sees only its own devices |
| No host Docker socket | Socket is NOT mounted; inner Docker is isolated |
| SSH key-only auth | Password authentication disabled |
| Resource limits | cgroup limits (memory, CPU) enforced |
By default, containers receive 50% of host resources:
| Resource | Default | Configuration |
|---|---|---|
| Memory | 50% of host RAM | [resources].memory_limit or --memory |
| CPU | 50% of host cores | [resources].cpu_limit or --cpus |
| Memory swap | Same as memory limit | Prevents OOM via swap |
- Malicious container images: Use trusted base images only
- Network isolation: Containers can reach the internet by default
- Kernel vulnerabilities: Depends on Sysbox/Docker security
- Supply chain attacks: Verify agent CLI installations
Decision: Use SSH for all container access instead of docker attach.
Rationale:
- VS Code Remote-SSH compatibility
- SSH agent forwarding for git operations
- Port tunneling for development servers
- Standard tooling (scp, rsync) works out of the box
- More robust than PTY via Docker API
Decision: Install separate docker-ce instead of using Docker Desktop.
Rationale:
- Docker Desktop does not support Sysbox runtime
- Avoids conflicts with existing Docker setup
- Full control over runtime configuration
- Dedicated socket prevents accidental cross-usage
Decision: Run systemd as PID 1 in containers.
Rationale:
- Enables real service management
- SSH daemon runs as proper service
- Docker daemon managed by systemd
- Init system handles cleanup on shutdown
- Matches production Linux environments
Decision: Split Dockerfile into base/sdks/full layers.
Rationale:
- Faster iteration during development
- Smaller images for minimal use cases
- Clear separation of concerns
- Easier updates to individual layers