# 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 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: