build(deploy): apess-api image + compose + Traefik router (B5)

- api/Dockerfile: node:22-alpine, builds better-sqlite3 from source for musl
  (prebuilds are glibc), runs tsx; healthcheck on /healthz
- .dockerignore: stops host node_modules (wrong-platform native binaries)
  from shadowing the in-container build — the root cause of an ERR_DLOPEN
  'Exec format error' found while validating the image
- docker-compose.yml: apess-api service on clawbooks-net + apess-data volume,
  ADMIN_CODE/JUDGE_CODE/CORS_ORIGIN env
- traefik/apess-api.yml: file-provider router for api.apess.redclaw.dev
  (reuses the *.redclaw.dev cert; WS proxied automatically)
- deploy/README: API prerequisites (.env codes, DNS, router), deploy/verify
  (REST + WS smoke tests), rollback notes; .env gitignored

Validated by building + running the image: /healthz=ok, PUT/GET /teams with
auth (401 without code), WS snapshot on connect, WS 4401 on bad code.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-16 19:39:22 -07:00
co-authored by Claude Opus 4.8
parent bd3250c9c7
commit b3567718fc
5 changed files with 122 additions and 10 deletions
+8
View File
@@ -0,0 +1,8 @@
# Never copy host-built artifacts into images — they may be the wrong platform
# (e.g. a darwin better-sqlite3 binary) and would shadow the in-container build.
**/node_modules
**/dist
.git
**/*.log
deploy/.env
.env
+33
View File
@@ -0,0 +1,33 @@
# APESS 2026 collective API — Node + Express + better-sqlite3 + ws.
# better-sqlite3 ships a prebuilt binary for most targets; the build deps are a
# fallback for node-gyp when no prebuild matches the base image.
FROM node:22-alpine AS build
WORKDIR /app
# Build better-sqlite3 from source for musl — the downloaded prebuilds are glibc
# and fail to load on Alpine ("Exec format error").
ENV npm_config_build_from_source=true
RUN apk add --no-cache python3 make g++ \
&& corepack enable && corepack prepare [email protected] --activate
COPY api/package.json api/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --config.confirmModulesPurge=false || pnpm install --no-frozen-lockfile
# Force a from-source compile of the native binding for musl, replacing any
# downloaded glibc prebuild that would fail to load on Alpine.
RUN BS_DIR="$(find node_modules/.pnpm -maxdepth 4 -type d -path '*/better-sqlite3@*/node_modules/better-sqlite3' | head -1)" \
&& rm -f "$BS_DIR/build/Release/better_sqlite3.node" \
&& npm --prefix "$BS_DIR" run build-release \
&& node -e "new (require('better-sqlite3'))(':memory:').close(); console.log('better-sqlite3 OK')"
COPY api/ ./
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV DB_PATH=/data/apess.db
# tsx + sources + built native modules carried over from the build stage
COPY --from=build /app ./
RUN mkdir -p /data
EXPOSE 3000
VOLUME ["/data"]
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:3000/healthz > /dev/null || exit 1
# run the tsx binary directly — pnpm/corepack is only in the build stage
CMD ["node_modules/.bin/tsx", "src/index.ts"]
+37 -8
View File
@@ -1,23 +1,33 @@
# APESS 2026 — gw-03 deploy # APESS 2026 — gw-03 deploy
Drops `apess-web` next to the existing ClawBooks stack on `zeroclaw-gw-03` (Architect). Reuses ClawBooks' Traefik v3.6 instance via the shared `clawbooks-net` Docker network. Drops `apess-web` (static SPA) and `apess-api` (collective backend) next to the existing ClawBooks stack on `zeroclaw-gw-03` (Architect). Reuses ClawBooks' Traefik v3.6 instance via the shared `clawbooks-net` Docker network.
- `apess.redclaw.dev``apess-web` (nginx static)
- `api.apess.redclaw.dev``apess-api` (Node/Express + SQLite + WebSocket) — powers `/admin` and `/judge`
## Prerequisites (one-time, on gw-03) ## Prerequisites (one-time, on gw-03)
1. **Cloudflare DNS** — add `A` records (proxied): 1. **Cloudflare DNS** — add `A` records (proxied):
- `apess.redclaw.dev``37.27.250.130` - `apess.redclaw.dev``37.27.250.130`
- `api.apess.redclaw.dev``37.27.250.130` (backend, future) - `api.apess.redclaw.dev``37.27.250.130`
2. **Cloudflare Origin Certificate** — issue from CF dashboard: 2. **Cloudflare Origin Certificate** — issue from CF dashboard:
- Hostnames: `*.redclaw.dev`, `redclaw.dev` (covers all subdomains) - Hostnames: `*.redclaw.dev`, `redclaw.dev` (covers all subdomains`api.` included)
- Validity: 15 years - Validity: 15 years
- Save: - Save:
- `/etc/ssl/redclaw/apess.pem` (root:root 644) - `/etc/ssl/redclaw/apess.pem` (root:root 644)
- `/etc/ssl/redclaw/apess.key` (root:root 600) - `/etc/ssl/redclaw/apess.key` (root:root 600)
3. **Traefik dynamic config** — copy `traefik/apess.yml` into the live config dir: 3. **Traefik dynamic config** — copy both router files into the live config dir:
```bash ```bash
sudo cp deploy/traefik/apess.yml /home/redclaw/projects/clawbooks/traefik/dynamic/ sudo cp deploy/traefik/apess.yml deploy/traefik/apess-api.yml \
/home/redclaw/projects/clawbooks/traefik/dynamic/
``` ```
Traefik hot-reloads the dynamic dir; no restart needed. Traefik hot-reloads the dynamic dir; no restart needed.
4. **Access codes** — create `~/projects/apress/deploy/.env` (git-ignored) with the
shared instructor/judge codes the API checks:
```bash
ADMIN_CODE=<instructor code from the slide>
JUDGE_CODE=<judge panel code>
```
## Deploy ## Deploy
@@ -30,22 +40,41 @@ rsync -avz --exclude node_modules --exclude dist --exclude .git \
# On gw-03 # On gw-03
ssh [email protected] ssh [email protected]
cd ~/projects/apress cd ~/projects/apress
docker compose -f deploy/docker-compose.yml up -d --build docker compose --env-file deploy/.env -f deploy/docker-compose.yml up -d --build
docker logs -f apess-web # verify nginx is up docker logs -f apess-web # verify nginx is up
docker logs -f apess-api # verify "[apess-api] listening on :3000"
``` ```
## Verify ## Verify
```bash ```bash
curl -fsS https://apess.redclaw.dev/ | head -5 # Frontend
curl -fsS https://apess.redclaw.dev/healthz # → ok curl -fsS https://apess.redclaw.dev/healthz # → ok
# Backend REST
curl -fsS https://api.apess.redclaw.dev/healthz # → ok
curl -fsS -o /dev/null -w '%{http_code}\n' \
https://api.apess.redclaw.dev/teams # → 401 (no code)
curl -fsS -X PUT https://api.apess.redclaw.dev/teams/smoke \
-H 'content-type: application/json' -d '{"name":"smoke","kit":"KIT-00"}' # → 204
curl -fsS https://api.apess.redclaw.dev/teams \
-H "X-Access-Code: $ADMIN_CODE" | head -c 200 # → JSON incl. the smoke team
# Backend WebSocket (run days before the event, not on July 27)
# from a browser console on https://apess.redclaw.dev:
# new WebSocket(`wss://api.apess.redclaw.dev/ws?code=${ADMIN_CODE}`).onmessage = e => console.log(e.data)
# → a {"type":"snapshot",...} frame on connect
``` ```
If the WS won't connect through Cloudflare/Traefik on the day, `/admin` and `/judge`
degrade automatically to ~5s REST polling — every live feature has a REST fallback.
## Rollback ## Rollback
```bash ```bash
ssh [email protected] "cd ~/projects/apress && docker compose -f deploy/docker-compose.yml down" ssh [email protected] "cd ~/projects/apress && docker compose -f deploy/docker-compose.yml down"
# Remove DNS records if needed # The apess-data volume (submissions/scores) persists across `down`; add `-v` to wipe it.
# Remove DNS records if needed.
``` ```
## Why this lives on gw-03 ## Why this lives on gw-03
+22
View File
@@ -13,6 +13,28 @@ services:
# Routing is defined in deploy/traefik/apess.yml (file provider) to avoid # Routing is defined in deploy/traefik/apess.yml (file provider) to avoid
# docker-label discovery races with the neighboring ClawBooks stack. # docker-label discovery races with the neighboring ClawBooks stack.
apess-api:
build:
context: ..
dockerfile: api/Dockerfile
image: apess-api:latest
container_name: apess-api
restart: unless-stopped
networks:
- clawbooks-net
environment:
# set ADMIN_CODE / JUDGE_CODE on the host (e.g. an .env file next to this)
ADMIN_CODE: ${ADMIN_CODE:?set ADMIN_CODE}
JUDGE_CODE: ${JUDGE_CODE:?set JUDGE_CODE}
CORS_ORIGIN: https://apess.redclaw.dev
DB_PATH: /data/apess.db
volumes:
- apess-data:/data
# Routing is defined in deploy/traefik/apess-api.yml (file provider).
networks: networks:
clawbooks-net: clawbooks-net:
external: true external: true
volumes:
apess-data:
+20
View File
@@ -0,0 +1,20 @@
# APESS 2026 — collective API Traefik dynamic config (file provider)
# Lives at /home/redclaw/projects/clawbooks/deploy/traefik/dynamic/apess-api.yml on gw-03.
# Cert: reuses the *.redclaw.dev Cloudflare Origin Cert already loaded by apess.yml.
# Service backend: apess-api container on clawbooks-net (port 3000).
# WebSocket (/ws) is proxied automatically by Traefik v3 — no extra middleware.
http:
routers:
apess-api:
rule: "Host(`api.apess.redclaw.dev`)"
entryPoints:
- websecure
service: apess-api
tls: {}
priority: 1000
services:
apess-api:
loadBalancer:
servers:
- url: "http://apess-api:3000"