Files
ROMFASTSQL/proxmox/lxc102-docker/README.md
Claude Agent e72c1f9f48 docs(lxc102): installer + procedură de replicare pentru sbt/statusline
- scripts/install-sbt.sh: instalare idempotentă (scripturi + statusline +
  patch settings.json) cu verificări de mediu
- README: replicare de la zero (transfer prin pct exec, installer, sandbox,
  ce se pierde la recrearea sandbox-ului), verificare rapidă, căi noi

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:38:40 +00:00

15 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

Instalare / reinstalare: scripts/install-sbt.sh — vezi „Replicare de la zero" mai jos.

sbt — tmux + sandbox + agent

Nu lansează niciun agent — deschide shell-uri în sandbox, agentul îl pornești manual (claude, opencode, jcode…).

sbt                          # sesiune 1, shell în /home/workspace
sbt 2 proiect-a              # sesiune 2, shell în /home/workspace/proiect-a
sbt 1 proiect-a opencode     # opțional: lansează explicit un agent în pane-ul stâng
sbt list | sbt kill 2 | sbt detach 2

Sesiunile se numesc sbx1, sbx2, sbx3. Layout: 2 pane-uri pe orizontală, ambele shell în sandbox, în directorul proiectului. Dacă ceri explicit un agent (argumentul 3), acesta rulează în pane-ul stâng și, la ieșire, pane-ul rămâne cu un shell (exec bash -l) — 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.

WARN: could not acquire docker hub refresh lock … context deadline exceeded — apare când două procese sbx exec pornesc simultan (cele două pane-uri) și se calcă pe lock-ul de refresh al credențialelor Docker Hub. E inofensiv (proceeding without cross-process lock), dar zgomotos. Reprodus determinist:

for i in 1 2 3; do sbx exec claude-workspace true & done; wait   # 3 warning-uri
sbx exec claude-workspace true                                   # curat

De aceea sbt pornește al doilea pane cu 4 secunde întârziere (SBT_PANE_DELAY) — la ≥4s lock-ul e liber și warning-ul dispare. Atașarea nu e întârziată, doar intrarea în sandbox a pane-ului din dreapta.

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 efemersettings.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.

Replicare de la zero

Presupune sbx deja funcțional (vezi „Depanare" mai jos) și userul work în grupul kvm.

1. Copiază scripts/ din acest repo pe LXC 102. LXC-ul nu are acces direct la repo, deci transfer prin pct exec (rulat de pe LXC 171, unde e clonat repo-ul):

cd proxmox/lxc102-docker
tar czf - scripts | base64 -w0 | ssh root@10.0.20.201 \
  "pct exec 102 -- bash -c 'rm -rf /tmp/sbt-install; mkdir -p /tmp/sbt-install;
   cd /tmp/sbt-install && base64 -d | tar xzf - && chown -R work:work /tmp/sbt-install'"

2. Rulează installer-ul ca work (idempotent, poate fi rulat oricând la update):

ssh root@10.0.20.201 "pct exec 102 -- su - work -c 'cd /tmp/sbt-install/scripts && ./install-sbt.sh'"

Pune sbt/agents/sb în ~/.local/bin, statusline-ul în /home/workspace/.agent/, adaugă statusLine în ~/.claude/settings.json al userului work și verifică tmux / sbx / jq / PATH.

3. Sandbox-ul. Scripturile țintesc implicit sandbox-ul claude-workspace (numele rezultă din agent + directorul de lucru, la sbx create claude /home/workspace). Alt sandbox se dă prin variabila SANDBOX:

SANDBOX=alt-sandbox sbt 2 proiect-b

4. După recrearea sandbox-ului trebuie refăcut ce stă pe overlay-ul efemer /home/agent: login-ul Claude Code (~/.claude/.credentials.json) și orice unelte instalate manual în ~/.local/bin din sandbox. Persistă doar /home/workspace (virtiofs) și volumele montate de sbx peste ~/.claude/{projects,sessions,todos,skills,shell-snapshots,statsig} — de aceea statusline-ul stă în /home/workspace/.agent/ și e re-legat de sbt la fiecare intrare.

Verificare rapidă

# statusline randează în sandbox (fără a porni un agent)
sbx exec claude-workspace bash -lc '. /home/workspace/.agent/bootstrap-statusline.sh;
  jq -c .statusLine ~/.claude/settings.json; claude --version'

# sesiune tmux fără terminal (test): panes trebuie să fie vii, cu prompt agent@claude-workspace
sbt 3 test-sbt; sleep 10
tmux list-panes -t sbx3 -F '#{pane_index} dead=#{pane_dead}'
tmux capture-pane -p -t sbx3:0.0 | tail -2
tmux kill-session -t sbx3; rm -rf /home/workspace/test-sbt

open terminal failed: not a terminal la final e normal când sbt e rulat neinteractiv — sesiunea se creează oricum, doar atașarea eșuează.


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

# 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:
    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

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
/home/work/.local/bin/{sbt,agents,sb} scripturile de lucru (sursa versionată: scripts/)
/home/work/.tmux.conf prefix Ctrl+A, mouse on
/home/workspace/ bind-mount virtiofs — singurul spațiu persistent partajat cu sandbox-ul
/home/workspace/.agent/statusline.sh statusline Claude Code (host + sandbox)
/home/workspace/.agent/bootstrap-statusline.sh re-leagă statusline-ul în sandbox la fiecare intrare