Compare commits

..
Author SHA1 Message Date
overseerandClaude Opus 5.5 d1f8f4b15b Task 6e203451: Address runbook review findings F-01..F-03
- F-01: document the enforced DOCKER-USER firewall on :8080 (Traefik and
  host RETURN, DROP everyone else), its persisting unit
  nestquest-api-firewall.service, and operator verification commands;
  restore step 6 no longer suggests hitting :8080 from the HA box.
- F-02: restore step 5 uses `dc up -d litestream` so the sidecar is
  created on a fresh box instead of `dc start`, which cannot start a
  never-created container.
- F-03: pin both reference routers to the `websecure` entrypoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 08:50:26 -04:00
overseerandClaude Opus 5.5 eb540d804a Task 6e203451: Deployment and restore runbook
Add deploy/RUNBOOK.md: the operator procedure to rebuild the API
deployment end to end (image build, compose and the nestquest-db
volume, Traefik TLS and routing on nukapi-03, Authentik OIDC, secrets,
network separation, Litestream backup), restore the database from the
SFTP replica, and verify the result. Point api/README.md at it.

The restore steps were rehearsed locally against a file replica using
the committed compose file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 08:40:45 -04:00
5 changed files with 15 additions and 64 deletions
+2 -6
View File
@@ -14,16 +14,12 @@
# the SQLite database lives on the mounted volume at
# /var/lib/nestquest (compose supplies NESTQUEST_DB_PATH pointing
# there — configuration is environment-supplied, never baked in).
#
# Both stages use the same base image pinned by digest, and
# api/requirements.txt pins exact versions, so a rebuild reproduces
# the deployed image instead of floating to whatever is newest.
FROM python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9 AS builder
FROM python:3.12-slim AS builder
COPY api/requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir --prefix=/install -r /tmp/requirements.txt
FROM python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9
FROM python:3.12-slim
# The staged dependency install from the builder (no pip, no wheels).
COPY --from=builder /install /usr/local
+6 -29
View File
@@ -7,32 +7,9 @@
# runtime dependencies HERE — this file is what the container image
# installs (see the Dockerfile at the repository root).
#
# EXACT pins: the versions installed in the deployed image
# (nestquest-api:latest on the API box, read with importlib.metadata),
# so a rebuild resolves the same packages. The four direct
# dependencies come first; the rest is their resolved transitive
# closure, pinned too so nothing floats. The dev group's floors in
# pyproject.toml are unchanged. Bump deliberately, together with the
# Dockerfile's digest-pinned base image.
fastapi==0.141.1
uvicorn==0.53.0
pyjwt[crypto]==2.15.0
httpx==0.28.1
# Transitive dependencies of the above.
annotated-doc==0.0.5
annotated-types==0.8.0
anyio==4.15.1
certifi==2026.7.22
cffi==2.1.1
click==8.5.0
cryptography==50.0.1
h11==0.16.0
httpcore==1.0.9
idna==3.20
pycparser==3.0
pydantic==2.13.5
pydantic_core==2.46.5
starlette==1.7.0
typing-inspection==0.4.4
typing_extensions==4.16.0
# Compatible ranges mirroring the dev group's floors; the API is a
# server, so these are the only packages its runtime needs.
fastapi>=0.115
uvicorn>=0.30
pyjwt[crypto]>=2.9
httpx>=0.28
+6 -16
View File
@@ -266,16 +266,8 @@ committed `deploy/litestream.yml` (`host-key`, the NAS's ECDSA key), so no
-A DOCKER-USER -p tcp --dport 8080 -j DROP # every other source
The rules are persisted by the systemd oneshot unit
**`nestquest-api-firewall.service`**, committed as
`deploy/systemd/nestquest-api-firewall.service` (ordered after
`docker.service`, it idempotently applies the three rules at every
boot). Install and enable it on the API box, from the repository
checkout, **before** exposing the API:
sudo cp deploy/systemd/nestquest-api-firewall.service /etc/systemd/system/ \
&& sudo systemctl daemon-reload \
&& sudo systemctl enable --now nestquest-api-firewall.service
**`nestquest-api-firewall.service`** on the API box (enabled; ordered
after `docker.service`, it applies the three rules at every boot).
The HA box and every other client must use the Traefik hostname,
never `:8080` directly. Check it:
@@ -290,12 +282,10 @@ committed `deploy/litestream.yml` (`host-key`, the NAS's ECDSA key), so no
# public path still works:
curl -s -o /dev/null -w '%{http_code}\n' https://nestquest.cubecraftlabs.com/health # 200
On a rebuilt API box, install the unit with the commands above before
exposing the API. The committed file is the authoritative copy; the
deployed unit must match it:
diff deploy/systemd/nestquest-api-firewall.service \
/etc/systemd/system/nestquest-api-firewall.service # no output
On a rebuilt API box, recreate and enable the unit before exposing the
API (`sudo systemctl enable --now nestquest-api-firewall.service`); the
deployed unit is the authoritative copy
(`systemctl cat nestquest-api-firewall.service`).
## 7. Backup (Litestream to the Synology NAS)
+1 -1
View File
@@ -56,7 +56,7 @@ services:
# (uid/gid 999, owner of the volume) with a read-only root filesystem,
# no privilege escalation and no Linux capabilities.
litestream:
image: litestream/litestream:0.5.17@sha256:4b02b9859a6b6b4087d8b8944e15f7e984bd7957cba322bbeee38b0e27b9656a
image: litestream/litestream:0.5.17
restart: unless-stopped
command: ["replicate", "-config", "/etc/litestream.yml"]
user: "999:999"
@@ -1,12 +0,0 @@
[Unit]
Description=Restrict NestQuest API port 8080 to Traefik (DOCKER-USER)
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c "iptables -C DOCKER-USER -s 10.60.1.33/32 -p tcp --dport 8080 -j RETURN 2>/dev/null || iptables -I DOCKER-USER 1 -s 10.60.1.33 -p tcp --dport 8080 -j RETURN; iptables -C DOCKER-USER -s 10.60.1.14/32 -p tcp --dport 8080 -j RETURN 2>/dev/null || iptables -I DOCKER-USER 2 -s 10.60.1.14 -p tcp --dport 8080 -j RETURN; iptables -C DOCKER-USER -p tcp --dport 8080 -j DROP 2>/dev/null || iptables -A DOCKER-USER -p tcp --dport 8080 -j DROP"
[Install]
WantedBy=multi-user.target