homelab-codex-ws/services/ai-cluster/docker-compose.yml
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

149 lines
4.9 KiB
YAML

# AI Cluster — target node: SOLARIA (compute/GPU).
#
# Authored from the live VPS stack at /home/dockeruser/docker/ai-cluster/ (read-only
# recon 2026-07-27). The VPS stack is still running and is NOT touched by this file;
# see CUTOVER.md for the switch-over plan.
#
# Deliberate differences from the VPS original:
# - images built from ./src/* instead of pre-built local `ai-cluster-*` images
# - openclaw publishes on SOLARIA's Tailscale IP only (was 0.0.0.0:8000)
# - the external `npm_default` network is gone (it does not exist on SOLARIA;
# NPM on the VPS will proxy to openclaw over Tailscale instead)
# - mosquitto binds SOLARIA's Tailscale IP (was the VPS Tailscale IP)
# - telegram-bot service is not carried over (it does not run on the VPS either)
# - secrets come from env_file, not from ${} interpolation
#
# Secrets: /opt/homelab/config/ai-cluster/.env (see env.example). Compose validates
# env_file paths at parse time, which is why service-ops-worker mounts that same
# directory at the same absolute path — otherwise `docker compose ps` inside that
# container fails to parse this file.
services:
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
networks:
- ai-cluster
mosquitto:
image: eclipse-mosquitto:2
container_name: mosquitto
restart: unless-stopped
ports:
# SOLARIA Tailscale IP. Never 0.0.0.0 — the broker has auth but is not
# meant to be reachable off-mesh.
- "100.100.231.104:1883:1883"
volumes:
# mosquitto.conf + acl ship in src/mosquitto/ and are copied to this path
# at deploy; `passwd` is generated on the node and never committed.
- /opt/homelab/config/ai-cluster/mosquitto:/mosquitto/config:ro
networks:
- ai-cluster
openclaw:
build:
context: ./src/openclaw
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
restart: unless-stopped
env_file:
- /opt/homelab/config/ai-cluster/.env
environment:
REDIS_URL: redis://redis:6379/0
MQTT_HOST: mosquitto
MQTT_PORT: 1883
ports:
# Tailscale-only. NPM on the VPS proxies to 100.100.231.104:8000 over the
# mesh — see CUTOVER.md. `healthcheck.sh` therefore probes the Tailscale
# IP, not localhost.
- "100.100.231.104:8000:8000"
healthcheck:
# The image is python:3.12-slim: no wget, no curl (verified against the
# running VPS image). urllib is the only thing available.
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()"]
interval: 10s
timeout: 5s
retries: 5
depends_on:
- redis
- mosquitto
networks:
- ai-cluster
codex-worker:
# Builds from ./src/worker (default Dockerfile → worker.py). The VPS also has
# a stale `codex-worker/worker.py` prototype that nothing builds — not carried
# over; see service.yaml notes.
build:
context: ./src/worker
restart: unless-stopped
env_file:
- /opt/homelab/config/ai-cluster/.env
environment:
# AGENT_ID/ROLE are MQTT routing targets. Kept as `vps-*` on purpose so
# existing task producers keep working across the move; renaming is a
# separate, coordinated change (CUTOVER.md, follow-up).
AGENT_ID: vps-dev-1
ROLE: dev
REDIS_URL: redis://redis:6379/0
MQTT_HOST: mosquitto
MQTT_PORT: 1883
REQUEST_TIMEOUT_SECONDS: 30
depends_on:
- redis
- mosquitto
networks:
- ai-cluster
planner-worker:
build:
context: ./src/worker
dockerfile: Dockerfile.planner
restart: unless-stopped
env_file:
- /opt/homelab/config/ai-cluster/.env
environment:
AGENT_ID: vps-planner-1
ROLE: planner
MQTT_HOST: mosquitto
MQTT_PORT: 1883
depends_on:
- mosquitto
networks:
- ai-cluster
service-ops-worker:
build:
context: ./src/worker
dockerfile: Dockerfile.service-ops
restart: unless-stopped
env_file:
- /opt/homelab/config/ai-cluster/.env
environment:
AGENT_ID: vps-service-ops-1
ROLE: service-ops
MQTT_HOST: mosquitto
MQTT_PORT: 1883
COMPOSE_PROJECT_NAME: ai-cluster
volumes:
# This worker shells out to `docker compose ps|restart` from /app, so it
# needs the compose file and a .env in its cwd for interpolation.
# The repo-path mount is host-specific → hosts/solaria/runtime/ai-cluster/.
- /var/run/docker.sock:/var/run/docker.sock
- /opt/homelab/config/ai-cluster/.env:/app/.env:ro
# Same absolute path as the env_file directive above, so compose can parse
# the mounted compose file from inside this container.
- /opt/homelab/config/ai-cluster:/opt/homelab/config/ai-cluster:ro
depends_on:
- mosquitto
networks:
- ai-cluster
volumes:
redis_data:
networks:
ai-cluster:
driver: bridge