Configuration

Nginx

Ports, server name, document root, IPv6, and the drop-in directories for anything the variables do not cover.

Basics

VariableDefaultDescription
NGINX_PORT80HTTP listen port. Use 8080 or higher as non-root.
NGINX_SERVER_NAME_server_name. Space-separated list accepted.
NGINX_DOCUMENT_ROOT/var/www/htmlWeb root. Mount your public directory here.
NGINX_FASTCGI_PASS127.0.0.1:9000Where PHP requests go. Change together with PHP_FPM_LISTEN.
NGINX_LISTEN_IPV6trueAdd 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 .php to PHP-FPM with SCRIPT_FILENAME set from the document root.
  • Serves /health with a plain 200 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-origin and Permissions-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_size from PHP_POST_MAX_SIZE.

Drop-in directories

Three directories are included at startup. Mount files into them or COPY them in a derived image.

DirectoryScopeUse for
/etc/nginx/http-conf.d/*.confhttp blockmap, upstream, log_format, limit_req_zone, proxy_cache_path
/etc/nginx/after.d/*.confinside the app server block, after the built-in locationsextra location blocks, rewrites, headers, error_page
/etc/nginx/http.d/*.confadditional server blocksa 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