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".
116 lines
4.1 KiB
Docker
116 lines
4.1 KiB
Docker
# 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"]
|