fix(deploy): server on host network so it sees the booth LAN / device VLAN
In prod the containerized server sat on the Docker bridge (172.18.0.x) and could
only see eth0 — so the setup backend-IP picker (net.ts networkInterfaces) showed
just the Docker IP, the server couldn't reach the relay or fetch Hikvision ISAPI
snapshots, and push devices (readers/cameras) couldn't reach it. The server is the
ONLY container doing device I/O, so put it on the HOST network namespace.
- docker-compose.prod.yml: server + proxy → network_mode: host (server detaches the
base `parking` network via `networks: !reset []`). server VISION_URL=127.0.0.1:8089.
vision stays BRIDGED (it never touches a device — the server hands it JPEG bytes)
but publishes 8089 on 127.0.0.1 only, so the host-net server reaches it over
loopback while the ANPR service stays off the LAN.
- docker-compose.yml: VISION_URL is now ${VISION_URL:-http://vision:8089} so dev keeps
compose-DNS service-name routing; prod overrides to loopback.
- Caddyfile: reverse_proxy 127.0.0.1:3000 (was server:3000 — service DNS doesn't
resolve on host net). Dev doesn't use Caddy, so unaffected.
Merge validated for both envs (booth.sh config, exit 0). Host-net side effect: the
container ping_group_range sysctl is a no-op — the HOST must set it for reader ICMP
liveness (see appliance-provisioning).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -9,5 +9,7 @@
|
||||
# CA / internal cert, use `tls /path/cert.pem /path/key.pem`.
|
||||
:80 {
|
||||
encode gzip
|
||||
reverse_proxy server:3000
|
||||
# Host network (prod): the server runs on the host's net namespace (to reach the booth LAN /
|
||||
# device VLAN), so reach it over loopback, not the compose service name `server`.
|
||||
reverse_proxy 127.0.0.1:3000
|
||||
}
|
||||
|
||||
+31
-18
@@ -7,24 +7,25 @@
|
||||
# See wiki/decisions/container-deployment.md.
|
||||
|
||||
services:
|
||||
# Reverse proxy: :80 → server:3000 (WebSocket /api/ws upgrades pass through natively).
|
||||
# Caddy is a single static binary with a one-line proxy config; swapping http:// for the
|
||||
# site's real hostname later enables automatic HTTPS. The booth is reached at
|
||||
# http://<name-or-ip>/ (the name set via hosts/DNS on-site — NOT baked into any image).
|
||||
# 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
|
||||
ports:
|
||||
- "80:80"
|
||||
# - "443:443" # uncomment when moving to TLS (and set a real hostname in Caddyfile)
|
||||
# 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
|
||||
networks:
|
||||
- parking
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
@@ -33,14 +34,22 @@ services:
|
||||
|
||||
server:
|
||||
restart: always
|
||||
# No published port — only the proxy reaches the server, over the private network.
|
||||
expose:
|
||||
- "3000"
|
||||
# Let the server ICMP-ping push-only readers (Dingtian/GEE QR) for an honest
|
||||
# online/offline status WITHOUT CAP_NET_RAW: opening ping_group_range to all gids
|
||||
# enables `/bin/ping` in unprivileged SOCK_DGRAM mode for the non-root runtime user.
|
||||
# (The reader exposes no TCP port, so a connect-probe can't work — see reader.ts /
|
||||
# wiki/entities/dingtian-qr-reader.md.)
|
||||
# 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
|
||||
# Reader liveness ping (Dingtian/GEE QR push-only readers): on host net the server uses the
|
||||
# HOST's net.ipv4.ping_group_range — ensure the host sets it (see appliance-provisioning).
|
||||
# The container-level sysctl below is a no-op under host net but kept as documentation.
|
||||
sysctls:
|
||||
- net.ipv4.ping_group_range=0 2147483647
|
||||
logging:
|
||||
@@ -52,9 +61,13 @@ services:
|
||||
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
|
||||
# No published ports — vision is reached only by the server over the private network.
|
||||
ports:
|
||||
- "127.0.0.1:8089:8089"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
|
||||
+5
-2
@@ -13,8 +13,11 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: /data/parking.sqlite
|
||||
# Reach the vision service over the private compose network by service name.
|
||||
VISION_URL: http://vision:8089
|
||||
# Reach the vision service. DEV: the private compose-network service name (`vision`).
|
||||
# PROD: the server runs on the HOST network (to see the booth LAN / device VLAN — it's the
|
||||
# only container doing device I/O), where compose DNS doesn't resolve, so the prod override
|
||||
# sets VISION_URL=http://127.0.0.1:8089 and vision publishes 8089 on the host loopback.
|
||||
VISION_URL: ${VISION_URL:-http://vision:8089}
|
||||
VISION_ENABLED: ${VISION_ENABLED:-1}
|
||||
# JWT signing secret MUST be provided at deploy (no insecure default — see auth.ts).
|
||||
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in the env/.env}
|
||||
|
||||
Reference in New Issue
Block a user