Bläddra i källkod

feat(docker): containerize site with configurable host port

Add Dockerfile (nginx:1.27-alpine), nginx.conf (gzip, cache, CSP and
security headers, no HSTS — left to outer proxy), and docker-compose
service `bchanot-web`. Host port is configurable via PORT env var
(default 8080) and bound to 127.0.0.1 so the container sits behind a
reverse proxy. Container hardened with read_only fs, cap_drop ALL,
no-new-privileges, and tmpfs for nginx runtime dirs. Healthcheck via
wget on /. Also adds .dockerignore and .env.example, and ignores .env.

Usage:
  cp .env.example .env
  docker compose up -d --build

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bastien 1 dag sedan
förälder
incheckning
7957b04de0
6 ändrade filer med 165 tillägg och 0 borttagningar
  1. 20 0
      .dockerignore
  2. 4 0
      .env.example
  3. 3 0
      .gitignore
  4. 26 0
      Dockerfile
  5. 41 0
      docker-compose.yml
  6. 71 0
      nginx.conf

+ 20 - 0
.dockerignore

@@ -0,0 +1,20 @@
+# Source control / Claude / local-only
+.git
+.gitignore
+.claude
+CLAUDE.md
+README.md
+
+# Docker meta (not needed inside the image)
+Dockerfile
+docker-compose.yml
+.dockerignore
+.env
+.env.example
+nginx.conf.bak
+
+# Editor / OS noise
+*.swp
+.DS_Store
+node_modules
+graphify-out

+ 4 - 0
.env.example

@@ -0,0 +1,4 @@
+# Host port the bchanot-web container is exposed on.
+# Reverse proxy (nginx/Caddy/Traefik) on the host should proxy_pass to
+# http://127.0.0.1:${PORT}.
+PORT=8080

+ 3 - 0
.gitignore

@@ -13,3 +13,6 @@ graphify-out/
 
 # Local Claude settings (per-user overrides — keep .claude/ tracked otherwise)
 .claude/settings.local.json
+
+# Docker
+.env

+ 26 - 0
Dockerfile

@@ -0,0 +1,26 @@
+# Static site for bchanot.fr
+# nginx:alpine serves index.html + CV (HTML + PDF).
+
+FROM nginx:1.27-alpine
+
+# Custom nginx config (gzip, cache, security headers).
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+# Site assets.
+WORKDIR /usr/share/nginx/html
+RUN rm -rf ./*
+
+COPY index.html ./
+COPY CV_Bastien_Chanot.html ./
+COPY CV_Bastien_Chanot.pdf ./
+
+# Non-root hardening: nginx:alpine already drops privileges to "nginx" user
+# for worker processes. Master runs as root only to bind port 80 inside
+# the container — fine because the host port is the one exposed.
+EXPOSE 80
+
+# Basic healthcheck: nginx must serve index.html.
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+  CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1
+
+CMD ["nginx", "-g", "daemon off;"]

+ 41 - 0
docker-compose.yml

@@ -0,0 +1,41 @@
+# docker-compose for bchanot.fr static site.
+#
+# Usage:
+#   cp .env.example .env
+#   # edit .env to set the host port (default 8080)
+#   docker compose up -d --build
+#
+# Host port is bound to 127.0.0.1 so the container is reachable only by a
+# reverse proxy running on the same machine. Change to 0.0.0.0:${PORT} if
+# you need LAN access for testing.
+
+services:
+  bchanot-web:
+    build:
+      context: .
+      dockerfile: Dockerfile
+    image: bchanot-web:latest
+    container_name: bchanot-web
+    restart: unless-stopped
+    ports:
+      - "127.0.0.1:${PORT:-8080}:80"
+    healthcheck:
+      test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"]
+      interval: 30s
+      timeout: 3s
+      retries: 3
+      start_period: 5s
+    read_only: true
+    tmpfs:
+      - /var/cache/nginx
+      - /var/run
+      - /tmp
+    security_opt:
+      - no-new-privileges:true
+    cap_drop:
+      - ALL
+    cap_add:
+      - CHOWN
+      - SETGID
+      - SETUID
+      - NET_BIND_SERVICE

+ 71 - 0
nginx.conf

@@ -0,0 +1,71 @@
+# nginx server block for bchanot.fr static site.
+# Container listens on port 80; host port is configured via docker-compose
+# (PORT env var). A host-level reverse proxy (nginx, Traefik, Caddy) should
+# terminate TLS and proxy_pass to http://127.0.0.1:${PORT}.
+
+server {
+    listen 80;
+    listen [::]:80;
+    server_name _;
+
+    root /usr/share/nginx/html;
+    index index.html;
+
+    # Security headers. HSTS is intentionally NOT set here — leave it to the
+    # outer reverse proxy that terminates TLS, otherwise it may be sent over
+    # plain HTTP between proxy and container.
+    add_header X-Content-Type-Options "nosniff" always;
+    add_header X-Frame-Options "SAMEORIGIN" always;
+    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+    add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
+    # CSP: inline CSS + JS are allowed (project convention), fonts from Google.
+    add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
+
+    # Forwarded headers — trust the upstream reverse proxy.
+    real_ip_header X-Forwarded-For;
+    set_real_ip_from 0.0.0.0/0;
+
+    # Compression.
+    gzip on;
+    gzip_vary on;
+    gzip_min_length 1024;
+    gzip_proxied any;
+    gzip_comp_level 6;
+    gzip_types
+        text/plain
+        text/css
+        text/html
+        text/javascript
+        application/javascript
+        application/json
+        application/xml
+        application/pdf
+        image/svg+xml;
+
+    # Long cache for the PDF (regenerated rarely, content-hash not used).
+    location ~* \.pdf$ {
+        expires 7d;
+        add_header Cache-Control "public, max-age=604800";
+    }
+
+    # Short cache for HTML so content updates land fast.
+    location ~* \.html$ {
+        expires 1h;
+        add_header Cache-Control "public, max-age=3600, must-revalidate";
+    }
+
+    # Logs to stdout/stderr (default in nginx:alpine).
+    access_log /var/log/nginx/access.log;
+    error_log /var/log/nginx/error.log warn;
+
+    # Block access to dotfiles (defense-in-depth — none are shipped anyway).
+    location ~ /\. {
+        deny all;
+        return 404;
+    }
+
+    # Default: serve files, fall back to 404.
+    location / {
+        try_files $uri $uri/ =404;
+    }
+}