refactor(docker): build and ship a single API + UI image

Collapse the separate backend (Node) and frontend (nginx) images into
one image where the Express backend serves the built React app in
addition to the API and game content, so deployment is a single
container and one origin.

- Add a root Dockerfile with frontend-builder + backend-builder stages
  producing one runtime image; remove backend/ and frontend/ Dockerfiles,
  the nginx config, and the frontend entrypoint
- Serve the frontend dist and SPA fallback from Express (config.serveFrontend
  / FRONTEND_DIST_PATH), before auth/maintenance so the shell always loads and
  before the API 404 so client routes resolve to index.html
- Port the SPA-oriented CSP from nginx into Helmet, gated on serveFrontend;
  keep the strict API-only policy otherwise
- Reduce both compose files to a single flashpoint-web service on WEB_PORT
  -> 3100; drop API_PORT, BACKEND_HOST, BACKEND_PORT
- Build one image in the release workflow instead of a two-service matrix
- Update .env.example, README, workflow README, and env var docs

Also add .gitattributes (*.sh eol=lf) and normalize the entrypoint line
endings — without it, Windows checkouts bake CRLF into the image and the
shebang fails with "no such file or directory".
This commit is contained in:
Darkraise
2026-08-11 16:02:06 +07:00
parent a41eac0101
commit 3ba60aedf9
16 changed files with 289 additions and 592 deletions
+115
View File
@@ -0,0 +1,115 @@
# Production Dockerfile for Flashpoint Web (single image: API + UI)
#
# The backend serves both the REST API / game content and the built React UI,
# so this image runs a single Node process. Build from the repo root:
# docker build -t flashpoint-web .
# =============================================================================
# Stage 1: Frontend builder - Build the React app
# =============================================================================
FROM node:20.18.2-alpine AS frontend-builder
WORKDIR /app
# Copy workspace root and frontend package files
COPY package.json package-lock.json ./
COPY frontend/package.json ./frontend/
# Install frontend dependencies (frozen lockfile for reproducible builds)
RUN npm ci --workspace=frontend --include-workspace-root --prefer-offline --no-audit
# Copy frontend source and build static assets
COPY frontend ./frontend/
WORKDIR /app/frontend
RUN npm run build
# =============================================================================
# Stage 2: Backend builder - Compile TypeScript
# =============================================================================
FROM node:20.18.2-alpine AS backend-builder
WORKDIR /app
# Copy workspace root and backend package files
COPY package.json package-lock.json ./
COPY backend/package.json ./backend/
# Install backend dependencies (frozen lockfile for reproducible builds)
RUN npm ci --workspace=backend --include-workspace-root --prefer-offline --no-audit
# Copy backend source and build
COPY backend ./backend/
WORKDIR /app/backend
RUN npm run build
# =============================================================================
# Stage 3: Production - Optimized runtime
# =============================================================================
FROM node:20.18.2-alpine
# Metadata labels
LABEL org.opencontainers.image.title="Flashpoint Web"
LABEL org.opencontainers.image.description="Self-hosted web app for Flashpoint Archive (API + UI)"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.source="https://github.com/darkraise/flashpoint-web"
WORKDIR /app
# Install runtime dependencies
# - curl: health checks
# - tzdata: timezone support
# - su-exec: lightweight tool to drop privileges (like gosu)
# - shadow: provides usermod/groupmod for dynamic UID/GID
RUN apk add --no-cache curl tzdata su-exec shadow
# Copy workspace root package files
COPY --from=backend-builder /app/package.json /app/package-lock.json ./
# Copy backend package files
COPY --from=backend-builder /app/backend/package.json ./backend/
# Install production dependencies only and clean npm cache to reduce image size
RUN npm ci --workspace=backend --include-workspace-root --omit=dev --prefer-offline --no-audit && \
npm cache clean --force
# Copy built backend
COPY --from=backend-builder /app/backend/dist ./backend/dist
# Copy migration files to dist directory (needed at runtime)
COPY --from=backend-builder /app/backend/src/migrations ./backend/dist/migrations
# Copy entrypoint script
COPY --from=backend-builder /app/backend/docker-entrypoint.sh ./backend/docker-entrypoint.sh
# Copy built frontend — served by the backend (config.frontendDistPath resolves
# to /app/frontend/dist at runtime)
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
# Create data and logs directories, make entrypoint executable, fix ownership
# Uses existing 'node' user (UID 1000) from base image
RUN mkdir -p /app/data /app/logs && \
chmod +x /app/backend/docker-entrypoint.sh && \
chown -R node:node /app
# Set working directory to backend (entrypoint runs `node dist/server.js`)
WORKDIR /app/backend
# Expose port (serves both API and UI)
EXPOSE 3100
# Health check is configured in docker-compose.yml for flexibility
# Environment variables
ENV NODE_ENV=production
ENV PUID=1000
ENV PGID=1000
# Start as root, entrypoint will drop privileges after setting up user
USER root
# Production command using entrypoint script
# The entrypoint handles:
# 1. Dynamic UID/GID setup (if PUID/PGID differ from defaults)
# 2. Database copy (if enabled)
# 3. Privilege drop and application startup
ENTRYPOINT ["./docker-entrypoint.sh"]