diff --git a/docker-compose.yml b/docker-compose.yml index 305791b..57c152e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,8 @@ services: image: nginx:alpine profiles: [nginx] ports: - - "8080:80" + - "80:80" + - "443:443" environment: - DOMAIN=${DOMAIN:-localhost} volumes: @@ -27,6 +28,7 @@ services: # nginx:alpine ships its own default.conf (server_name localhost) — # collides with ours whenever DOMAIN=localhost. Null it out. - /dev/null:/etc/nginx/conf.d/default.conf:ro + - /etc/letsencrypt:/etc/letsencrypt:ro depends_on: - app networks: diff --git a/nginx/templates/20-speech-server.conf.template b/nginx/templates/20-speech-server.conf.template index d6cc3a2..3363733 100644 --- a/nginx/templates/20-speech-server.conf.template +++ b/nginx/templates/20-speech-server.conf.template @@ -3,13 +3,30 @@ # scanning / domain-fronting probes against the container's bare IP). server { listen 80 default_server; + listen 443 ssl default_server; server_name _; + ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; return 444; } +# HTTP → HTTPS redirect server { listen 80; server_name ${DOMAIN}; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name ${DOMAIN}; + + ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; # Docker's embedded DNS (127.0.0.11). A static `upstream {}` block # resolves the app container's IP once at nginx startup and caches it @@ -23,6 +40,7 @@ server { add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy no-referrer always; + add_header Strict-Transport-Security "max-age=31536000" always; # App serves no static files today; costs nothing to block dotfiles anyway. location ~ /\. {