name: nginx-php-behind-npm description: > Bind a PHP website served by the host nginx behind Nginx Proxy Manager when the NPM container already occupies port 80/. Covers the exact conflict pattern: default site → nginx can't start → proxy never reaches PHP-FPM, and the known good resolution order.
Nginx + PHP behind Nginx Proxy Manager
Use this when the host nginx and the NPM container both want port 80/443 and your PHP site must be reached through a domain managed by NPM.
Trigger
- NPM proxy exists but upstream returns 502 Bad Gateway
nginx -tok, butssshows only docker-proxy on 80nginxfails to start withbind() to 0.0.0.0:80 failed
Required belief before proceeding
Nginx on the host and NPM cannot both bind 80. The host nginx must use a non-overlapping port (e.g. 8087+). After this, NPM forwards traffic to the host nginx, which talks to php-fpm.
Fixed order
- Delete the default site when it collides with NPM:
rm -f /etc/nginx/sites-enabled/default
nginx -t && nginx -c /etc/nginx/nginx.conf -g "daemon on;" || true
ss -tlnp | grep ':8086\|:8087\|:80\|nginx'
Ubuntu 22.04 php-fpm8.1 socket path is
/run/php/php8.1-fpm.sock. Do not guess the path.Server block for the site listens on a free high port:
server {
listen 8087 default_server;
server_name _;
root /path/to/site;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Restart host nginx. Then
curl -I http://127.0.0.1:8087must return200 OK.Update the NPM proxy host to
forward_scheme=http,forward_host=154.41.135.101,forward_port=<chosen port>,ssl_forced=false,http2_support=false.Verify from outside:
curl -I http://<domain>and expect200 OK.
Pitfalls
- Do not use
nginx -s reloadwhennginx -tpassed butsslsites are present. Restart is the calmer choice. - If
nginxfails to start,ss -tlnpand/var/log/nginx/error.logis the unlock pattern. - If PHP returns 502, check
ls /run/php/*.sockfirst. - Site directory under
/root/is invisible towww-databy default: runchmod o+rx /rootandchmod -R o+rx /path/to/sitebefore testing. - PHP-FPM socket path varies by distro/version:
discover with
find /run /var/run -name 'php*-fpm*.sock'instead of hardcoding. - NPM API is schema-strict: PUT payload must match the exact
GET /api/nginx/proxy-hosts/<id>keys. Extra/missing fields yield400 {code:400, message:'data must NOT have additional properties'}. - NPM proxy
ssl_forced=truewith an existingcertificate_idis valid even when the upstream is HTTP. Accept the current certificate state instead of forcingssl_forced=false. - If
nginx -s stoporreloadcomplains about/run/nginx.pidmissing, use:nginx -c /etc/nginx/nginx.conf -g "daemon on;"to restart cleanly instead ofreload.
Pretty URL rewrites after binding
Once the site is live, internal pretty URLs like /product/<slug> and /news/<slug> still 404 if Nginx doesn't rewrite them to the PHP router. Add these blocks before the generic location ~ \.php$ block:
location ~ ^/product/(.+)$ {
rewrite ^/product/(.+)$ /product.php?slug=$1 last;
}
location ~ ^/news/(.+)\.html$ {
rewrite ^/news/(.+)\.html$ /news_detail.php?slug=$1 last;
}
location ~ ^/news/(.+)$ {
rewrite ^/news/(.+)$ /news_detail.php?slug=$1 last;
}
product.php already strips a leading /product/, and news_detail.php strips /news/ and .html, so the router itself usually does not need code changes — the rewrite alone is enough.
Link verification expectations
fonts.googleapis.comandfonts.gstatic.comcommonly 404/block from mainland hosts; this is environmental, not a site bug — rely on the localfilesystem-servedbundle-*.cssfallbacks instead.mailto:andtel:links fail HTTP probes with "No connection adapters"; they are fine in a real browser.- JS template tokens like
/product/${product.slug}appear as literal strings in raw HTML crawls but render correctly client-side after JS interpolation.