# LXC 102 - Docker + Portainer + Docker Sandboxes (sbx) **Director:** `proxmox/lxc102-docker/` **VMID:** 102 **IP intern:** 10.0.20.113 | **Tailscale:** 100.73.147.55 **Host Proxmox:** pvemini (10.0.20.201) **Rol:** Host Docker general + Portainer + rulare agenți AI (opencode, Claude Code) izolați în Docker Sandboxes --- ## Informații Container | Parametru | Valoare | |-----------|---------| | VMID | 102 | | Hostname | docker.romfast.ro | | IP intern | 10.0.20.113 | | Host Proxmox | pvemini | | Storage | local-zfs (30GB) | | RAM | 12GB (swap 4GB) — ridicat de la 4GB/512MB pe 2026-07-31, vezi „Memorie" | | CPU | 4 cores | | OS | Debian 13 (trixie) | | Features | `nesting=1,fuse=1` | | onboot | da | | Creat cu | community-scripts ProxmoxVE — Docker LXC | Containerul are bind-mount pentru `/dev/kvm` (`lxc.mount.entry: /dev/kvm dev/kvm none bind,optional,create=file`) — **obligatoriu** pentru microVM-ul folosit de sbx. ## Componente | Componentă | Versiune | Note | |------------|----------|------| | Docker Engine | 29.6.x | dockerd de sistem (root) | | Portainer CE | latest | container `portainer`, porturi 9000 / 9443 | | Docker Sandboxes (`sbx`) | 0.37.1 | pachet `docker-sbx`, daemon per-user | > Atenție: LXC **100** este un container separat, tot numit `portainer`. Portainer-ul activ folosit rulează în LXC 102. **User de lucru pentru sbx:** `work` (uid 1000). Daemonul `sandboxd` pornește automat per-user la prima comandă `sbx`. --- ## Comenzi uzuale ```bash # Acces ssh root@10.0.20.201 "pct exec 102 -- bash" # Docker ssh root@10.0.20.201 "pct exec 102 -- docker ps" # sbx (întotdeauna ca user work, NU ca root) ssh root@10.0.20.201 "pct exec 102 -- su - work -c 'sbx ls'" ssh root@10.0.20.201 "pct exec 102 -- su - work -c 'sbx diagnose'" # Creare sandbox + atașare (interactiv, din shell-ul lui work) sbx create opencode /cale/proiect sbx run --name sbx tui # dashboard interactiv sbx rm -f # Daemon sbx daemon status sbx daemon stop ``` **Log daemon (prima oprire când ceva nu merge):** `/home/work/.local/state/sandboxes/sandboxes/sandboxd/daemon.log` --- ## Scripturi de lucru (userul `work`) Trei scripturi în `/home/work/.local/bin/`, copii versionate în `scripts/` din acest director: | Script | Rol | |--------|-----| | `agents` | pornește sandbox-ul și îl ține pornit (keepalive); nu lansează niciun agent | | `sb` | un shell interactiv în sandbox, într-un director de proiect | | `sbt` | **sesiune tmux** cu sandbox + agent — echivalentul lui `start-agent.sh` de pe LXC 171 | ### `sbt` — tmux + sandbox + agent ```bash sbt # sesiune 1, /home/workspace, claude în pane-ul stâng sbt 2 proiect-a # sesiune 2, /home/workspace/proiect-a sbt 1 proiect-a opencode # alt agent (orice binar din sandbox: opencode, jcode…) sbt 1 proiect-a shell # doar shell, fără agent sbt list | sbt kill 2 | sbt detach 2 ``` Sesiunile se numesc `sbx1`, `sbx2`, `sbx3`. Layout: 2 pane-uri pe orizontală — stânga cu agentul, dreapta shell. Când agentul se oprește, pane-ul rămâne cu un shell în sandbox (`exec bash -l`), deci sesiunea nu moare. **tmux rulează pe host (LXC 102), nu în sandbox** — în imaginea sandbox-ului nu există tmux (și nici `git`). Fiecare pane e un `sbx exec -it -w bash -l`. Consecința utilă: sesiunea supraviețuiește oricărei reporniri a agentului, iar `sbt` apelează întâi `agents up`, deci sandbox-ul e pornit chiar dacă nu era. Prefixul tmux e `Ctrl+A` (cu `Ctrl+B` ca secundar) — vezi `/home/work/.tmux.conf`. ### Statusline Claude Code în sandbox Același statusline ca pe LXC 171 (model, context, cote 5h / weekly / sonnet din API-ul OAuth Anthropic). Două fișiere, ambele pe **`/home/workspace/.agent/`**, adică pe bind-mount-ul virtiofs — singurul loc persistent văzut identic din host și din sandbox: | Cale | Rol | |------|-----| | `/home/workspace/.agent/statusline.sh` | scriptul propriu-zis (rulat de Claude Code) | | `/home/workspace/.agent/bootstrap-statusline.sh` | scrie `statusLine` în `~/.claude/settings.json` din sandbox | `sbt` face `source` la bootstrap la fiecare intrare în sandbox. Este necesar pentru că **`/home/agent` din sandbox stă pe overlay efemer** — `settings.json` se pierde la recrearea sandbox-ului, în timp ce `/home/workspace` rămâne. Bootstrap-ul e idempotent și nu suprascrie restul setărilor (merge cu `jq`). Statusline-ul e activat și pentru `claude` rulat direct pe host ca `work`, prin aceeași cale. **De reținut:** - `git` lipsește din imaginea sandbox-ului, deci linia 1 nu arată branch-ul acolo (degradează curat). - Datele de cotă vin de la `https://api.anthropic.com/api/oauth/usage`, cu token-ul din `~/.claude/.credentials.json` **al sandbox-ului** (login separat de cel al userului `work`). Cache 60s în `/tmp/claude-statusline-usage.json`. Dacă API-ul răspunde `rate_limit_error`, barele arată `0% ↺ --` până la următoarea reîmprospătare — nu e o defecțiune de config. --- ## Cum funcționează sbx (relevant pentru depanare) `sbx` **nu** folosește dockerd-ul de sistem. Are propriul containerd embedded, per-user, care pornește fiecare sandbox într-un **microVM** (runtime `nerdbox`, shim `/usr/libexec/containerd-shim-nerdbox-v1`, VMM în `/usr/libexec/lib/libsailor.so`). De aici rezultă trei dependențe de mediu care nu sunt evidente: 1. acces read/write la `/dev/kvm` pentru userul care rulează sbx; 2. `mkfs.ext4` (din `/usr/sbin`) în PATH — snapshotter-ul erofs formatează layer-ul rw; 3. un build al pachetului compatibil cu glibc-ul din container. Erorile din TUI sunt mereu generice (`500 Internal Server Error: failed to run sandbox container`). **Cauza reală e doar în `daemon.log`.** --- ## Depanare: "failed to run sandbox container" (rezolvat 2026-07-30) La primul sandbox opencode, TUI-ul returna: ``` Failed to create sandbox: create local sandbox: create sandbox via POST /sandbox: request failed: 500 Internal Server Error: failed to run sandbox container ``` `sbx diagnose` trecea toate cele 9 verificări — nu ajută la acest tip de eroare. Din `daemon.log` au rezultat trei probleme suprapuse, care se manifestau una după alta: | # | Eroare în daemon.log | Cauză | Fix | |---|----------------------|-------|-----| | 1 | `creating ephemeral volume for "/var/lib/docker": unknown volume driver: block` | VMM nefuncțional (vezi #3); driver-ul `block` e oferit de runtime-ul microVM | rezolvat de #3 | | 2 | `mkfs.ext4 failed: exec: "mkfs.ext4": executable file not found in $PATH` | Debian nu pune `/usr/sbin` în PATH pentru useri non-root | adăugat `/usr/sbin` în PATH-ul lui `work` | | 3 | `failed to create shim task: ttrpc: closed` + `libsailor.so: version GLIBC_2.43 not found` | era instalat build-ul **Ubuntu 26.04 (resolute)** pe Debian 13 (glibc 2.41) | reinstalat build-ul **noble** (glibc 2.39) | În plus, userul `work` **nu era în grupul `kvm`**, deci shim-ul microVM nu putea deschide `/dev/kvm`. ### Fix aplicat ```bash # 1. build-ul corect: repo-ul Docker pentru Debian NU publică docker-sbx, doar cel de Ubuntu. # Pe Debian 13 (glibc 2.41) se folosește build-ul noble (glibc 2.39), NU resolute (cere glibc 2.43). curl -fsSLO https://download.docker.com/linux/ubuntu/dists/noble/pool/stable/amd64/docker-sbx_0.37.1-1~ubuntu.24.04~noble_amd64.deb apt-get install -y --allow-downgrades ./docker-sbx_0.37.1-1~ubuntu.24.04~noble_amd64.deb # 2. acces la KVM (necesită re-login pentru user work) usermod -aG kvm work # 3. mkfs.ext4 în PATH — adăugat în /home/work/.profile PATH="/usr/local/sbin:/usr/sbin:/sbin:$PATH" ``` Verificare finală: `sbx create opencode .` → sandbox `running`, iar în interior `docker version` returnează 29.6.1 (deci merge și varianta docker-in-docker a template-ului). ### De reținut pentru viitor - **Pachetul nu se actualizează cu `apt upgrade`** — nu provine dintr-un repo configurat. La update, descarcă manual varianta `~noble`. Dacă iei din greșeală `~resolute`, sandbox-urile se rup din nou identic. - Dacă apare orice eroare 500 la creare: citește `daemon.log`, nu `sbx diagnose`. - Verificări rapide de mediu: ```bash su - work -c 'test -w /dev/kvm && echo KVM_OK; command -v mkfs.ext4; ldd /usr/libexec/lib/libsailor.so | grep "not found"' ``` Toate trei trebuie să fie OK / fără output la ultima. - Există și un daemon `sbx` vechi rulând ca **root** (rămas de la instalarea inițială, binar șters, ~590MB RSS). Poate fi oprit cu `sbx daemon stop` ca root. Recomandat: sbx se folosește **doar** din userul `work`. - Sandbox-urile pot ieși în rețea doar prin proxy-ul sbx, către host-uri permise de politică (`sbx policy`). --- ## Memorie — OOM cronic pe 4GB (rezolvat 2026-07-31) Alertă pe email „OOM x2 on pvemini", cu procese mici ucise (`dbus-daemon`, `oom_score_adj:200`) — derutant, pentru că victimele nu sunt vinovatul. Mesajul kernel dă containerul real: `oom_memcg=/lxc/102`. **OOM local containerului**, NU presiune de host — pvemini avea 24 Gi disponibili și zram funcțional. ### Bugetul real (măsurat, cu ZERO sandbox-uri active) | Proces | RSS | |--------|-----| | `sbx daemon start` | ~863 MB | | `claude` | ~317 MB | | VS Code Remote-SSH (5 forks) | ~450 MB | | tailscaled + dockerd + containerd + portainer | ~205 MB | | **Baseline** | **~1.9 GB** | Cu limita veche de 4096 MB, pornirea unui sandbox real (`containerd-shim` observat la **1.9 GB**) ducea totalul la ~3.8 GB → plafon atins → cascadă de OOM-kill. Contoare cumulate la momentul incidentului: `oom_kill 24`, `max 9279` depășiri de `memory.high`, `memory.peak` fix pe limită. Swap-ul de 512 MB era epuizat complet (510/512). > **`sbx daemon` NU are memory leak.** RSS-ul urcă la ~863 MB în timpul pornirii și apoi se > plafonează (verificat prin eșantionare la 10s timp de un minut). O citire de ~335 MB > înseamnă doar că l-ai prins în mijlocul startup-ului, tipic după un kill. ### Fix aplicat ```bash ssh root@10.0.20.201 "pct set 102 --memory 12288 --swap 4096" # live, fără restart ``` Limita cgroup **nu rezervă** memorie — containerul consumă doar cât folosește. Verificare: ```bash pct exec 102 -- free -h # 12Gi total cat /sys/fs/cgroup/lxc/102/memory.events # oom_kill nu mai crește ``` Contoarele din `memory.events` sunt cumulate de la pornirea containerului, deci valorile vechi rămân afișate — ce contează e că nu mai cresc. ### De reținut - LXC 102 **nu e „doar Docker"**: rulează sbx (sandbox-uri agenți AI) + VS Code Remote + Claude, toate sub userul `work`. Dimensionează-l ca mediu de dezvoltare, nu ca host Docker. - La orice OOM pe pvemini, citește **`oom_memcg=`** din mesajul kernel înainte de orice altceva — host-ul are marjă, deci aproape sigur e un cgroup de container. - Caz înrudit în aceeași zi, cu alt mecanism (swap fictiv fără backing pe host): `../cluster/incidents/2026-07-31-lxc110-dns-tailscale-oom.md` --- ## Fișiere și căi importante | Cale | Conținut | |------|----------| | `/home/work/.local/state/sandboxes/sandboxes/sandboxd/daemon.log` | log daemon sbx (sursa de adevăr la erori) | | `/home/work/.local/state/sandboxes/sandboxes/sandboxd/sandboxd.sock` | socket daemon | | `/usr/libexec/containerd-shim-nerdbox-v1` | shim microVM | | `/usr/libexec/lib/libsailor.so` | bibliotecă VMM (sensibilă la versiunea de glibc) | | `/home/work/.profile` | conține fix-ul de PATH pentru `mkfs.ext4` |