feat: Docker images, compose stacks, nginx config, Gitea CI

- site image: multi-stage node build -> unprivileged nginx (non-root, read-only)
- nginx: CSP + security headers, immutable asset caching, revalidated HTML,
  canonical trailing slash, preserved /Linkedin 301, legacy-WP 410s, custom 404
- externalise theme-init so CSP uses script-src 'self' (no inline hash)
- prod + dev compose; .env.example; relay Dockerfile fixed (image ships app user)
- Gitea Actions: quality, e2e, lighthouse budgets, relay build, image push on main
- verified: both images build; relay healthz 200; site serves EN/NO with CSP + redirect

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-04 06:21:35 +02:00
parent 033c9ec315
commit 9088dcffb9
11 changed files with 322 additions and 20 deletions
+18
View File
@@ -0,0 +1,18 @@
# Copy to .env and fill in. Secrets never go in git or in the image (DOCKER_SPEC §3).
# --- Contact relay: SMTP (submission endpoint of your mail provider) ---
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD= # secret — provide via env file (chmod 600) or a Docker secret
RELAY_FROM= # optional From address; defaults to SMTP_USER / RELAY_TO
RELAY_TO=connor.babbington@cesnimda.co.uk
RELAY_ALLOWED_ORIGIN=https://cesnimda.co.uk
RELAY_RATE_LIMIT=5 # requests per window per IP
RELAY_WINDOW_SECONDS=600
# --- Site build ---
PUBLIC_SITE_URL=https://cesnimda.co.uk
# --- Infra ---
PROXY_NETWORK=web # name of the existing external reverse-proxy docker network
+32
View File
@@ -0,0 +1,32 @@
# Development stack (DOCKER_SPEC §1). Contributors need only Docker — Astro HMR and
# the relay with dotnet watch, with /api/contact proxied by the Astro dev server.
services:
site-dev:
image: node:22-alpine
working_dir: /app
command: sh -c "corepack enable && pnpm install && pnpm dev --host --port 4321"
environment:
- ASTRO_TELEMETRY_DISABLED=1
volumes:
- ../site:/app
- site_node_modules:/app/node_modules
ports:
- '4321:4321'
relay-dev:
image: mcr.microsoft.com/dotnet/sdk:9.0-alpine
working_dir: /src
command: sh -c "dotnet watch run --urls http://+:8081 --non-interactive"
environment:
- DOTNET_USE_POLLING_FILE_WATCHER=1
- Smtp__Host=${SMTP_HOST:-}
- Relay__ToAddress=${RELAY_TO:-dev@example.com}
- Relay__AllowedOrigin=http://localhost:4321
volumes:
- ../relay:/src
ports:
- '8081:8081'
volumes:
site_node_modules:
+47
View File
@@ -0,0 +1,47 @@
# Production stack (DOCKER_SPEC §2). Publishes no host ports — the existing host
# reverse proxy routes cesnimda.co.uk -> site:8080 and /api/contact -> relay:8081
# over the shared external proxy network.
services:
site:
build:
context: ../site
dockerfile: Dockerfile
image: git.cesnimda.uk/cesnimda/resumesite-site:latest
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
- /var/cache/nginx
- /var/run
networks: [web]
logging:
driver: json-file
options: { max-size: '10m', max-file: '3' }
relay:
build:
context: ../relay
dockerfile: Dockerfile
image: git.cesnimda.uk/cesnimda/resumesite-relay:latest
restart: unless-stopped
read_only: true
environment:
- Smtp__Host=${SMTP_HOST}
- Smtp__Port=${SMTP_PORT:-587}
- Smtp__User=${SMTP_USER}
- Smtp__Password=${SMTP_PASSWORD}
- Relay__FromAddress=${RELAY_FROM:-}
- Relay__ToAddress=${RELAY_TO}
- Relay__AllowedOrigin=${RELAY_ALLOWED_ORIGIN:-https://cesnimda.co.uk}
- Relay__RateLimitPerWindow=${RELAY_RATE_LIMIT:-5}
- Relay__WindowSeconds=${RELAY_WINDOW_SECONDS:-600}
networks: [web]
logging:
driver: json-file
options: { max-size: '10m', max-file: '3' }
networks:
web:
external: true
name: ${PROXY_NETWORK:-web}