Drupal + nginx local (macOS)
Stack
- Homebrew:
nginx,php(8.2+), MySQL/MariaDB as needed - Docroot: project
docroot/orweb/(Composer Drupal layouts) - URL pattern:
http://example.localhoston port 80
Install & start
brew install nginx php
brew services start php
# nginx on :80 needs root:
sudo brew services start nginx
Confirm PHP-FPM listens (pick one in the vhost):
# TCP (common default)
lsof -i :9000
# or socket
ls "$(brew --prefix)/var/run/php-fpm.sock"
nginx.conf
Ensure http includes server configs:
http {
include servers/*;
}
Path: $(brew --prefix)/etc/nginx/nginx.conf
Server files: $(brew --prefix)/etc/nginx/servers/example.conf
Server block template
Replace paths and fastcgi_pass as needed.
server {
listen 80;
server_name example.localhost;
root /absolute/path/to/docroot;
index index.php;
client_max_body_size 64M;
location / {
try_files $uri /index.php?$query_string;
}
# Required for Drupal image styles + stage_file_proxy.
# Do NOT use try_files that only serves existing files and returns 404
# without hitting PHP — missing files must reach Drupal.
location ~ ^/sites/.*/files/ {
try_files $uri /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# Prefer socket if present, else TCP:
# fastcgi_pass unix:/opt/homebrew/var/run/php-fpm.sock;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
location ~ /\. {
deny all;
}
}
Apply:
nginx -t
sudo nginx -s reload
DNS / hosts
Modern macOS resolves *.localhost to 127.0.0.1. If not:
sudo sh -c 'echo "127.0.0.1 example.localhost" >> /etc/hosts'
Drupal settings
sites/default/settings.local.php (or include from settings.php):
$settings['trusted_host_patterns'] = [
'^example\.localhost$',
];
// Optional: pull files from production (no trailing slash)
$config['stage_file_proxy.settings']['origin'] = 'https://www.example.com';
DB: point $databases at local MySQL, or use whatever local DB you run.
Checklist after config
rootpoints at Drupal web root (index.phpvisible there)- Module
stage_file_proxyenabled only on non-prod; origin = real prod HTTPS host (not a redirect-only domain) curl -I http://example.localhost→ 200/302 from Drupal, not nginx 403/404- Port conflict:
lsof -i :80— stop Apache/DDEV/other nginx first
Common failures
| Symptom | Cause |
|---|---|
| 502 Bad Gateway | PHP-FPM not running or wrong fastcgi_pass |
404 on /sites/default/files/... |
nginx serves files dir without fallback to index.php |
| Blank / permission errors | Wrong root path or PHP user can’t read files |
| Trusted host error | Missing trusted_host_patterns for example.localhost |
| stage_file_proxy no images | Wrong origin, or files location bypasses Drupal |