- 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>
340 lines
15 KiB
Markdown
340 lines
15 KiB
Markdown
# 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 <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`…).
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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 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.
|
|
|
|
### 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):
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```bash
|
|
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ă
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```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` |
|
|
| `/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 |
|