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:
co-authored by
Claude Opus 4.8
parent
bd3250c9c7
commit
b3567718fc
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user