diff --git a/faceai/README.md b/faceai/README.md index ee31220c..bcb04f06 100644 --- a/faceai/README.md +++ b/faceai/README.md @@ -84,7 +84,7 @@ The `processor` service is built from `docker/processor.Dockerfile`, which uses ### Persistent Logs -The checked-in local Compose stack now redirects the relevant Node service logs into `faceai/logs` on the host. +The checked-in local Compose stack now mirrors the relevant Node service logs to both Docker stdout/stderr and `faceai/logs` on the host. After `docker compose up --build`, inspect: @@ -95,6 +95,8 @@ After `docker compose up --build`, inspect: This keeps the useful processor diagnostics outside the Docker-managed runtime volume so they survive container rebuilds and can be inspected directly from the workspace. +Because the service entrypoints now mirror output instead of redirecting it away, the same startup and runtime messages are also visible through `docker logs regalami-faceai`, `docker logs regalami-faceai-processor`, and Portainer's container log viewer. + The current bundled Linux `face_matcher` binary is a PyInstaller build that requires `GLIBC_2.38` or newer and the `libxcb.so.1` runtime library. The checked-in local processor image satisfies that requirement. ### Run The Browser Test @@ -211,14 +213,20 @@ services: image: forgejo.maddoscientisto.net/maddo/faceai-client:latest container_name: regalami-faceai restart: unless-stopped - command: sh -c "mkdir -p /data/logs && npm run start >> /data/logs/backend.log 2>&1" + command: + - node + - docker/run-with-log-file.mjs + - /data/logs/backend.log + - npm + - run + - start environment: NODE_ENV: production PORT: 3001 FACEAI_FRONTEND_URL: https://ai.regalamiunsorriso.it FACEAI_PUBLIC_BASE_URL: https://ai.regalamiunsorriso.it FACEAI_LEGACY_RETURN_URL: https://www.regalamiunsorriso.it/faceai_return.php - FACEAI_SHARED_SECRET: change-this-to-a-long-random-secret + FACEAI_SHARED_SECRET: disagio-spaghetti-science-lol-boh FACEAI_SESSION_COOKIE: rus_faceai_session FACEAI_REDIS_URL: redis://redis:6379 FACEAI_QUEUE_NAME: faceai-searches @@ -228,19 +236,32 @@ services: FACEAI_PKL_ROOT: /data/pkl FACEAI_ENABLE_LOCAL_LEGACY_STATIC: 0 volumes: - - /var/docker/faceai/runtime:/data/runtime - - /var/docker/faceai/logs:/data/logs + - /mnt/storage/data/faceai/runtime:/data/runtime + - /mnt/storage/data/faceai/logs:/data/logs - /mnt/nas12/nas2/RUS:/data/pkl:ro ports: - "127.0.0.1:3001:3001" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3001/health | grep -q '\"ok\":true'"] + interval: 10s + timeout: 5s + retries: 6 + start_period: 20s depends_on: - - redis + redis: + condition: service_healthy processor: image: forgejo.maddoscientisto.net/maddo/faceai-client:latest container_name: regalami-faceai-processor restart: unless-stopped - command: sh -c "mkdir -p /data/logs && npm run start:processor >> /data/logs/processor.log 2>&1" + command: + - node + - docker/run-with-log-file.mjs + - /data/logs/processor.log + - npm + - run + - start:processor environment: NODE_ENV: production FACEAI_REDIS_URL: redis://redis:6379 @@ -252,18 +273,24 @@ services: FACEAI_WORKER_CONCURRENCY: 2 FACEAI_WORKER_TIMEOUT_MS: 300000 volumes: - - /var/docker/faceai/runtime:/data/runtime - - /var/docker/faceai/logs:/data/logs + - /mnt/storage/data/faceai/runtime:/data/runtime + - /mnt/storage/data/faceai/logs:/data/logs - /mnt/nas12/nas2/RUS:/data/pkl:ro - - /var/docker/faceai/bin/Face_Recognition_Unix:/opt/face-recognition:ro + - /mnt/storage/data/faceai/bin/Face_Recognition_Unix:/opt/face-recognition:ro depends_on: - - redis + redis: + condition: service_healthy redis: image: redis:7-alpine container_name: regalami-faceai-redis restart: unless-stopped command: redis-server --appendonly no + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 12 ``` This pattern assumes a reverse proxy on the host publishes `https://ai.regalamiunsorriso.it` and forwards to `127.0.0.1:3001`. The processor is internal-only and does not expose any public port. @@ -392,7 +419,9 @@ In the provided Docker Compose stack, that wiring is already done with: FACEAI_LEGACY_RETURN_URL=http://localhost:8080/faceai_return.php ``` -The log wiring is also already done in the checked-in Compose file with a host bind mount for `./logs:/data/logs`, so both the backend and the processor write persistent diagnostics into the workspace. +The log wiring is also already done in the checked-in Compose file with a host bind mount for `./logs:/data/logs`, so both the backend and the processor write persistent diagnostics into the workspace while also remaining visible through Docker and Portainer container logs. + +The Compose contract now also includes an HTTP healthcheck on the public FaceAI service and a Redis readiness check. That makes `docker compose ps` meaningful during rollout: `faceai` only becomes healthy after `GET /health` returns `{"ok":true}`, and both the public site and the processor wait for Redis readiness before their own startup sequence begins. The local PHP simulator also needs the legacy bridge feature flag enabled: diff --git a/faceai/docker-compose.yml b/faceai/docker-compose.yml index e9b2fda2..e8ecb506 100644 --- a/faceai/docker-compose.yml +++ b/faceai/docker-compose.yml @@ -2,8 +2,17 @@ services: faceai: image: node:20-alpine container_name: regalami-faceai + restart: unless-stopped working_dir: /app - command: sh -c "mkdir -p /data/logs && npm run start --workspace @regalami/faceai-backend >> /data/logs/backend.log 2>&1" + command: + - node + - docker/run-with-log-file.mjs + - /data/logs/backend.log + - npm + - run + - start + - --workspace + - "@regalami/faceai-backend" environment: PORT: 3001 FACEAI_FRONTEND_URL: http://localhost:3001 @@ -12,7 +21,7 @@ services: FACEAI_PKL_ROOT: /data/pkl FACEAI_ENABLE_LOCAL_LEGACY_STATIC: 1 FACEAI_LOCAL_LEGACY_STATIC_ROOT: /legacy-www - FACEAI_SHARED_SECRET: change-me + FACEAI_SHARED_SECRET: disagio-spaghetti-science-lol-boh FACEAI_SESSION_COOKIE: rus_faceai_session FACEAI_REDIS_URL: redis://redis:6379 FACEAI_RUNTIME_ROOT: /data/runtime @@ -26,8 +35,15 @@ services: - faceai-runtime:/data/runtime ports: - "3001:3001" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3001/health | grep -q '\"ok\":true'"] + interval: 10s + timeout: 5s + retries: 6 + start_period: 20s depends_on: - - redis + redis: + condition: service_healthy processor: build: @@ -35,8 +51,17 @@ services: dockerfile: docker/processor.Dockerfile image: regalami-faceai-processor-local container_name: regalami-faceai-processor + restart: unless-stopped working_dir: /app - command: sh -c "mkdir -p /data/logs && npm run start --workspace @regalami/faceai-processor >> /data/logs/processor.log 2>&1" + command: + - node + - docker/run-with-log-file.mjs + - /data/logs/processor.log + - npm + - run + - start + - --workspace + - "@regalami/faceai-processor" environment: FACEAI_REDIS_URL: redis://redis:6379 FACEAI_QUEUE_NAME: faceai-searches @@ -52,27 +77,38 @@ services: - ../test_pkl:/data/pkl:ro - faceai-runtime:/data/runtime depends_on: - - redis + redis: + condition: service_healthy redis: image: redis:7-alpine container_name: regalami-faceai-redis + restart: unless-stopped command: redis-server --appendonly no + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 12 legacy-php: image: php:8.3-apache container_name: regalami-legacy-php + restart: unless-stopped environment: FACEAI_FEATURE_ENABLED: 1 FACEAI_BACKEND_INTERNAL_URL: http://faceai:3001 FACEAI_FRONTEND_URL: http://localhost:3001 - FACEAI_SHARED_SECRET: change-me + FACEAI_SHARED_SECRET: disagio-spaghetti-science-lol-boh FACEAI_ALLOW_DEV_HANDOFF: 1 FACEAI_IDENTITY_COOKIE: rus_faceai_identity volumes: - ../www:/var/www/html ports: - "8080:80" + depends_on: + faceai: + condition: service_healthy volumes: faceai-runtime: diff --git a/faceai/docker/run-with-log-file.mjs b/faceai/docker/run-with-log-file.mjs new file mode 100644 index 00000000..4aec6518 --- /dev/null +++ b/faceai/docker/run-with-log-file.mjs @@ -0,0 +1,53 @@ +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { spawn } from 'node:child_process'; + +const [, , logPath, ...commandArgs] = process.argv; + +if (!logPath || commandArgs.length === 0) { + process.stderr.write('Usage: node docker/run-with-log-file.mjs [args...]\n'); + process.exit(1); +} + +await fsp.mkdir(path.dirname(logPath), { recursive: true }); + +const logStream = fs.createWriteStream(logPath, { flags: 'a' }); +const child = spawn(commandArgs[0], commandArgs.slice(1), { + cwd: process.cwd(), + env: process.env, + stdio: ['inherit', 'pipe', 'pipe'] +}); + +function writeChunk(target, chunk) { + target.write(chunk); + logStream.write(chunk); +} + +child.stdout.on('data', (chunk) => { + writeChunk(process.stdout, chunk); +}); + +child.stderr.on('data', (chunk) => { + writeChunk(process.stderr, chunk); +}); + +child.on('error', (error) => { + const message = `${error.stack || error.message}\n`; + writeChunk(process.stderr, message); + logStream.end(() => { + process.exit(1); + }); +}); + +child.on('close', (code, signal) => { + logStream.end(() => { + if (signal) { + process.kill(process.pid, signal); + return; + } + + process.exit(code ?? 1); + }); +});