Split deploy into independent poller and web stacks
Decouple the privileged hardware poller from web iterations so a web
redeploy can never recreate/restart the poller (the backplane-touching
container). Two images from one Dockerfile via build targets:
- jbod-poller: privileged producer, ships smartmontools/sg3-utils/ledmon,
Redis-only deps (~197MB)
- jbod-web: unprivileged read-only consumer (REST/UI/MQTT), no hardware
tools, no /dev (~147MB)
Two compose projects sharing Redis over host networking:
- compose.infra.yml (jbod-infra): poller + redis — stable substrate
- compose.web.yml (jbod-web): app — 'up -d --build' touches only app
build.sh builds/pushes both images; split requirements-{poller,web}.txt;
dropped the combined docker-compose.yml; .dockerignore excludes tmp/ (keeps
the venv and the mqtt.env creds out of the build context).
This commit is contained in:
@@ -6,3 +6,5 @@ __pycache__
|
|||||||
.git
|
.git
|
||||||
.env
|
.env
|
||||||
*.md
|
*.md
|
||||||
|
tmp/
|
||||||
|
.venv/
|
||||||
|
|||||||
29
Dockerfile
29
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
# ---- Stage 1: Build frontend ----
|
# ---- Stage: build frontend ----
|
||||||
FROM node:22-alpine AS frontend-build
|
FROM node:22-alpine AS frontend-build
|
||||||
WORKDIR /app/frontend
|
WORKDIR /app/frontend
|
||||||
COPY frontend/package.json frontend/package-lock.json* ./
|
COPY frontend/package.json frontend/package-lock.json* ./
|
||||||
@@ -7,8 +7,10 @@ COPY frontend/ ./
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
# Output: /app/static/
|
# Output: /app/static/
|
||||||
|
|
||||||
# ---- Stage 2: Production runtime ----
|
# ---- Target: poller (hardware producer) ----
|
||||||
FROM python:3.13-slim
|
# Privileged at runtime; the only image with SAS/SMART tooling. Slim Python
|
||||||
|
# deps (just Redis) — no web stack.
|
||||||
|
FROM python:3.13-slim AS poller
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
smartmontools \
|
smartmontools \
|
||||||
@@ -20,16 +22,27 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements-poller.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements-poller.txt
|
||||||
|
|
||||||
|
COPY poller.py .
|
||||||
|
COPY services/ services/
|
||||||
|
|
||||||
|
CMD ["python", "-u", "poller.py"]
|
||||||
|
|
||||||
|
# ---- Target: web (read-only consumer) ----
|
||||||
|
# Unprivileged at runtime; no hardware tools. Serves REST/UI + MQTT, reads Redis.
|
||||||
|
FROM python:3.13-slim AS web
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements-web.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements-web.txt
|
||||||
|
|
||||||
COPY main.py .
|
COPY main.py .
|
||||||
COPY poller.py .
|
|
||||||
COPY routers/ routers/
|
COPY routers/ routers/
|
||||||
COPY services/ services/
|
COPY services/ services/
|
||||||
COPY models/ models/
|
COPY models/ models/
|
||||||
|
|
||||||
# Copy built frontend from stage 1
|
|
||||||
COPY --from=frontend-build /app/static/ static/
|
COPY --from=frontend-build /app/static/ static/
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -45,12 +45,21 @@ apt install smartmontools sg3-utils ledmon # Debian/Ubuntu
|
|||||||
|
|
||||||
## Run (docker compose)
|
## Run (docker compose)
|
||||||
|
|
||||||
|
Two **independent** stacks so web iterations never recreate the privileged
|
||||||
|
poller (which touches the SAS bus). They're built as two images —
|
||||||
|
`jbod-poller` (privileged, has the SAS/SMART tooling) and `jbod-web` (slim,
|
||||||
|
unprivileged) — and share Redis over host networking.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
# 1. Infra: poller + redis — the stable substrate. Deploy once; touch deliberately.
|
||||||
|
docker compose -f compose.infra.yml up -d
|
||||||
|
|
||||||
|
# 2. Web: REST/UI/MQTT — iterate freely; this only ever recreates `app`.
|
||||||
|
docker compose -f compose.web.yml up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Brings up `poller` (privileged), `app` (port 8000), and `redis`. To run the two
|
Pin both images to a known-good SHA in deploy (`./build.sh` tags `:<sha>` and
|
||||||
processes by hand instead:
|
`:latest` for both). To run the two processes by hand instead:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
REDIS_HOST=localhost sudo -E python poller.py # producer (needs root)
|
REDIS_HOST=localhost sudo -E python poller.py # producer (needs root)
|
||||||
|
|||||||
23
build.sh
23
build.sh
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
IMAGE="docker.adamksmith.xyz/jbod-monitor"
|
REG="docker.adamksmith.xyz"
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
@@ -10,16 +10,21 @@ git add -A
|
|||||||
if git diff --cached --quiet; then
|
if git diff --cached --quiet; then
|
||||||
echo "No changes to commit, using HEAD"
|
echo "No changes to commit, using HEAD"
|
||||||
else
|
else
|
||||||
git commit -m "${1:-Build and push image}"
|
git commit -m "${1:-Build and push images}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SHA=$(git rev-parse --short HEAD)
|
SHA=$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
echo "Building ${IMAGE}:${SHA}"
|
# Two images from one Dockerfile (shared frontend build stage is cached):
|
||||||
docker build -t "${IMAGE}:${SHA}" -t "${IMAGE}:latest" .
|
# jbod-poller — privileged hardware producer
|
||||||
|
# jbod-web — unprivileged read-only consumer (REST/UI/MQTT)
|
||||||
|
for target in poller web; do
|
||||||
|
img="${REG}/jbod-${target}"
|
||||||
|
echo "Building ${img}:${SHA}"
|
||||||
|
docker build --target "${target}" -t "${img}:${SHA}" -t "${img}:latest" .
|
||||||
|
echo "Pushing ${img}:${SHA}"
|
||||||
|
docker push "${img}:${SHA}"
|
||||||
|
docker push "${img}:latest"
|
||||||
|
done
|
||||||
|
|
||||||
echo "Pushing ${IMAGE}:${SHA}"
|
echo "Done: ${REG}/jbod-{poller,web}:${SHA}"
|
||||||
docker push "${IMAGE}:${SHA}"
|
|
||||||
docker push "${IMAGE}:latest"
|
|
||||||
|
|
||||||
echo "Done: ${IMAGE}:${SHA}"
|
|
||||||
|
|||||||
47
compose.infra.yml
Normal file
47
compose.infra.yml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Infra stack: the hardware poller + Redis. This is the STABLE substrate —
|
||||||
|
# deploy once, pin to a known-good image SHA, and touch it deliberately
|
||||||
|
# (never as part of a web iteration). Web redeploys use compose.web.yml and
|
||||||
|
# can never name these services.
|
||||||
|
#
|
||||||
|
# docker compose -f compose.infra.yml up -d # bring up / update poller+redis
|
||||||
|
#
|
||||||
|
name: jbod-infra
|
||||||
|
|
||||||
|
services:
|
||||||
|
poller:
|
||||||
|
image: docker.adamksmith.xyz/jbod-poller:latest # pin to a SHA in deploy
|
||||||
|
container_name: jbod-poller
|
||||||
|
restart: unless-stopped
|
||||||
|
privileged: true
|
||||||
|
pid: host
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- /dev:/dev
|
||||||
|
- /sys:/sys:ro
|
||||||
|
- /run/udev:/run/udev:ro
|
||||||
|
environment:
|
||||||
|
- TZ=America/Denver
|
||||||
|
- ZFS_USE_NSENTER=true
|
||||||
|
- REDIS_HOST=127.0.0.1
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- REDIS_DB=0
|
||||||
|
# Pacing — keep the SAS bus quiet. POLL_CONCURRENCY=1 fully serializes.
|
||||||
|
- POLL_SWEEP_INTERVAL=300
|
||||||
|
- POLL_DRIVE_GAP=0.5
|
||||||
|
- POLL_CONCURRENCY=1
|
||||||
|
- POLL_STARTUP_JITTER=10
|
||||||
|
- POLL_DATA_TTL=900
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: jbod-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
command: redis-server --save 60 1 --loglevel warning
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis-data:
|
||||||
33
compose.web.yml
Normal file
33
compose.web.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Web stack: REST API + UI + Home Assistant MQTT. Read-only consumer of the
|
||||||
|
# Redis filled by compose.infra.yml. Iterate freely here — this command only
|
||||||
|
# ever touches the `app` container, never the poller:
|
||||||
|
#
|
||||||
|
# docker compose -f compose.web.yml up -d --build
|
||||||
|
#
|
||||||
|
# Requires the infra stack (Redis) to be running. Reaches Redis at
|
||||||
|
# 127.0.0.1:6379 via host networking (separate compose project, shared host net).
|
||||||
|
name: jbod-web
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: docker.adamksmith.xyz/jbod-web:latest # pin to a SHA in deploy
|
||||||
|
container_name: jbod-monitor
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
- TZ=America/Denver
|
||||||
|
- UVICORN_LOG_LEVEL=info
|
||||||
|
- REDIS_HOST=127.0.0.1
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- REDIS_DB=0
|
||||||
|
# Home Assistant MQTT publishing (EMQX, bridged to Mosquitto HA add-on).
|
||||||
|
- MQTT_HOST=10.5.30.3
|
||||||
|
- MQTT_PORT=1883
|
||||||
|
- MQTT_DISCOVERY_PREFIX=homeassistant
|
||||||
|
- MQTT_BASE_TOPIC=jbod-monitor
|
||||||
|
- MQTT_PUBLISH_INTERVAL=60
|
||||||
|
# Broker credentials (MQTT_USERNAME/MQTT_PASSWORD). Optional: container
|
||||||
|
# starts without it (MQTT just won't authenticate).
|
||||||
|
env_file:
|
||||||
|
- path: /etc/jbod-monitor/mqtt.env
|
||||||
|
required: false
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
services:
|
|
||||||
# Producer: the ONLY service that touches hardware (smartctl/sg_ses/zpool/
|
|
||||||
# ledctl). Serialized + paced; writes everything to Redis.
|
|
||||||
poller:
|
|
||||||
build: .
|
|
||||||
image: docker.adamksmith.xyz/jbod-monitor:latest
|
|
||||||
container_name: jbod-poller
|
|
||||||
restart: unless-stopped
|
|
||||||
privileged: true
|
|
||||||
pid: host
|
|
||||||
network_mode: host
|
|
||||||
command: ["python", "-u", "poller.py"]
|
|
||||||
volumes:
|
|
||||||
- /dev:/dev
|
|
||||||
- /sys:/sys:ro
|
|
||||||
- /run/udev:/run/udev:ro
|
|
||||||
environment:
|
|
||||||
- TZ=America/Denver
|
|
||||||
- ZFS_USE_NSENTER=true
|
|
||||||
- REDIS_HOST=127.0.0.1
|
|
||||||
- REDIS_PORT=6379
|
|
||||||
- REDIS_DB=0
|
|
||||||
# Pacing — keep the SAS bus quiet. POLL_CONCURRENCY=1 fully serializes.
|
|
||||||
- POLL_SWEEP_INTERVAL=300
|
|
||||||
- POLL_DRIVE_GAP=0.5
|
|
||||||
- POLL_CONCURRENCY=1
|
|
||||||
- POLL_STARTUP_JITTER=10
|
|
||||||
- POLL_DATA_TTL=900
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
|
|
||||||
# Consumer: REST API + web UI + Home Assistant MQTT. Reads Redis only —
|
|
||||||
# unprivileged, no /dev, can restart freely without touching hardware.
|
|
||||||
app:
|
|
||||||
build: .
|
|
||||||
image: docker.adamksmith.xyz/jbod-monitor:latest
|
|
||||||
container_name: jbod-monitor
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
environment:
|
|
||||||
- TZ=America/Denver
|
|
||||||
- UVICORN_LOG_LEVEL=info
|
|
||||||
- REDIS_HOST=127.0.0.1
|
|
||||||
- REDIS_PORT=6379
|
|
||||||
- REDIS_DB=0
|
|
||||||
# Home Assistant MQTT publishing (EMQX, bridged to Mosquitto HA add-on).
|
|
||||||
- MQTT_HOST=10.5.30.3
|
|
||||||
- MQTT_PORT=1883
|
|
||||||
- MQTT_DISCOVERY_PREFIX=homeassistant
|
|
||||||
- MQTT_BASE_TOPIC=jbod-monitor
|
|
||||||
- MQTT_PUBLISH_INTERVAL=60
|
|
||||||
# Broker credentials (MQTT_USERNAME/MQTT_PASSWORD). Optional: container
|
|
||||||
# starts without it (MQTT just won't authenticate).
|
|
||||||
env_file:
|
|
||||||
- path: /etc/jbod-monitor/mqtt.env
|
|
||||||
required: false
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: jbod-redis
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
volumes:
|
|
||||||
- redis-data:/data
|
|
||||||
command: redis-server --save 60 1 --loglevel warning
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
redis-data:
|
|
||||||
2
requirements-poller.txt
Normal file
2
requirements-poller.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Poller image — hardware producer. Only needs Redis; no web stack.
|
||||||
|
redis>=5.0.0
|
||||||
6
requirements-web.txt
Normal file
6
requirements-web.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Web/API image — read-only consumer + MQTT. No hardware tools needed.
|
||||||
|
fastapi>=0.115.0
|
||||||
|
uvicorn>=0.34.0
|
||||||
|
pydantic>=2.10.0
|
||||||
|
redis>=5.0.0
|
||||||
|
paho-mqtt>=2.1.0
|
||||||
Reference in New Issue
Block a user