Files
parking_solution/docker-compose.prod.yml
T
julian 1ea1aa4189
Build & push images / images (push) Successful in 2m45s
CI / check (push) Successful in 39s
fix(deploy): pass the USB printer (usblp) into the host-net server container
The USB ESC/POS printer is the host's /dev/usb/lpN (usblp char device, major 180),
but the container has its own /dev — `docker exec server ls /dev/usb` → "No such
file or directory", so probeUsb's open() ENOENTs and the printer is always offline
regardless of the path set in setup. Containerization isolates host hardware (same
root cause as the network fix); USB needs explicit passthrough:

- volumes: /dev/usb:/dev/usb  → the lpN NODES appear inside the container
- device_cgroup_rules: 'c 180:* rmw'  → permit the usblp char major (180), and the
  `:*` minor wildcard survives lp0/lp1/lp2 renumbering across replug/boot-order.

Binding the /dev/usb DIR (not a single `devices:` node) is what survives renumber.
Merge verified: parking-data ledger volume preserved (lists append), host net intact,
config valid. Booth prereq: `usblp` loaded at boot + printer attached before start,
else /dev/usb is absent.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-27 14:11:51 +02:00

92 lines
4.4 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.)
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: