045892bc94
USB passthrough (1ea1aa4) made /dev/usb/lp1 visible in the container, but the node
is `crw-rw---- root:lp` (660) and the server runs as the non-root `app` user, not in
`lp` — so open(O_WRONLY) → EACCES → printer still "offline". Add the host's `lp` GID
(7 on this Ubuntu booth, verified `getent group lp` → lp:x:7:) via group_add, so the
app process gains the supplementary group that owns the node. Least-privilege: no
world-writable device, no root, no image rebuild. (If a future booth's lp GID differs,
update the number.)
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
101 lines
5.0 KiB
YAML
101 lines
5.0 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
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
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"
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
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"
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
volumes:
|
|
caddy-data:
|
|
caddy-config:
|