Configuration
Nginx
Ports, server name, document root, IPv6, and the drop-in directories for anything the variables do not cover.
Basics
| Variable | Default | Description |
|---|---|---|
NGINX_PORT | 80 | HTTP listen port. Use 8080 or higher as non-root. |
NGINX_SERVER_NAME | _ | server_name. Space-separated list accepted. |
NGINX_DOCUMENT_ROOT | /var/www/html | Web root. Mount your public directory here. |
NGINX_FASTCGI_PASS | 127.0.0.1:9000 | Where PHP requests go. Change together with PHP_FPM_LISTEN. |
NGINX_LISTEN_IPV6 | true | Add listen [::]:<port> alongside IPv4. |
Setting NGINX_SERVER_NAME is worth doing even for a single-site container: it becomes the HTTPS redirect target, which closes an open-redirect vector. See SSL and HTTPS.
What the default server block does
- Routes everything through
try_files $uri $uri/ /index.php?$query_string, the pattern Laravel, Symfony and most frameworks expect. - Passes
.phpto PHP-FPM withSCRIPT_FILENAMEset from the document root. - Serves
/healthwith a plain200 OK. - Allows
/.well-known/and denies every other dot-prefixed path. - Sets
X-Frame-Options: SAMEORIGIN,X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-originandPermissions-Policy: geolocation=(), microphone=(), camera=(). - Hides the Nginx version, sets 60-second client and FastCGI timeouts, and enables gzip for text assets.
- Sizes
client_max_body_sizefromPHP_POST_MAX_SIZE.
Drop-in directories
Three directories are included at startup. Mount files into them or COPY them in a derived image.
| Directory | Scope | Use for |
|---|---|---|
/etc/nginx/http-conf.d/*.conf | http block | map, upstream, log_format, limit_req_zone, proxy_cache_path |
/etc/nginx/after.d/*.conf | inside the app server block, after the built-in locations | extra location blocks, rewrites, headers, error_page |
/etc/nginx/http.d/*.conf | additional server blocks | a second vhost, a metrics endpoint on another port |
Example: rate limit the login route
http-conf.d/limits.conf
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
after.d/login.conf
location = /login {
limit_req zone=login burst=5 nodelay;
try_files $uri /index.php?$query_string;
}
docker run --rm -d -p 8080:8080 -e NGINX_PORT=8080 \
-v $(pwd)/nginx/http-conf.d:/etc/nginx/http-conf.d:ro \
-v $(pwd)/nginx/after.d:/etc/nginx/after.d:ro \
-v $(pwd)/public:/var/www/html:ro \
adamrollogi/php-nginx:8.4
Example: long-lived cache headers for assets
after.d/assets.conf
location ~* \.(css|js|woff2|svg|png|jpg|webp|avif)$ {
expires 1y;
add_header Cache-Control "public, immutable" always;
try_files $uri =404;
}
add_header inside a location replaces the server-level headers for that location, so repeat the security headers if you need them on those responses.Regular-expression locations in after.d are evaluated after the built-in \.php$ location, so PHP handling always takes precedence.
Disabling IPv6
Some hosts set net.ipv6.conf.all.disable_ipv6=1, which makes listen [::] fail. Set NGINX_LISTEN_IPV6=false and the entrypoint strips those listeners.
Reading the rendered configuration
The effective files are in /run/nginx:
docker exec app cat /run/nginx/http.d/app.conf
docker exec app nginx -c /run/nginx/nginx.conf -T | less