c21babf293
Operator asked for bounded container logs (~2 months of history), human- readable timestamps, and clarity on levels. Levels already existed (LOG_LEVEL env → pino, default info; warn+ teed into app_logs, queryable at /setup/logs) — the "level":30 / epoch-ms "time" in docker logs were pino defaults. - server.ts logger: stamp ISO-8601 UTC time (timestamp fn) and level NAMES (formatters.level) so `docker logs` reads human. - log-service.ts pinoDbStream: accept BOTH level encodings (name + numeric) — the label switch would otherwise have silently stopped warn+ persistence into app_logs. New log-service-stream.test.ts pins both encodings, the info-stays-stdout-only rule, and the never-throws fallback. - docker-compose.prod.yml: json-file caps resized from 10m×3 (≈30 MB — days, not months) to ≈2 months by volume: server 20m×30, vision 20m×10, proxy 10m×5. json-file rotates by SIZE; time-based isn't a driver feature — comment says to revisit if `docker logs` holds under ~60 days. - app_logs retention default aligned 30→60 days (LOG_RETENTION_DAYS still overrides). Wiki: app-logs.md gains the container-log store section (rotation, format, LOG_LEVEL knob) + retention update; log.md entry. Suite 282 green. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
109 lines
5.6 KiB
YAML
109 lines
5.6 KiB
YAML
# PROD override: pull pinned registry images (no local build), restart always, real
|
||
# recognizer, and a CADDY reverse proxy in front so operators reach the booth on a clean
|
||
# port-80 URL (no :3000) — and a path to real TLS later. Server + vision stay INTERNAL
|
||
# (only Caddy publishes a port). Use with the base file and pin TAG to the branch you deploy:
|
||
# REGISTRY=git.infra.msai.al/mca/parking_solution TAG=main \
|
||
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||
# See wiki/decisions/container-deployment.md.
|
||
|
||
services:
|
||
# Reverse proxy: :80 → server (127.0.0.1:3000). On the HOST network (see the server note),
|
||
# so it reaches the host-net server over loopback and publishes :80 directly on the host.
|
||
# WebSocket /api/ws upgrades pass through natively. Swapping http:// for the site's real
|
||
# hostname later enables automatic HTTPS. Reached at http://<name-or-ip>/ (name via hosts/DNS
|
||
# on-site — NOT baked into any image).
|
||
proxy:
|
||
image: caddy:2-alpine
|
||
restart: always
|
||
# Host network: Caddy listens on the host's :80 and proxies the host-net server on
|
||
# 127.0.0.1:3000. (No `ports:` mapping — host mode publishes directly.)
|
||
network_mode: host
|
||
# host mode is mutually exclusive with a named network; the base file doesn't attach proxy,
|
||
# so nothing to null here (server does — see below).
|
||
volumes:
|
||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||
- caddy-data:/data
|
||
- caddy-config:/config
|
||
depends_on:
|
||
- server
|
||
# ≈2 months (see the server note): Caddy logs errors only — 10 MB × 5 is plenty.
|
||
logging:
|
||
driver: json-file
|
||
options:
|
||
max-size: "10m"
|
||
max-file: "5"
|
||
|
||
server:
|
||
restart: always
|
||
# HOST NETWORK — the crux of the appliance. The server is the ONLY container doing device
|
||
# I/O (camera ISAPI snapshots, relay control, receiving reader/alarm pushes), all on the
|
||
# booth's LAN / isolated device VLAN (10.0.10.x). On a bridge network it sees only the Docker
|
||
# subnet (172.18.0.x) — it can't reach the relay, can't be reached by push devices, and the
|
||
# backend-IP picker (net.ts networkInterfaces) only sees eth0. Host mode puts it on the real
|
||
# NICs. Vision stays bridged (it never touches a device — the server hands it JPEG bytes).
|
||
network_mode: host
|
||
# host mode is mutually exclusive with a named network — detach the base file's `parking`
|
||
# attachment (compose errors otherwise: "network_mode and networks cannot both be set").
|
||
networks: !reset []
|
||
# Listens on :3000 directly on the host (Caddy proxies it). Loopback to vision:
|
||
environment:
|
||
VISION_URL: http://127.0.0.1:8089
|
||
# NB: NO `sysctls:` here. net.ipv4.ping_group_range is a per-netns sysctl; under host net
|
||
# there is no separate namespace, and runc REFUSES it ("not allowed in host network
|
||
# namespace"). Reader liveness ping uses the HOST's setting instead — the booth host must
|
||
# set net.ipv4.ping_group_range (see appliance-provisioning §7 / disk-os-hardening).
|
||
#
|
||
# USB PRINTER PASSTHROUGH. A USB ESC/POS printer (Rongta/Cashino) is the kernel `usblp` char
|
||
# device /dev/usb/lpN on the HOST — the container has its own /dev and can't see it (probeUsb
|
||
# open() → ENOENT → printer always "offline"). Two parts, both needed:
|
||
# - bind-mount /dev/usb so the lpN NODES appear inside the container, and
|
||
# - a device-cgroup rule permitting the usblp char major (180) so the kernel allows the
|
||
# open(). `180:*` covers lp0/lp1/lp2… so a USB replug/boot-order renumber still works
|
||
# (the printer's path can move; set Connection=USB + the matching /dev/usb/lpN in setup).
|
||
# (Bind-mounting the dir, not a single `devices:` node, is what survives renumbering.)
|
||
#
|
||
# ...AND access: the lpN node is `crw-rw---- root:lp` (mode 660). The server runs as the
|
||
# non-root `app` user, which is NOT in `lp`, so open(O_WRONLY) → EACCES → still "offline".
|
||
# group_add the HOST's `lp` GID (numeric — `getent group lp`, typically 7 on Debian/Ubuntu)
|
||
# so the app process gains that supplementary group and can write the 660 node. Least-
|
||
# privilege (no world-writable device, no root, no rebuild). VERIFY the GID on the booth;
|
||
# if the host's lp GID differs, change the number here.
|
||
group_add:
|
||
- "7"
|
||
volumes:
|
||
- /dev/usb:/dev/usb
|
||
device_cgroup_rules:
|
||
- "c 180:* rmw"
|
||
# LOG ROTATION (2026-07-04). Docker's json-file driver rotates by SIZE, not time —
|
||
# these caps are sized to hold ≈2 MONTHS at observed booth rates (the operator's
|
||
# chosen diagnostic window; revisit if `docker logs` shows less than ~60 days of
|
||
# history). Server gets the most (request + device chatter): 20 MB × 30 = 600 MB
|
||
# ceiling. NB: `docker logs` only reaches back as far as these files. The queryable
|
||
# warn+ store (app_logs, /setup/logs) has its own matching 60-day retention.
|
||
logging:
|
||
driver: json-file
|
||
options:
|
||
max-size: "20m"
|
||
max-file: "30"
|
||
|
||
vision:
|
||
restart: always
|
||
# The real ANPR engine. The image baked the model weights at build (offline-first).
|
||
# Stays on the bridge network (isolated — it makes NO outbound device calls), but PUBLISHES
|
||
# 8089 on the host LOOPBACK ONLY so the host-net server can reach it. 127.0.0.1 binding keeps
|
||
# it off the booth LAN — nothing on the network can hit the ANPR service.
|
||
environment:
|
||
VISION_RECOGNIZER: fast_alpr
|
||
ports:
|
||
- "127.0.0.1:8089:8089"
|
||
# ≈2 months (see the server note): vision logs less — 20 MB × 10 = 200 MB ceiling.
|
||
logging:
|
||
driver: json-file
|
||
options:
|
||
max-size: "20m"
|
||
max-file: "10"
|
||
|
||
volumes:
|
||
caddy-data:
|
||
caddy-config:
|