diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7e0f814 --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..2f1d1c0 --- /dev/null +++ b/Dockerfile.dev @@ -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 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..327247e --- /dev/null +++ b/docker-compose.dev.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7c94205 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/packages/chess/Dockerfile b/packages/chess/Dockerfile new file mode 100644 index 0000000..36bcf34 --- /dev/null +++ b/packages/chess/Dockerfile @@ -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 diff --git a/packages/chess/nginx.conf b/packages/chess/nginx.conf new file mode 100644 index 0000000..0645e10 --- /dev/null +++ b/packages/chess/nginx.conf @@ -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; + } +} diff --git a/packages/server/Dockerfile b/packages/server/Dockerfile new file mode 100644 index 0000000..5864736 --- /dev/null +++ b/packages/server/Dockerfile @@ -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"]