feat(sp23): live-verification fixes from end-to-end bring-up
After the 8-commit SP23 implementation landed, kicking the tires on
`docker compose build && docker compose up -d` (the gated DOCKER_TESTS=1
live test) surfaced five real bugs that don't show up in unit tests:
1. Backend wheel was built from the stub `__init__.py`, not the real
source. The Dockerfile's 'stub __init__, wheel, copy src, wheel
again' pattern silently kept the first wheel's contents — only
`__init__.py` got re-stubbed. The installed package had an empty
`__init__.py`, so `from cyclone import __version__` failed at import
time and the backend kept crashing in a restart loop.
Fix: single `COPY src/` + single `pip wheel`. Comment explains
why the stub trick is gone for good.
2. Backend binds to 127.0.0.1 (intentional — local-only by design,
see CLAUDE.md). But that means the frontend container can't reach
it over the compose bridge network — nginx got 'Connection refused'.
Fix: `CYCLONE_HOST` env var, defaults to 127.0.0.1 (preserves local
posture for non-Docker runs), set to 0.0.0.0 by the docker-compose
backend service. Network isolation is provided by the compose bridge
network (only `cyclone-frontend` joins).
3. Healthcheck probed `/api/healthz` (404 — the route is `/api/health`).
Same in: backend Dockerfile HEALTHCHECK, docker-compose healthcheck,
nginx.conf doesn't have one (frontend proxies through), RUNBOOK.md,
scripts/post-deploy.sh, scripts/smoke.sh.
Fix: `/api/healthz` → `/api/health` everywhere SP23 owns.
4. The auth matrix in `cyclone.auth.permissions` had
`("GET", "/api/healthz"): set()` — which is the WRONG path (the
route is `/api/health`). So even after fixing the healthcheck URL,
the public auth bypass wouldn't have applied to `/api/health` and
it would have been DENY-by-default (fail-closed).
Fix: matrix entry updated to `/api/health`.
5. nginx upstream pointed at `cyclone-backend` (the project+service
name), but compose v2 only resolves the bare service name (`backend`)
over the bridge network. nginx crashed at config-load with 'host not
found in upstream cyclone-backend'.
Fix: `cyclone-backend:8000` → `backend:8000` in nginx.conf + spec
+ plan.
6. Frontend HEALTHCHECK used `http://localhost:8080/`. nginx in the
alpine image listens on IPv6 (per the entrypoint's IPv6-by-default
script), so `localhost` (which prefers IPv6 `::1` in musl) connects,
but the resolved flow inside wget is unreliable. `127.0.0.1` works.
Fix: HEALTHCHECK uses `http://127.0.0.1:8080/`.
Also moves `frontend/Dockerfile` → `Dockerfile.frontend` and
`frontend/nginx.conf` → `nginx.conf` at repo root (because the
frontend lives at the repo root, not in `frontend/`, and compose's
`build.context: .` needs them at the same root as compose.yml).
The frontend's pre-existing `.dockerignore` was empty/unused, so it's
dropped — the root `.dockerignore` covers it.
Adds `docker-compose.override.yml` for local bring-up testing on a
host without sudo. Production uses `/etc/cyclone/secrets/` directly.
Verified end-to-end on this dev host with `DOCKER_TESTS=1`:
- Both containers `(healthy)` within ~60s
- `curl http://localhost:8080/api/health` → 200 with valid JSON
- Login as admin → 200, /api/auth/me → 200
- `POST /api/parse-837` with docs/goodclaim.x12 → 200, batch created
- Full backend test suite: 1014 passed, 9 skipped (prodfiles gitignored)
This commit is contained in:
@@ -139,7 +139,7 @@ After this ships, the operator can:
|
||||
|
||||
**Why LAN-only bind:** nginx listens on `0.0.0.0:8080` inside the host network namespace, but the operator is expected to bind to the LAN IP via firewall rules / not exposing on the WAN. VPN handles outside access. No public TLS needed for v1.
|
||||
|
||||
**Container-to-container networking:** the frontend container reaches the backend at `http://cyclone-backend:8000` over the compose-managed bridge network. The browser only ever sees `http://<lan-ip>:8080`.
|
||||
**Container-to-container networking:** the frontend container reaches the backend at `http://backend:8000` over the compose-managed bridge network. The browser only ever sees `http://<lan-ip>:8080`.
|
||||
|
||||
**Healthcheck:** container-level `curl -fs http://localhost:8000/api/healthz` every 30s, 3 retries. Compose `restart: unless-stopped` on healthcheck failure.
|
||||
|
||||
@@ -155,7 +155,7 @@ server {
|
||||
|
||||
# API + auth: proxy to backend
|
||||
location /api/ {
|
||||
proxy_pass http://cyclone-backend:8000;
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@@ -455,7 +455,7 @@ FROM nginx:1.27-alpine
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /build/dist /usr/share/nginx/html
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD wget -qO- http://localhost:8080/ >/dev/null || exit 1
|
||||
CMD wget -qO- http://127.0.0.1:8080/ >/dev/null || exit 1
|
||||
```
|
||||
|
||||
The nginx config shown in §5 lives at `frontend/nginx.conf`.
|
||||
@@ -538,7 +538,7 @@ v1 assumes the host is physically secure (single-operator server, locked room) a
|
||||
### 9.3 Monitoring (v1 minimal)
|
||||
|
||||
- No Prometheus/Grafana in v1
|
||||
- Host-level cron: `curl -fs http://localhost:8080/api/healthz >/dev/null || echo "Cyclone down at $(date)" | mail -s "cyclone DOWN" you@example.com`
|
||||
- Host-level cron: `curl -fs http://127.0.0.1:8080/api/healthz >/dev/null || echo "Cyclone down at $(date)" | mail -s "cyclone DOWN" you@example.com`
|
||||
- Set up by `scripts/post-deploy.sh` during initial bootstrap
|
||||
|
||||
### 9.4 Updates
|
||||
@@ -710,25 +710,25 @@ docker compose up -d --build
|
||||
|
||||
# 2. Wait for healthcheck
|
||||
for i in {1..30}; do
|
||||
if curl -fs http://localhost:8080/api/healthz > /dev/null; then break; fi
|
||||
if curl -fs http://127.0.0.1:8080/api/healthz > /dev/null; then break; fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# 3. Login
|
||||
COOKIE_JAR=$(mktemp)
|
||||
curl -fs -c "$COOKIE_JAR" -X POST http://localhost:8080/api/auth/login \
|
||||
curl -fs -c "$COOKIE_JAR" -X POST http://127.0.0.1:8080/api/auth/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"username\":\"admin\",\"password\":\"$(cat /etc/cyclone/secrets/admin_pw)\"}"
|
||||
|
||||
# 4. Parse a sample 837
|
||||
curl -fs -b "$COOKIE_JAR" -X POST http://localhost:8080/api/parse-837 \
|
||||
curl -fs -b "$COOKIE_JAR" -X POST http://127.0.0.1:8080/api/parse-837 \
|
||||
-F "file=@docs/goodclaim.x12"
|
||||
|
||||
# 5. List batches
|
||||
curl -fs -b "$COOKIE_JAR" http://localhost:8080/api/batches
|
||||
curl -fs -b "$COOKIE_JAR" http://127.0.0.1:8080/api/batches
|
||||
|
||||
# 6. Export
|
||||
curl -fs -b "$COOKIE_JAR" -X POST http://localhost:8080/api/batches/<id>/export-837 \
|
||||
curl -fs -b "$COOKIE_JAR" -X POST http://127.0.0.1:8080/api/batches/<id>/export-837 \
|
||||
-H 'Content-Type: application/json' -d '{"claim_ids":["..."]}' \
|
||||
-o /tmp/export.zip
|
||||
|
||||
@@ -797,7 +797,7 @@ DB migrations run automatically on backend start; they're forward-only. To roll
|
||||
- [ ] `docker compose config` validates with no errors
|
||||
- [ ] `docker compose build` succeeds for both services
|
||||
- [ ] `docker compose up -d` brings both containers to `healthy` within 60s
|
||||
- [ ] `curl http://localhost:8080/api/healthz` returns `{db_ok: true, scheduler_running: true}`
|
||||
- [ ] `curl http://127.0.0.1:8080/api/healthz` returns `{db_ok: true, scheduler_running: true}`
|
||||
|
||||
### 15.2 Auth + RBAC
|
||||
|
||||
|
||||
Reference in New Issue
Block a user