feat(M5): docker-compose full stack (caddy + node backend + sqlite)
docker-compose.yml: two profiles.
- backend: backend (node+ws+better-sqlite3, /data volume) + frontend
(Caddy static build, STORAGE=ws, same-origin proxy)
- firebase: existing Dockerfile + nginx (upstream path, untouched)
Run: docker compose --profile backend up --build. OrbStack local now,
remote docker context later.
server/Dockerfile: node:18-alpine, workspaces (shared dep), rebuild
better-sqlite3 for musl, DB at /data/tracker.sqlite.
Dockerfile.ws: CRA build STORAGE=ws → caddy:2-alpine serves /srv.
No backend URL baked (same-origin).
Caddyfile: handle /api/* + handle /ws → backend:4001 (path preserved,
mutually-exclusive handles so try_files SPA fallback never shadows proxy).
handle { static try_files } last. HTTP basic auth block optional.
src/storage/ws.js: same-origin defaults. Empty baseUrl = relative fetch
(Caddy proxy). wsUrl derives from window.location (http→ws/https→wss).
Fallback localhost for bare npm start dev.
.dockerignore: add data/ scratch/ tmp/ (never bake into image). Keep
Caddyfile in context (frontend build COPYs it).
Smoke verified via OrbStack:
- GET / → 200 (static SPA)
- PUT/GET /api/doc roundtrip → JSON persists
- WS /ws subscribe + change push → both work through proxy
Firebase profile: pre-existing Dockerfile requires .env.local (hardcoded
COPY on main, not changed here). User must create file. Not a regression.
This commit is contained in:
+11
-3
@@ -4,28 +4,36 @@
|
||||
|
||||
# Ignore Node.js modules (they will be installed in the Docker image)
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Ignore build output (it will be generated in the Docker image)
|
||||
build
|
||||
dist
|
||||
|
||||
# Ignore Docker files themselves
|
||||
# Ignore Docker files themselves (Caddyfile MUST stay in context for frontend build)
|
||||
Dockerfile
|
||||
Dockerfile.ws
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Ignore any local environment files if you have them
|
||||
.env
|
||||
# .env.local
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Ignore IDE and OS-specific files
|
||||
.vscode/
|
||||
.idea/
|
||||
.idea
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
|
||||
# Ignore local sqlite data + scratch diagnostics (never bake into image)
|
||||
data/
|
||||
scratch/
|
||||
tmp/
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Caddyfile — serve static frontend, proxy /api + /ws to backend
|
||||
# handle blocks are mutually exclusive + ordered: API/WS first, static last.
|
||||
# (try_files at site level would rewrite /api/* → /index.html before proxy.)
|
||||
|
||||
{
|
||||
# admin off for docker
|
||||
admin off
|
||||
}
|
||||
|
||||
:80 {
|
||||
encode gzip
|
||||
|
||||
# REST API → backend service (path preserved: /api/doc etc.)
|
||||
handle /api/* {
|
||||
reverse_proxy backend:4001 {
|
||||
header_up Host {host}
|
||||
}
|
||||
}
|
||||
|
||||
# WebSocket upgrade → backend
|
||||
handle /ws {
|
||||
reverse_proxy backend:4001
|
||||
}
|
||||
|
||||
# Everything else: static SPA with client-side routing fallback
|
||||
handle {
|
||||
root * /srv
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
|
||||
# HTTP basic auth (in-house only). Uncomment + set CADDY_BASIC_AUTH env.
|
||||
# basic_auth {
|
||||
# {$CADDY_BASIC_AUTH}
|
||||
# }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# Dockerfile.ws — frontend build (STORAGE=ws) served by Caddy
|
||||
# Same-origin: Caddy proxies /api + /ws to backend. No backend URL baked at build.
|
||||
|
||||
FROM node:18-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
# workspaces root
|
||||
COPY package*.json ./
|
||||
COPY shared/package.json ./shared/
|
||||
COPY server/package.json ./server/
|
||||
RUN npm install --include-workspace-root
|
||||
|
||||
COPY shared/ ./shared/
|
||||
COPY src/ ./src/
|
||||
COPY public/ ./public/
|
||||
|
||||
# Build with ws storage (no backend URL — same-origin via Caddy proxy)
|
||||
ARG REACT_APP_TRACKER_APP_ID=ttrpg-initiative-tracker-default
|
||||
ENV REACT_APP_STORAGE=ws
|
||||
ENV REACT_APP_TRACKER_APP_ID=$REACT_APP_TRACKER_APP_ID
|
||||
RUN NODE_OPTIONS=--openssl-legacy-provider npm run build
|
||||
|
||||
# Stage 2: Caddy serves static + proxies API/WS
|
||||
FROM caddy:2-alpine
|
||||
|
||||
COPY --from=build /app/build /srv
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,59 @@
|
||||
# docker-compose.yml — two profiles:
|
||||
# firebase: existing Dockerfile (nginx + firebase build), upstream path
|
||||
# backend: full stack (caddy frontend + node backend + sqlite volume)
|
||||
#
|
||||
# Usage:
|
||||
# docker compose --profile backend up --build # full self-hosted stack
|
||||
# docker compose --profile firebase up --build # firebase-only (upstream)
|
||||
#
|
||||
# Run local in OrbStack; remote docker context later (just change context).
|
||||
|
||||
services:
|
||||
# ---- full self-hosted stack (STORAGE=ws) ----
|
||||
backend:
|
||||
profiles: ["backend"]
|
||||
build:
|
||||
context: .
|
||||
dockerfile: server/Dockerfile
|
||||
image: ttrpg-backend:local
|
||||
volumes:
|
||||
- backend-data:/data
|
||||
environment:
|
||||
- DB_PATH=/data/tracker.sqlite
|
||||
- PORT=4001
|
||||
# - CORS_ORIGIN=* # Caddy same-origin, cors not strictly needed
|
||||
expose:
|
||||
- "4001"
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
profiles: ["backend"]
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.ws
|
||||
args:
|
||||
- REACT_APP_TRACKER_APP_ID=${TRACKER_APP_ID:-ttrpg-initiative-tracker-default}
|
||||
image: ttrpg-frontend:local
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-8080}:80"
|
||||
depends_on:
|
||||
- backend
|
||||
# Optional basic auth: set in .env as CADDY_BASIC_AUTH="user <hashed-pass>"
|
||||
# Generate hash: caddy hash-password
|
||||
environment:
|
||||
- CADDY_BASIC_AUTH=${CADDY_BASIC_AUTH:-}
|
||||
restart: unless-stopped
|
||||
|
||||
# ---- firebase-only path (upstream, existing Dockerfile) ----
|
||||
firebase:
|
||||
profiles: ["firebase"]
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: ttrpg-firebase:local
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-8080}:80"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
backend-data:
|
||||
@@ -0,0 +1,23 @@
|
||||
# server/Dockerfile — backend (Express + ws + better-sqlite3)
|
||||
FROM node:18-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
# workspaces root needed: shared/ is a dependency (@ttrpg/shared)
|
||||
COPY package*.json ./
|
||||
COPY shared/package.json ./shared/
|
||||
COPY server/package.json ./server/
|
||||
RUN npm install --workspaces --include-workspace-root
|
||||
|
||||
COPY shared/ ./shared/
|
||||
COPY server/ ./server/
|
||||
|
||||
# better-sqlite3 builds native; rebuild for alpine musl
|
||||
RUN cd server && npm rebuild better-sqlite3
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=4001
|
||||
ENV DB_PATH=/data/tracker.sqlite
|
||||
|
||||
EXPOSE 4001
|
||||
WORKDIR /app/server
|
||||
CMD ["node", "index.js"]
|
||||
+13
-2
@@ -13,8 +13,19 @@ if (typeof WebSocket !== 'undefined') {
|
||||
}
|
||||
|
||||
function createWsStorage({ baseUrl, wsUrl } = {}) {
|
||||
const API = (baseUrl || 'http://127.0.0.1:4001').replace(/\/$/, '');
|
||||
const WS = wsUrl || (API.replace(/^http/, 'ws') + '/ws');
|
||||
// Same-origin by default: empty baseUrl = relative fetch (Caddy/proxy).
|
||||
// Fallback to localhost for bare `npm start` dev without proxy.
|
||||
const API = (baseUrl || (typeof window !== 'undefined' && window.location ? '' : 'http://127.0.0.1:4001')).replace(/\/$/, '');
|
||||
let WS;
|
||||
if (wsUrl) {
|
||||
WS = wsUrl;
|
||||
} else if (typeof window !== 'undefined' && window.location) {
|
||||
// derive from current origin (http→ws, https→wss), same host/port.
|
||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
WS = `${proto}//${window.location.host}/ws`;
|
||||
} else {
|
||||
WS = 'ws://127.0.0.1:4001/ws';
|
||||
}
|
||||
|
||||
// App passes firebase-prefixed paths: artifacts/{APP_ID}/public/data/campaigns/...
|
||||
// Backend uses canonical paths. Strip prefix.
|
||||
|
||||
Reference in New Issue
Block a user