From 3e2fbebac718f66ec181060e426e844451bfb979 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Thu, 16 Jul 2026 23:29:01 +0100 Subject: [PATCH] Add Dockerfile, frontend image, and local compose stack --- .dockerignore | 16 ++++++ .gitignore | 1 - Dockerfile | 28 +++++++++++ docker-compose.yml | 107 +++++++++++++++++++++++++++++++++++++++++ frontend/.dockerignore | 6 +++ frontend/Dockerfile | 22 +++++++++ frontend/nginx.conf | 20 ++++++++ 7 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4818621 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +node_modules +frontend +logs +coverage +.git +.gitignore +.env +.env.* +!.env.example +*.md +!README.md +test_*.html +.claude +docker-compose*.yml +Dockerfile +.dockerignore diff --git a/.gitignore b/.gitignore index 884b9fc..527e4a0 100644 --- a/.gitignore +++ b/.gitignore @@ -121,7 +121,6 @@ ehthumbs.db Thumbs.db # Docker -.dockerignore docker-compose.override.yml # Database diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0ea6824 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Shattered Void MMO - backend (Express + WebSocket) +# bookworm-slim rather than alpine: bcrypt ships glibc prebuilds, musl would force a source build. + +FROM node:20-bookworm-slim AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM node:20-bookworm-slim AS runtime +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# winston-daily-rotate-file writes here but does not create the directory itself +RUN mkdir -p logs && chown -R node:node /app + +USER node +EXPOSE 3000 + +HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=5 \ + CMD curl -fsS http://localhost:3000/health || exit 1 + +CMD ["node", "src/server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..040a600 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,107 @@ +# Shattered Void MMO - local dev stack +# frontend -> http://localhost:8080 +# backend -> http://localhost:3000 +# Bring up with: docker compose up -d --build + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${DB_NAME:-shattered_void_dev} + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-devpassword} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-shattered_void_dev}"] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 + + # Runs to completion before the backend starts, so the schema always exists first. + migrate: + build: . + command: sh -c "npx knex migrate:latest && npx knex seed:run" + environment: + NODE_ENV: development + DB_HOST: postgres + DB_PORT: 5432 + DB_NAME: ${DB_NAME:-shattered_void_dev} + DB_USER: ${DB_USER:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-devpassword} + depends_on: + postgres: + condition: service_healthy + restart: "no" + + backend: + build: . + ports: + - "3000:3000" + environment: + NODE_ENV: development + PORT: 3000 + HOST: 0.0.0.0 + LOG_LEVEL: debug + + DB_HOST: postgres + DB_PORT: 5432 + DB_NAME: ${DB_NAME:-shattered_void_dev} + DB_USER: ${DB_USER:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-devpassword} + + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_DB: 0 + + JWT_PLAYER_SECRET: ${JWT_PLAYER_SECRET:-dev-player-secret} + JWT_ADMIN_SECRET: ${JWT_ADMIN_SECRET:-dev-admin-secret} + JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-dev-refresh-secret} + JWT_ISSUER: shattered-void-mmo + + # The browser loads the SPA from :8080 but calls the API on :3000 — both must be allowed. + CORS_ALLOWED_ORIGINS: http://localhost:8080,http://localhost:3000,http://127.0.0.1:8080 + CORS_CREDENTIALS: "true" + WEBSOCKET_CORS_ORIGIN: http://localhost:8080,http://localhost:3000 + + ENABLE_GAME_TICK: "false" + ENABLE_ADMIN_ROUTES: "true" + ENABLE_DEBUG_ENDPOINTS: "true" + volumes: + - ./logs:/app/logs + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + migrate: + condition: service_completed_successfully + + frontend: + build: + context: ./frontend + args: + VITE_API_URL: http://localhost:3000 + VITE_WS_URL: http://localhost:3000 + ports: + - "8080:80" + depends_on: + - backend + +volumes: + postgres_data: + redis_data: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..8875ec6 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +.git +*.md +Dockerfile +.dockerignore diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..e4d4051 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,22 @@ +# Shattered Void MMO - frontend (Vite/React build served by nginx) + +FROM node:20-bookworm-slim AS build +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . + +# Vite inlines env vars at build time, so the API URL must be known here, not at runtime. +ARG VITE_API_URL=http://localhost:3000 +ARG VITE_WS_URL=http://localhost:3000 +ENV VITE_API_URL=$VITE_API_URL +ENV VITE_WS_URL=$VITE_WS_URL + +RUN npm run build + +FROM nginx:1.27-alpine AS runtime +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..ebbc6a8 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # React Router owns the routing; every unknown path must fall through to the SPA. + location / { + try_files $uri $uri/ /index.html; + } + + location /healthz { + access_log off; + return 200 "ok\n"; + } + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + gzip_min_length 1024; +}