Running multiple runtimes behind a single web server saves hardware costs and simplifies infrastructure. You might have a traditional PHP application handling standard web requests, alongside a Node.js process managing real-time data feeds. It's an efficient way to structure a deployment. Nginx handles this split through upstream blocks and location matching. If you misconfigure the proxy headers, your Node application will log the server loopback IP address instead of real client connections. That's a debugging nightmare when you're trying to trace client requests in production. If you miss the WebSocket upgrade flags, your socket connections drop instantly. You won't get a helpful error message either. The browser console will just show a failed handshake and keep trying.
Setting this architecture up requires careful planning of your directory structure and port assignments. You don't want your Node process exposed directly to the public internet on port 3000. It needs to hide behind Nginx so that SSL termination and rate limiting happen at the edge. Let's look at how to structure the configuration file so it doesn't turn into an unmaintainable mess of duplicate rules.
Configuring Upstreams and PHP-FPM
You need to define separate routing paths for your PHP files and your Node.js application server. PHP relies on a FastCGI process manager, while Node listens on a local network port. Start by declaring an upstream block for your Node process and a server block that handles both runtimes cleanly. You'll specify keepalive connections in the upstream block to reduce latency on repeated requests.
upstream node_app {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name example.com;
root /var/www/html/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location /api/ {
proxy_pass http://node_app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
}
}The configuration above delegates requests matching the /api/ prefix to the Node process. Everything else falls through to the standard PHP router or static file matcher. Always verify your syntax changes with a test command before applying them, as detailed in Zero-Downtime Nginx Reloads and Config Testing to prevent unexpected downtime. If you skip the syntax check, a missing semicolon in your proxy block will take down your entire web server on the next reload.
You'll notice the PHP block uses a Unix socket rather than a TCP port. That's intentional. Unix sockets avoid the TCP overhead and give you better performance when PHP-FPM and Nginx live on the identical machine. Make sure your file permissions on the socket match your user configuration, or you'll see permission denied errors in your error log.
Handling WebSockets and Upgrade Headers
Node applications frequently use WebSockets for live features. Standard HTTP proxy settings will break these connections because Nginx closes the channel after the initial response unless explicitly told to keep it open. You must explicitly pass the Upgrade and Connection headers from the client request to the upstream target. Without them, the protocol switch handshake fails immediately.
Create a dedicated location block for your WebSocket endpoint that overrides the default proxy buffering behavior. Buffering delays real-time frame delivery and causes erratic timeout drops. You don't want your real-time chat or telemetry data sitting in an Nginx buffer when it should be pushing directly to the client.
location /socket.io/ {
proxy_pass http://node_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}Timeout values matter immensely for persistent connections. If you leave the timeout at the default sixty seconds, idle WebSocket clients will get disconnected constantly, forcing their client-side scripts to reconnect in a tight loop. Setting the read and send timeouts to a full day keeps long-lived connections stable. You'll also want to monitor your file descriptors if you have thousands of concurrent users, because each open WebSocket consumes a socket connection on the host.
Proxy buffering is another subtle trap. Leaving proxy_buffering enabled on an event stream or WebSocket proxy will cause data chunks to pool in memory until the buffer fills up. Your users will experience strange multi-second delays before seeing updates. Turning it off for real-time endpoints fixes the lag instantly.
Managing Process Reliability
Mixing runtimes on a single host means you also need to manage how those processes start and fail. PHP-FPM runs as a system service managed by your package manager, but your custom Node script needs a reliable supervisor to restart it after a crash or server reboot. You can build a system service for your Node process, following principles similar to those outlined in Systemd Services for Long-Running Processes to ensure it starts automatically when the machine boots.
Managing log files across both ecosystems requires distinct rotation policies so your disk does not fill up with uncompressed access logs, a topic covered thoroughly in Mastering Logrotate Configuration. Node applications often log aggressively to standard output, and capturing that output into files requires a proper rotation setup to prevent disk space exhaustion.
Check your error logs frequently when setting up mixed proxy environments. Mismatched socket paths or incorrect FastCGI parameters will throw immediate gateway errors. Keep your file permissions strict on the PHP-FPM socket file so the web user can read and write without exposing local escalation vectors to the Node process. You'll save yourself hours of debugging by keeping these security boundaries tight from the beginning.











