WordPress is groot geworden op Apache, waar een site-eigenaar een .htaccess-bestand in de webroot zette en pretty permalinks, directory-beveiliging en de regels voor PHP-uitvoering automatisch werkten zonder dat je de serverconfig hoefde aan te raken. Nginx negeert .htaccess gewoon helemaal: elke rewrite, elke accessregel en elke expires-header moet in de nginx-config zelf staan. Dat is de enige echte reden waarom een WordPress-op-nginx-setup lastiger is dan hij eruitziet. De regels zelf zijn goed gedocumenteerd, maar ze zijn verspreid over de nginx-recepten van WordPress, de nginx core module reference, de PHP-FPM configuratiedocs en reference-configs uit de community zoals SpinupWP's wordpress-nginx repository. In dit artikel zet ik die regels bij elkaar in één werkend serverblok, leg ik uit waarom elke directive er staat en geef ik je de exacte configuratie om op een verse Debian- of Ubuntu-server over te nemen.
Doel en scope
Aan het einde van dit artikel heb je een werkend nginx-serverblok dat een WordPress-site serveert op https://yoursite.nl over HTTPS, elke permalink door index.php routeert, PHP doorgeeft aan PHP-FPM via een Unix-socket, statische assets een jaar in de browsercache zet, de paden blokkeert die nginx moet afvangen omdat .htaccess dat niet meer doet en optioneel een multisite-netwerk in subdirectory-mode bedient. De scope laat FastCGI-caching bewust buiten beschouwing (dat staat in WordPress + Nginx FastCGI cache) en ook TLS-certificaten, omdat beide onderwerpen groot genoeg zijn voor hun eigen artikel.
Vereisten
Dit artikel overschrijft de standaard audience van de WordPress-categorie. De meeste wordpress-KB-artikelen gaan ervan uit dat je alleen wp-admin en een hostingpaneel hebt. Nginx serverblokken configureren is de uitzondering: je hebt root of sudo nodig op de server zelf. Zit je op managed hosting zonder shell-toegang, dan is dit artikel niet het juiste startpunt en kun je beter de WordPress-installatie-instructies van die managed host volgen.
Je hebt nodig:
- Een Debian 12, Ubuntu 22.04 of Ubuntu 24.04 server met root- of sudo-toegang. De paden in dit artikel (
/etc/nginx/sites-available/,/run/php/php8.3-fpm.sock,www-dataals webserver-user) volgen de Debian-conventies. RHEL-achtige distributies gebruiken/etc/nginx/conf.d/,/var/run/php-fpm/www.sockennginxals webserver-user; de directives zelf zijn identiek, alleen de paden verschillen. - Nginx 1.25.1 of nieuwer. Dit artikel gebruikt de moderne
http2 on;-directive (geïntroduceerd in nginx 1.25.1), niet de ouderelisten 443 ssl http2;-syntax. Zit je vast op nginx 1.24 of ouder, dan werkt de oude syntax nog gewoon; zie de noot onderaan het listen-blok. - PHP 8.3 met PHP-FPM geïnstalleerd (
php8.3-fpm-pakket op Debian en Ubuntu). PHP 8.3 wordt ondersteund tot november 2026 volgens de PHP supported versions-pagina; PHP 8.4 werkt identiek en alleen het socket-pad verandert. - WordPress al uitgepakt in
/var/www/yoursite.nl/public/metwp-config.phpop z'n plek en een bereikbare database. Dit artikel gaat over de webserver, niet over de WordPress-install. - Een TLS-certificaat voor het domein (meestal van Let's Encrypt via
certbot). Dit artikel gaat ervan uit dat de certificaatbestanden op de standaardpaden/etc/letsencrypt/live/yoursite.nl/staan. De HTTPS-redirect en SSL-parameters staan erin, maar het uitgeven van het certificaat zelf valt buiten de scope.
Waarom WordPress anders werkt op nginx dan op Apache
Op Apache bevat het .htaccess-bestand dat WordPress standaard aanmaakt de rewriteregels voor pretty permalinks, de directory-index en de regels die directe toegang tot wp-config.php blokkeren. Apache leest .htaccess bij elk request en past die regels inline toe. De site-eigenaar hoeft de Apache-config niet aan te raken.
Nginx leest .htaccess gewoon niet. De nginx-ontwikkelaars hebben bewust geen support voor .htaccess toegevoegd: per-directory configbestanden zijn traag (ze moeten bij elk request opnieuw worden gelezen) en het alternatief (één globale config die bij de start geladen wordt) is sneller en explicieter. Dat betekent dat drie categorieën gedrag die Apache-gebruikers voor lief nemen, met de hand in de nginx-config moeten:
- Permalink-routing. Apache's
RewriteRule . /index.php [L]wordt in nginxtry_files $uri $uri/ /index.php?$args;binnen hetlocation /-blok. - Security-regels. Apache's
<Files wp-config.php> Require all denied </Files>of de regel die PHP-uitvoering inwp-content/uploads/blokkeert, wordt in nginx een explicietlocation-blok metdeny all;of een regex-gebaseerde restrictie. - Headers voor statische assets. Apache-modules als
mod_expiresenmod_deflateworden in nginx vervangen doorexpires- engzip-directives die in het serverblok staan.
Het praktische gevolg: een WordPress-nginx-config is iets langer dan een WordPress-Apache-setup, en elk WordPress-op-nginx-artikel dat zegt "plak dit location /-blok erin en klaar" is onvolledig.
Anatomie van een WordPress-serverblok
Een productieklaar nginx-serverblok voor WordPress heeft vijf onderdelen, ongeveer in deze volgorde in het bestand:
- Een
server-blok op poort 80 dat alle platte HTTP omleidt naar HTTPS. - Het hoofd-
server-blok dat luistert op 443 met TLS en HTTP/2. - Binnen het hoofdblok:
root,index, detry_files-regel voor permalinks, de PHP-FPM pass, caching van statische assets en delocation-blokken voor security. - Is de site een multisite-netwerk in subdirectory-mode, dan komt daar een extra rewriteblok bij.
- Optioneel: een los
server-blok datwww.yoursite.nlredirect naar het non-www-domein (of andersom).
De volledige config staat onderaan dit artikel. De secties daartussen leggen elk stuk uit.
De try_files-directive waar elke permalink van afhangt
Dit is de regel waardoor WordPress-permalinks werken. In het hoofd-serverblok:
location / {
try_files $uri $uri/ /index.php?$args;
}
De try_files-directive controleert de argumenten van links naar rechts en gebruikt de eerste die bestaat. Voor een request naar /2026/04/mijn-post/ kijkt nginx achtereenvolgens naar:
$uri: is er een bestand op/var/www/yoursite.nl/public/2026/04/mijn-post/? Nee (WordPress-posts zijn virtuele URL's, geen directories op disk).$uri/: is er een directory op dat pad met een indexbestand? Nee./index.php?$args: val terug op WordPress en geef de query string mee. WordPress leest dan het originele request uit$_SERVER['REQUEST_URI']en lost het op via de eigen rewrite-engine.
Voor een request naar /wp-content/uploads/2026/03/hero.jpg slaagt diezelfde driestapscheck al in stap één: het bestand staat op disk, dus nginx serveert het direct zonder PHP aan te roepen. Dat is waarom WordPress-op-nginx snel is: statische bestanden raken PHP nooit, en dynamische URL's raken PHP precies één keer.
Verwachte output: Met dit blok actief en Instellingen > Permalinks op "Berichtnaam" of een andere pretty-format retourneert https://yoursite.nl/voorbeeldpagina/ de gerenderde pagina, en levert https://yoursite.nl/wp-content/uploads/2024/01/hero.jpg direct de afbeelding. Je kunt bevestigen dat statische bestanden niet door PHP gaan door de PHP-FPM access log te volgen: alleen dynamische URL's horen daar voorbij te komen.
De meest gemaakte fout op dit blok is /index.php$is_args$args in plaats van /index.php?$args. Allebei werken. Het subtiele verschil: $is_args wordt ? als er argumenten zijn en blijft leeg als dat niet zo is, terwijl het letterlijke ? altijd een ? oplevert. De WordPress-docs gebruiken ?$args; de SpinupWP reference-config gebruikt $is_args$args. Beide zijn correct, maar kies er één en meng ze niet.
De PHP-FPM pass configureren
De overgave aan PHP-FPM is de tweede dragende directive. Plaats dit blok in het hoofd-serverblok, na het location /-blok:
location ~ \.php$ {
# Guard: alleen naar PHP-FPM sturen voor bestanden die echt bestaan.
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
}
Drie dingen in dit blok zijn belangrijk.
De guard try_files $uri =404;. Zonder die regel geeft nginx een request als /wp-content/uploads/malicious.jpg/actually.php gewoon door aan PHP-FPM, die (afhankelijk van hoe SCRIPT_FILENAME wordt opgelost) een door een aanvaller geüpload bestand als PHP kan uitvoeren. Dit is de klasse bugs die bekend staat als de PHP-FPM path traversal-kwetsbaarheid. De guard zorgt dat de URL naar een bestaand bestand op disk verwijst voordat PHP-FPM het ziet.
De regel fastcgi_pass unix:...sock. Op een single-server-setup waar nginx en PHP-FPM op dezelfde machine draaien is een Unix-domainsocket nipt sneller dan een TCP-verbinding, omdat de three-way handshake en de kernel-netwerkstack worden overgeslagen. De PHP-FPM documentatie beschrijft beide syntaxen: listen = /run/php/php8.3-fpm.sock voor een socket en listen = 127.0.0.1:9000 voor TCP. Voor de meeste WordPress-sites is de socket de juiste default. Gebruik TCP als PHP-FPM op een aparte server draait, in een aparte container zit of als je load wil balancen over meerdere PHP-FPM pools. De socket moet lees- en schrijfbaar zijn voor de nginx-user (www-data op Debian en Ubuntu); de defaults listen.owner = www-data en listen.mode = 0660 in de PHP-FPM pool-config regelen dat al.
De SCRIPT_FILENAME-parameter. De regel include fastcgi_params; haalt de standaardset FastCGI-variabelen binnen die bij nginx wordt meegeleverd, maar zet SCRIPT_FILENAME niet. PHP-FPM gebruikt die parameter om te bepalen welk PHP-bestand hij moet uitvoeren, en als die ontbreekt of verkeerd staat, krijg je ofwel "No input file specified" ofwel het verkeerde bestand wordt uitgevoerd. De waarde $document_root$fastcgi_script_name combineert de root-directive van de site met het padgedeelte van de URL (bijvoorbeeld /index.php) en levert een echt absoluut pad op. Gebruik $request_filename hier niet tenzij je de nginx-broncode hebt gelezen en de edge cases kent; $document_root$fastcgi_script_name is de canonieke en veilige vorm.
Socket versus TCP: korte beslisgids
| Factor | Unix-socket | TCP op 127.0.0.1 |
|---|---|---|
| Latency | Marginaal lager (geen TCP-handshake) | Iets hoger |
| Throughput bij zeer hoge concurrency | Lager (één socketbestand) | Hoger (meerdere verbindingen) |
| Werkt cross-machine | Nee | Ja |
| Werkt cross-container | Alleen met gedeelde volume | Ja |
| Default op Debian en Ubuntu | Ja | Nee |
Voor een single-server WordPress-install is de Unix-socket de default en de juiste keuze. Stap over op TCP als je PHP-FPM in een aparte Docker-container draait of op een eigen app-server. De EasyEngine socket-vs-TCP-vergelijking merkt op dat TCP bij zeer hoge concurrency beter schaalt omdat het socketbestand dan een contention-punt wordt, maar voor een gewone WordPress-site bereik je dat niveau toch nooit.
Caching van statische assets
Nginx kan browsercache-headers zetten op statische bestanden, zodat terugkerende bezoekers niet bij elke paginalading dezelfde afbeeldingen, CSS en JavaScript opnieuw downloaden. In het hoofd-serverblok:
# Cache afbeeldingen, video, audio, fonts en moderne image-formats voor een jaar.
location ~* \.(?:jpg|jpeg|gif|png|avif|webp|ico|svg|mp4|mp3|ogg|ogv|webm)$ {
expires 1y;
access_log off;
}
# Cache CSS en JavaScript een jaar.
location ~* \.(?:css|js)$ {
expires 1y;
access_log off;
}
# Cache webfonts een jaar en stuur CORS zodat ze laden over subdomeinen.
location ~* \.(?:woff|woff2|ttf|otf|eot)$ {
expires 1y;
access_log off;
add_header Access-Control-Allow-Origin *;
}
De expires 1y-directive zet zowel Expires als Cache-Control: max-age=31536000. Een jaar is de industriestandaard voor versioned static assets en is wat SpinupWP's reference-config gebruikt in static-files.conf. access_log off houdt je access log leesbaar door de 200+ statische requests per paginalading eruit te filteren; wil je later statische assets debuggen, dan zet je het tijdelijk weer aan. De CORS-header op fonts staat er zodat een CDN of subdomein dat naar je uploads wijst fonts kan laden zonder cross-origin-block.
Gzip zet je globaal aan in /etc/nginx/nginx.conf met gzip on; en een gzip_types-lijst. Op Debian en Ubuntu staat er standaard al een redelijk gzip-blok in de nginx.conf, dus vaak hoef je daar per site niks aan te passen. Brotli vergt een extra module (libnginx-mod-brotli op Debian testing, of een custom build) en is nice-to-have, geen must-have.
Security-regels die nginx nodig heeft omdat .htaccess wordt genegeerd
Dit zijn de regels die Apache-WordPress-gebruikers gratis meekrijgen via de meegeleverde .htaccess. Op nginx moeten ze expliciet zijn. Plaats ze in het hoofd-serverblok:
# Blokkeer directe toegang tot verborgen bestanden zoals .htaccess, .htpasswd, .git.
# Uitzondering voor /.well-known/ zodat Let's Encrypt-renewals blijven werken.
location ~* /\.(?!well-known\/) {
deny all;
}
# Blokkeer directe toegang tot config-, log- en includebestanden op extensie.
location ~ \.(ini|log|conf)$ {
deny all;
}
# Blokkeer PHP-uitvoering in de uploads-directory.
# Werkt voor single-site uploads (wp-content/uploads) en multisite (wp-content/blogs.dir/*/files).
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
# Blokkeer directe toegang tot wp-config.php van buitenaf.
location = /wp-config.php {
deny all;
}
# Blokkeer xmlrpc.php tenzij je dat specifiek nodig hebt (Jetpack, sommige mobiele apps).
# Haal het commentaar weg als je XML-RPC actief gebruikt.
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
Waarom elk van deze ertoe doet:
- Verborgen bestanden.
.git,.env,.DS_Storeen.htaccesszelf horen nooit gepubliceerd te worden. De regex/\.(?!well-known\/)blokkeert elk padsegment dat met een punt begint, behalve/.well-known/(dat RFC 8615 reserveert voor protocolmetadata zoals Let's Encrypt's HTTP-01-challenge en security.txt). Een fout die ik vaak zie: een rigoureuslocation ~ /\.-blok dat Let's Encrypt-renewals stilletjes breekt. - PHP in uploads. WordPress staat ingelogde gebruikers (met de
upload_files-capability) toe afbeeldingen te uploaden. Als een aanvaller een.php-bestand weet te uploaden (via een kwetsbare plugin, een media-library-bug of een bestandsextensiecheck die alleen naar het MIME-type kijkt), wordt dat bestand direct door nginx als uitvoerbare PHP geserveerd, tenzij deze regel erin staat..phpblokkeren in/uploads/en/files/is de riem-en-bretels-verdediging die "code execution" omzet in "404". De SpinupWP reference-config levert precies deze regel inexclusions.confen de WordPress developer docs raden hetzelfde aan. - wp-config.php. WordPress zet het databasewachtwoord, de tabel-prefix en de auth-salts in
wp-config.php. Als een foute PHP-handler ervoor zorgt dat het bestand als platte tekst wordt geserveerd in plaats van uitgevoerd, staat elk secret plotseling publiekelijk online. De explicietedeny all;op/wp-config.phpis een tweede verdedigingslinie bovenop het.php-locationblok. - xmlrpc.php. De XML-RPC-endpoint van WordPress is het historische doelwit van brute-force-loginaanvallen en amplificatieaanvallen. Gebruik je Jetpack, de WordPress mobiele app of een plugin die XML-RPC nodig heeft niet, blokkeer het dan. Doe je dat wel, haal deze regel dan weg en leun op applicatie-niveau hardening.
Hardening-headers (X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security) zijn ook de moeite waard. Die staan in WordPress security hardening, ik ga de volledige lijst hier niet dubbel zetten.
Multisite: de extra rewriteregels
Single-site WordPress en WordPress-subdomeinmultisite werken allebei met alleen try_files $uri $uri/ /index.php?$args;. Subdirectory-multisite is anders: de subsite-URL heeft een prefix-segment dat eruit gestript moet worden voor het request naar PHP gaat. Zonder het extra rewriteblok eindigt een request naar https://yoursite.nl/marketing/wp-admin/ als https://yoursite.nl/marketing/wp-admin/ en dat geeft een 404, want er is geen marketing/wp-admin-directory op disk.
Het canonieke rewriteblok, gedocumenteerd in de WordPress nginx-recepten en meegeleverd in SpinupWP's multisite-subdirectory.conf, hoort in het hoofd-serverblok, boven het location /-blok:
# Subdirectory-multisite-rewrites: strip de subsite-prefix voor routing.
# Alleen van toepassing als het requestbestand niet op disk bestaat.
if (!-e $request_filename) {
rewrite /wp-admin$ $scheme://$host$request_uri/ permanent;
rewrite ^(/[^/]+)?(/wp-.*) $2 last;
rewrite ^(/[^/]+)?(/.*\.php) $2 last;
}
Wat elke regel doet:
rewrite /wp-admin$ ... permanent;hangt een trailing slash achter requests die eindigen op/wp-admin(zonder slash). Zonder deze regel geeft WordPress af en toe een 301-loop op subsite-adminURL's.rewrite ^(/[^/]+)?(/wp-.*) $2 last;matcht een leidende subsite-slug (/marketing) gevolgd door een willekeurig/wp-*-pad (/wp-admin/,/wp-content/,/wp-includes/,/wp-login.php) en strijkt de subsite-prefix weg. Na de rewrite ziet het request eruit als een single-site request en handelen de latere blokken het normaal af.rewrite ^(/[^/]+)?(/.*\.php) $2 last;doet hetzelfde voor elke.php-URL onder een subsite-prefix.if (!-e $request_filename)beschermt het hele blok, zodat echte bestanden op disk (afbeeldingen in/wp-content/uploads/, statische assets) direct worden geserveerd en nooit herschreven.
De WordPress developer docs gebruiken hier een if-blok, ook al waarschuwt de nginx-documentatie tegen if binnen location. Die waarschuwing klopt, maar geldt niet op server-blok-niveau, en de -e-bestandstest is een van de gevallen waarin if expliciet veilig is.
Subdomein-multisite heeft dit blok niet nodig. In subdomein-mode is elke subsite een eigen hostname (bijvoorbeeld marketing.yoursite.nl), en nginx's server_name-directive routeert het request naar het juiste serverblok zonder dat er URL's herschreven hoeven worden. Zie WordPress multisite installeren voor de volledige multisite-beslisgids.
Debuggen: waar nginx en PHP-FPM hun logs schrijven
Als er iets stuk is, staat het antwoord bijna altijd in een van vier logbestanden. Op Debian en Ubuntu staan die op:
/var/log/nginx/error.logvoor fouten op nginx-niveau: slechte rewriteregels, ontbrekende bestanden, permission errors op de webroot, mislukte SSL-handshakes./var/log/nginx/access.logvoor de volledige request log./var/log/php8.3-fpm.logvoor PHP-FPM pool-fouten: gecrashte workers, uitgeputte pool, socket-permission-problemen.wp-content/debug.login de WordPress-directory, als jeWP_DEBUG_LOGinwp-config.phphebt aangezet. Daar logt WordPress zelf de PHP-warnings en -errors.
Draai sudo tail -f /var/log/nginx/error.log /var/log/php8.3-fpm.log in een terminal terwijl je de site in een andere terminal reloadt. Negen van de tien keer is de foutmelding expliciet. Veelvoorkomende patronen:
FastCGI sent in stderr: "Primary script unknown":SCRIPT_FILENAMEklopt niet of het bestand bestaat niet op disk. Controleer of deroot-directive naar de juiste directory wijst en of$document_root$fastcgi_script_namenaar een bestaand bestand resolvt.connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied): de nginx-user kan de PHP-FPM-socket niet lezen. Checklisten.owner,listen.groupenlisten.modein/etc/php/8.3/fpm/pool.d/www.conftegen de nginx-user (www-data).upstream sent too big header: een PHP-script genereert een hele grote response header. Verhoogfastcgi_buffer_sizeenfastcgi_buffersin het PHP-location-blok.client intended to send too large body: het request is groter danclient_max_body_size. De nginx-default is 1 MB. WordPress-uploads lopen hierop stuk; zetclient_max_body_size 64m;in het serverblok om het gelijk te trekken met de PHPupload_max_filesize.
Voor een bredere handleiding bij het lezen van logs, zie de WordPress debug log aanzetten en lezen.
Verifieer het eindresultaat
Als de config op z'n plek staat, loop deze checks in volgorde door:
- Syntax-check:
sudo nginx -t. Moetsyntax is okentest is successfulprinten. Zo niet, dan zegt de output precies welk bestand en welke regel fout is. - Reload:
sudo systemctl reload nginx. Geen output is de successituatie. - Statische-bestandstest:
curl -sI https://yoursite.nl/wp-includes/js/wp-emoji-release.min.js | head -5. VerwachtHTTP/2 200en eencache-control: max-age=31536000-header (de cache voor één jaar). Krijg je 404, dan klopt derootniet; krijg jeHTTP/1.1 200zondercache-control, dan matcht het static-asset-blok niet. - Permalink-test: Zet
Instellingen > Permalinksop "Berichtnaam" in wp-admin. Bezoekhttps://yoursite.nl/voorbeeldpagina/. De pagina hoort te renderen. Krijg je een nginx-404, dan ontbreekt of klopt hettry_files-blok niet. - wp-admin-test: Log in op
https://yoursite.nl/wp-admin/. De pagina's horen met stijlen te verschijnen. Laadt wp-admin als platte HTML zonder CSS, dan wordtwp-includes/niet als statische bestanden geserveerd (meestal een te agressieve regex). - Security-regeltest:
curl -sI https://yoursite.nl/wp-config.php. VerwachtHTTP/2 403.curl -sI https://yoursite.nl/wp-content/uploads/test.php(ook als dat bestand niet bestaat): verwachtHTTP/2 403. - Logcheck: Tail
/var/log/nginx/error.logterwijl je rondklikt op de site. Tijdens normaal gebruik horen daar geen warnings of errors te verschijnen.
Het serverblok werkt pas goed als alle zeven checks slagen.
Wat een WordPress-op-nginx-setup NIET is
Omdat dit artikel een hele hoop kleine regels in één config samenpakt, zijn er drie misvattingen die expliciet benoemd moeten worden.
"De ingebouwde rewriteregels van WordPress werken op nginx net als op Apache." Nee. WordPress schrijft .htaccess bij het opslaan van je permalinks; nginx negeert .htaccess volledig. De permalink-rewrite moet in nginx zelf staan, via try_files of een expliciete rewrite. Als je op een nginx-site op Instellingen > Permalinks op Opslaan klikt, verandert er aan de webserverkant helemaal niets.
"Het location ~ \.php$-blok is optioneel als ik PHP-FPM gebruik." Dat is het niet. Zonder een location-blok dat matcht op .php en fastcgi_pass aanroept, serveert nginx .php-bestanden als platte tekst naar de browser. De eerste keer dat een bezoeker op een verkeerd geconfigureerde server https://yoursite.nl/wp-config.php opent, verschijnt je databasewachtwoord in hun browservenster. Het PHP-location-blok is wat .php-URL's omzet van "serveer als statisch bestand" naar "voer uit via PHP-FPM". Zie je rauwe PHP-broncode in de browser, dan is dit het blok dat ontbreekt.
"Nginx negeert .htaccess, dus security-regels zijn niet meer nodig." Dat is juist de verkeerde conclusie. Apache's .htaccess is nu precies het bestand dat directe toegang tot wp-config.php blokkeert, PHP-uitvoering in wp-content/uploads/ tegenhoudt en .git en andere verborgen bestanden dichtzet. Omdat nginx .htaccess niet leest, moet elk van die regels naar het nginx-serverblok, anders valt de bescherming weg. "Geen .htaccess" betekent niet "geen regels"; het betekent dat de regels ergens anders moeten staan.
De complete eindconfiguratie
Hieronder staat het geassembleerde serverblok, in de volgorde zoals het in /etc/nginx/sites-available/yoursite.nl hoort te staan. Symlink het naar /etc/nginx/sites-enabled/ met sudo ln -s /etc/nginx/sites-available/yoursite.nl /etc/nginx/sites-enabled/, en dan sudo nginx -t && sudo systemctl reload nginx.
# Redirect alle platte HTTP naar HTTPS.
server {
listen 80;
listen [::]:80;
server_name yoursite.nl www.yoursite.nl;
return 301 https://yoursite.nl$request_uri;
}
# Redirect www naar non-www over HTTPS.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.yoursite.nl;
ssl_certificate /etc/letsencrypt/live/yoursite.nl/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yoursite.nl/privkey.pem;
return 301 https://yoursite.nl$request_uri;
}
# Hoofd-WordPress-serverblok.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on; # nginx 1.25.1+; ouder: voeg 'http2' toe aan listen
server_name yoursite.nl;
root /var/www/yoursite.nl/public;
index index.php;
# TLS. Uitgegeven via certbot; renewal via de certbot systemd-timer.
ssl_certificate /etc/letsencrypt/live/yoursite.nl/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yoursite.nl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# Per-site logs.
access_log /var/log/nginx/yoursite.nl.access.log;
error_log /var/log/nginx/yoursite.nl.error.log;
# Sta WordPress media-uploads tot 64 MB toe (matcht PHP upload_max_filesize).
client_max_body_size 64m;
# Verberg nginx-versie in Server:-headers.
server_tokens off;
# Security-headers (basisset; zie wordpress-security-hardening voor meer).
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# -----------------------------------------------------------------------
# Subdirectory-multisite rewrites. Haal dit blok weg voor single-site of
# subdomein-multisite.
# -----------------------------------------------------------------------
# if (!-e $request_filename) {
# rewrite /wp-admin$ $scheme://$host$request_uri/ permanent;
# rewrite ^(/[^/]+)?(/wp-.*) $2 last;
# rewrite ^(/[^/]+)?(/.*\.php) $2 last;
# }
# WordPress permalink-routing: val terug op index.php als geen bestand matcht.
location / {
try_files $uri $uri/ /index.php?$args;
}
# Statische-asset-caching: afbeeldingen, video, audio, SVG, moderne formats.
location ~* \.(?:jpg|jpeg|gif|png|avif|webp|ico|svg|mp4|mp3|ogg|ogv|webm)$ {
expires 1y;
access_log off;
}
# Statische-asset-caching: CSS en JavaScript.
location ~* \.(?:css|js)$ {
expires 1y;
access_log off;
}
# Statische-asset-caching: webfonts, met CORS voor cross-subdomein-laden.
location ~* \.(?:woff|woff2|ttf|otf|eot)$ {
expires 1y;
access_log off;
add_header Access-Control-Allow-Origin *;
}
# Security: blokkeer verborgen bestanden behalve /.well-known/ (Let's Encrypt).
location ~* /\.(?!well-known\/) {
deny all;
}
# Security: blokkeer config-, log- en includebestanden op extensie.
location ~ \.(ini|log|conf)$ {
deny all;
}
# Security: blokkeer PHP-uitvoering in uploads (single-site en multisite).
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
# Security: blokkeer directe toegang tot wp-config.php als tweede verdediging.
location = /wp-config.php {
deny all;
}
# Security: blokkeer xmlrpc.php. Haal weg als je Jetpack of de mobiele app gebruikt.
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
# PHP-handoff: door naar PHP-FPM via Unix-socket.
location ~ \.php$ {
try_files $uri =404; # weiger requests voor niet-bestaande .php
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
fastcgi_read_timeout 300; # geef imports tot 5 min de tijd
}
}
Toepassen met:
sudo nginx -t && sudo systemctl reload nginx
Voor de andere kant van de serverstack (full-page caching in nginx zodat PHP alleen nog bij cache misses draait), ga door met WordPress + Nginx FastCGI cache. Hoort dit serverblok bij een verhuizing vanaf een andere host, dan staat de volledige overstap in WordPress verhuizen naar een nieuwe host of domein. En als de PHP-workers onder druk beginnen op te stapelen, loopt het sizing-werk door in PHP-FPM tuning voor WordPress.