# HTTPS server block — activated after SSL certificates are acquired server { listen 443 ssl; http2 on; server_name DOMAIN_PLACEHOLDER; ssl_certificate /etc/letsencrypt/live/DOMAIN_PLACEHOLDER/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/DOMAIN_PLACEHOLDER/privkey.pem; include /etc/nginx/snippets/ssl-params.conf; root /var/www/html; index index.php index.html; # Nginx healthcheck endpoint location /nginx-health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } # ACME challenge (for renewals) location /.well-known/acme-challenge/ { root /var/www/certbot; } # Rate limit wp-login.php location = /wp-login.php { limit_req zone=wplogin burst=3 nodelay; fastcgi_pass wordpress:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_param HTTPS on; } # Block xmlrpc.php at nginx level location = /xmlrpc.php { deny all; return 403; } # Block access to sensitive files location ~ /\. { deny all; } location ~* /(?:uploads|files)/.*\.php$ { deny all; } # WordPress permalinks location / { try_files $uri $uri/ /index.php?$args; } # PHP handling via FastCGI to WordPress container location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass wordpress:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_param HTTPS on; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; fastcgi_read_timeout 300; } # Static file caching # NOTE: add_header in a location block overrides ALL parent add_header directives, # so security headers must be repeated here. location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ { expires 30d; add_header Cache-Control "public, immutable"; add_header Strict-Transport-Security "max-age=63072000" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; access_log off; } } # HTTP to HTTPS redirect (replaces the HTTP-only block) server { listen 80; server_name DOMAIN_PLACEHOLDER; # ACME challenge must remain accessible over HTTP location /.well-known/acme-challenge/ { root /var/www/certbot; } # Nginx healthcheck endpoint location /nginx-health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } location / { return 301 https://$host$request_uri; } }