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 ──────────────────────────────────
|
# ── Build stage: compile C extensions ──────────────────────────────────
|
||||||
FROM python:3.12-slim AS builder
|
FROM python:3.12.9-slim-bookworm AS builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
@ -11,24 +11,25 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
||||||
|
|
||||||
# ── Runtime stage: lean production image ───────────────────────────────
|
# ── 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
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy pre-built Python packages from builder
|
# Copy pre-built Python packages from builder
|
||||||
COPY --from=builder /install /usr/local
|
COPY --from=builder /install /usr/local
|
||||||
|
|
||||||
# Copy application code
|
# Copy application code with correct ownership — avoids redundant chown layer
|
||||||
COPY . .
|
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
|
USER appuser
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Run migrations and start server
|
# Use entrypoint with exec so uvicorn runs as PID 1 and receives signals (DW-5)
|
||||||
# --no-server-header: suppresses uvicorn version disclosure
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
# --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 '*'"]
|
|
||||||
|
|||||||
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
|
env_file: .env
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- backend_net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
|
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
cpus: "1.0"
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build: ./backend
|
build: ./backend
|
||||||
@ -18,11 +25,19 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- backend_net
|
||||||
|
- frontend_net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
cpus: "1.0"
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build: ./frontend
|
build: ./frontend
|
||||||
@ -32,6 +47,24 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
backend:
|
backend:
|
||||||
condition: service_healthy
|
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:
|
volumes:
|
||||||
postgres_data:
|
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
|
# Build stage
|
||||||
FROM node:20-alpine AS build
|
FROM node:20.18-alpine AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files
|
# Copy package files
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies from lockfile (DW-3)
|
||||||
RUN npm install
|
RUN npm ci
|
||||||
|
|
||||||
# Copy source files
|
# Copy source files
|
||||||
COPY . .
|
COPY . .
|
||||||
@ -16,7 +16,7 @@ COPY . .
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Production stage — unprivileged nginx (runs as non-root, listens on 8080)
|
# 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 built files from build stage
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|||||||
@ -41,7 +41,7 @@ server {
|
|||||||
gzip on;
|
gzip on;
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
gzip_min_length 1024;
|
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)
|
# Block dotfiles (except .well-known for ACME/Let's Encrypt) (PT-04)
|
||||||
location ~ /\.(?!well-known) {
|
location ~ /\.(?!well-known) {
|
||||||
@ -122,28 +122,12 @@ server {
|
|||||||
include /etc/nginx/proxy-params.conf;
|
include /etc/nginx/proxy-params.conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
# API proxy
|
# API proxy (catch-all for non-rate-limited endpoints)
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://backend:8000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection '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;
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
include /etc/nginx/proxy-params.conf;
|
||||||
# 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# SPA fallback - serve index.html for all routes
|
# 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-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
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