- sbt: sesiuni tmux (sbx1..3) care intră în sandbox și lansează agentul (claude/opencode/orice binar); tmux rulează pe host, panes prin sbx exec - statusline.sh + bootstrap-statusline.sh pe /home/workspace/.agent (virtiofs persistent), reinstalat la fiecare intrare fiindcă /home/agent din sandbox e overlay efemer - fix printf "--" -> printf -- "--" (eroare de usage când API-ul de cote nu răspunde) - copii versionate ale scripturilor agents/sb + documentație README Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11 KiB
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
# 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 <sandbox>
sbx tui # dashboard interactiv
sbx rm -f <sandbox>
# 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
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 <dir> <sandbox> 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:
gitlipseș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.jsonal sandbox-ului (login separat de cel al useruluiwork). Cache 60s în/tmp/claude-statusline-usage.json. Dacă API-ul răspunderate_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:
- acces read/write la
/dev/kvmpentru userul care rulează sbx; mkfs.ext4(din/usr/sbin) în PATH — snapshotter-ul erofs formatează layer-ul rw;- 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
# 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, nusbx diagnose. - Verificări rapide de mediu:
Toate trei trebuie să fie OK / fără output la ultima.
su - work -c 'test -w /dev/kvm && echo KVM_OK; command -v mkfs.ext4; ldd /usr/libexec/lib/libsailor.so | grep "not found"' - Există și un daemon
sbxvechi rulând ca root (rămas de la instalarea inițială, binar șters, ~590MB RSS). Poate fi oprit cusbx daemon stopca root. Recomandat: sbx se folosește doar din userulwork. - 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 daemonNU 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
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:
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 |