Why Drupal with Nginx and PHP-FPM needs deliberate setup
A team choosing Nginx for a new Drupal build, or inheriting an Nginx server that someone else configured, needs to know one thing first: Drupal runs on Nginx, but the protections that Apache sites get from .htaccess files do not carry over automatically. Drupal.org's system requirements list Apache, Nginx and other web servers as supported, with MariaDB, MySQL, PostgreSQL or SQLite as the database. Support for the server does not mean the server is configured safely. On Apache, the bundled .htaccess file blocks access to sensitive paths, controls rewrites and sets some directory behavior. On Nginx, equivalent rules must be written by hand in the server block, so a missing rule usually means an exposed path rather than an error message.
The PHP layer is set by release. According to the Drupal.org PHP requirements page, Drupal 10 requires PHP 8.1 or later, Drupal 11 requires PHP 8.3 or later and Drupal 12 requires PHP 8.5 or later, and no supported Drupal version runs on PHP 7. Before choosing PHP-FPM settings, confirm which Drupal release the site will run and the PHP version your Linux distribution or container image provides. A pool sized and tuned for one PHP version can behave differently after an upgrade, so the PHP version, the Drupal release and the PHP-FPM pool configuration should be decided together and recorded in the same change.
Teams need this topic in three situations: new builds where no .htaccess baseline exists yet, Apache-to-Nginx moves where existing rules must be translated and checked, and container platforms where the web server, PHP runtime and file paths are packaged separately. In each case, confirm with your own security and operations staff which paths must be blocked, where private files will live, which headers must reach the browser through any load balancer or CDN, and who owns the Nginx configuration after launch. These decisions shape the work that follows.
Which Nginx and PHP-FPM configuration fits which situation
This table compares the server layout, socket type, process manager and private file handling decisions that shape an Nginx and PHP-FPM setup for Drupal. Read each row against your site count, traffic pattern and security requirements, then use the trade-off column to see what you accept with each choice.
| Decision | Option | Trade-off | When to choose it |
|---|---|---|---|
| Server layout | Single-site server block | Simple to test and review, but each new site means another block and certificate to maintain | One public site with a stable hostname and no plans for sibling sites |
| Server layout | Multisite or wildcard host block | One configuration covers many hostnames, but a mistake in the shared rules affects every site | Many sites from one codebase with core multisite and consistent rules across them |
| PHP-FPM socket | Unix socket | Lower overhead and no network exposure, but Nginx and PHP-FPM must run on the same host or share the socket path | Nginx and PHP-FPM run on one server or in a pod with a shared volume |
| PHP-FPM socket | TCP connection | Works across hosts and containers, but the port must be restricted to trusted sources | PHP-FPM runs on a separate host, container group or managed compute service |
| Process management | Static pool | Predictable memory use and fast response, but idle workers still consume memory | Steady traffic on a server with memory you have measured and reserved for PHP |
| Process management | Dynamic pool | Scales worker count with load, but needs headroom so spikes do not exhaust memory | Traffic that rises and falls through the day on a shared host |
| Process management | On-demand pool | Frees memory when idle, but the first request after an idle period waits for a worker to start | Low-traffic or internal sites where memory matters more than first-request latency |
| Private files | Served by PHP through access checks | Permissions follow Drupal's file access rules, but every download uses a PHP worker | Files need per-user access checks and volumes are modest |
| Private files | Dedicated internal location | Keeps downloads off PHP workers, but the access logic must be duplicated or handed off, so confirm it with your security team | Large downloads or high file traffic, with a documented and tested access pattern |
How to set up Drupal behind Nginx and PHP-FPM
This sequence takes a Drupal codebase from a PHP version check to a verified Nginx and PHP-FPM stack, with each protection that Apache would normally get from .htaccess rebuilt in the server block and confirmed by a test.
Confirm the PHP version for the target release
Before any server work, the team checks which Drupal release the site will run and the PHP version that release requires. Drupal 10 requires PHP 8.1 or later, Drupal 11 requires PHP 8.3 or later and Drupal 12 requires PHP 8.5 or later, and no supported Drupal version runs on PHP 7. The platform engineer records the chosen PHP minor version, confirms the operating system packages provide it, and checks the Drupal PHP requirements page again before the build is frozen. The step is done when the target version is written into the build specification and a CLI check on a test host reports it.
Install and tune PHP-FPM pools
The platform engineer installs the PHP-FPM service for the chosen version and creates a dedicated pool for the Drupal site, running under its own system user so that the web server and PHP processes do not share write access beyond what Drupal needs. Pool settings for the process manager, maximum children, request termination and PHP memory limit are set from the expected load profile, not from defaults. Sizing inputs should come from the capacity plan. The step closes when the pool socket or port is listening, the service starts cleanly on reboot, and a test PHP file reports the expected version and pool user.
Write the server block and front controller routing
The engineer writes an Nginx server block that sends every request that is not a real file or directory to Drupal's index.php, which is the front controller pattern. Clean URLs depend on this try_files rule, and the block passes PHP requests to the FPM socket with the correct SCRIPT_FILENAME. The document root points at the web directory of the Composer build, never at the project root. The step is complete when the home page loads, a node path with a clean URL resolves, and a request for a missing path returns Drupal's own 404 page rather than a server error.
Deny sensitive paths and file types
Apache sites rely on .htaccess to block directories and file types that must never be served, such as vendor, config sync files, scripts and backup archives. In Nginx these rules must be written explicitly in the server block. The team denies access to hidden files, the vendor and config directories, source and archive extensions, and any PHP file outside the approved front controller. The engineer then runs requests against each protected path and confirms that the response is 403 or 404, not the file content. Any path that returns a 200 blocks the release until fixed.
Configure private file handling
Private files must be served only through Drupal's access checks. The engineer configures the private file path outside the public web root and adds a server rule so that requests to the private files location are denied directly, leaving Drupal to stream permitted downloads. The public files directory stays readable, and the team confirms that the file system settings in the site configuration match these paths. Verification includes an anonymous request to a private file that must return 403, and a logged-in request for a file the account is allowed to see that must return the file with the expected content type.
Pass required headers through
Headers set by the application, such as cache directives, content type, and security headers configured in Drupal or a reverse proxy, must reach the browser unchanged. The engineer checks for fastcgi_pass_header or hidden header directives that strip values, sets the forwarded protocol and host headers that Drupal needs to build correct URLs behind a proxy, and confirms the trusted host configuration matches the public hostname. The step is done when a response inspection shows the expected headers on HTML, CSS and private file responses, and when generated links use the correct scheme.
Verify each layer and record the results
The team runs one repeatable test pass after the configuration is complete and keeps the results with the build record. The pass covers the PHP version reported to the site, the pool user and socket, clean URL resolution, the denied-path matrix, the private file tests and the header inspection. It also confirms that Nginx error logs and PHP-FPM slow logs are written where the operations team expects them. The step closes when every check passes or has a documented exception, and the same checks are scheduled for each configuration change and each Drupal upgrade.
Recreating what .htaccess does under Apache
Drupal relies on a front controller pattern: requests for paths that are not real files are passed to index.php, which then resolves the URL alias or route. Under Apache, the rewrite rules shipped in .htaccess do this. Nginx has no equivalent file, so the same behavior must be written as a try_files directive in the server block, and the PHP handler must receive the script name and path info correctly.
- Front controller fallback: Use
try_files $uri /index.php?$query_string;so existing static files are served directly and everything else reaches Drupal. - Query string preservation: Confirm that
$query_stringis passed through, since Drupal uses query parameters for AJAX, views exposed filters and image styles. - Redirect of the front page path: Drupal's rules redirect requests to
index.phppaths; replicate this only if the shipped rules in your Drupal version still contain it. - Clean URL and alias handling: Test node aliases, paths with trailing slashes and multilingual prefixes after the change, not just the home page.
- Installer and update paths: Decide who can reach
/core/install.phpand/update.php, and restrict them rather than leaving them open.
Confirm the exact rewrite behavior against the Drupal version you run, because the shipped rules change between releases.
Apache's .htaccess in Drupal denies direct access to directories and file types that should never be served, such as configuration, vendor code, backup files and sensitive text files. Nginx needs explicit location blocks for each of these. A missing block does not produce an error; the file is simply served to anyone who requests it.
- Protected directories: Deny access to paths such as
sites/*/private,coreinternals that are not meant to be web-accessible, and any Composervendordirectory placed under the web root. - Sensitive file types: Block requests for
.txt,.md,.yml,.twig,.sh,.log,.sqland similar files that may exist in the codebase or be created by tools. - Hidden files: Deny dotfiles such as
.git,.envand editor backups, using a pattern that still allows the ACME challenge path if certificates are issued on the same server. - Version control and backups: Ensure
.gitdirectories and archive files are excluded from the deployed web root as well as blocked in configuration. - Review on upgrade: Compare the blocked patterns against the shipped
.htaccessafter each Drupal upgrade, because new directories or file types may need the same treatment.
Run a scripted request test against a list of these paths from outside the network and expect a denial for each one.
Drupal's private file system is served through PHP, so access checks run for each download. Apache's .htaccess keeps the private directory from being served as static files. In Nginx, the private directory must be unreachable by URL, and Drupal must be the only route to its contents. Public and private file handling should be designed together, because the two behave differently.
- Private path outside the web root: Where possible, place the private file directory outside the document root so no web server rule is needed to hide it.
- Denied location when inside the root: If the private directory is under the web root, add a
locationblock that returns a 403 or 404 for its path before any static file rule matches. - Image style and derivative paths: Check that generated image derivatives are served as expected and that private images still pass through Drupal's access checks.
- Streaming and large files: Confirm timeouts,
client_max_body_sizefor uploads andX-SendfileorX-Accel-Redirectsettings if your site uses them for large downloads. - Object storage variants: If files are moved to object storage, the Nginx rules for private files change completely; confirm that design before copying rules across.
Test a direct URL to a private file as an anonymous user and as an authenticated user without permission. Both should fail.
Behind a load balancer or reverse proxy, Drupal sees the proxy's address and the internal protocol unless headers are passed through and trusted correctly. Apache deployments often handle this in virtual host configuration or modules, while Nginx requires explicit proxy_set_header lines and a trusted proxy list in Drupal's settings. Getting this wrong affects logs, IP-based controls, HTTPS detection and generated links.
- Client address: Set
X-Forwarded-ForandX-Real-IPin Nginx, and list only your load balancer addresses in Drupal'sreverse_proxysettings so clients cannot spoof their IP. - HTTPS detection: Pass
X-Forwarded-Protoand make Drupal'sreverse_proxy_protohandling match, so that generated links and secure cookies usehttps. - Host header: Pass the original host and enforce it with
trusted_host_patternsinsettings.php, which the.htaccessfile does not replace. - Security response headers: Set headers such as
X-Content-Type-Options,Referrer-PolicyandContent-Security-Policyin Nginx withadd_header ... always, and confirm they appear on PHP responses and static files. - Upgrade review: After each Drupal upgrade, compare the header behavior and any new headers Drupal expects with the shipped rules and the Drupal.org documentation.
Check the result with a request through the full path and inspect the headers Drupal receives, the links it generates and the log entries it writes.
Security settings that must survive the move from Apache
Apache sites often inherit protections from the .htaccess files that ship with Drupal, and Nginx ignores those files entirely. Each rule below must be rebuilt in the server configuration, then re-verified after every security release.
Blocked configuration and vendor paths
Nginx does not read .htaccess, so requests for /core, /vendor, /sites/*/files/config and Composer files such as composer.json can reach the application unless location blocks deny them. Deny these paths explicitly and return a 404 rather than a redirect. Test each path with curl after every deployment, because a new release or a new contributed module can add files that were never on the list before.
No PHP execution in upload directories
Anything a visitor uploads into sites/default/files must never run as PHP. Add a location block that refuses to pass .php requests under the files path to PHP-FPM, and confirm that a test file uploaded through a Drupal form returns as plain content. This control matters most on sites that accept anonymous or low-trust uploads, such as comment attachments or webform file fields.
Private files kept off the web server
Private files should be served only through Drupal's access checks, so the Nginx root must not map the private file directory to a public location. Confirm that a direct request for a known private file path returns a denial, and that the request is served only when Drupal's file access logic allows it. Ask whether the private path sits outside the web root, since that choice removes the risk entirely.
Trusted host names set per environment
Drupal's trusted_host_patterns setting rejects requests whose Host header does not match a listed name, which blocks host header attacks against password reset links. Nginx should also reject unknown server names with a default server block that returns no content. Set the list in each environment's settings file, and check it after each domain change or staging copy, since a copied production setting can leave staging open to the wrong names.
Security headers passed through the proxy
Headers that Apache sites often set in .htaccess or the virtual host, such as X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Content-Security-Policy, must be added in Nginx with add_header. Be careful that add_header inside a nested location replaces the parent set, so headers can silently disappear on some paths. Verify headers on pages, assets and files, and confirm them again when a reverse proxy or CDN sits in front.
Post-release verification after each advisory
Security releases ship on the third Wednesday of the month, and the Drupal Security Team publishes advisories with CVE identifiers and risk scores for core and contributed projects. Each release should trigger a rerun of the path, upload, host and header checks, plus a review of the advisory to confirm whether a server rule change is needed. Ask your partner to document this as a standing runbook, since a security release can be applied to Drupal while the Nginx rules remain unchanged.
Questions teams ask about Nginx and PHP-FPM for Drupal
What drives the cost of moving a Drupal site from Apache to Nginx?
The main cost driver is how much Apache-specific behavior the site depends on, not the Nginx install itself. Apache sites usually rely on .htaccess rules for clean URLs, blocked paths, private file handling and response headers, and each of those must be rebuilt as Nginx server blocks and tested. Cost rises with the number of sites, the number of custom rewrite or header rules, the number of environments, and how much load testing and staging validation the client requires. Ask any partner to price the work from a written inventory of your current .htaccess files and server configuration, and ask whether the quote covers staging validation, rollback and handover documentation. Most of the effort is one-time, while ongoing configuration updates recur with each Drupal and PHP change.
How long does an Nginx migration take for Drupal?
Duration depends on the number of sites, the number of custom rules and how many environments must be validated before cutover. A single site with standard Drupal rules is a different job from a multisite estate with custom redirects, private file paths and API routes. The fastest way to get a reliable estimate is to inventory every rule in your current configuration, identify which ones are standard Drupal behavior and which are custom, then agree a test plan with your team. No duration is given here because it should come from your own inventory and the written scope, not from a generic figure. Ask for the timeline to be split into inventory, build, staging test, cutover and post-cutover monitoring so you can see where time is spent.
What does the work need from our team?
The work needs three things from your side: access to a staging environment that matches production closely enough to test real requests, traffic data from your web server or CDN logs so the PHP-FPM pool and cache behavior can be checked against actual use, and named owners for the private file policy. The private file owner decides which paths must never be served directly, such as private downloads, generated reports or user uploads that require a permission check. Your team also needs to confirm the current .htaccess files, any custom rewrite rules and the list of sites served from each codebase. Without these inputs, the rebuild is based on assumptions and the cutover test is weaker.
Can our in-house team move Drupal to Nginx without a partner?
Yes, a capable in-house team can do this, especially for a single site with standard Drupal rules and a team that already runs Nginx and PHP-FPM. The questions to answer honestly are whether someone on the team has tested Drupal's routing, private file and header behavior under Nginx, whether there is a staging environment that can be broken without affecting users, and whether the team can own the configuration after launch. Multisite estates, custom rewrite logic and strict compliance evidence usually take more coordination and documentation. A fixed-scope engagement can cover the inventory, rebuild and test, while your team keeps ownership of the running system.
Do security releases change our Nginx configuration burden?
Security releases in the Drupal core and contributed modules generally change PHP code, not the Nginx configuration, but they still need a deployment plan that tests the full stack. Drupal publishes security releases on the third Wednesday of the month, and bugfix releases on the first Wednesday, so your team needs a repeatable routine to pull, test and deploy on a short notice. The Nginx side matters when a security advisory or a release changes the paths, headers or file handling that your server blocks rely on. Ask for the configuration to be kept in version control, with a regression test that checks blocked paths, clean URLs and headers after each deployment.
Do Drupal 7 sites need a different path than an Nginx change?
Yes. Drupal 7 sites cannot simply be reconfigured or updated to a supported version. Drupal.org documentation explains that Drupal 6 and 7 sites are migrated into a new Drupal site with the core Migrate, Migrate Drupal and Migrate Drupal UI modules or with Drush, and themes must be rebuilt because they cannot be migrated. Drupal 7 security support ended on January 5, 2025, so the web server move alone does not restore supported status. If a site cannot migrate yet, the Drupal Association's Extended Security Support Provider Program lists certified vendors that provide security coverage in the interim. Plan the Nginx configuration for the new Drupal site as part of that migration, not as a change to the old one.
How do we check that Nginx protections match what .htaccess provided?
Start with an inventory of every rule in your current .htaccess files, then write a test list that checks each rule's effect against Nginx. The test list should include blocked directories, direct requests to private files, clean URL routing, front controller behavior, response headers and any redirects. Run the same requests against the old and new configurations in staging and compare status codes, response headers and file access. Keep the test list in version control so it runs again after each deployment. Ask your partner or internal team to show the results, including failed or unexpected responses, before cutover.
What changes for your team after launch
After launch, the web server configuration stops being a one-time setup and becomes code your team owns. The Nginx server block, the fastcgi_pass target, the try_files rules that replace the .htaccess behavior, and the private file location all belong in version control with the same review and rollback path as Drupal code. Every change to a location block should go through a pull request and a staging check, because a single edited rule can break clean URLs, expose the private files path or drop a security header without any Drupal error appearing.
PHP-FPM tuning is also ongoing work rather than a setting you choose once. Pool sizes, pm.max_children, process manager mode and memory limits should be adjusted from observed data: slow request logs, pool status output, memory per worker and queue wait times under real traffic. Drupal updates can add or change rules as well. A core or contributed module release may require a new route, a changed file path or an extra header, so each update should include a diff of the Nginx configuration reviewed against the release notes. Confirm the current supported PHP versions for your Drupal release on the PHP requirements page before you plan a runtime upgrade.
The on-call person needs a simple map of where errors originate. Nginx access and error logs, with HTTP 502 and 504 responses, usually point to the web tier or to PHP-FPM not accepting connections. PHP fatal errors and Drupal watchdog entries point to application code or settings. Database connection failures and slow queries point to MariaDB, MySQL or PostgreSQL. Write this map into a runbook, name who owns each layer, and rehearse one failure of each type in staging. Your partner or internal team should confirm which logs are retained, where they are shipped and who can read them.
How DeployOpen scopes and delivers Drupal web tier work
DeployOpen plans, builds, documents and supports the web tier for Drupal inside your own infrastructure, from the Nginx server block and PHP-FPM pools to the header and file path rules your site depends on. Every engagement starts with a written scope so you know what changes, what gets tested and who maintains it afterward.
How we scope
- We inventory your current Apache rules, rewrite logic and access controls before proposing any Nginx configuration.
- The written scope lists each protection, header and private path to be recreated, with the test that proves it.
- Engagements are fixed-scope, with deliverables and timelines agreed in writing before work starts and a quote following from that scope.
How we build
- We write the Nginx server block, PHP-FPM pool configuration and clean URL routing in version-controlled files.
- Pool sizing is set from measured memory and worker behavior in your environment, with load testing planned before launch.
- We test denied paths, private file access, trusted host handling and pass-through headers against your Drupal version and deployment.
How we hand over
- You receive the configuration files, a route-by-route map of what replaces each Apache rule, and the test scripts used.
- Documentation covers how to reload PHP-FPM and Nginx safely, and how to change pool limits as traffic grows.
- We walk your operations team through the runbook for security releases, configuration changes and rollback steps.
How we support
- Ongoing support, upgrades and continuous testing are available as retainers, covering web tier changes after launch.
- Retained work can include re-running the protection tests after Drupal core and PHP version updates.
- Confirm with us during scoping which team owns Nginx and PHP-FPM changes after handover and how requests are logged.

