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

7.5 KiB
Raw Blame History

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

sudo mkdir -p /opt/homelab/config/ai-cluster/mosquitto

Populate the secrets file from the template:

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:

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:

ss -tlnp | grep -E ':8000|:1883'

1. Build

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

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

./healthcheck.sh

Then, individually:

  • openclaw: curl -sf http://100.100.231.104:8000/health
  • MQTT auth: publish a task and watch for a result —
    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:

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:

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

# 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

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.