How to Set Up Nginx for a PHP Application

How to Set Up Nginx for a PHP Application

How to Set Up Nginx for a PHP Application

PHP and Nginx is one of the most common pairings in web hosting, but it works differently than people coming from Apache often expect. Apache traditionally embeds PHP directly via mod_php; Nginx doesn’t run PHP itself at all — it hands .php requests off to a separate process, PHP-FPM (FastCGI Process Manager), over a socket or TCP connection, and PHP-FPM does the actual execution. Understanding that split is the key to getting this configuration right, debugging it when something breaks, and tuning it properly under load.

This guide walks through the complete setup for a typical PHP application (works for a plain PHP app, and covers the specific tweaks needed for common frameworks like Laravel and WordPress), including PHP-FPM configuration, testing, security hardening, and performance tuning.

How Nginx + PHP-FPM Fit Together

The request flow looks like this:

  1. A browser requests https://example.com/index.php.
  2. Nginx receives the request. For static files (images, CSS, JS), Nginx serves them directly — no PHP involved.
  3. For .php requests, Nginx passes the request to PHP-FPM via the fastcgi_pass directive, using either a Unix socket or a TCP address.
  4. PHP-FPM executes the PHP code, generates a response, and hands it back to Nginx.
  5. Nginx sends that response to the browser.

This separation is actually a strength — Nginx stays lightweight and handles static content and connection management extremely efficiently, while PHP-FPM can be tuned, scaled, and even run on a separate server entirely if needed, independent of the web server itself.

Requirements

Step 1: Install Nginx and PHP-FPM

sudo apt update
sudo apt install nginx php8.3-fpm php8.3-cli php8.3-mysql php8.3-xml php8.3-mbstring php8.3-curl php8.3-zip -y

Adjust the PHP version and extension list to match your application’s requirements (a Laravel app typically needs mbstring, xml, curl, zip, and a database driver; WordPress needs mysqli support and often gd or imagick for image handling).

Verify PHP-FPM is running:

sudo systemctl status php8.3-fpm

Find its socket path (this varies by distro/version):

ls /run/php/

You should see something like php8.3-fpm.sock.

Step 2: Set Up the Application Directory

sudo mkdir -p /var/www/example.com
sudo chown -R www-data:www-data /var/www/example.com

Deploy your application code here (via git clone, scp, CI/CD pipeline, etc.). For this guide we’ll assume a standard structure with index.php at the root, or in a public/ subdirectory for frameworks like Laravel.

Step 3: Configure Nginx for a Basic PHP App

Create /etc/nginx/sites-available/example.com:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/example.com;
    index index.php index.html;

    access_log /var/log/nginx/example.com_access.log;
    error_log  /var/log/nginx/example.com_error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff2?)$ {
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
        access_log off;
    }
}

Key parts explained:

Enable the site:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Step 4: Framework-Specific Adjustments

Laravel (or any framework using a public/ document root):

root /var/www/example.com/public;

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

The only real difference from the generic config is pointing root at the public/ subdirectory rather than the project root — this keeps the rest of your application code (config files, .env, vendor directory) outside the web-servable path entirely, which is both correct framework structure and a meaningful security boundary.

WordPress:

location / {
    try_files $uri $uri/ /index.php?$args;
}

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

location ~* /wp-config.php {
    deny all;
}

location ~* wp-content/uploads/.*\.php$ {
    deny all;
}

Note the $args instead of $query_string (WordPress’s own rewrite conventions expect this), and the explicit block on PHP execution inside the uploads directory — a very common target for malware uploads exploiting vulnerable plugins, and worth blocking outright since legitimate uploads never need to execute as PHP.

Step 5: Tune PHP-FPM

Edit the pool config, typically at /etc/php/8.3/fpm/pool.d/www.conf:

pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500

Restart PHP-FPM after changes:

sudo systemctl restart php8.3-fpm

Step 6: Test the Setup

Create a quick test file:

echo "<?php phpinfo(); ?>" | sudo tee /var/www/example.com/info.php

Visit http://example.com/info.php in a browser — you should see the full PHP configuration page. Delete this file immediately after testing (sudo rm /var/www/example.com/info.php) since it exposes detailed server configuration information publicly.

Test your actual application routes, particularly ones using pretty URLs/routing, to confirm try_files is working correctly.

Troubleshooting Common Issues

502 Bad Gateway. PHP-FPM isn’t running, or the socket path in your Nginx config doesn’t match the actual socket PHP-FPM is listening on. Check sudo systemctl status php8.3-fpm and confirm the socket path with ls /run/php/.

PHP files download instead of executing. The location ~ \.php$ block is missing, misconfigured, or being overridden by another location block matching first. Check block ordering — Nginx location matching has specific precedence rules (exact match > longest prefix > regex in order).

“File not found” errors despite the file existing. Usually a SCRIPT_FILENAME mismatch — confirm root in your server block is correct and fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; is present exactly as shown.

White screen with no error. PHP error display is likely off (correct for production) but nothing’s logging either. Check PHP-FPM’s own error log:

sudo tail -f /var/log/php8.3-fpm.log

And confirm display_errors = Off and log_errors = On in php.ini for production, with error_log pointed somewhere you’re actually monitoring.

Pretty URLs/routing 404. try_files directive is missing or incorrect — this is the single most common PHP+Nginx misconfiguration for anyone coming from an Apache/.htaccess background, since Nginx doesn’t read .htaccess files at all.

Security Considerations

Performance Tips

opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0

(validate_timestamps=0 means OPcache won’t check for file changes — great for production performance, but you’ll need to reload PHP-FPM after deploys since it won’t pick up code changes automatically.)

fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=PHPCACHE:100m inactive=60m;

location ~ \.php$ {
    fastcgi_cache PHPCACHE;
    fastcgi_cache_valid 200 30m;
    add_header X-FastCGI-Cache $upstream_cache_status;
    ...
}

Real-World Use Cases

Best Practices

Wrapping Up

Nginx and PHP-FPM is a proven, high-performance combination once you understand the fundamental split between “Nginx serves and routes” and “PHP-FPM executes.” Most of the friction people hit — 502s, 404s on pretty URLs, PHP files downloading instead of running — traces back to a handful of specific misconfigurations covered here: socket path mismatches, missing try_files, or an absent PHP location block. Get the base config right, tune pm.max_children to your actual server’s memory, turn on OPcache, and this setup will comfortably handle everything from a small personal site to a high-traffic production application.

Exit mobile version