feat: add production Docker setup with multi-stage builds

- Add production Dockerfiles for frontend and backend
  * Frontend: multi-stage build with nginx serving static files
  * Backend: multi-stage build with Prisma generation
- Create production nginx configuration (nginx/conf.d.prod/)
  * Routes to frontend-prod:80 and backend-prod:3000
  * Supports WebSocket connections for Socket.IO
- Update docker-compose.yml to use production config
  * Add env_file support for backend-prod
  * Mount production nginx config directory
- Add .env.production.example template for deployment
This commit is contained in:
Radosław Gierwiało
2025-11-15 17:21:25 +01:00
parent b50c20fae7
commit a400068053
5 changed files with 171 additions and 57 deletions

View File

@@ -1,69 +1,24 @@
# Production Environment Configuration
# NEVER commit this file with real values!
# Use environment variables or secrets manager in production
# Server # Server
NODE_ENV=production NODE_ENV=production
PORT=3000 PORT=3000
# CORS - Your production domains # CORS
CORS_ORIGIN=https://spotlight.cam,https://www.spotlight.cam CORS_ORIGIN=http://localhost
# Database - Use managed database or strong credentials # Database (production)
# NEVER use default passwords in production! DATABASE_URL=postgresql://spotlightcam:spotlightcam123@db-prod:5432/spotlightcam?schema=public
DATABASE_URL=postgresql://prod_user:STRONG_PASSWORD_HERE@db:5432/spotlightcam_prod
# JWT - CRITICAL: Generate strong secrets # JWT (CHANGE THESE IN PRODUCTION!)
# Generate with: openssl rand -base64 64 JWT_SECRET=production-secret-key-CHANGE-THIS-IN-REAL-PRODUCTION
JWT_SECRET=CHANGE_THIS_TO_RANDOM_64_CHAR_STRING
JWT_EXPIRES_IN=24h JWT_EXPIRES_IN=24h
# AWS SES - Production credentials # AWS SES (REPLACE WITH YOUR CREDENTIALS)
# BEST PRACTICE: Use IAM roles instead of access keys AWS_REGION=eu-central-1
AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=your-aws-access-key-id
AWS_ACCESS_KEY_ID=AK......... AWS_SECRET_ACCESS_KEY=your-aws-secret-access-key
AWS_SECRET_ACCESS_KEY=change-it
SES_FROM_EMAIL=noreply@spotlight.cam SES_FROM_EMAIL=noreply@spotlight.cam
SES_FROM_NAME=spotlight.cam SES_FROM_NAME=spotlight.cam
# Email Settings # Email Settings
FRONTEND_URL=https://spotlight.cam FRONTEND_URL=http://localhost
VERIFICATION_TOKEN_EXPIRY=24h VERIFICATION_TOKEN_EXPIRY=24h
# Security Settings - Production (strict)
RATE_LIMIT_ENABLED=true
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
RATE_LIMIT_AUTH_MAX=5
RATE_LIMIT_EMAIL_MAX=3
ENABLE_CSRF=true
BODY_SIZE_LIMIT=10kb
LOG_LEVEL=warn
# Password Policy - Enforced in production
PASSWORD_MIN_LENGTH=8
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_NUMBER=true
PASSWORD_REQUIRE_SPECIAL=false
# Account Lockout - Enabled in production
ENABLE_ACCOUNT_LOCKOUT=true
MAX_LOGIN_ATTEMPTS=5
LOCKOUT_DURATION_MINUTES=15
# Database Connection Pool
DB_POOL_MIN=2
DB_POOL_MAX=10
# Monitoring (optional)
SENTRY_DSN=
NEW_RELIC_LICENSE_KEY=
# IMPORTANT SECURITY NOTES:
# 1. Generate JWT_SECRET with: openssl rand -base64 64
# 2. Use AWS IAM roles instead of access keys when possible
# 3. Use environment variables or secrets manager (AWS Secrets Manager, HashiCorp Vault)
# 4. Never commit .env files to version control
# 5. Rotate all secrets regularly (every 90 days)
# 6. Use strong database passwords (20+ characters, mixed case, numbers, symbols)

57
backend/Dockerfile.prod Normal file
View File

@@ -0,0 +1,57 @@
# Multi-stage build for production
# Stage 1: Build
FROM node:20-alpine AS builder
# Install OpenSSL for Prisma
RUN apk add --no-cache openssl
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install all dependencies (including devDependencies for Prisma generation)
RUN npm ci
# Copy application files
COPY . .
# Generate Prisma Client
RUN npx prisma generate
# Stage 2: Production
FROM node:20-alpine
# Install OpenSSL for Prisma
RUN apk add --no-cache openssl
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install only production dependencies
RUN npm ci --only=production
# Copy application files and generated Prisma Client from builder
COPY --from=builder /app/src ./src
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
# Copy entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Set environment to production
ENV NODE_ENV=production
# Expose port
EXPOSE 3000
# Set entrypoint
ENTRYPOINT ["docker-entrypoint.sh"]
# Start the application
CMD ["node", "src/server.js"]

View File

@@ -30,7 +30,7 @@ services:
- backend-prod - backend-prod
volumes: volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/conf.d.prod:/etc/nginx/conf.d:ro
- ./ssl:/etc/nginx/ssl:ro - ./ssl:/etc/nginx/ssl:ro
ports: ports:
- "80:80" - "80:80"
@@ -122,6 +122,8 @@ services:
args: args:
- NODE_ENV=production - NODE_ENV=production
container_name: spotlightcam-backend-prod container_name: spotlightcam-backend-prod
env_file:
- ./backend/.env.production
expose: expose:
- "3000" - "3000"
environment: environment:

36
frontend/Dockerfile.prod Normal file
View File

@@ -0,0 +1,36 @@
# Multi-stage build for production
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production --ignore-scripts
# Copy all dependencies for build (including devDependencies)
RUN npm install
# Copy project files
COPY . .
# Build the application
RUN npm run build
# Stage 2: Production
FROM nginx:alpine
# Copy built assets from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration (if needed)
# COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,64 @@
upstream frontend {
server frontend-prod:80;
}
upstream backend {
server backend-prod:3000;
}
server {
listen 80;
server_name localhost;
client_max_body_size 500M;
# Frontend - Static files served by nginx
location / {
proxy_pass http://frontend;
proxy_http_version 1.1;
# Standard proxy headers
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Backend API
location /api {
proxy_pass http://backend;
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# WebSocket for Socket.IO
location /socket.io {
proxy_pass http://backend;
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;
# Timeouts for WebSocket
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
}