fix(qnap): default host-port 18080 + documenteer cross-build deploy
- docker-compose.yml + package.json: AGENT_HEALTH_PORT_HOST default
van 8080 -> 18080. QTS-webinterface bezet host-poort 8080; eerdere
default veroorzaakte een Container-Station retry-loop die de QTS-UI
onderuit haalde. Container-interne 8080 blijft ongewijzigd.
- README.md: drie nieuwe / uitgebreide secties op basis van een echte
deploy-sessie op TS-664:
* "Vereisten op de NAS" verduidelijkt dat /share/Agent een echte
QTS Shared Folder moet zijn (geen handmatige mkdir op de
16 MB /share-tmpfs).
* "Deploy - cross-build vanaf Mac" beschrijft de Apple Silicon ->
amd64 flow met buildx, docker save | gzip, scp en docker load.
* "Veelvoorkomende issues" verzamelt de gotchas: tmpfs-share,
docker not on PATH bij non-interactive ssh (source /etc/profile),
yaml control-character corruption na halve scp, en .env-loss
na rm -rf van een corrupt-tmpfs-share.
This commit is contained in:
parent
847ba96870
commit
6fb439cbd6
3 changed files with 251 additions and 9 deletions
256
README.md
256
README.md
|
|
@ -13,7 +13,7 @@ nooit zelf.
|
||||||
│ │
|
│ │
|
||||||
│ ┌─ container: agent-runner ────────────────────────────────┐ │
|
│ ┌─ container: agent-runner ────────────────────────────────┐ │
|
||||||
│ │ PID 1: tini → run-agent.sh (daemon-loop) │ │
|
│ │ PID 1: tini → run-agent.sh (daemon-loop) │ │
|
||||||
│ │ ├─ health-server.js (8080 → host) │ │
|
│ │ ├─ health-server.js (8080 → host 18080) │ │
|
||||||
│ │ └─ claude -p (per-batch, met MCP via stdio) │ │
|
│ │ └─ claude -p (per-batch, met MCP via stdio) │ │
|
||||||
│ │ └─ scrum4me-mcp → Neon Postgres │ │
|
│ │ └─ scrum4me-mcp → Neon Postgres │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
|
|
@ -52,16 +52,30 @@ fouten.
|
||||||
| `bin/check-tokens.sh` | Pre-flight: API-token, OAuth-token, DB-bereikbaarheid |
|
| `bin/check-tokens.sh` | Pre-flight: API-token, OAuth-token, DB-bereikbaarheid |
|
||||||
| `bin/job-prepare.sh` | Per-job: bare-fetch + clone-via-reference naar `/tmp/job-<id>` |
|
| `bin/job-prepare.sh` | Per-job: bare-fetch + clone-via-reference naar `/tmp/job-<id>` |
|
||||||
| `bin/job-cleanup.sh` | Per-job: logs naar `/var/log`, working tree weg |
|
| `bin/job-cleanup.sh` | Per-job: logs naar `/var/log`, working tree weg |
|
||||||
| `bin/health-server.js` | HTTP-endpoint op 8080 dat state.json en marker-files leest |
|
| `bin/health-server.js` | HTTP-endpoint op 8080 (intern) dat state.json en marker-files leest |
|
||||||
| `bin/rotate-logs.sh` | Compress/cleanup van oude `.log`-bestanden |
|
| `bin/rotate-logs.sh` | Compress/cleanup van oude `.log`-bestanden |
|
||||||
| `.env.example` | Alle env-vars met uitleg |
|
| `.env.example` | Alle env-vars met uitleg |
|
||||||
|
|
||||||
## Vereisten op de NAS
|
## Vereisten op de NAS
|
||||||
|
|
||||||
- Container Station 2+ (Docker compose v2)
|
- Container Station 2+ (Docker compose v2)
|
||||||
- Drie shares aangemaakt: `/share/Agent/cache`, `/share/Agent/logs`, `/share/Agent/state`
|
- **`Agent` als QTS Shared Folder** op een echte volume (bv. `CACHEDEV1_DATA`).
|
||||||
- Of één share `/share/Agent` waaronder de drie subdirs vallen
|
Niet een `mkdir /share/Agent` — `/share` zelf is een 16 MB tmpfs en handmatige
|
||||||
- Internet-uitgang naar `api.anthropic.com`, `github.com`, je Neon-host, `registry.npmjs.org`
|
directories overleven geen reboot. Aanmaken via Control Panel → Privilege →
|
||||||
|
Shared Folders → Create. QTS legt dan automatisch de symlink
|
||||||
|
`/share/Agent → /share/CACHEDEV1_DATA/Agent`.
|
||||||
|
- Drie subdirs onder die share: `/share/Agent/cache`, `/share/Agent/logs`,
|
||||||
|
`/share/Agent/state`. Aanmaken via File Station of via SSH na share-creatie.
|
||||||
|
- Internet-uitgang naar `api.anthropic.com`, `github.com`, je Neon-host, `registry.npmjs.org`.
|
||||||
|
|
||||||
|
> **Verifieer** vóór je deployt dat `/share/Agent` echt op disk staat:
|
||||||
|
> ```bash
|
||||||
|
> ssh admin@<nas> 'ls -la /share/ | grep Agent; df -h /share/Agent'
|
||||||
|
> ```
|
||||||
|
> Verwacht een symlink (`l...Agent -> /share/CACHEDEV1_DATA/Agent`) en een
|
||||||
|
> df-uitvoer met TB-grootte op `cachedev1`/`cachedev2`. Als je hier `tmpfs 16M`
|
||||||
|
> ziet, is de share geen geregistreerde QTS Shared Folder en zal elke transfer
|
||||||
|
> >16 MB falen met `scp: write remote ... Failure`.
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
|
|
@ -95,10 +109,130 @@ docker compose build
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
# 5. Verifiëren
|
# 5. Verifiëren
|
||||||
curl http://nas.local:8080/health
|
curl http://nas.local:18080/health
|
||||||
docker compose logs -f
|
docker compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **QNAP-port:** host-poort 8080 is bezet door de QTS-webinterface; daarom
|
||||||
|
> mapt deze stack standaard `18080:8080`. Override via
|
||||||
|
> `AGENT_HEALTH_PORT_HOST` in `.env` als je een andere host-poort wilt.
|
||||||
|
|
||||||
|
## Deploy — cross-build vanaf Mac (Apple Silicon → amd64-NAS)
|
||||||
|
|
||||||
|
Alternatief voor de in-place build hierboven. Bouw de image op je Mac voor
|
||||||
|
`linux/amd64`, schrijf 'm naar een tarball, transfer naar de NAS en laad daar.
|
||||||
|
Handig als de NAS te langzaam is om te builden (npm install op een N5095 met
|
||||||
|
NAS-storage is traag) of als je geen `git push` wilt voor elke iteratie.
|
||||||
|
|
||||||
|
**Vóór je begint:** controleer dat `/share/Agent` een echte QTS Shared Folder is
|
||||||
|
(zie [Vereisten op de NAS](#vereisten-op-de-nas)). Dat is de meest voorkomende
|
||||||
|
val.
|
||||||
|
|
||||||
|
### 1. Tokens en `.env` op je Mac
|
||||||
|
|
||||||
|
Zelfde tokens als in [Deploy](#deploy). Hou er rekening mee dat je **twee
|
||||||
|
`.env`-bestanden** kunt willen: één voor lokaal Mac-testen
|
||||||
|
(`AGENT_PLATFORM=linux/arm64`, `AGENT_UID=501`, paths onder `/Users/...`) en
|
||||||
|
één voor NAS-runtime. De NAS-versie wordt bij stap 4 ge-scp'd en met `sed`
|
||||||
|
geschikt gemaakt.
|
||||||
|
|
||||||
|
### 2. Image bouwen voor amd64
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/<jij>/Development/scrum4me-docker
|
||||||
|
|
||||||
|
docker buildx build \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--build-arg MCP_GIT_REF=main \
|
||||||
|
--build-arg CLAUDE_CODE_VERSION=latest \
|
||||||
|
--build-arg AGENT_UID=1000 \
|
||||||
|
--build-arg AGENT_GID=1000 \
|
||||||
|
-t scrum4me-agent-runner:local \
|
||||||
|
--load \
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
`AGENT_UID/GID=1000` zijn de **NAS-UIDs**, niet je Mac-UIDs (vaak `501/20`).
|
||||||
|
Bij verkeerde UIDs kan de container niet schrijven naar de bind-mounts.
|
||||||
|
|
||||||
|
Verifieer architectuur:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker image inspect scrum4me-agent-runner:local --format '{{.Architecture}}'
|
||||||
|
# verwacht: amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Image naar tarball
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker save scrum4me-agent-runner:local | gzip > scrum4me-agent-runner-amd64.tar.gz
|
||||||
|
shasum -a 256 scrum4me-agent-runner-amd64.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
De tarball is ~580 MB voor de huidige image-size. Hij staat in `.gitignore`,
|
||||||
|
dus geen risico op accidenteel committen.
|
||||||
|
|
||||||
|
### 4. NAS voorbereiden + bestanden transferren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env naar de NAS (de Mac-versie; we patchen path-velden zo direct)
|
||||||
|
scp .env admin@<nas>:/share/Agent/scrum4me-agent-runner/.env
|
||||||
|
|
||||||
|
# image-tarball + bijgewerkte compose
|
||||||
|
scp scrum4me-agent-runner-amd64.tar.gz docker-compose.yml package.json README.md \
|
||||||
|
admin@<nas>:/share/Agent/scrum4me-agent-runner/
|
||||||
|
```
|
||||||
|
|
||||||
|
Zet de NAS-specifieke waarden in `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh admin@<nas> "
|
||||||
|
source /etc/profile
|
||||||
|
cd /share/Agent/scrum4me-agent-runner
|
||||||
|
sed -i \
|
||||||
|
-e 's|^NAS_BASE=.*|NAS_BASE=/share/Agent|' \
|
||||||
|
-e 's|^AGENT_BASE=.*|AGENT_BASE=/share/Agent|' \
|
||||||
|
-e 's|^AGENT_PLATFORM=.*|AGENT_PLATFORM=linux/amd64|' \
|
||||||
|
-e 's|^AGENT_UID=.*|AGENT_UID=1000|' \
|
||||||
|
-e 's|^AGENT_GID=.*|AGENT_GID=1000|' \
|
||||||
|
-e 's|^AGENT_HEALTH_PORT_HOST=.*|AGENT_HEALTH_PORT_HOST=18080|' \
|
||||||
|
.env
|
||||||
|
chmod 600 .env
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **`source /etc/profile` is verplicht** in non-interactieve ssh op QNAP. Zonder
|
||||||
|
> die regel staat `docker` niet op `$PATH` (Container Station's binary zit onder
|
||||||
|
> `/share/CACHEDEV*_DATA/.qpkg/container-station/...`) en faalt elk
|
||||||
|
> `docker`-commando met `command not found`.
|
||||||
|
|
||||||
|
### 5. Image laden + container recreaten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh admin@<nas> "
|
||||||
|
source /etc/profile
|
||||||
|
set -e
|
||||||
|
cd /share/Agent/scrum4me-agent-runner
|
||||||
|
|
||||||
|
# checksum-verificatie (optioneel maar aan te raden)
|
||||||
|
sha256sum scrum4me-agent-runner-amd64.tar.gz
|
||||||
|
|
||||||
|
# image laden — overschrijft bestaande :local tag
|
||||||
|
gunzip -c scrum4me-agent-runner-amd64.tar.gz | docker load
|
||||||
|
|
||||||
|
# container recreate; --no-build voorkomt onbedoelde NAS-side build
|
||||||
|
docker compose up -d --no-build --force-recreate agent
|
||||||
|
|
||||||
|
docker compose ps
|
||||||
|
curl -fsS http://localhost:18080/health | head -c 800
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Verwacht in de health-output `cache_free_bytes` met een groot getal
|
||||||
|
(TB-orde) — dat is je signaal dat `/var/cache` op echte disk zit.
|
||||||
|
Een waarde rond `16 MB` (`16777216`) betekent dat je per ongeluk
|
||||||
|
nog steeds op tmpfs draait.
|
||||||
|
|
||||||
## Updaten (handmatig, bewust)
|
## Updaten (handmatig, bewust)
|
||||||
|
|
||||||
`SCRUM4ME_TOKEN` of `CLAUDE_CODE_OAUTH_TOKEN` rouleer je via een rebuild:
|
`SCRUM4ME_TOKEN` of `CLAUDE_CODE_OAUTH_TOKEN` rouleer je via een rebuild:
|
||||||
|
|
@ -136,7 +270,7 @@ docker exec scrum4me-agent df -h /var/cache
|
||||||
|
|
||||||
## Health-endpoint
|
## Health-endpoint
|
||||||
|
|
||||||
`GET http://<nas>:8080/health` retourneert:
|
`GET http://<nas>:18080/health` retourneert:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -178,6 +312,114 @@ Push gaat over dezelfde token: `git push -u origin feat/story-<id>`
|
||||||
slaagt zonder prompt. `gh pr create` (voor producten met `auto_pr=true`)
|
slaagt zonder prompt. `gh pr create` (voor producten met `auto_pr=true`)
|
||||||
gebruikt dezelfde `GH_TOKEN` via de `gh` CLI's standaard env-detect.
|
gebruikt dezelfde `GH_TOKEN` via de `gh` CLI's standaard env-detect.
|
||||||
|
|
||||||
|
## Veelvoorkomende issues
|
||||||
|
|
||||||
|
Verzameld uit echte deploy-sessies op een TS-664 met QTS 5.x. Eerst checken
|
||||||
|
voor je gaat tweaken.
|
||||||
|
|
||||||
|
### `/share/Agent` is `tmpfs 16M` — scp faalt op grote files
|
||||||
|
|
||||||
|
**Symptoom:**
|
||||||
|
```
|
||||||
|
scp: write remote ".../scrum4me-agent-runner-amd64.tar.gz": Failure
|
||||||
|
```
|
||||||
|
Eerst 100 % bij kleine files, dan blijvend "Failure" zodra een file de tmpfs
|
||||||
|
vol maakt.
|
||||||
|
|
||||||
|
**Oorzaak:** `/share/Agent` is geen geregistreerde QTS Shared Folder maar een
|
||||||
|
gewone directory in de root-tmpfs. Na elke reboot is alle inhoud weg en is
|
||||||
|
`/share/Agent` slechts een entry in de 16 MB `/share`-tmpfs.
|
||||||
|
|
||||||
|
**Fix:** maak `Agent` aan via Control Panel → Privilege → Shared Folders →
|
||||||
|
Create. Daarna verschijnt automatisch `lrwxrwxrwx /share/Agent → /share/CACHEDEV1_DATA/Agent`.
|
||||||
|
Als die symlink ontbreekt omdat er al een tmpfs-directory `/share/Agent` staat:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh admin@<nas> "
|
||||||
|
source /etc/profile
|
||||||
|
docker stop scrum4me-agent 2>/dev/null
|
||||||
|
docker rm scrum4me-agent 2>/dev/null
|
||||||
|
rm -rf /share/Agent
|
||||||
|
ln -s /share/CACHEDEV1_DATA/Agent /share/Agent
|
||||||
|
mkdir -p /share/Agent/{cache,logs,state,scrum4me-agent-runner}
|
||||||
|
ls -la /share/ | grep Agent # moet nu een symlink tonen
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **`rm -rf /share/Agent` is veilig** zolang `/share/Agent` nog tmpfs is —
|
||||||
|
> alle content stond in RAM en zou de volgende reboot toch verdwenen zijn.
|
||||||
|
> Maar verifieer eerst met `df -h /share/Agent` dat 't echt tmpfs is.
|
||||||
|
|
||||||
|
### `docker: command not found` in non-interactieve ssh
|
||||||
|
|
||||||
|
**Symptoom:**
|
||||||
|
```bash
|
||||||
|
ssh admin@<nas> 'docker ps'
|
||||||
|
# sh: line 1: docker: command not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Oorzaak:** QNAP's `admin`-user heeft `docker` op `$PATH` via
|
||||||
|
`/etc/profile.d/*.sh` van Container Station. Login-shells laden die scripts;
|
||||||
|
non-interactieve `ssh user@host 'cmd'` doet dat **niet**.
|
||||||
|
|
||||||
|
**Fix:** `source /etc/profile` aan het begin van je remote command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh admin@<nas> "source /etc/profile && docker ps"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `yaml: control characters are not allowed`
|
||||||
|
|
||||||
|
**Symptoom:**
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
# yaml: control characters are not allowed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Oorzaak:** een eerdere `scp` van `docker-compose.yml` faalde halverwege en
|
||||||
|
liet een file met NUL-bytes / partial writes achter. De file-grootte klopt
|
||||||
|
maar de inhoud is corrupt.
|
||||||
|
|
||||||
|
**Fix:** scp opnieuw vanaf je werkstation. Voor het stoppen van een al-
|
||||||
|
draaiende container heb je de yml niet nodig:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stop scrum4me-agent && docker rm scrum4me-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
Daarna `scp docker-compose.yml admin@<nas>:/share/Agent/scrum4me-agent-runner/`
|
||||||
|
en pas dan `docker compose up -d --no-build --force-recreate`.
|
||||||
|
|
||||||
|
### QTS-console-menu Python-traceback bij ssh-commando
|
||||||
|
|
||||||
|
**Symptoom:** lange Python-traceback over `consolemenu_q/prompt_utils.py:31`
|
||||||
|
en `Inappropriate ioctl for device`, gevolgd door je daadwerkelijke output.
|
||||||
|
|
||||||
|
**Oorzaak:** QTS' admin-shell start een Python-TUI (qts-console-menu) die
|
||||||
|
crasht zonder echte TTY. Niet-fataal — je commando wordt alsnog gerund.
|
||||||
|
|
||||||
|
**Fix:** negeer de traceback. Als je 'm écht weg wilt, gebruik
|
||||||
|
`ssh -tt admin@<nas> '...'` voor een geforceerde pseudo-TTY, maar pas op:
|
||||||
|
sommige scripts hangen daarop omdat ze interactie verwachten.
|
||||||
|
|
||||||
|
### `.env` is weg na `rm -rf /share/Agent`
|
||||||
|
|
||||||
|
**Symptoom:** na het opruimen van een tmpfs-`/share/Agent` weigert
|
||||||
|
`docker compose up`:
|
||||||
|
```
|
||||||
|
env file /share/Agent/scrum4me-agent-runner/.env not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Oorzaak:** `.env` zit in `.gitignore` en wordt nooit door `git pull` of `scp
|
||||||
|
.` automatisch teruggezet. Bij het wegnuken van een corrupte share verdwijnt-ie
|
||||||
|
mee.
|
||||||
|
|
||||||
|
**Fix:** scp 'm vanaf je werkstation, en patch path-velden op de NAS-zijde
|
||||||
|
(zie [Deploy — cross-build](#deploy--cross-build-vanaf-mac-apple-silicon--amd64-nas)
|
||||||
|
stap 4). Zorg dat je `.env` op je werkstation **alle** secrets bevat — vooral
|
||||||
|
`SCRUM4ME_TOKEN`, dat in een Mac-only `.env` makkelijk ontbreekt omdat
|
||||||
|
lokale tests soms zonder Scrum4Me-API draaien.
|
||||||
|
|
||||||
## Bekende grenzen
|
## Bekende grenzen
|
||||||
|
|
||||||
- **Eén actieve job tegelijk.** De wrapper-loop is sequentieel. Voor
|
- **Eén actieve job tegelijk.** De wrapper-loop is sequentieel. Voor
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ services:
|
||||||
- /tmp:size=4g,mode=1777
|
- /tmp:size=4g,mode=1777
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "${AGENT_HEALTH_PORT_HOST:-8080}:8080"
|
- "${AGENT_HEALTH_PORT_HOST:-18080}:8080"
|
||||||
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"down": "docker compose down",
|
"down": "docker compose down",
|
||||||
"logs": "docker compose logs -f",
|
"logs": "docker compose logs -f",
|
||||||
"rebuild": "docker compose build --no-cache && docker compose up -d",
|
"rebuild": "docker compose build --no-cache && docker compose up -d",
|
||||||
"health": "curl -fsS http://localhost:${AGENT_HEALTH_PORT_HOST:-8080}/health | jq ."
|
"health": "curl -fsS http://localhost:${AGENT_HEALTH_PORT_HOST:-18080}/health | jq ."
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue