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:
+8
-12
@@ -11,17 +11,17 @@ FLASHPOINT_HOST_PATH=/path/to/Flashpoint
|
||||
JWT_SECRET=replace-with-a-secure-random-string
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Container Images (production only)
|
||||
# Container Image (production only)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Override image tag (images from Docker Hub: darkraise/flashpoint-*)
|
||||
# Override image tag (image from Docker Hub: darkraise/flashpoint-web)
|
||||
# IMAGE_TAG=latest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ports (host mapping)
|
||||
# Port (host mapping)
|
||||
# ---------------------------------------------------------------------------
|
||||
# These map host ports to container ports (container ports are hardcoded)
|
||||
# Maps a host port to the container, which serves both the UI and the API
|
||||
# on port 3100
|
||||
WEB_PORT=80
|
||||
API_PORT=3100
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data Storage
|
||||
@@ -43,7 +43,7 @@ LOG_LEVEL=info
|
||||
OTEL_ENABLED=false
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
||||
# OTEL_API_KEY=your-api-key-here
|
||||
OTEL_SERVICE_NAME=flashpoint-web-backend
|
||||
OTEL_SERVICE_NAME=flashpoint-web
|
||||
OTEL_TRACES_ENABLED=true
|
||||
OTEL_METRICS_ENABLED=true
|
||||
# OTEL_METRICS_EXPORT_INTERVAL=60000
|
||||
@@ -93,10 +93,6 @@ ENABLE_CACHE_PREWARM=true
|
||||
# ---------------------------------------------------------------------------
|
||||
# HOME_RECENT_HOURS=24
|
||||
# ENABLE_CGI=false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Frontend Nginx (Docker only)
|
||||
# ---------------------------------------------------------------------------
|
||||
# BACKEND_HOST=backend
|
||||
# BACKEND_PORT=3100
|
||||
# Serve the built frontend from the backend (on by default in production)
|
||||
# SERVE_FRONTEND=true
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Shell scripts must use LF so they run inside Linux containers
|
||||
*.sh text eol=lf
|
||||
+21
-42
@@ -6,18 +6,17 @@ This directory contains automated workflows for the Flashpoint Web project.
|
||||
|
||||
### 1. Docker Build and Push (`docker-build-push.yml`)
|
||||
|
||||
Builds and pushes Docker images for all three services to container registries.
|
||||
Builds and pushes the single Docker image to the container registry.
|
||||
|
||||
#### Triggers
|
||||
|
||||
- **Version tags** (`v*.*.*`): Builds and pushes images with version tags and `latest` tag
|
||||
- **Version tags** (`v*.*.*`): Builds and pushes the image with version tags and `latest` tag
|
||||
- **Manual dispatch**: Can be triggered manually from GitHub Actions tab
|
||||
|
||||
#### Services Built
|
||||
#### Image Built
|
||||
|
||||
- `flashpoint-backend` - REST API server
|
||||
- `flashpoint-frontend` - React web UI (Nginx)
|
||||
- `flashpoint-game-service` - Game content proxy and ZIP server
|
||||
- `flashpoint-web` - Single image serving both the REST API (including game
|
||||
content) and the React web UI
|
||||
|
||||
#### Container Registry
|
||||
|
||||
@@ -43,11 +42,9 @@ The workflow pushes images to **Docker Hub**.
|
||||
- `DOCKERHUB_USERNAME`: Your Docker Hub username
|
||||
- `DOCKERHUB_TOKEN`: The access token from step 2
|
||||
|
||||
4. **Images will be pushed to**:
|
||||
4. **Image will be pushed to**:
|
||||
```
|
||||
docker.io/darkraise/flashpoint-backend:latest
|
||||
docker.io/darkraise/flashpoint-frontend:latest
|
||||
docker.io/darkraise/flashpoint-game-service:latest
|
||||
docker.io/darkraise/flashpoint-web:latest
|
||||
```
|
||||
|
||||
#### Image Tags
|
||||
@@ -98,51 +95,34 @@ The workflow uses GitHub Actions cache to speed up builds:
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
##### Pull and Run Images
|
||||
##### Pull and Run the Image
|
||||
|
||||
```bash
|
||||
# Pull latest images
|
||||
docker pull darkraise/flashpoint-backend:latest
|
||||
docker pull darkraise/flashpoint-frontend:latest
|
||||
docker pull darkraise/flashpoint-game-service:latest
|
||||
# Pull the latest image
|
||||
docker pull darkraise/flashpoint-web:latest
|
||||
|
||||
# Or use a specific version
|
||||
docker pull darkraise/flashpoint-backend:1.0.0
|
||||
docker pull darkraise/flashpoint-web:1.0.0
|
||||
```
|
||||
|
||||
##### Use in Docker Compose
|
||||
|
||||
Update your `docker-compose.yml` to use pre-built images:
|
||||
Update your `docker-compose.yml` to use the pre-built image:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
image: darkraise/flashpoint-backend:latest
|
||||
flashpoint-web:
|
||||
image: darkraise/flashpoint-web:latest
|
||||
# Remove build section
|
||||
ports:
|
||||
- "3100:3100"
|
||||
# ... rest of config
|
||||
|
||||
frontend:
|
||||
image: darkraise/flashpoint-frontend:latest
|
||||
ports:
|
||||
- "80:8080"
|
||||
# ... rest of config
|
||||
|
||||
game-service:
|
||||
image: darkraise/flashpoint-game-service:latest
|
||||
ports:
|
||||
- "22500:22500"
|
||||
- "22501:22501"
|
||||
- "80:3100"
|
||||
# ... rest of config
|
||||
```
|
||||
|
||||
Then simply run:
|
||||
```bash
|
||||
docker-compose pull # Pull latest images
|
||||
docker-compose up -d # Start services
|
||||
docker compose pull # Pull the latest image
|
||||
docker compose up -d # Start the service
|
||||
```
|
||||
|
||||
#### Making Images Public
|
||||
@@ -150,7 +130,7 @@ docker-compose up -d # Start services
|
||||
By default, Docker Hub images are private. To make them public:
|
||||
|
||||
1. Go to https://hub.docker.com/repositories
|
||||
2. Click on the repository (e.g., `flashpoint-backend`)
|
||||
2. Click on the repository (e.g., `flashpoint-web`)
|
||||
3. Click "Settings"
|
||||
4. Change visibility to "Public"
|
||||
|
||||
@@ -179,10 +159,9 @@ docker login docker.io
|
||||
#### Performance
|
||||
|
||||
Typical build times (optimized for amd64 only):
|
||||
- First build: 3-5 minutes per service
|
||||
- Subsequent builds (with cache): 1-2 minutes per service
|
||||
- Parallel execution: All 3 services build simultaneously
|
||||
- Total workflow time: ~6-10 minutes (down from 12+ minutes)
|
||||
- First build: 3-5 minutes
|
||||
- Subsequent builds (with cache): 1-2 minutes
|
||||
- Frontend and backend build stages run within a single image build
|
||||
|
||||
**Optimizations applied:**
|
||||
- Single platform (amd64) instead of multi-platform
|
||||
|
||||
@@ -13,16 +13,6 @@ jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
service:
|
||||
- name: backend
|
||||
context: .
|
||||
dockerfile: ./backend/Dockerfile
|
||||
- name: frontend
|
||||
context: .
|
||||
dockerfile: ./frontend/Dockerfile
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -52,7 +42,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_DOCKERHUB }}/${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-${{ matrix.service.name }}
|
||||
images: ${{ env.REGISTRY_DOCKERHUB }}/${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-web
|
||||
tags: |
|
||||
# Tag with full version (v1.0.0 -> 1.0.0)
|
||||
type=semver,pattern={{version}}
|
||||
@@ -63,16 +53,16 @@ jobs:
|
||||
# Latest tag
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push Docker images
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ${{ matrix.service.context }}
|
||||
file: ${{ matrix.service.dockerfile }}
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.service.name }}
|
||||
cache-to: type=gha,scope=${{ matrix.service.name }},mode=max
|
||||
cache-from: type=gha,scope=flashpoint-web
|
||||
cache-to: type=gha,scope=flashpoint-web,mode=max
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
|
||||
@@ -90,17 +80,15 @@ jobs:
|
||||
run: |
|
||||
echo "## Docker Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Images Built:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`flashpoint-backend\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`flashpoint-frontend\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Image Built:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`flashpoint-web\` (API + UI)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Registry:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Docker Hub: \`${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-*\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Docker Hub: \`${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-web\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Pull Commands:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Pull Command:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker pull ${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-backend:latest" >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker pull ${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-frontend:latest" >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker pull ${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-web:latest" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
create-release:
|
||||
@@ -127,13 +115,12 @@ jobs:
|
||||
# Get the previous tag
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||
|
||||
echo "## Docker Images" >> release_notes.md
|
||||
echo "## Docker Image" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
echo "Pull the images from Docker Hub:" >> release_notes.md
|
||||
echo "Pull the image from Docker Hub:" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
echo "\`\`\`bash" >> release_notes.md
|
||||
echo "docker pull ${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-backend:${{ steps.version.outputs.version }}" >> release_notes.md
|
||||
echo "docker pull ${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-frontend:${{ steps.version.outputs.version }}" >> release_notes.md
|
||||
echo "docker pull ${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-web:${{ steps.version.outputs.version }}" >> release_notes.md
|
||||
echo "\`\`\`" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
|
||||
|
||||
+115
@@ -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"]
|
||||
@@ -74,12 +74,12 @@ mkdir flashpoint-web && cd flashpoint-web
|
||||
<!-- prettier-ignore -->
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
image: darkraise/flashpoint-backend:${IMAGE_TAG:-latest}
|
||||
container_name: flashpoint-backend
|
||||
flashpoint-web:
|
||||
image: darkraise/flashpoint-web:${IMAGE_TAG:-latest}
|
||||
container_name: flashpoint-web
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${API_PORT:-3100}:3100"
|
||||
- "${WEB_PORT:-80}:3100"
|
||||
volumes:
|
||||
- ${FLASHPOINT_HOST_PATH:?FLASHPOINT_HOST_PATH is required}:/data/flashpoint:ro
|
||||
- ${DATA_PATH:-./data}:/app/data
|
||||
@@ -102,28 +102,6 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
frontend:
|
||||
image: darkraise/flashpoint-frontend:${IMAGE_TAG:-latest}
|
||||
container_name: flashpoint-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${WEB_PORT:-80}:8080"
|
||||
environment:
|
||||
- PUID=${PUID:-1000}
|
||||
- PGID=${PGID:-1000}
|
||||
- TZ=${TZ:-UTC}
|
||||
- BACKEND_HOST=${BACKEND_HOST:-backend}
|
||||
- BACKEND_PORT=${BACKEND_PORT:-3100}
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8080/"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
```
|
||||
|
||||
### 3. Create `.env`
|
||||
@@ -136,11 +114,10 @@ FLASHPOINT_HOST_PATH=/path/to/Flashpoint
|
||||
JWT_SECRET=your-secure-secret-key
|
||||
|
||||
# Optional — uncomment and edit as needed
|
||||
# WEB_PORT=80 # Frontend port (default: 80)
|
||||
# API_PORT=3100 # Backend API port (default: 3100)
|
||||
# WEB_PORT=80 # Host port for the app (UI + API) (default: 80)
|
||||
# PUID=1000 # Host user ID (Linux — run 'id -u')
|
||||
# PGID=1000 # Host group ID (Linux — run 'id -g')
|
||||
# DOMAIN=http://localhost # Public URL of the frontend
|
||||
# DOMAIN=http://localhost # Public URL of the app
|
||||
# LOG_LEVEL=info # debug, info, warn, error
|
||||
# TZ=UTC # Timezone
|
||||
# DATA_PATH=./data # Persistent app data
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# Production Dockerfile for Flashpoint Web Backend
|
||||
|
||||
# =============================================================================
|
||||
# Stage 1: Builder - Build TypeScript
|
||||
# =============================================================================
|
||||
FROM node:20.18.2-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy workspace root package files
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
# Copy backend package files
|
||||
COPY backend/package.json ./backend/
|
||||
|
||||
# Install all dependencies for the backend workspace
|
||||
# Use npm ci with frozen lockfile for faster, reproducible builds
|
||||
RUN npm ci --workspace=backend --include-workspace-root --prefer-offline --no-audit
|
||||
|
||||
# Copy backend source code
|
||||
COPY backend ./backend/
|
||||
|
||||
# Build TypeScript (run from backend directory)
|
||||
WORKDIR /app/backend
|
||||
RUN npm run build
|
||||
|
||||
# =============================================================================
|
||||
# Stage 2: Production - Optimized runtime
|
||||
# =============================================================================
|
||||
FROM node:20.18.2-alpine
|
||||
|
||||
# Metadata labels
|
||||
LABEL org.opencontainers.image.title="Flashpoint Web Backend"
|
||||
LABEL org.opencontainers.image.description="REST API for Flashpoint Archive"
|
||||
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=builder /app/package.json /app/package-lock.json ./
|
||||
|
||||
# Copy backend package files
|
||||
COPY --from=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 application
|
||||
COPY --from=builder /app/backend/dist ./backend/dist
|
||||
|
||||
# Copy migration files to dist directory (needed at runtime)
|
||||
COPY --from=builder /app/backend/src/migrations ./backend/dist/migrations
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY --from=builder /app/backend/docker-entrypoint.sh ./backend/docker-entrypoint.sh
|
||||
|
||||
# Create data and logs directories
|
||||
# Make entrypoint script executable
|
||||
# Use 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
|
||||
WORKDIR /app/backend
|
||||
|
||||
# Expose port
|
||||
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"]
|
||||
@@ -69,6 +69,16 @@ export const config = {
|
||||
port: 3100, // Hardcoded — Docker EXPOSE and healthcheck depend on this value
|
||||
host: '0.0.0.0',
|
||||
|
||||
// Single-image deployment: the backend also serves the built frontend and its
|
||||
// SPA fallback. On by default in production; opt in elsewhere via SERVE_FRONTEND.
|
||||
serveFrontend: process.env.SERVE_FRONTEND
|
||||
? process.env.SERVE_FRONTEND === 'true'
|
||||
: process.env.NODE_ENV === 'production',
|
||||
// __dirname is backend/dist at runtime, so this resolves to the frontend build
|
||||
// copied next to the backend in the image (/app/frontend/dist).
|
||||
frontendDistPath:
|
||||
process.env.FRONTEND_DIST_PATH ?? path.resolve(__dirname, '../../frontend/dist'),
|
||||
|
||||
flashpointPath,
|
||||
flashpointDbPath: `${flashpointPath}/Data/flashpoint.sqlite`,
|
||||
flashpointHtdocsPath: `${flashpointPath}/Legacy/htdocs`,
|
||||
|
||||
+76
-5
@@ -1,6 +1,8 @@
|
||||
// IMPORTANT: OpenTelemetry must be imported FIRST for auto-instrumentation
|
||||
import './telemetry';
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import express, { Express } from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
@@ -82,13 +84,37 @@ async function startServer() {
|
||||
app.use('/game-proxy', gameCors, gameProxyRouter);
|
||||
app.use('/game-zip', gameCors, gameZipRouter);
|
||||
|
||||
// When the backend also serves the SPA, the CSP mirrors the policy the former
|
||||
// nginx frontend used. 'unsafe-inline' (styles) is required by Radix/shadcn
|
||||
// inline style attributes; 'unsafe-eval' (scripts) is required by Ruffle's WASM.
|
||||
// No upgrade-insecure-requests, so self-hosted HTTP deployments keep working.
|
||||
// When running API-only, keep the strict 'none' policy.
|
||||
// TODO(security): tighten via per-request nonces + 'wasm-unsafe-eval' once the
|
||||
// SPA and Ruffle are verified to work without the broad 'unsafe-*' allowances.
|
||||
const cspDirectives: Record<string, string[]> = config.serveFrontend
|
||||
? {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", 'data:', 'https:'],
|
||||
fontSrc: ["'self'", 'data:'],
|
||||
connectSrc: ["'self'"],
|
||||
frameSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
baseUri: ["'self'"],
|
||||
formAction: ["'self'"],
|
||||
frameAncestors: ["'none'"],
|
||||
}
|
||||
: {
|
||||
defaultSrc: ["'none'"],
|
||||
frameAncestors: ["'none'"],
|
||||
};
|
||||
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'none'"],
|
||||
frameAncestors: ["'none'"],
|
||||
},
|
||||
useDefaults: !config.serveFrontend,
|
||||
directives: cspDirectives,
|
||||
},
|
||||
crossOriginEmbedderPolicy: false,
|
||||
frameguard: { action: 'deny' },
|
||||
@@ -121,6 +147,51 @@ async function startServer() {
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(compression());
|
||||
|
||||
// Single-image deployment: the backend serves the built frontend and its SPA
|
||||
// fallback. Registered before auth/maintenance so the app shell always loads
|
||||
// (the SPA then reflects login/maintenance state from API responses), and
|
||||
// before the API catch-all 404 in setupRoutes so client routes reach index.html.
|
||||
if (config.serveFrontend) {
|
||||
const distPath = config.frontendDistPath;
|
||||
const indexHtml = path.join(distPath, 'index.html');
|
||||
|
||||
if (fs.existsSync(indexHtml)) {
|
||||
app.use(
|
||||
express.static(distPath, {
|
||||
index: false,
|
||||
maxAge: '1y',
|
||||
immutable: true,
|
||||
setHeaders: (res, filePath) => {
|
||||
// Hashed assets are immutable; index.html must always be revalidated.
|
||||
if (filePath.endsWith('index.html')) {
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const backendPrefixes = ['/api/', '/game-proxy/', '/game-zip/', '/proxy/'];
|
||||
app.get('*', (req, res, next) => {
|
||||
if (
|
||||
req.path === '/health' ||
|
||||
backendPrefixes.some((prefix) => req.path.startsWith(prefix))
|
||||
) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
res.sendFile(indexHtml);
|
||||
});
|
||||
|
||||
logger.info(`🖥️ Serving frontend from ${distPath}`);
|
||||
} else {
|
||||
logger.warn(
|
||||
`⚠️ SERVE_FRONTEND is enabled but no build was found at ${indexHtml} — skipping static serving`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
app.use(requestTimeout(TimeoutConfig.DEFAULT));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
@@ -282,7 +353,7 @@ async function startServer() {
|
||||
server.keepAliveTimeout = 65000; // 65s - slightly above common load balancer timeout
|
||||
server.headersTimeout = 66000; // Must be > keepAliveTimeout
|
||||
server.timeout = 120000; // 2 min max for any request (including game file streaming)
|
||||
server.maxConnections = 500; // Defense-in-depth; nginx handles primary limiting
|
||||
server.maxConnections = 500; // Defense-in-depth against connection exhaustion
|
||||
|
||||
const playTrackingService = new PlayTrackingService();
|
||||
const playSessionCleanupInterval = setInterval(
|
||||
|
||||
+10
-39
@@ -1,14 +1,11 @@
|
||||
# =============================================================================
|
||||
# Flashpoint Web - Development Docker Compose
|
||||
# =============================================================================
|
||||
# Builds images locally from source code.
|
||||
# Builds the single API + UI image locally from source.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.dev.yml up -d --build
|
||||
#
|
||||
# Rebuild specific service:
|
||||
# docker compose -f docker-compose.dev.yml up -d --build backend
|
||||
#
|
||||
# View logs:
|
||||
# docker compose -f docker-compose.dev.yml logs -f
|
||||
#
|
||||
@@ -18,24 +15,25 @@
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
backend:
|
||||
flashpoint-web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./backend/Dockerfile
|
||||
image: flashpoint-backend:dev
|
||||
container_name: flashpoint-backend
|
||||
dockerfile: ./Dockerfile
|
||||
image: flashpoint-web:dev
|
||||
container_name: flashpoint-web
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${API_PORT:-3100}:3100"
|
||||
# Backend now includes game content serving on /game-proxy and /game-zip
|
||||
# Serves both the UI and the API (/api, /game-proxy, /game-zip) on one port
|
||||
- "${WEB_PORT:-80}:3100"
|
||||
volumes:
|
||||
- ${FLASHPOINT_HOST_PATH:?FLASHPOINT_HOST_PATH is required}:/data/flashpoint:ro
|
||||
- backend-db:/app/data
|
||||
- ${LOGS_PATH:-./logs}/backend:/app/logs
|
||||
- ${LOGS_PATH:-./logs}:/app/logs
|
||||
environment:
|
||||
- PUID=${PUID:-1000}
|
||||
- PGID=${PGID:-1000}
|
||||
- NODE_ENV=${NODE_ENV:-development}
|
||||
- SERVE_FRONTEND=true
|
||||
- TZ=${TZ:-UTC}
|
||||
- DOMAIN=${DOMAIN:-http://localhost:${WEB_PORT:-80}}
|
||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||
@@ -56,7 +54,7 @@ services:
|
||||
- OTEL_ENABLED=${OTEL_ENABLED:-false}
|
||||
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
|
||||
- OTEL_API_KEY=${OTEL_API_KEY:-}
|
||||
- OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-flashpoint-web-backend}
|
||||
- OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-flashpoint-web}
|
||||
- OTEL_TRACES_ENABLED=${OTEL_TRACES_ENABLED:-true}
|
||||
- OTEL_METRICS_ENABLED=${OTEL_METRICS_ENABLED:-true}
|
||||
- OTEL_METRICS_EXPORT_INTERVAL=${OTEL_METRICS_EXPORT_INTERVAL:-60000}
|
||||
@@ -70,33 +68,6 @@ services:
|
||||
networks:
|
||||
- flashpoint-network
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./frontend/Dockerfile
|
||||
image: flashpoint-frontend:dev
|
||||
container_name: flashpoint-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${WEB_PORT:-80}:8080"
|
||||
environment:
|
||||
- PUID=${PUID:-1000}
|
||||
- PGID=${PGID:-1000}
|
||||
- TZ=${TZ:-UTC}
|
||||
- BACKEND_HOST=${BACKEND_HOST:-backend}
|
||||
- BACKEND_PORT=${BACKEND_PORT:-3100}
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8080/"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
networks:
|
||||
- flashpoint-network
|
||||
|
||||
networks:
|
||||
flashpoint-network:
|
||||
driver: bridge
|
||||
|
||||
+9
-44
@@ -1,7 +1,8 @@
|
||||
# =============================================================================
|
||||
# Flashpoint Web - Production Docker Compose
|
||||
# =============================================================================
|
||||
# Uses pre-built images from container registry.
|
||||
# Single image serving both the API and the web UI.
|
||||
# Uses a pre-built image from the container registry.
|
||||
#
|
||||
# Usage:
|
||||
# 1. Copy .env.example to .env and configure required variables
|
||||
@@ -16,13 +17,13 @@
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
backend:
|
||||
image: darkraise/flashpoint-backend:${IMAGE_TAG:-latest}
|
||||
container_name: flashpoint-backend
|
||||
flashpoint-web:
|
||||
image: darkraise/flashpoint-web:${IMAGE_TAG:-latest}
|
||||
container_name: flashpoint-web
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${API_PORT:-3100}:3100"
|
||||
# Backend now includes game content serving on /game-proxy and /game-zip
|
||||
# Serves both the UI and the API (/api, /game-proxy, /game-zip) on one port
|
||||
- "${WEB_PORT:-80}:3100"
|
||||
volumes:
|
||||
- ${FLASHPOINT_HOST_PATH:?FLASHPOINT_HOST_PATH is required}:/data/flashpoint:ro
|
||||
- ${DATA_PATH:-./data}:/app/data
|
||||
@@ -31,6 +32,7 @@ services:
|
||||
- PUID=${PUID:-1000}
|
||||
- PGID=${PGID:-1000}
|
||||
- NODE_ENV=production
|
||||
- SERVE_FRONTEND=true
|
||||
- TZ=${TZ:-UTC}
|
||||
- DOMAIN=${DOMAIN:-http://localhost:${WEB_PORT:-80}}
|
||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||
@@ -51,7 +53,7 @@ services:
|
||||
- OTEL_ENABLED=${OTEL_ENABLED:-false}
|
||||
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
|
||||
- OTEL_API_KEY=${OTEL_API_KEY:-}
|
||||
- OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-flashpoint-web-backend}
|
||||
- OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-flashpoint-web}
|
||||
- OTEL_TRACES_ENABLED=${OTEL_TRACES_ENABLED:-true}
|
||||
- OTEL_METRICS_ENABLED=${OTEL_METRICS_ENABLED:-true}
|
||||
- OTEL_METRICS_EXPORT_INTERVAL=${OTEL_METRICS_EXPORT_INTERVAL:-60000}
|
||||
@@ -78,43 +80,6 @@ services:
|
||||
networks:
|
||||
- flashpoint-network
|
||||
|
||||
frontend:
|
||||
image: darkraise/flashpoint-frontend:${IMAGE_TAG:-latest}
|
||||
container_name: flashpoint-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${WEB_PORT:-80}:8080"
|
||||
environment:
|
||||
- PUID=${PUID:-1000}
|
||||
- PGID=${PGID:-1000}
|
||||
- TZ=${TZ:-UTC}
|
||||
- BACKEND_HOST=${BACKEND_HOST:-backend}
|
||||
- BACKEND_PORT=${BACKEND_PORT:-3100}
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8080/"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "5m"
|
||||
max-file: "3"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "0.5"
|
||||
memory: 256M
|
||||
reservations:
|
||||
cpus: "0.25"
|
||||
memory: 128M
|
||||
networks:
|
||||
- flashpoint-network
|
||||
|
||||
networks:
|
||||
flashpoint-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -67,6 +67,16 @@ openssl rand -hex 64
|
||||
| `ENABLE_CGI` | false | Enable PHP CGI execution for legacy game content |
|
||||
| `HOME_RECENT_HOURS` | 24 | Hours to look back for "recently added" games |
|
||||
|
||||
**Frontend Serving:**
|
||||
|
||||
The backend serves the built React UI in the single-image deployment, so the app
|
||||
and API share one origin.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------------- | ----------------------------- | -------------------------------------------------------------- |
|
||||
| `SERVE_FRONTEND` | true in production, else false | Serve the built frontend and SPA fallback from the backend |
|
||||
| `FRONTEND_DIST_PATH` | `<backend>/../frontend/dist` | Location of the built frontend (rarely needed) |
|
||||
|
||||
**OpenTelemetry:**
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -74,7 +84,7 @@ openssl rand -hex 64
|
||||
| `OTEL_ENABLED` | false | Enable distributed tracing and metrics |
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | http://localhost:4318 | OTLP collector endpoint |
|
||||
| `OTEL_API_KEY` | - | API key for authentication |
|
||||
| `OTEL_SERVICE_NAME` | flashpoint-web-backend | Service name in traces/metrics |
|
||||
| `OTEL_SERVICE_NAME` | flashpoint-web | Service name in traces/metrics |
|
||||
| `OTEL_TRACES_ENABLED` | true | Enable trace export (when OTEL enabled) |
|
||||
| `OTEL_METRICS_ENABLED` | true | Enable metrics export (when OTEL enabled)|
|
||||
| `OTEL_METRICS_EXPORT_INTERVAL`| 60000 | Metrics export interval (ms) |
|
||||
@@ -82,11 +92,11 @@ openssl rand -hex 64
|
||||
|
||||
## Frontend Variables
|
||||
|
||||
The frontend has no environment variables in development or production. API
|
||||
calls are proxied through the backend.
|
||||
The frontend has no environment variables of its own. API calls use relative
|
||||
paths (`/api`), so they resolve against whatever origin serves the app.
|
||||
|
||||
- **Development**: Via Vite proxy to `http://localhost:3100`
|
||||
- **Production**: Via Nginx reverse proxy (configured by `BACKEND_HOST`/`BACKEND_PORT`)
|
||||
- **Development**: Vite dev server on port 5173 proxies `/api` to `http://localhost:3100`
|
||||
- **Production**: The backend serves the built UI and the API from the same origin
|
||||
|
||||
## Docker Environment Variables
|
||||
|
||||
@@ -101,10 +111,9 @@ Location: `.env` in project root (used by docker-compose)
|
||||
|
||||
**Port Mapping:**
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------- | ------- | --------------------- |
|
||||
| `WEB_PORT` | 80 | Frontend exposed port |
|
||||
| `API_PORT` | 3100 | Backend exposed port |
|
||||
| Variable | Default | Description |
|
||||
| ---------- | ------- | ------------------------------------------------------ |
|
||||
| `WEB_PORT` | 80 | Host port mapped to the container (serves UI + API) |
|
||||
|
||||
**Container Settings:**
|
||||
|
||||
@@ -117,13 +126,6 @@ Location: `.env` in project root (used by docker-compose)
|
||||
| `DATA_PATH` | ./data | Host path for persistent data (production compose) |
|
||||
| `LOGS_PATH` | ./logs | Host path for log files |
|
||||
|
||||
**Frontend Nginx:**
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------- | -------- | --------------------------------------- |
|
||||
| `BACKEND_HOST` | backend | Backend hostname (Docker service name) |
|
||||
| `BACKEND_PORT` | 3100 | Backend port for Nginx proxy_pass |
|
||||
|
||||
All backend variables listed above are also passable via docker-compose. See
|
||||
`docker-compose.yml` for the full list with defaults.
|
||||
|
||||
@@ -137,7 +139,6 @@ FLASHPOINT_HOST_PATH=/path/to/flashpoint
|
||||
JWT_SECRET=$(openssl rand -hex 64)
|
||||
DOMAIN=https://flashpoint.example.com
|
||||
WEB_PORT=80
|
||||
API_PORT=3100
|
||||
LOG_LEVEL=warn
|
||||
EOF
|
||||
```
|
||||
@@ -168,7 +169,6 @@ FLASHPOINT_HOST_PATH=/data/flashpoint
|
||||
JWT_SECRET=CHANGE-THIS-TO-A-RANDOM-64-CHARACTER-STRING
|
||||
DOMAIN=https://flashpoint.example.com
|
||||
WEB_PORT=80
|
||||
API_PORT=3100
|
||||
LOG_LEVEL=warn
|
||||
```
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# Nginx configuration for Flashpoint Web Frontend
|
||||
# Serves static files and handles SPA routing
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip compression for better performance
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml text/javascript
|
||||
application/json application/javascript application/xml+rss
|
||||
application/rss+xml font/truetype font/opentype
|
||||
application/vnd.ms-fontobject image/svg+xml;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
|
||||
# Content Security Policy
|
||||
# Using 'self' for connect-src and frame-src since nginx proxies all backend
|
||||
# traffic through the same origin — no need for localhost references
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none';" always;
|
||||
|
||||
# Permissions Policy (formerly Feature-Policy)
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
|
||||
# Backend API
|
||||
location ^~ /api/ {
|
||||
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
|
||||
# Game proxy/media passthrough (through backend)
|
||||
location ^~ /proxy/ {
|
||||
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
|
||||
# Game content proxy - routes to backend (game service is integrated)
|
||||
location ^~ /game-proxy/ {
|
||||
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
|
||||
# Game ZIP content - routes to backend (game service is integrated)
|
||||
location ^~ /game-zip/ {
|
||||
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
|
||||
# SPA routing - serve index.html for all routes
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Cache static assets with versioned filenames
|
||||
# NOTE: Must repeat security headers — nginx add_header in location blocks
|
||||
# replaces ALL parent-level add_header directives
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none';" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
}
|
||||
|
||||
# Don't cache index.html to ensure updates are seen
|
||||
# NOTE: Must repeat security headers — see comment above
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none';" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
}
|
||||
|
||||
# Error handling
|
||||
error_page 404 /index.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
# Main nginx configuration for Flashpoint Web Frontend
|
||||
# Optimized for Docker container running as non-root user
|
||||
|
||||
# Don't use 'user' directive when running as non-root
|
||||
# worker_processes auto is fine for most cases
|
||||
worker_processes auto;
|
||||
|
||||
# Log to stdout/stderr for Docker (no file permission issues)
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
# PID file location (must be writable by non-root user)
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
# Hide nginx version in headers and error pages
|
||||
server_tokens off;
|
||||
|
||||
# Include mime types
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging to stdout for Docker
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /dev/stdout main;
|
||||
|
||||
# Performance optimizations
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Temp directories (must be writable by non-root user)
|
||||
client_body_temp_path /tmp/client_temp;
|
||||
proxy_temp_path /tmp/proxy_temp;
|
||||
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||
scgi_temp_path /tmp/scgi_temp;
|
||||
|
||||
# Include server configurations
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
# Production Dockerfile for Flashpoint Web Frontend
|
||||
|
||||
# =============================================================================
|
||||
# Stage 1: Builder - Build for production
|
||||
# =============================================================================
|
||||
FROM node:20.18.2-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy workspace root package files
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
# Copy frontend package files
|
||||
COPY frontend/package.json ./frontend/
|
||||
|
||||
# Install dependencies for frontend workspace
|
||||
# Use npm ci with frozen lockfile for faster, reproducible builds
|
||||
RUN npm ci --workspace=frontend --include-workspace-root --prefer-offline --no-audit
|
||||
|
||||
# Copy frontend source code
|
||||
COPY frontend ./frontend/
|
||||
|
||||
# Build application (run from frontend directory)
|
||||
WORKDIR /app/frontend
|
||||
RUN npm run build
|
||||
|
||||
# =============================================================================
|
||||
# Stage 2: Production - Nginx serving static files
|
||||
# =============================================================================
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
# Metadata labels
|
||||
LABEL org.opencontainers.image.title="Flashpoint Web Frontend"
|
||||
LABEL org.opencontainers.image.description="React web UI for Flashpoint Archive"
|
||||
LABEL org.opencontainers.image.version="1.0.0"
|
||||
LABEL org.opencontainers.image.source="https://github.com/darkraise/flashpoint-web"
|
||||
|
||||
# Install runtime dependencies
|
||||
# - gettext: provides envsubst for nginx config templating
|
||||
RUN apk add --no-cache gettext
|
||||
|
||||
# Copy built files from builder
|
||||
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx configuration from .nginx/ folder
|
||||
# Main config: logs to stdout/stderr, uses /tmp for temp files (no permission issues)
|
||||
COPY frontend/.nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY frontend/.nginx/default.conf.template /etc/nginx/templates/default.conf.template
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY frontend/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
# Default backend host (can be overridden at runtime)
|
||||
ENV BACKEND_HOST=backend \
|
||||
BACKEND_PORT=3100
|
||||
|
||||
# Set up permissions for nginx user (already exists in nginx:alpine)
|
||||
RUN chown -R nginx:nginx /usr/share/nginx/html && \
|
||||
chown -R nginx:nginx /etc/nginx/conf.d && \
|
||||
chmod +x /docker-entrypoint.sh
|
||||
|
||||
# Expose port (8080 for non-root user)
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check is configured in docker-compose.yml for flexibility.
|
||||
# When running standalone (docker run), add --health-cmd manually.
|
||||
|
||||
# Run as non-root user (nginx user from base image)
|
||||
USER nginx
|
||||
|
||||
# Production command using entrypoint script
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# =============================================================================
|
||||
# Docker Entrypoint for Flashpoint Web Frontend
|
||||
# =============================================================================
|
||||
# This script handles:
|
||||
# 1. Nginx configuration templating
|
||||
# 2. Application startup
|
||||
# =============================================================================
|
||||
|
||||
echo "Flashpoint Web Frontend - Starting..."
|
||||
|
||||
# =============================================================================
|
||||
# Nginx Configuration Templating
|
||||
# =============================================================================
|
||||
# Process nginx config template with environment variables
|
||||
|
||||
# Set defaults for environment variables (envsubst doesn't support :-default syntax)
|
||||
export BACKEND_HOST="${BACKEND_HOST:-backend}"
|
||||
export BACKEND_PORT="${BACKEND_PORT:-3100}"
|
||||
|
||||
echo "Processing nginx configuration..."
|
||||
envsubst '${BACKEND_HOST} ${BACKEND_PORT}' < /etc/nginx/templates/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Display configuration summary
|
||||
echo ""
|
||||
echo "Configuration Summary:"
|
||||
echo " Backend: ${BACKEND_HOST}:${BACKEND_PORT}"
|
||||
echo ""
|
||||
|
||||
# Start nginx
|
||||
echo "Starting Nginx..."
|
||||
exec nginx -g "daemon off;"
|
||||
Reference in New Issue
Block a user