Files
parking_solution/docker-compose.prod.yml
julian c21babf293
Build & push images / images (push) Successful in 2m54s
CI / check (push) Successful in 41s
feat(logging): ~2-month container rotation, ISO timestamps, level names
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
2026-07-04 19:47:53 +02:00

109 lines
5.6 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: