19 KiB
Docker Configuration Review
Review Date: 2026-01-30 Reviewer: Claude Code (Sonnet 4.5)
Executive Summary
The Docker configuration for Flashpoint Web provides two deployment approaches:
- Multi-service compose (recommended): Separate containers for backend, frontend, and game-service
- Monolithic image: Single container with all services (appears unmaintained)
Overall Assessment: 🟡 Good foundation with several areas for improvement
Critical Issues Found
- ❌ Backend/Frontend Dockerfiles run in development mode (not production-ready)
- ❌ Nginx config has a circular proxy issue (port 22500 → localhost:22500)
- ⚠️ Security: Services run as root (except game-service)
- ⚠️ Missing multi-stage builds for backend/frontend
- ⚠️ Hardcoded Windows path as default
1. docker-compose.yml Review
✅ Strengths
-
Well-structured service separation
- Clear separation of concerns (backend, game-service, frontend)
- Proper dependency ordering (
frontend → backend → game-service)
-
Good health checks
healthcheck: test: ["CMD", "node", "-e", "..."] interval: 30s timeout: 10s retries: 3 start_period: 40s -
Read-only volume mounts
volumes: - ${FLASHPOINT_HOST_PATH}:/data/flashpoint:roPrevents containers from modifying Flashpoint data
-
Comprehensive documentation
- Excellent inline comments explaining usage
- Clear environment variable documentation
-
Configurable via environment variables
- All ports externally configurable
- Sensible defaults provided
⚠️ Issues & Recommendations
Issue 1: Incorrect DOMAIN Default
Current:
DOMAIN=${DOMAIN:-http://localhost}
Problem: Frontend runs on port 80, but CORS points to http://localhost (no port = port 80), while the backend runs on 3100.
Fix:
DOMAIN=${DOMAIN:-http://localhost:${WEB_PORT:-80}}
Issue 2: Frontend Environment Variable Timing
Current:
environment:
- VITE_API_URL=http://localhost:${API_PORT:-3100}
Problem: Vite env vars are injected at build time, not runtime. Setting them in docker-compose has no effect.
Fix: Frontend needs runtime config via window.env or build-time injection:
frontend:
build:
context: ./frontend
args:
- VITE_API_URL=http://localhost:${API_PORT:-3100}
Issue 3: No Resource Limits
Current: No CPU/memory limits defined
Recommendation: Add resource constraints:
backend:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
Issue 4: No Logging Configuration
Recommendation: Add centralized logging:
services:
backend:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Issue 5: Hardcoded Windows Path Default
Current:
volumes:
- ${FLASHPOINT_HOST_PATH:-D:/Flashpoint}:/data/flashpoint:ro
Problem: Fails on Linux/macOS
Fix:
volumes:
- ${FLASHPOINT_HOST_PATH:?FLASHPOINT_HOST_PATH must be set}:/data/flashpoint:ro
2. Service Dockerfiles Review
Backend Dockerfile ❌ Critical Issues
Current (backend/Dockerfile):
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3001
CMD ["npm", "run", "dev"] # ❌ DEVELOPMENT MODE!
Problems:
- ❌ Runs
npm run dev(tsx watch mode) - NOT production-ready - ❌ Includes dev dependencies and source files in final image
- ❌ No build step
- ❌ Runs as root user
- ❌ No multi-stage build (bloated image)
- ❌ Port mismatch: Dockerfile exposes 3001, but docker-compose uses 3100
Recommended Fix:
# Multi-stage production build
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build && \
npm prune --production
# Production image
FROM node:20-alpine
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S flashpoint && \
adduser -S flashpoint -u 1001
# Copy built artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
# Change ownership
RUN chown -R flashpoint:flashpoint /app
# Switch to non-root user
USER flashpoint
# Expose correct port
EXPOSE 3100
# Health check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3100/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1));"
# Production command
CMD ["node", "dist/server.js"]
Frontend Dockerfile ❌ Critical Issues
Current (frontend/Dockerfile):
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] # ❌ DEV MODE!
Problems:
- ❌ Runs Vite dev server - NOT production-ready
- ❌ No build step
- ❌ Includes entire node_modules and source
- ❌ Serves on port 5173, but docker-compose expects port 80
- ❌ No Nginx for static file serving
Recommended Fix (Multi-stage with Nginx):
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci
# Copy source and build
COPY . .
# Build args for runtime config
ARG VITE_API_URL=http://localhost:3100
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build
# Production stage with Nginx
FROM nginx:alpine
# Copy built files
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Create non-root user
RUN addgroup -g 1001 -S flashpoint && \
adduser -S flashpoint -u 1001 && \
chown -R flashpoint:flashpoint /usr/share/nginx/html /var/cache/nginx /var/run
# Switch to non-root user
USER flashpoint
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
Required nginx.conf for frontend:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
# SPA routing
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 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;
}
Game Service Dockerfile ✅ Excellent
Current (game-service/Dockerfile):
FROM node:20-alpine
RUN apk add --no-cache python3 make g++
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build && npm prune --production && rm -rf src tsconfig.json
RUN addgroup -g 1001 -S flashpoint && adduser -S flashpoint -u 1001
RUN chown -R flashpoint:flashpoint /app
USER flashpoint
EXPOSE 22500 22501
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=30s \
CMD node -e "require('http').get('http://localhost:22500/', (res) => { process.exit(res.statusCode < 500 ? 0 : 1); }).on('error', () => process.exit(1));"
CMD ["node", "dist/index.js"]
Strengths:
- ✅ Multi-stage build process (build + cleanup)
- ✅ Non-root user (flashpoint:1001)
- ✅ Production dependencies only
- ✅ Health check included
- ✅ Proper cleanup of source files
- ✅ Native module support (python3, make, g++)
Minor Improvement: Could use true multi-stage build to avoid build tools in final image:
# Build stage
FROM node:20-alpine AS builder
RUN apk add --no-cache python3 make g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
RUN addgroup -g 1001 -S flashpoint && adduser -S flashpoint -u 1001
RUN chown -R flashpoint:flashpoint /app
USER flashpoint
EXPOSE 22500 22501
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=30s \
CMD node -e "require('http').get('http://localhost:22500/', (res) => { process.exit(res.statusCode < 500 ? 0 : 1); }).on('error', () => process.exit(1));"
CMD ["node", "dist/index.js"]
3. Monolithic Dockerfile Review
File: ./Dockerfile (root level)
Status: ⚠️ Appears unmaintained, conflicts with docker-compose approach
Issues:
- References missing files:
docker/nginx.conf(exists, but different from what compose needs)docker/start.sh(exists)
- Bundles backend + frontend but missing game-service
- Port 3001 vs 3100 inconsistency
- Not referenced in docker-compose
Recommendation:
- Either update to match current architecture (include game-service)
- OR remove and focus on docker-compose approach
- OR document as "single-container deployment" alternative
4. Nginx Configuration Review
File: docker/nginx.conf
⚠️ Critical Issue: Circular Proxy
Lines 80-97:
server {
listen 22500;
server_name localhost;
location / {
proxy_pass http://localhost:22500; # ❌ Proxies to itself!
Problem: This creates an infinite loop. Nginx listens on 22500 and proxies to localhost:22500 (itself).
Fix: This section should either:
- Be removed (game-service is accessed directly on port 22500)
- Proxy to the game-service container:
proxy_pass http://game-service:22500;
Recommended removal:
# Remove this entire server block - game-service is accessed directly
✅ Strengths
-
Good gzip configuration
gzip on; gzip_vary on; gzip_comp_level 6; -
Proper static asset caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } -
SPA routing support
try_files $uri $uri/ /index.html;
⚠️ Security Improvements Needed
Add security headers:
server {
listen 80;
server_name localhost;
# 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;
# CSP header (adjust based on your needs)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
5. .dockerignore Review
File: .dockerignore
✅ Strengths
- Excludes node_modules, dist, build artifacts
- Excludes development files (.env, .vscode)
- Excludes tests and documentation
⚠️ Missing Entries
Add:
# Docker files (don't need in containers)
**/Dockerfile*
**/docker-compose*.yml
**/.dockerignore
# Database files (user.db should be in volume, not image)
**/user.db
**/user.db-journal
**/user.db-wal
# Git
**/.git
**/.github
# CI/CD
**/.gitlab-ci.yml
**/.travis.yml
**/Jenkinsfile
6. Security Review
Critical Security Issues
-
Running as root ❌
- Backend: Runs as root
- Frontend: Runs as root
- Game-service: ✅ Runs as non-root (flashpoint:1001)
-
No network segmentation ⚠️
- All services on same bridge network
- Consider separating frontend from backend/game-service
-
Missing security headers ⚠️
- No CSP, X-Frame-Options, etc.
-
Unlimited resource usage ⚠️
- No memory/CPU limits
- Risk of resource exhaustion
Security Recommendations
# docker-compose.yml additions
services:
backend:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if binding to port < 1024
read_only: true
tmpfs:
- /tmp
7. Production Readiness Checklist
❌ Not Production-Ready (Current State)
| Component | Status | Issue |
|---|---|---|
| Backend Dockerfile | ❌ | Runs in dev mode |
| Frontend Dockerfile | ❌ | Runs Vite dev server |
| Game Service | ✅ | Production-ready |
| docker-compose | ⚠️ | Needs fixes |
| Nginx config | ⚠️ | Circular proxy issue |
| Security | ❌ | Root users, no limits |
✅ Production-Ready (After Fixes)
Required changes:
- Rebuild backend/frontend Dockerfiles with multi-stage builds
- Fix nginx circular proxy
- Add non-root users to all services
- Add resource limits
- Fix DOMAIN environment variable
- Add security headers
- Implement proper logging configuration
8. Image Size Optimization
Current Estimated Sizes
- Backend: ~400MB (includes all source + node_modules)
- Frontend: ~400MB (includes Vite dev server)
- Game Service: ~200MB (optimized)
After Multi-Stage Builds
- Backend: ~150MB (built dist + production deps only)
- Frontend: ~50MB (static files + nginx)
- Game Service: ~150MB (already optimized)
Total savings: ~650MB → ~350MB (46% reduction)
9. Recommendations Summary
Immediate Priority (Breaking Issues)
- Fix Backend Dockerfile - Switch to production build
- Fix Frontend Dockerfile - Use Nginx instead of Vite dev server
- Fix Nginx circular proxy - Remove or fix port 22500 server block
- Fix DOMAIN - Use correct frontend URL
High Priority (Security)
- Add non-root users to backend and frontend
- Add resource limits to all services
- Add security headers to Nginx config
- Add logging configuration
Medium Priority (Optimization)
- True multi-stage builds for all services
- Fix hardcoded Windows path - require env var
- Add health checks to all services
- Update .dockerignore with additional exclusions
Low Priority (Nice to Have)
- Network segmentation - separate frontend from backend
- Decide on monolithic Dockerfile - update or remove
- Add docker-compose.prod.yml variant
- Consider docker secrets for JWT_SECRET
10. Example Production-Ready docker-compose.yml
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile.prod
image: flashpoint-backend:latest
container_name: flashpoint-backend
restart: unless-stopped
ports:
- "${API_PORT:-3100}:3100"
volumes:
- ${FLASHPOINT_HOST_PATH:?FLASHPOINT_HOST_PATH must be set}:/data/flashpoint:ro
- backend-db:/app/data # For user.db
environment:
- NODE_ENV=production
- PORT=3100
- HOST=0.0.0.0
- FLASHPOINT_PATH=/data/flashpoint
- FLASHPOINT_DB_PATH=/data/flashpoint/Data/flashpoint.sqlite
- FLASHPOINT_HTDOCS_PATH=/data/flashpoint/Legacy/htdocs
- FLASHPOINT_IMAGES_PATH=/data/flashpoint/Data/Images
- FLASHPOINT_LOGOS_PATH=/data/flashpoint/Data/Logos
- FLASHPOINT_PLAYLISTS_PATH=/data/flashpoint/Data/Playlists
- FLASHPOINT_GAMES_PATH=/data/flashpoint/Data/Games
- GAME_SERVICE_PROXY_URL=http://game-service:22500
- GAME_SERVICE_GAMEZIP_URL=http://game-service:22501
- DOMAIN=http://localhost:${WEB_PORT:-80}
- LOG_LEVEL=${LOG_LEVEL:-info}
depends_on:
game-service:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3100/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1));"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- backend-network
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
game-service:
build:
context: ./game-service
dockerfile: Dockerfile
image: flashpoint-game-service:latest
container_name: flashpoint-game-service
restart: unless-stopped
ports:
- "${PROXY_PORT:-22500}:22500"
- "${GAMEZIP_PORT:-22501}:22501"
volumes:
- ${FLASHPOINT_HOST_PATH:?FLASHPOINT_HOST_PATH must be set}:/data/flashpoint:ro
environment:
- NODE_ENV=production
- PROXY_PORT=22500
- GAMEZIPSERVER_PORT=22501
- FLASHPOINT_PATH=/data/flashpoint
- FLASHPOINT_HTDOCS_PATH=/data/flashpoint/Legacy/htdocs
- FLASHPOINT_GAMES_PATH=/data/flashpoint/Data/Games
- EXTERNAL_FALLBACK_URLS=${EXTERNAL_FALLBACK_URLS:-http://infinity.flashpointarchive.org/Flashpoint/Legacy/htdocs}
- LOG_LEVEL=${LOG_LEVEL:-info}
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:22500/', (res) => { process.exit(res.statusCode < 500 ? 0 : 1); }).on('error', () => process.exit(1));"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- backend-network
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.1'
memory: 128M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.prod
args:
- VITE_API_URL=http://localhost:${API_PORT:-3100}
image: flashpoint-frontend:latest
container_name: flashpoint-frontend
restart: unless-stopped
ports:
- "${WEB_PORT:-80}:80"
depends_on:
backend:
condition: service_healthy
networks:
- frontend-network
- backend-network
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
deploy:
resources:
limits:
cpus: '0.25'
memory: 128M
reservations:
cpus: '0.1'
memory: 64M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
frontend-network:
driver: bridge
backend-network:
driver: bridge
volumes:
backend-db:
driver: local
Conclusion
The Docker configuration has a solid foundation but requires significant updates to be production-ready. The game-service Dockerfile is excellent and should serve as a template for the other services.
Priority Actions:
- Rebuild backend/frontend Dockerfiles with production builds
- Fix nginx circular proxy issue
- Add security hardening (non-root users, resource limits)
- Update environment variable handling
After implementing these fixes, the Docker deployment will be production-ready and follow industry best practices.