infra: docker compose dev + production Dockerfiles

- docker-compose.dev.yml: hot-reload dev stack (rete-watch + server :7357 + web :5173)
- docker-compose.yml: production stack
- packages/chess/Dockerfile + nginx.conf: chess web image
- packages/server/Dockerfile: server image
- Dockerfile.dev: shared dev base image (Bun + workspace deps preinstalled)
- .dockerignore: build context exclusions

Used by Playwright e2e tests (run via .sisyphus/scripts/run-pw.sh which connects to the running dev stack instead of spawning its own server).
This commit is contained in:
Joey Yakimowich-Payne 2026-04-27 13:44:17 -06:00
commit 9d408b5996
No known key found for this signature in database
7 changed files with 286 additions and 0 deletions

23
.dockerignore Normal file
View file

@ -0,0 +1,23 @@
**/node_modules
**/dist
**/coverage
**/.vite
**/*.tsbuildinfo
**/test-results
**/playwright-report
**/playwright-transform-cache-*
.git
.github
.sisyphus
node-compile-cache
docs
*.log
.DS_Store
org.chromium.Chromium.*
.org.chromium.Chromium.*
7xOfJqsDU787fe61oSCq1
IJGn1F-WxLUCbfU1lu8pU
KMr2_dqTOvh2R-Vg1E6cO
.playwright-mcp
Dockerfile*
docker-compose*.yml

16
Dockerfile.dev Normal file
View file

@ -0,0 +1,16 @@
# syntax=docker/dockerfile:1.7
# Dev image — Bun + workspace deps preinstalled.
# Source is NOT baked in; docker-compose.dev.yml bind-mounts the repo
# at /app so file edits propagate live into the container.
FROM oven/bun:1.3
WORKDIR /app
# Manifests only — keeps the install layer cacheable across source edits.
COPY package.json bun.lock ./
COPY tsconfig.base.json tsconfig.json ./
COPY packages/rete/package.json packages/rete/
COPY packages/chess/package.json packages/chess/
COPY packages/server/package.json packages/server/
RUN bun install --frozen-lockfile

92
docker-compose.dev.yml Normal file
View file

@ -0,0 +1,92 @@
# Houserules — DEV stack with hot reload.
#
# Start: docker compose -f docker-compose.dev.yml up
# Stop: docker compose -f docker-compose.dev.yml down
#
# web → Vite dev server on http://localhost:5173 (HMR enabled)
# server → Bun WS on ws://localhost:7357/ws, --hot on file change
# rete-watch → tsup --watch keeps packages/rete/dist fresh so that
# changes to rete trigger HMR in the chess app (chess
# resolves @paratype/rete via its dist/ "import" entry)
#
# How the volume layout works:
# - The whole repo is bind-mounted at /app so source edits on the host
# are seen instantly by the containers.
# - Each package's `node_modules/` is an anonymous volume, which
# SHADOWS the host directory. That keeps the container's
# bun-installed deps (Linux binaries, correct symlink targets)
# from being clobbered by whatever your host has installed.
x-dev-image: &dev-image
build:
context: .
dockerfile: Dockerfile.dev
image: paratype/dev:local
x-dev-volumes: &dev-volumes
- .:/app
- /app/node_modules
- /app/packages/rete/node_modules
- /app/packages/chess/node_modules
- /app/packages/server/node_modules
services:
rete-watch:
<<: *dev-image
container_name: paratype-rete-watch
working_dir: /app/packages/rete
# tsup rebuilds dist/ on every src change. Vite picks the new
# bundle up through the bind mount and triggers HMR in chess.
command: >
sh -c "bunx tsup src/index.ts --format esm,cjs --watch"
volumes: *dev-volumes
restart: unless-stopped
server:
<<: *dev-image
container_name: paratype-server-dev
working_dir: /app
# Wait for the first rete build, then run with --hot. `bun --hot`
# preserves module state across reloads so live WebSocket
# connections survive code edits.
command: >
sh -c "until [ -f packages/rete/dist/index.js ]; do
echo 'waiting for rete dist...'; sleep 1;
done &&
bun --hot packages/server/src/index.ts"
environment:
PORT: "7357"
LOG_LEVEL: info
ALLOWED_ORIGINS: "http://localhost:5173,http://127.0.0.1:5173"
ports:
- "7357:7357"
volumes: *dev-volumes
depends_on:
- rete-watch
restart: unless-stopped
web:
<<: *dev-image
container_name: paratype-web-dev
working_dir: /app
# --host 0.0.0.0 so the dev server is reachable from outside the
# container. HMR uses the same port (5173 → 5173), so the default
# client config "just works".
command: >
sh -c "until [ -f packages/rete/dist/index.js ]; do
echo 'waiting for rete dist...'; sleep 1;
done &&
bun run --filter @paratype/chess dev --host 0.0.0.0"
environment:
# Browser-visible URL of the WS server.
VITE_WS_URL: "ws://localhost:7357/ws"
# Force chokidar polling — file events through bind mounts can be
# flaky on macOS/Windows. Harmless on Linux.
CHOKIDAR_USEPOLLING: "true"
ports:
- "5173:5173"
volumes: *dev-volumes
depends_on:
- rete-watch
- server
restart: unless-stopped

47
docker-compose.yml Normal file
View file

@ -0,0 +1,47 @@
# Houserules — local dev stack.
#
# Frontend → http://localhost:5173 (nginx serving the Vite build)
# Backend → ws://localhost:7357/ws (Bun WebSocket server)
#
# The browser talks to the backend directly, so VITE_WS_URL is baked
# into the built bundle and ALLOWED_ORIGINS on the server must list
# every origin the page can be served from.
services:
server:
build:
context: .
dockerfile: packages/server/Dockerfile
image: paratype/chess-server:local
container_name: paratype-server
environment:
PORT: "7357"
LOG_LEVEL: info
# Origins the browser will load the SPA from. Add prod hosts here
# when you deploy.
ALLOWED_ORIGINS: "http://localhost:5173,http://127.0.0.1:5173"
ports:
- "7357:7357"
healthcheck:
test: ["CMD", "sh", "-c", "bun -e 'fetch(\"http://localhost:7357/healthz\").then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))'"]
interval: 10s
timeout: 3s
retries: 5
start_period: 5s
restart: unless-stopped
web:
build:
context: .
dockerfile: packages/chess/Dockerfile
args:
# Browser-visible URL of the server above.
VITE_WS_URL: ws://localhost:7357/ws
image: paratype/chess-web:local
container_name: paratype-web
ports:
- "5173:80"
depends_on:
server:
condition: service_healthy
restart: unless-stopped

36
packages/chess/Dockerfile Normal file
View file

@ -0,0 +1,36 @@
# syntax=docker/dockerfile:1.7
# Vite-built React SPA for @paratype/chess, served by nginx.
# Build context: monorepo root.
FROM oven/bun:1.3 AS deps
WORKDIR /app
COPY package.json bun.lock ./
COPY tsconfig.base.json tsconfig.json ./
COPY packages/rete/package.json packages/rete/
COPY packages/chess/package.json packages/chess/
COPY packages/server/package.json packages/server/
RUN bun install --frozen-lockfile
# ---- builder
FROM deps AS builder
# rete must be built first — chess imports it via the "import" export
# which points at dist/index.js.
COPY packages/rete packages/rete
RUN bun run --filter @paratype/rete build
COPY packages/chess packages/chess
# Bake the WebSocket URL into the bundle. Override at build time with
# --build-arg VITE_WS_URL=wss://your-host/ws
ARG VITE_WS_URL=ws://localhost:7357/ws
ENV VITE_WS_URL=$VITE_WS_URL
RUN bun run --filter @paratype/chess build
# ---- runtime
FROM nginx:1.27-alpine AS runtime
COPY --from=builder /app/packages/chess/dist /usr/share/nginx/html
COPY packages/chess/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

18
packages/chess/nginx.conf Normal file
View file

@ -0,0 +1,18 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# SPA history fallback — react-router uses client-side routes.
location / {
try_files $uri $uri/ /index.html;
}
# Cache hashed Vite assets aggressively.
location /assets/ {
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
}

View file

@ -0,0 +1,54 @@
# syntax=docker/dockerfile:1.7
# Authoritative Bun WebSocket server for @paratype/chess.
# Build context: monorepo root (so workspace deps resolve).
FROM oven/bun:1.3 AS deps
WORKDIR /app
# Manifests first for cacheable installs.
COPY package.json bun.lock ./
COPY tsconfig.base.json tsconfig.json ./
COPY packages/rete/package.json packages/rete/
COPY packages/chess/package.json packages/chess/
COPY packages/server/package.json packages/server/
RUN bun install --frozen-lockfile
# ---- builder: compile @paratype/rete (server resolves its "import"
# field to dist/index.js, so we MUST build it before runtime).
FROM deps AS builder
COPY packages/rete packages/rete
RUN bun run --filter @paratype/rete build
# Bring in remaining sources needed at runtime.
COPY packages/chess/src packages/chess/src
COPY packages/chess/package.json packages/chess/
COPY packages/server packages/server
# ---- runtime
FROM oven/bun:1.3-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production \
PORT=7357 \
LOG_LEVEL=info
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
# rete: built dist + its symlinked node_modules
COPY --from=builder /app/packages/rete/dist packages/rete/dist
COPY --from=builder /app/packages/rete/package.json packages/rete/package.json
COPY --from=builder /app/packages/rete/node_modules packages/rete/node_modules
# chess: source consumed by server.ts via "default" export, plus its
# per-package node_modules so symlinked deps (zod, etc.) resolve.
COPY --from=builder /app/packages/chess/src packages/chess/src
COPY --from=builder /app/packages/chess/package.json packages/chess/package.json
COPY --from=builder /app/packages/chess/node_modules packages/chess/node_modules
# server: full copy already includes its node_modules.
COPY --from=builder /app/packages/server packages/server
EXPOSE 7357
USER bun
CMD ["bun", "run", "packages/server/src/index.ts"]