Phase 1: Docker infrastructure optimization
- Add .dockerignore for backend and frontend (DC-1: eliminates node_modules/ and .env from build context) - Delete start.sh with --reload flag (DC-2: superseded by Dockerfile CMD) - Create entrypoint.sh with exec uvicorn (DW-5: proper PID 1 signal handling) - Pin base images to patch-level tags (DW-1: reproducible builds) - Reorder Dockerfile: create appuser before COPY, use --chown (DW-2) - Switch to npm ci for lockfile-enforced installs (DW-3) - Add network segmentation: backend_net + frontend_net (DW-4: db unreachable from frontend container) - Add deploy.resources limits to all services (DW-6: OOM protection) - Refactor proxy-params.conf to include security headers, deduplicate from nginx.conf location blocks (DW-7) - Add image/svg+xml to gzip_types (DS-1) - Add wget healthcheck for frontend service (DS-2) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a73bd17f47
commit
dbad9c69b3
43
backend/.dockerignore
Normal file
43
backend/.dockerignore
Normal file
@ -0,0 +1,43 @@
|
||||
# Version control
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Python artifacts
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
.eggs
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
venv
|
||||
env
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Environment files — never bake secrets into the image
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Tests
|
||||
tests
|
||||
pytest.ini
|
||||
.pytest_cache
|
||||
.coverage
|
||||
htmlcov
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
LICENSE
|
||||
|
||||
# Dev scripts
|
||||
start.sh
|
||||
|
||||
# Docker files (no need to copy into the image)
|
||||
Dockerfile
|
||||
docker-compose*.yaml
|
||||
@ -1,5 +1,5 @@
|
||||
# ── Build stage: compile C extensions ──────────────────────────────────
|
||||
FROM python:3.12-slim AS builder
|
||||
FROM python:3.12.9-slim-bookworm AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@ -11,24 +11,25 @@ COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
||||
|
||||
# ── Runtime stage: lean production image ───────────────────────────────
|
||||
FROM python:3.12-slim
|
||||
FROM python:3.12.9-slim-bookworm
|
||||
|
||||
# Create non-root user first, then copy with correct ownership (DW-2)
|
||||
RUN useradd -m -u 1000 appuser
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy pre-built Python packages from builder
|
||||
COPY --from=builder /install /usr/local
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
# Copy application code with correct ownership — avoids redundant chown layer
|
||||
COPY --chown=appuser:appuser . .
|
||||
|
||||
# Make entrypoint executable
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Run migrations and start server
|
||||
# --no-server-header: suppresses uvicorn version disclosure
|
||||
# --proxy-headers: reads X-Forwarded-Proto/For from reverse proxy so redirects use correct scheme
|
||||
# --forwarded-allow-ips '*': trusts proxy headers from any IP (nginx is on Docker bridge network)
|
||||
CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --no-server-header --proxy-headers --forwarded-allow-ips '*'"]
|
||||
# Use entrypoint with exec so uvicorn runs as PID 1 and receives signals (DW-5)
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
||||
13
backend/entrypoint.sh
Normal file
13
backend/entrypoint.sh
Normal file
@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "Running database migrations..."
|
||||
alembic upgrade head
|
||||
|
||||
echo "Starting uvicorn..."
|
||||
exec uvicorn app.main:app \
|
||||
--host 0.0.0.0 \
|
||||
--port 8000 \
|
||||
--no-server-header \
|
||||
--proxy-headers \
|
||||
--forwarded-allow-ips '*'
|
||||
@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run database migrations
|
||||
echo "Running database migrations..."
|
||||
alembic upgrade head
|
||||
|
||||
# Start the FastAPI application
|
||||
echo "Starting FastAPI application..."
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
@ -5,11 +5,18 @@ services:
|
||||
env_file: .env
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- backend_net
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: "1.0"
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
@ -18,11 +25,19 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- backend_net
|
||||
- frontend_net
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: "1.0"
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
@ -32,6 +47,24 @@ services:
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- frontend_net
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "--quiet", "http://localhost:8080/"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 128M
|
||||
cpus: "0.5"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
backend_net:
|
||||
driver: bridge
|
||||
frontend_net:
|
||||
driver: bridge
|
||||
|
||||
25
frontend/.dockerignore
Normal file
25
frontend/.dockerignore
Normal file
@ -0,0 +1,25 @@
|
||||
# Dependencies — rebuilt inside the container from lockfile
|
||||
node_modules
|
||||
|
||||
# Build output — rebuilt inside the container
|
||||
dist
|
||||
|
||||
# Version control
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
LICENSE
|
||||
|
||||
# Docker files
|
||||
Dockerfile
|
||||
docker-compose*.yaml
|
||||
@ -1,13 +1,13 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS build
|
||||
FROM node:20.18-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
# Install dependencies from lockfile (DW-3)
|
||||
RUN npm ci
|
||||
|
||||
# Copy source files
|
||||
COPY . .
|
||||
@ -16,7 +16,7 @@ COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage — unprivileged nginx (runs as non-root, listens on 8080)
|
||||
FROM nginxinc/nginx-unprivileged:alpine
|
||||
FROM nginxinc/nginx-unprivileged:1.27-alpine
|
||||
|
||||
# Copy built files from build stage
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
@ -41,7 +41,7 @@ server {
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json image/svg+xml;
|
||||
|
||||
# Block dotfiles (except .well-known for ACME/Let's Encrypt) (PT-04)
|
||||
location ~ /\.(?!well-known) {
|
||||
@ -122,28 +122,12 @@ server {
|
||||
include /etc/nginx/proxy-params.conf;
|
||||
}
|
||||
|
||||
# API proxy
|
||||
# API proxy (catch-all for non-rate-limited endpoints)
|
||||
location /api {
|
||||
proxy_pass http://backend:8000;
|
||||
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 $forwarded_proto;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# PT-L01: Prevent browser caching of authenticated API responses
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
# Security headers (must be repeated — nginx add_header in a location block
|
||||
# overrides server-level add_header directives, so all headers must be explicit)
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
|
||||
include /etc/nginx/proxy-params.conf;
|
||||
}
|
||||
|
||||
# SPA fallback - serve index.html for all routes
|
||||
|
||||
@ -4,3 +4,13 @@ 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 $forwarded_proto;
|
||||
|
||||
# Security headers (repeated per location — nginx add_header in a location block
|
||||
# overrides server-level directives, so all headers must be explicit)
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user