homelab-codex-ws/services/ai-cluster/CUTOVER.md
oskar b124e54e66 feat(ai-cluster): manifest + zrodla pod SOLARIA (autoring, cutover NIE wykonany)
Stack ai-cluster (openclaw, codex/planner/service-ops workery, redis, mosquitto)
biega na VPS spoza GitOps, z /home/dockeruser/docker/ai-cluster/. Branch
feat/vps-service-migration (862c04a) probowal wciagnac go do GitOps *w miejscu*,
na VPS. Ten etap pomijamy: workloady sa compute'owe, VPS ma 4 GiB bez swapu i rolę
ingressu, SOLARIA ma 64 GiB i GPU. Migrujemy od razu na SOLARIA, VPS zostaje samym
NPM-em proxujacym po Tailscale.

Zero deployu. Kontenery na VPS nietkniete — recon byl read-only (katalog nalezy do
dockeruser, odczyt przez efemeryczny kontener z mountem :ro, bez dotykania
dzialajacych kontenerow). Plan przelaczenia: services/ai-cluster/CUTOVER.md.

Zrodla wciagniete do services/ai-cluster/src/ (openclaw, worker, mosquitto).
Pominiete: .env i mosquitto/passwd (sekrety), start-codex.sh* (smiec spoza stacku),
__pycache__.

Ustalenia z reconu, ktore zmienily manifest z 862c04a:

- codex-worker/ na VPS to NIE osobny build context ani duplikat worker/ — to
  martwy 609-bajtowy prototyp na `requests` (biblioteka nie wystepuje w zadnym
  requirements.txt), bez Dockerfile'a i bez referencji w compose. Realny
  codex-worker buduje sie z ./worker (Dockerfile default). Nie przenoszony.
- image: ai-cluster-* -> build: z src/openclaw i src/worker (3 Dockerfile'e nad
  jednym kontekstem: default/planner/service-ops).
- openclaw: usuniete publish 0.0.0.0:8000 -> "100.100.231.104:8000:8000";
  usunieta siec npm_default (nie istnieje na SOLARIA).
- mosquitto: bind 100.95.58.48 -> 100.100.231.104.
- mem_limity urealnione (256m workery/openclaw, 64m redis/mosquitto) i przeniesione
  do hosts/solaria/runtime/. Zmierzone RSS na VPS: workery 7-10 MiB, openclaw
  24.6 MiB, redis 4.8, mosquitto 4.2 — limity to zapas ~10x, nie reakcja na presje.
- healthcheck zostaje na python/urllib: obraz openclaw (python:3.12-slim) nie ma
  ani wget, ani curl — sprawdzone na zywym obrazie.
- GATEWAY_BASE_URL http://piha:8080 -> http://100.108.208.3:8080. Tailscale DNS
  jest WYLACZONY na SOLARIA (`tailscale dns status` -> disabled), wiec zadna nazwa
  MagicDNS sie nie rozwiazuje. Na VPS bare `piha` tez nie rozwiazuje sie (rc=2),
  czyli ten default byl martwy juz tam.
- telegram-bot nie przenoszony (na VPS nie biega zaden kontener). telegram_bot.py
  zostaje w src/, bo openclaw/Dockerfile go COPY-uje — bez niego build sie wywala.
- sekrety wylacznie przez env_file /opt/homelab/config/ai-cluster/.env, zero
  interpolacji ${VAR:?} — stack nie zalezy od cwd deployu. service-ops-worker
  montuje ten katalog pod ta sama sciezka absolutna, bo compose waliduje env_file
  na etapie parsowania i inaczej `docker compose ps` w tym kontenerze padnie.
- AGENT_ID zostaja `vps-*`: to cele routingu MQTT, nie hostnamey. Zmiana nazw
  lamie producentow adresujacych konkretnego agenta — osobne, skoordynowane zadanie.

Konsumenci mosquitto do aktualizacji: w repo BRAK. chelsty (z2m, frigate,
stability-agent) uzywaja wlasnego, host-networkowego brokera i musza zostac
offline-first. Lista i sposob potwierdzenia klientow spoza repo — CUTOVER.md krok 5.

DoD (docker build + smoke + pytest) swiadomie odroczone: zadanie zabrania budowania
obrazow. Pokrywaja to kroki 1-3 CUTOVER.md, do wykonania w sesji cutoveru.
Zweryfikowane tutaj: `docker compose config` z overrideem (rc=0, bindy host_ip,
mem_limity, merge volumes), skladnia YAML, `bash -n healthcheck.sh`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:30:00 +02:00

187 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ai-cluster — VPS → SOLARIA cutover plan
**Not executed.** This document is the plan for a later session. As of the last
edit the stack runs untouched on the VPS from
`/home/dockeruser/docker/ai-cluster/`, and nothing has been built on SOLARIA.
**Rollback posture:** the VPS stack stays up and untouched through every step
below. It is only stopped in step 7, after SOLARIA has been confirmed healthy and
all consumers have been repointed. Until then, rollback is "do nothing".
## 0. Prerequisites on SOLARIA
```bash
sudo mkdir -p /opt/homelab/config/ai-cluster/mosquitto
```
Populate the secrets file from the template:
```bash
sudo cp services/ai-cluster/env.example /opt/homelab/config/ai-cluster/.env
sudo chmod 600 /opt/homelab/config/ai-cluster/.env
# fill in MQTT_PASSWORD
```
Mosquitto config — the two committed files, plus a `passwd` generated on the node:
```bash
sudo cp services/ai-cluster/src/mosquitto/{mosquitto.conf,acl} \
/opt/homelab/config/ai-cluster/mosquitto/
docker run --rm -v /opt/homelab/config/ai-cluster/mosquitto:/m eclipse-mosquitto:2 \
mosquitto_passwd -c -b /m/passwd codex '<same password as MQTT_PASSWORD>'
sudo chmod 600 /opt/homelab/config/ai-cluster/mosquitto/passwd
```
The password must match `MQTT_PASSWORD` in `.env`. Reusing the VPS password keeps
any not-yet-migrated client working during the overlap; rotating it is cleaner but
means every consumer must be updated in the same window.
Check nothing on SOLARIA already holds `100.100.231.104:8000` or `:1883`:
```bash
ss -tlnp | grep -E ':8000|:1883'
```
## 1. Build
```bash
cd ~/homelab-codex-ws/services/ai-cluster
docker compose -f docker-compose.yml \
-f ../../hosts/solaria/runtime/ai-cluster/docker-compose.override.yml build
```
Four images build from `src/`: openclaw, and three from `src/worker`. Nothing is
pulled from a registry except the `redis:7-alpine` and `eclipse-mosquitto:2` bases.
## 2. Start
```bash
docker compose -f docker-compose.yml \
-f ../../hosts/solaria/runtime/ai-cluster/docker-compose.override.yml up -d
docker compose ps
```
Expect a container-name collision on `mosquitto` only if something else on SOLARIA
already uses that fixed name — the VPS one is a different host, so no conflict.
## 3. Verify
```bash
./healthcheck.sh
```
Then, individually:
- **openclaw**: `curl -sf http://100.100.231.104:8000/health`
- **MQTT auth**: publish a task and watch for a result —
```bash
mosquitto_sub -h 100.100.231.104 -p 1883 -u codex -P '<pass>' -t 'codex/results' -C 1 &
mosquitto_pub -h 100.100.231.104 -p 1883 -u codex -P '<pass>' -t 'codex/tasks' \
-m '{"id":"cutover-1","target":"role:dev","goal":"cutover smoke test"}'
```
- **gateway reachability**: confirm `GATEWAY_BASE_URL` actually answers from inside
a worker — `docker exec ai-cluster-codex-worker-1 python -c "import urllib.request;
print(urllib.request.urlopen('http://100.108.208.3:8080').status)"`
- **memory**: `docker stats --no-stream` — nothing should sit near its `mem_limit`.
Reference RSS measured on the VPS: workers 710 MiB, openclaw ~25 MiB.
- **service-ops-worker**: confirm it can parse the mounted compose file —
`docker exec ai-cluster-service-ops-worker-1 docker compose ps`. This is the step
that catches a missing `/opt/homelab/config/ai-cluster/.env` inside the container.
## 4. Repoint NPM on the VPS
In the NPM admin UI, change the proxy host that currently forwards to the local
openclaw so it forwards to `100.100.231.104` port `8000`.
The VPS reaches SOLARIA over Tailscale; confirm first from the VPS shell:
```bash
curl -sf --max-time 5 http://100.100.231.104:8000/health
```
NPM's config is not in Git — this is a UI change, so note the previous value before
editing it in case of rollback.
## 5. Repoint mosquitto consumers
Recon on 2026-07-27 found **no repo-managed consumer** of the VPS broker at
`100.95.58.48:1883`. Specifically:
| Candidate | Verdict |
|---|---|
| `hosts/chelsty-infra/runtime/stability-agent/` | `MQTT_HOST=mosquitto` — its own **host-networked** broker on CHELSTY-INFRA. Not this broker. Offline-first, must stay that way. |
| `hosts/chelsty-infra/runtime/zigbee2mqtt/`, `frigate/` | Same local CHELSTY broker. Not this broker. |
| `services/stability-agent/env.example` | Default `MQTT_HOST=mosquitto`; no host override sets a VPS IP. |
| `services/mosquitto/` | A **separate** repo manifest (`owner_node: vps`, ports `1883`+`9001`) that describes this same container. Overlaps with this stack — see follow-ups. |
| ai-cluster's own containers | Use the in-network name `mosquitto`; move with the stack, no change needed. |
So the expected consumer list to update is **empty** — but the grep only covers
this repo. Before step 7, confirm there is no out-of-repo client by watching the
broker on the VPS for connections that are not ai-cluster containers:
```bash
# on the VPS
docker logs --tail 200 mosquitto | grep -i 'new client'
```
Any client address that is not a `172.x` container IP from the ai-cluster network
is an external consumer that must be repointed to `100.100.231.104:1883` first.
## 6. Observation window
Leave both stacks up for a while. SOLARIA serves traffic; the VPS stack is idle but
alive. Confirm no errors in `docker compose logs -f` on SOLARIA and that NPM is not
throwing 502s.
## 7. Stop the VPS stack
Only after steps 36 are clean:
```bash
# on the VPS
cd /home/dockeruser/docker/ai-cluster
docker compose down
```
`down` without `-v` — it leaves the images, the bind-mounted config, and the
directory in place, so `docker compose up -d` restores the old stack if needed.
## 8. Confirm the memory win on the VPS
```bash
free -m
```
Expected reclaim is modest — the six containers measured ~58 MiB RSS combined on
2026-07-27, not the several hundred MiB the stack was once assumed to cost. The
real benefit is removing four unbounded Python processes from a 4 GiB no-swap box,
not the current byte count. Record the before/after in the session notes.
## 9. Repo cleanup, after the VPS stack is gone
- Delete `/home/dockeruser/docker/ai-cluster/` on the VPS (contains the only copy
of `.env`, `mosquitto/passwd`, and the dead `codex-worker/` prototype — check
nothing is still wanted before removing).
- Reconcile `services/mosquitto/` (see follow-ups).
- Update `inventory/topology.yaml`: `ai-cluster` is listed under the VPS services.
## Follow-ups, deliberately not done here
- **`services/mosquitto/` duplicates this stack's broker.** It declares
`owner_node: vps`, ports 1883+9001, and `/opt/homelab/data/mosquitto/*` paths that
do not match the running container. Two manifests describe one container. Needs a
decision: fold it into ai-cluster, or make it a standalone service on SOLARIA that
ai-cluster depends on.
- **`AGENT_ID=vps-*` on SOLARIA.** Values kept for routing compatibility. Rename to
`solaria-*` once every task producer is known and can be updated together.
- **`service-ops-worker` has `docker.sock` read-write on the GPU node.** It can
restart `ollama` and anything else on SOLARIA. The in-code allowlist limits it to
ps/inspect/logs/restart, but the blast radius is larger here than on the VPS.
Worth a security review before, not after, cutover.
- **`codex-worker/worker.py` on the VPS** is a dead 609-byte prototype (uses
`requests`, which is in no requirements file; no Dockerfile; not referenced by
compose). Not carried into `src/`. Delete it with the directory in step 9, or
rescue it first if it has value.
- **`telegram-bot`** is defined in the VPS compose but no container runs. Not carried
over. `telegram_bot.py` still ships because `openclaw/Dockerfile` COPYs it.