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>
This commit is contained in:
parent
54e830016c
commit
7957b04de0
20
.dockerignore
Normal file
20
.dockerignore
Normal file
@ -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
.env.example
Normal file
4
.env.example
Normal file
@ -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
.gitignore
vendored
3
.gitignore
vendored
@ -13,3 +13,6 @@ graphify-out/
|
|||||||
|
|
||||||
# Local Claude settings (per-user overrides — keep .claude/ tracked otherwise)
|
# Local Claude settings (per-user overrides — keep .claude/ tracked otherwise)
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.env
|
||||||
|
|||||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@ -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
docker-compose.yml
Normal file
41
docker-compose.yml
Normal file
@ -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
nginx.conf
Normal file
71
nginx.conf
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user