Step 13.1: containerize the server
CI / check (push) Has been cancelled
Details
CI / check (push) Has been cancelled
Details
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2bbcaa4d05
commit
5c9534036f
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Keep image builds small and free of local state.
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
**/coverage
|
||||||
|
**/test-results
|
||||||
|
**/playwright-report
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.pnpm-store
|
||||||
|
memory-bank
|
||||||
|
docs
|
||||||
|
deploy/backups
|
||||||
|
# The source map art is only an input to the asset script (decision 0.5.37).
|
||||||
|
apps/web/public/assets/map
|
||||||
12
DECISIONS.md
12
DECISIONS.md
|
|
@ -1295,3 +1295,15 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
|
|
||||||
- **All four cargo classes pay 50 % more per tonne per M km** (Standard 150, Heavy 210, High-Value 270, Dangerous 300). Fuel is unchanged, so every job is more profitable; the tutorial share rises too (a new player has ~2,950 cr at the deal instead of ~2,130).
|
- **All four cargo classes pay 50 % more per tonne per M km** (Standard 150, Heavy 210, High-Value 270, Dangerous 300). Fuel is unchanged, so every job is more profitable; the tutorial share rises too (a new player has ~2,950 cr at the deal instead of ~2,130).
|
||||||
- **Chosen with the balance simulator**: +50 % is the largest raise that keeps every Step 12.1 target (first Market ship 60 min, discovery 28, loan paid off in 127, both purchase orders reach four ships at 127). Doubling would put the first Market ship at ~28 minutes, below the 45–75 window.
|
- **Chosen with the balance simulator**: +50 % is the largest raise that keeps every Step 12.1 target (first Market ship 60 min, discovery 28, loan paid off in 127, both purchase orders reach four ships at 127). Doubling would put the first Market ship at ~28 minutes, below the 45–75 window.
|
||||||
|
|
||||||
|
## Phase 13 — hosting choices (decision 0.5.86)
|
||||||
|
|
||||||
|
- **Asked at the start of the phase, as the plan says**: a home server running Docker Compose at deadhead.belowland.com; a Caddy container in the stack for HTTPS and routing; nightly backups to a mounted folder or NAS share.
|
||||||
|
|
||||||
|
## Step 13.1
|
||||||
|
|
||||||
|
- **The server image runs the TypeScript with tsx**, exactly as `pnpm start` does, rather than adding a bundling step; `tsx` moved from the server's dev dependencies to its dependencies so the production install keeps it. The image is ~590 MB: `pnpm deploy --legacy` brings along some packages that better-auth's optional peers pull in. The non-legacy deploy mode would need the lockfile regenerated with injected workspace packages, which wasn't worth it for a home server.
|
||||||
|
- **Migrations run on every start** (`migrate-cli`, then `main`): they are idempotent, so a new image migrates the database before serving.
|
||||||
|
- **Postgres is not published** outside the stack; the server reaches it on the compose network. It gets its own named volume (`deadhead_postgres-data`).
|
||||||
|
- **The stack's settings live in `deploy/.env`** (git-ignored), with the public address as one setting (`PUBLIC_URL`) used for Better Auth, so sign-in accepts requests from the public site.
|
||||||
|
- **`deploy/check.sh` is the step's test**, run locally (it needs Docker); CI doesn't run it, because the Gitea runner's Docker access isn't set up for building images. It uses a throwaway project name and generated secrets and removes its containers and volumes, so it never touches a real deployment on the same machine.
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@
|
||||||
"fastify": "^5.12.5",
|
"fastify": "^5.12.5",
|
||||||
"pg": "^8.23.1",
|
"pg": "^8.23.1",
|
||||||
"pg-boss": "^12.36.0",
|
"pg-boss": "^12.36.0",
|
||||||
|
"tsx": "^4.23.15",
|
||||||
"zod": "^4.6.5"
|
"zod": "^4.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/pg": "^8.23.1",
|
"@types/pg": "^8.23.1",
|
||||||
"drizzle-kit": "^0.31.11",
|
"drizzle-kit": "^0.31.11"
|
||||||
"tsx": "^4.23.15"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Production settings for deploy/compose.yml. Copy to deploy/.env and fill in.
|
||||||
|
|
||||||
|
# The public address players use; sign-in only accepts requests from it.
|
||||||
|
PUBLIC_URL=https://deadhead.belowland.com
|
||||||
|
|
||||||
|
# Postgres (only reachable inside the stack).
|
||||||
|
POSTGRES_USER=deadhead
|
||||||
|
POSTGRES_DB=deadhead
|
||||||
|
# Generate with: openssl rand -base64 24
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
|
||||||
|
# At least 32 random characters. Generate with: openssl rand -base64 32
|
||||||
|
BETTER_AUTH_SECRET=
|
||||||
|
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Builds and starts the production stack under a throwaway project name with
|
||||||
|
# generated secrets, checks it, then removes it (containers and volumes).
|
||||||
|
# deploy/check.sh from the repository root or deploy/
|
||||||
|
# Exits non-zero if any check fails. Needs Docker with Compose v2.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
project="deadhead-check"
|
||||||
|
env_file="$(mktemp)"
|
||||||
|
secret() { node -e 'console.log(require("crypto").randomBytes(+process.argv[1]).toString("base64url"))' "$1"; }
|
||||||
|
cat > "$env_file" <<EOF
|
||||||
|
PUBLIC_URL=http://localhost:8080
|
||||||
|
POSTGRES_PASSWORD=$(secret 18)
|
||||||
|
BETTER_AUTH_SECRET=$(secret 32)
|
||||||
|
EOF
|
||||||
|
compose() { docker compose -p "$project" --env-file "$env_file" "$@"; }
|
||||||
|
cleanup() {
|
||||||
|
compose down -v --remove-orphans >/dev/null 2>&1 || true
|
||||||
|
rm -f "$env_file"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "Building and starting the stack…"
|
||||||
|
compose up -d --build --wait --wait-timeout 180
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
check() {
|
||||||
|
local name="$1" url="$2" expect="$3" body
|
||||||
|
body="$(compose exec -T server node -e "fetch('$url').then(r => r.text().then(t => console.log(r.status + ' ' + t)))")"
|
||||||
|
if [[ "$body" == $expect ]]; then
|
||||||
|
echo "ok $name: $body"
|
||||||
|
else
|
||||||
|
echo "FAIL $name: $body"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 13.1: the server, with migrations applied, answers health and time.
|
||||||
|
check "health" "http://127.0.0.1:3000/api/health" '200 {"status":"ok"}'
|
||||||
|
check "server time" "http://127.0.0.1:3000/api/time" '200 {"realTime":*'
|
||||||
|
|
||||||
|
exit "$fail"
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Production stack for a home server (Phase 13): Postgres and the API server.
|
||||||
|
# From this folder: docker compose --env-file .env up -d --build
|
||||||
|
# Settings come from deploy/.env (copy .env.example).
|
||||||
|
name: deadhead
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:18
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-deadhead}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in deploy/.env}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-deadhead}
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 30
|
||||||
|
|
||||||
|
server:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/server.Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgres://${POSTGRES_USER:-deadhead}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-deadhead}
|
||||||
|
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?set BETTER_AUTH_SECRET in deploy/.env}
|
||||||
|
BETTER_AUTH_URL: ${PUBLIC_URL:?set PUBLIC_URL in deploy/.env}
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||||
|
WORKERS_ENABLED: "true"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# The API server (Step 13.1). Build from the repository root:
|
||||||
|
# docker build -f deploy/server.Dockerfile .
|
||||||
|
# Runs the TypeScript sources with tsx, as `pnpm start` does; migrations run on
|
||||||
|
# every start (they are idempotent) before the server listens.
|
||||||
|
|
||||||
|
FROM node:24-slim AS build
|
||||||
|
RUN corepack enable
|
||||||
|
WORKDIR /repo
|
||||||
|
# Manifests first, so dependency layers are cached across source changes.
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
COPY apps/server/package.json apps/server/
|
||||||
|
COPY packages/content/package.json packages/content/
|
||||||
|
COPY packages/schema/package.json packages/schema/
|
||||||
|
COPY packages/sim/package.json packages/sim/
|
||||||
|
RUN pnpm install --frozen-lockfile --prod --filter "@deadhead/server..."
|
||||||
|
COPY tsconfig.base.json ./
|
||||||
|
COPY packages packages
|
||||||
|
COPY apps/server apps/server
|
||||||
|
# A self-contained copy of the server with its workspace packages and runtime
|
||||||
|
# dependencies (tsx is one: the server runs its TypeScript directly).
|
||||||
|
RUN pnpm --filter @deadhead/server deploy --legacy --prod /out
|
||||||
|
|
||||||
|
FROM node:24-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /out .
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
PORT=3000
|
||||||
|
EXPOSE 3000
|
||||||
|
USER node
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=3 \
|
||||||
|
CMD node -e "fetch('http://127.0.0.1:' + process.env.PORT + '/api/health').then(r => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
|
||||||
|
CMD ["sh", "-c", "node_modules/.bin/tsx src/db/migrate-cli.ts && exec node_modules/.bin/tsx src/main.ts"]
|
||||||
|
|
@ -452,6 +452,15 @@ pg-boss (`queue.ts`) in the app's Postgres database (schema `pgboss`), behind a
|
||||||
|
|
||||||
The app clock is `base clock + offset`; the offset is set only by the test-only `POST /api/test/clock-offset`, which is not registered in production.
|
The app clock is `base clock + offset`; the offset is set only by the test-only `POST /api/test/clock-offset`, which is not registered in production.
|
||||||
|
|
||||||
|
## Deployment (`deploy/`)
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `server.Dockerfile` | API image: production dependencies via `pnpm deploy`, TypeScript run with tsx, migrations then listen; non-root; health check on `/api/health` |
|
||||||
|
| `compose.yml` | Production stack (project `deadhead`): Postgres 18 (named volume, not published) and the server |
|
||||||
|
| `.env.example` | `PUBLIC_URL` (also Better Auth's URL), Postgres credentials, `BETTER_AUTH_SECRET`, log level; copy to `deploy/.env` |
|
||||||
|
| `check.sh` | Builds and starts the stack under a throwaway project, checks it, removes it |
|
||||||
|
|
||||||
## Client (`apps/web`)
|
## Client (`apps/web`)
|
||||||
|
|
||||||
Vite + React 19 + Tailwind 4, React Router 8 (declarative routes). In development, Vite proxies `/api` to the server at `127.0.0.1:3000` (same origin, decision 0.5.20).
|
Vite + React 19 + Tailwind 4, React Router 8 (declarative routes). In development, Vite proxies `/api` to the server at `127.0.0.1:3000` (same origin, decision 0.5.20).
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple
|
||||||
83. **Deal loan** (CR-013). The deal loan amount is Courier price + Clipper price + Earth bay lease, computed from content.
|
83. **Deal loan** (CR-013). The deal loan amount is Courier price + Clipper price + Earth bay lease, computed from content.
|
||||||
84. **Economic distance** (Step 12.1, user decision). Contract pay and fuel use `reference × (distance / reference)^exponent` instead of the plain distance (reference 0.384M km, Earth–Moon; exponent 0.65), so a Moon run is unchanged and long legs pay and cost on a gentler curve. Range, travel time, Long Haul and XP keep the plain distance.
|
84. **Economic distance** (Step 12.1, user decision). Contract pay and fuel use `reference × (distance / reference)^exponent` instead of the plain distance (reference 0.384M km, Earth–Moon; exponent 0.65), so a Moon run is unchanged and long legs pay and cost on a gentler curve. Range, travel time, Long Haul and XP keep the plain distance.
|
||||||
85. **A first contact the fleet can fly** (Step 12.1). While some undiscovered place is reachable, one fit slot on each board is a first-contact job sized and skilled for a ship that can fly it from there (decision 0.5.52's fit rules), so exploring never waits on board luck.
|
85. **A first contact the fleet can fly** (Step 12.1). While some undiscovered place is reachable, one fit slot on each board is a first-contact job sized and skilled for a ship that can fly it from there (decision 0.5.52's fit rules), so exploring never waits on board luck.
|
||||||
|
86. **Hosting** (Phase 13, user decision). The game runs on a home server with Docker Compose at deadhead.belowland.com; a Caddy container in the stack terminates HTTPS (Let's Encrypt), serves the built web app and proxies `/api`; nightly Postgres dumps go to a mounted folder or NAS share.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -316,3 +316,11 @@ Spec: `memory-bank/change-requests/CR-013-veterans-clipper.md` (received as "CR-
|
||||||
**Phase 12 complete — waiting for review.**
|
**Phase 12 complete — waiting for review.**
|
||||||
|
|
||||||
- [x] **Follow-up: base pay +50 %** (user decision). Every cargo class's base pay per tonne per M km: Standard 100 → 150, Heavy 140 → 210, High-Value 180 → 270, Dangerous 200 → 300. Medians over 7 seeds (Lighter-first): first Market purchase 60 min, first discovery 28, loan paid off 127, four ships 127 (Freighter-first 127); no breaches. Payout hand-calculation tests now read the rate from content.
|
- [x] **Follow-up: base pay +50 %** (user decision). Every cargo class's base pay per tonne per M km: Standard 100 → 150, Heavy 140 → 210, High-Value 180 → 270, Dangerous 200 → 300. Medians over 7 seeds (Lighter-first): first Market purchase 60 min, first discovery 28, loan paid off 127, four ships 127 (Freighter-first 127); no breaches. Payout hand-calculation tests now read the rate from content.
|
||||||
|
|
||||||
|
## Phase 13 — Deployment (Base Game)
|
||||||
|
|
||||||
|
Choices (asked at the start of the phase, decision 0.5.86): a home server running Docker Compose at deadhead.belowland.com; a Caddy container in the stack for HTTPS and routing; nightly backups to a mounted folder or NAS share.
|
||||||
|
|
||||||
|
- [x] **Step 13.1 — Containerize the server.** `deploy/server.Dockerfile` (build from the repo root): installs the server's production dependencies with pnpm, makes a self-contained copy with `pnpm deploy`, and runs the TypeScript with tsx (now a server runtime dependency) as a non-root user; migrations run on every start, then the server listens; a health check calls `/api/health`. `deploy/compose.yml` (project `deadhead`): Postgres 18 with a named volume, not published, and the server, which waits for Postgres to be healthy. Settings come from `deploy/.env` (`deploy/.env.example`: `PUBLIC_URL`, Postgres user/db/password, `BETTER_AUTH_SECRET`, log level). `.dockerignore` keeps local state and the source art out of builds. Test: `deploy/check.sh` builds and starts the stack under a throwaway project with generated secrets, waits until it is healthy, checks `/api/health` and `/api/time` from inside the server container, and removes everything (passes, ~25 s with a warm cache).
|
||||||
|
- [ ] Step 13.2 — Deploy the client
|
||||||
|
- [ ] Step 13.3 — Database backups
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ importers:
|
||||||
pg-boss:
|
pg-boss:
|
||||||
specifier: ^12.36.0
|
specifier: ^12.36.0
|
||||||
version: 12.36.0(@opentelemetry/api@1.9.1)
|
version: 12.36.0(@opentelemetry/api@1.9.1)
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.23.15
|
||||||
|
version: 4.23.15
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.6.5
|
specifier: ^4.6.5
|
||||||
version: 4.6.5
|
version: 4.6.5
|
||||||
|
|
@ -75,9 +78,6 @@ importers:
|
||||||
drizzle-kit:
|
drizzle-kit:
|
||||||
specifier: ^0.31.11
|
specifier: ^0.31.11
|
||||||
version: 0.31.11
|
version: 0.31.11
|
||||||
tsx:
|
|
||||||
specifier: ^4.23.15
|
|
||||||
version: 4.23.15
|
|
||||||
|
|
||||||
apps/web:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue