Skip to content

Running WordPress on nginx Without Apache

Most sites on the Noiz platform run two web servers: nginx at the front and Apache behind it. You can remove Apache from a single site’s request path, so nginx serves everything and talks to PHP-FPM directly. One web server instead of two.

That is a genuine performance tier, and it is how most specialist WordPress hosting is built. It also has one real cost, and this guide states it before anything else: nginx does not read .htaccess. This is the honest version of that trade, what replaces each rule you lose, and who should not take it.

Last reviewed: 4 August 2026, against the current Plesk Obsidian release. The configuration in this guide was verified on a live WordPress site running with no Apache in the request path. This guide is written for Noiz hosting and is kept current against Plesk, nginx and WordPress. It complements, and does not replace, the official documentation linked below.

  • A WordPress site on a Noiz plan whose control panel is Plesk.
  • The recommended web server configuration on Noiz hosting, if the two-web-server model is new to you. This guide assumes it.
  • A recent backup. This change is reversible in seconds, but the point of a backup is that you do not have to trust that sentence.
  • A list of what is in your site’s .htaccess. Everything in this guide follows from that file.

On the Apache & nginx Settings page for the domain, Proxy mode is cleared. One tick, and Apache leaves this site’s request path.

It is not the only setting that has to change, though, and the second one is not optional. Serve static files directly by nginx must be cleared at the same time, or the site’s custom nginx rules stop being applied and nothing tells you. That is covered in full two sections below, and it is worth reading before you touch either checkbox.

The nginx settings section of the Plesk Apache and nginx Settings page with the Proxy mode checkbox cleared, above Plesk’s caption reading Nginx proxies requests to Apache, turn off to stop using Apache

Plesk’s own description of the checkbox is one sentence: “Nginx proxies requests to Apache. Turn off to stop using Apache.”

After that, nginx receives the request, serves static files from disk itself, and passes PHP straight to PHP-FPM. Apache is still installed and still runs the panel’s own services, but it is no longer in the path for your site. Plesk supports this explicitly:

You can configure individual websites to be served only by nginx without stopping or disabling Apache, and it does not impact websites hosted using Apache in any way.

It is per-site, so a server can run a mix indefinitely. There is no flag day.

Plesk documents two other limitations: SSI, Perl and Python support become unavailable, and the PHP handler is restricted to the nginx-served options. See PHP handlers on Noiz hosting.

.htaccess becomes an inert text file. Not partly. Entirely.

Every rule in it stops applying: permalink rewrites, redirects, deny rules, expiry headers, hotlink protection, security-plugin blocks and cache-plugin page serving. WordPress’s own documentation is direct about the reason:

With Nginx there is no directory-level configuration file like Apache’s .htaccess or IIS’s web.config files. All configuration has to be done at the server level by an administrator, and WordPress cannot modify the configuration, like it can with Apache or IIS.

The part that catches people is that plugins do not notice. A caching or security plugin will carry on writing its block into .htaccess, report success, and have no effect whatsoever. Nothing errors.

The setting you must change at the same time

Section titled “The setting you must change at the same time”

This one is not obvious and it is the single most expensive mistake available here.

If Serve static files directly by nginx is switched on while Proxy mode is off, Plesk stops applying the site’s custom nginx directives. Plesk documents it in an article whose title is the whole warning: “Additional nginx directives are not applied when nginx proxy mode is disabled in Plesk”. The stated cause is that “the option ‘Serve static files directly by nginx’ interferes with additional nginx directives”, and the stated fix is to disable that option.

That matters because the custom directives box is where every rule you have just translated out of .htaccess would live. Leave the setting on, and your permalink rule, your deny rules and your security rules are all quietly absent. Plesk’s article records only that the directives are not applied, and nothing in the panel warns you.

So: turn Serve static files directly by nginx off when you turn Proxy mode off. It is redundant in this mode anyway. There is no Apache to bypass, so nginx serves your static files either way.

This is the payoff. WordPress on nginx is ordinary, and the whole permalink problem is one directive.

WordPress publishes this as its own single-site rule:

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

Read it left to right: is this a file on disk? Is it a directory? Neither? Then hand it to WordPress, with the query string intact.

Use the $is_args form instead. WordPress’s REST API documentation warns about a different variant, /index.php$args with no separator at all, and gives this as the replacement:

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

That is not a correction of the rule above it. Both forms carry the query string, and both fix permalinks. The one WordPress warns about is /index.php$args, missing the ? entirely, and its documented symptom is that query parameters such as ?page=2 or ?_embed are “not having any effect” on the REST API. Adding $is_args, “which will print a ? character if query arguments are found”, is what WordPress says “will allow WordPress to properly receive and interpret the query parameters”. Prefer $is_args$args anyway, for a smaller reason: $is_args is ? when the request has a query string and empty when it does not, so it never appends a bare ? to a request that had no parameters, and REQUEST_URI stays clean.

Verified on a live site with Apache out of the path: with that block and nothing else, the home page, permalinks, /wp-json/, feeds, sitemaps, wp-login.php and wp-admin all return 200.

You may not need to add it at all. Plesk generates a permalink fallback itself for sites it recognises as WordPress, and states that new WordPress installations have worked under nginx-served PHP since Plesk Obsidian 18.0.34. Test before you assume either way, with a real request rather than an admin screen: WordPress does not warn about missing rewrite rules when it detects nginx, so a site with broken permalinks looks perfectly healthy in Settings > Permalinks while every URL except the home page returns 404.

On Noiz shared plans the custom nginx directives field is managed by Noiz. Send the rules you need on a ticket and they will be applied and tested. On a VPS or dedicated server where you hold the panel’s administrator login, the field is on the same Apache & nginx Settings page.

Every common rule has a native equivalent, and several are better in nginx because they are compiled configuration rather than a file re-read on every request.

Your .htaccess does nginx does
WordPress permalink block try_files $uri $uri/ /index.php$is_args$args;
Redirect or RewriteRule ... [R=301] return 301 https://yourdomain.com/new-path;
deny from all, Require all denied location = /wp-config.php { deny all; }
ExpiresByType expires 30d;, scoped to a static-file location rather than the whole site. The Expires field on the same panel page does not apply here: it configures Apache’s mod_expires, and Apache is out of the path
Options -Indexes nothing to do, autoindex off is nginx’s default
Hotlink protection on HTTP_REFERER valid_referers none blocked server_names; with $invalid_referer
Password-protected directory auth_basic with the same .htpasswd file
ErrorDocument 404 /404.php error_page 404 /404.php;
Force HTTPS a server-level return 301, or Plesk’s own SEO-safe redirect setting
php_value / php_flag nothing to do, nginx never reads them. They were not harmless before the change either: with PHP-FPM and no mod_php loaded, an unguarded php_value line makes Apache return 500 for that directory. Per-site PHP values belong on the PHP Settings page

Five things that catch people out when translating:

  • Order decides which rule wins, and a wrong order still passes the configuration test. nginx checks regular-expression locations “in the order of their appearance in the configuration file” and “the search of regular expressions terminates on the first match”. A regular-expression deny such as location ~* /wp-config\.php$ { deny all; } must therefore appear before the block that handles .php files, or the PHP block matches first and the file is executed instead of denied. Ordering does not matter for the exact-match form in the table above, because an = match terminates the search immediately, which is why it is the safer one to write.
  • Be careful what you put ^~ on. Exact matches (location = /xmlrpc.php) and prefix matches (location ^~ /.well-known/) are more predictable than regular expressions and worth preferring. But nginx’s rule is that “if the longest matching prefix location has the ^~ modifier then regular expressions are not checked”, so a ^~ block that wins the prefix match switches off every regular-expression location for those URLs. Never put one on /wp-content/uploads/ on a WooCommerce store. It would suppress the woocommerce_uploads rule below and serve paid files to anyone who guesses the URL, and the configuration test would still pass.
  • return 301 sends the URL literally. Apache’s Redirect appends whatever path remains after the matched prefix and carries the query string through; nginx’s return does neither. Use return 301 https://yourdomain.com/new-path$is_args$args; to keep the query string, and $request_uri where the whole remaining path must be preserved. Translating a prefix redirect without this collapses every URL under it onto one page.
  • Query strings append by default. In Apache you add [QSA] to carry the query string across a rewrite. In nginx that is the default behaviour, so it is the absence of [QSA] that needs work, not its presence. Translating this backwards produces duplicated parameters on faceted pages.
  • Hotlink protection is not a security control on either server. nginx says so itself: fabricating a Referer header “is quite easy”, and the module exists “to block the mass flow of requests sent by regular browsers”, not to block them thoroughly. Regular browsers also legitimately omit the header.
  • Expiry headers behave slightly differently. Apache’s mod_expires stands down when the application already set the header. nginx’s expires does not: it is documented as “adding or modifying” those fields. Scope it to a static-file location rather than applying it site-wide, or a cart page can end up advertising itself as cacheable.

WooCommerce: do this before you sell a downloadable product

Section titled “WooCommerce: do this before you sell a downloadable product”

WooCommerce protects paid downloads by writing an .htaccess file containing deny from all into wp-content/uploads/woocommerce_uploads/. Under the Force downloads and X-Accel-Redirect methods, that file is the only thing stopping a direct request for a paid file. On nginx it does nothing.

WooCommerce documents the requirement itself:

If you use an NGINX server with the X-Accel-Redirect/X-Sendfile or Force downloads method, you need to modify your server configuration for proper file protection.

The replacement is one directive:

location ~* /wp-content/uploads/woocommerce_uploads/ {
internal;
}

internal means the location “can only be used for internal requests. For external requests, the client error 404 (Not Found) is returned”, while still permitting the internal redirect WooCommerce uses to deliver a legitimate purchase. Do not apply it to a store using the Redirect only download method, which needs those files publicly reachable.

Once it is in place, X-Accel-Redirect becomes available, which is a better delivery method than Force downloads: the file is handed to nginx to stream instead of being pushed through a PHP process that stays occupied for the whole download.

Say no to this tier if any of the following is true. These are disqualifications, not things to work around.

  • A security plugin manages your .htaccess. Its protections stop applying and its admin screen will not tell you. Some plugins do detect nginx and hide the options, which is better, but the protection is still gone.
  • A caching plugin serves pages through .htaccess rewrites. Some keep working through PHP instead and merely become slower; others stop entirely. Establish which before, not after.
  • You edit .htaccess yourself. If self-service rewrite and deny rules are part of how you run the site, this tier removes that and replaces it with a support request. That is a fair trade for some people and an unacceptable one for others.
  • You rely on SSI, Perl or Python. Plesk lists all three as unavailable in nginx-only mode.

Speed. One web server instead of two, and no Apache worker held for the length of every response. Apache’s own documentation notes that for proxied and FastCGI content “the MPM can’t predict the end of the response and a worker thread has to finish its work before returning the control to the listener”. Removing that hop removes that constraint.

A cost you pay even with an empty .htaccess. Apache’s documentation is unusually blunt here:

Thus, permitting .htaccess files causes a performance hit, whether or not you actually even use them!

The reason is that Apache looks for the file in every directory along the path to the requested file, on every request, whether or not any of them exist. A site with no .htaccess at all is paying for the search.

A smaller attack surface. In the twelve months to August 2026, Apache published two privilege-escalation issues in the same class, both described in Apache’s own words as allowing “local .htaccess authors to read files with the privileges of the httpd user”: CVE-2026-24072, fixed in httpd 2.4.67 on 4 May 2026, and CVE-2026-44119, fixed in httpd 2.4.68 on 8 June 2026. Apache rates both moderate; the CVSS v3.1 base scores recorded at NVD are 8.8 for CVE-2026-24072 and 5.5 for CVE-2026-44119. Ten separate researchers were credited on the second, which is a fair sign the class is being actively looked at.

Noiz applies these updates, so this is not a statement that your site is exposed today. The point is structural: WordPress requires .htaccess to be writable, so a compromised plugin inherits the ability to write one, and that capability keeps producing this class of issue. Under nginx-only, the file is inert.

There is also a version of the problem with no patch, and it is documented behaviour rather than a bug. AllowOverride FileInfo is what makes WordPress permalinks work. The same override group also grants SetHandler, Action and the mod_mime directives, so anyone who can write an .htaccess file can by design change how a file is executed. Apache’s own reference lists them together.

Configuration review cannot catch the failure modes here. A wrongly-ordered location passes the configuration test. Discarded directives pass it too. Only a live request proves anything. Run all of these against the site after the change:

Terminal window
D=yourdomain.com
curl -s -o /dev/null -w "home %{http_code}\n" "https://$D/"
curl -s -o /dev/null -w "permalink %{http_code}\n" "https://$D/a-real-post-url/"
curl -s -o /dev/null -w "rest api %{http_code}\n" "https://$D/wp-json/"
curl -s -o /dev/null -w "login %{http_code}\n" "https://$D/wp-login.php"
curl -s -o /dev/null -w "htaccess %{http_code}\n" "https://$D/.htaccess"
curl -s -o /dev/null -w "wp-config %{http_code}\n" "https://$D/wp-config.php"

The first four must be 200. /.htaccess must be 403 or 404: a 200 there means the file is being served as plain text, which publishes whatever is in it.

/wp-config.php is the one to read carefully, because the status code on its own proves nothing. With no Apache in the path, PHP-FPM executes wp-config.php like any other PHP file, so unless you have added a deny rule it returns 200 with an empty body. That is the correct result, not a failure. What matters is the body:

Terminal window
curl -sf "https://$D/wp-config.php" | head -c 40

That must print nothing. The -f suppresses the body of a 403 or 404, so silence covers all three safe outcomes: denied, not there, or executed and empty. Anything printed needs explaining, and anything containing <?php or DB_PASSWORD means the file is being served as source rather than executed. If you would rather see a 403 on that URL than an empty 200, add the wp-config.php deny rule from the translation table above, and put it before the block that handles .php files.

Two more worth running. Check that a query string survives:

Terminal window
curl -s "https://$D/wp-json/wp/v2/posts?per_page=1" | head -c 80

And check the media library still accepts a real upload from Media > Add New, since the upload size limit is enforced by nginx in this mode.

If the site sells downloadable products, request a known product file directly by URL. It must return 404.

Take the nginx directives out first. With Proxy mode on, location / belongs to Plesk’s own generated configuration, so leaving your own location / block in the directives field makes nginx refuse the configuration with duplicate location "/" and the reload fails. Remove the rules you added for nginx-only mode before you change anything else. They are redundant in any case once Apache is back in the path, because .htaccess is doing that work again. On a Noiz shared plan this is part of the same ticket.

Then select Proxy mode again. After that, check the PHP handler on the PHP Settings page and set it back to an Apache-served one if it is still nginx-served, because PHP being executed by nginx is what stops .htaccess running, and that is what you are rolling back for.

With the directives cleared first, the change is a configuration regeneration and a reload. It takes seconds, and it does not touch your files or your database.

Symptom: only the home page loads, everything else is a 404. The permalink rule is missing. Add the try_files block, and do not trust Settings > Permalinks, which shows no warning on nginx.

Symptom: ?page=2 or ?_embed has no effect on the REST API. Look for a try_files line ending in /index.php$args, with no ? and no $is_args. That is the broken form WordPress documents, and the query string is being glued onto the filename. Replace it with /index.php$is_args$args. A line ending in /index.php?$args is not this fault, so if that is what you have, the cause is elsewhere.

Symptom: a redirect that worked yesterday does not. It was in .htaccess. Translate it to a return 301 or hand it to a redirect plugin that works in PHP rather than by writing files.

Symptom: a security plugin reports everything is fine but its rules are clearly not applying. Expected. Its rules were .htaccess rules. Fold what you need into the nginx configuration.

Symptom: a paid download is reachable without buying it. The WooCommerce internal block is missing. Add it before anything else.

Symptom: WordPress generates http:// links on an https:// page. PHP is not being told the request arrived over TLS. Raise it on a ticket; it is a one-line server-side fix, not a WordPress setting.

Noiz can review a site against this list and tell you plainly whether it is a candidate, including what is in its .htaccess and which plugins would be affected. Open a ticket with the domain name and ask for an nginx-only assessment.