Nginx is one of the most popular web servers:

(For Drupal also?)

Drupal provides a customised .htaccess file to run Drupal on Apache web server. It will be very nice if default configuration for Nginx is also provided with Drupal core.

Remaining tasks

  • Remove outdated configuration (Drupal <10, PHP <8)
  • Check that all configurations from ALL .htaccess are duplicated here
  • Create an /example.nginx file containing example configuration
  • Test the example
  • Add /example.nginx in /.htaccess and /web.config
  • Update core/INSTALL.txt and create a new core/INSTALL.nginx.txt
  • Add GitLab CI tests for Nginx, to ensure that it works and we match the protections we provide for Apache
  • Document basic steps to set it up under https://www.drupal.org/docs/getting-started/system-requirements/nginx-setup
  • Document "How to set up Let’s Encrypt for Nginx" as well on ^^?

Issue fork drupal-2937161

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

gaurav.kapoor created an issue. See original summary.

gaurav.kapoor’s picture

Status: Active » Needs review
gaurav.kapoor’s picture

cilefen’s picture

Version: 8.5.x-dev » 8.6.x-dev
Issue tags: +nginx
sk33lz’s picture

Status: Needs review » Needs work

It looks like this example is based directly off the Nginx documentation found here, https://www.nginx.com/resources/wiki/start/topics/recipes/drupal/, which is a great start. This initial patch applies cleanly to 8.6.x, although we need to also update the core/INSTALL.txt and create a new core/INSTALL.nginx.txt file with specific documentation regarding installing Drupal on Nginx that explains additional requirements this config requires such as installing php-fpm.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

kunalgautam’s picture

Patch is successfully applied for version 10.1.x as well.

tstermitz’s picture

Drupal Media Systemt and NGINX file upload problem, client_max_body_size

NGINX Error log Message: "client intended to send too large body"

I recently rebuilt a new Digital Ocean server with LEMP, and installed Drupal 10 with success.

Media files uploads were sometimes failing without a user visible error message.

To fix this I had to add a client_max_body_size in TWO locations in my conf file. This directive is not suggested in any of the common Drupal NGINX conf examples.

Here is my working nginx.conf file main Server block:

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    root /var/www/mydomain/web;

    server_name mydomain.com www.mydomain.com;

    index index.html index.htm index.nginx-debian.html index.php;

    client_max_body_size 100M;

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

    location = /favicon.ico { log_not_found off; access_log off; }
    location = /robots.txt { log_not_found off; access_log off; allow all; }

    location @rewrite {
      rewrite ^/(.*)$ /index.php?q=$1;
    }

    location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
        try_files $uri @rewrite;
        expires max;
        log_not_found off;
    }

    location ~ \.php$ {
       try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        client_max_body_size 100M;
    }

    # Deny Access to all of Wordpress Front End files
        location ~* ^/(/wp-admin*|/wp-cron*|/wp-config*) {
        rewrite ^ / permanent;
    }

    ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem; # managed by Certbot

}
dww’s picture

Issue tags: +Security improvements

Marked #3336659: Add a sample nginx configuration file duplicate, tagging this for Security improvements. +1 to doing this!

Thanks,
-Derek

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture

That sounds like good material for help topic

Meantime there should be page like https://unit.nginx.org/howto/drupal/
So help topic can provide links to actual configuration maintained by upstream

neclimdul’s picture

Maybe, but I think something in core is a better fit. Especially since the documentation could then be tied to specific versions instead of generalized documentation which ends up needing a sprinkling of "Do X for 7, Y for 8-10, Z for 10.1+" which ends up needing quite a bit of expertise to get an actual working config.

Another reason is #1014086: Stampedes and cold cache performance issues with css/js aggregation broke things and caused this to bubble back up. There wasn't a clear way to really communicate that within core and how sites should fix it. Honestly, unless you find the thread in slack, I'm not sure the possible fixes are even documented anywhere.

Additionally, I know the "official" nginx version has had problems in the past as well so would very much support providing something with best practices as captured by the Drupal community.

gaele’s picture

Addition for multisite installation in a subdirectory:
https://samwaters.net/nginx-drupal-9-multisite-and-path-based-urls/

In short:

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

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

  #location /site2/ {
  #   try_files $uri /site2/index.php?$query_string;
  #}
o&#039;briat’s picture

There are some config from the default htaccess that is not present in the patch, for example:

# Protect files and directories from prying eyes.
<FilesMatch "\.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config|yarn\.lock|package\.json)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$">
  <IfModule mod_authz_core.c>
    Require all denied
  </IfModule>
  <IfModule !mod_authz_core.c>
    Order allow,deny
  </IfModule>
</FilesMatch>

Since this patch is related to V11, all mention of Drupal 8 should be removed and the Change records Asset aggregation deprecations and additions, hook_js_alter()/hook_css_alter() changes should be added.

andypost’s picture

Not sure it doable as one file as fastcgi backend needs definition too, but in case of https://unit.nginx.org/howto/drupal/ less configuration required

  1. +++ b/example.nginx
    @@ -0,0 +1,103 @@
    +        fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
    +        # Security note: If you're running a version of PHP older than the
    +        # latest 5.3, you should have "cgi.fix_pathinfo = 0;" in php.ini.
    +        # See http://serverfault.com/q/627903/94922 for details.
    +        include fastcgi_params;
    

    the file is missing

  2. +++ b/example.nginx
    @@ -0,0 +1,103 @@
    +    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
    

    webp/avif should be added

o&#039;briat’s picture

I tried to port https://www.drupal.org/project/drupal/issues/3327115 as

   # Protect files and directories from prying eyes.
    location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|composer\.(json|lock)|web\.config|yarn\.lock|package\.json$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ {
        deny all;
        return 404;
    }

If I understand it correctly apache match only the last part of the url/filepath (most right directory or filename) but nginx match the whole path.

So in order to deny all composer.json, composer.lock, web.config, yarn.lock or package.json, I move this part to specific part of the regex:


-    location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ {
+    location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|composer\.(json|lock)|web\.config|yarn\.lock|package\.json$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ {

Could someone test and validate this modification?

As I don't understand what Entries.*|Repository|Root|Tag|Template is referring too, it would be nice to document this part.

You could test it with

find web -type f -regextype posix-egrep -regex '.*\.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|.*\/(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config|yarn\.lock|package\.json)$|.*\/#.*#$|.*\.php(~|\.sw[op]|\.bak|\.orig|\.save)$' | sort | sed 's/web/https:\/\/myweb.site/' | xargs -I {} curl -s -o /dev/null -w "%{http_code} %{url_effective}\n" {} | grep -v 404
o&#039;briat’s picture

Issue summary: View changes
ericgsmith’s picture

@O'Briat I hit the same issue trying to update the regex with the change from #3327115 - your suggested config looks good to me - tested and now correctly seeing the expected 404 for composer.json, composer.lock, web.config, yarn.lock and package.json

ericgsmith’s picture

Assigned: Unassigned » ericgsmith

@O'Briat there looks to be a typo in the first diff - the regex is different from the test command - I was looking at the regex in your test command.

Your correct that the regex from the .htaccess file matches filenames and the nginx config matches URIs.

My colleague @Rosk0 and I built out some test cases for that regex https://regexr.com/7o2av

I essentially just replaced with ^ with \/ so that anywhere the .htaccess block was looking for the start of the string it now looks for /

I'm going to move the patch to an MR and address some of the comments above
Ah, I see you already opened MR - I missed that!

ericgsmith’s picture

Assigned: ericgsmith » Unassigned

ericgsmith’s picture

Status: Needs work » Needs review

Made the following changes to the branch:

Adjust regex - see commit message for explanation and test
Applied changes for asset aggregation from https://www.drupal.org/node/2888767#nginx-php-fpm
Added additional file types suggested in #23
Updated web.config, .htaccess and example.nginx to block the new example.nginx file

o&#039;briat’s picture

@ericgsmith: Great, the ^ should have been remove from my regexp ^(\.(?!well-known).*, thanks for the review & fix.

I didn't see it because it was already blocked by :

    location ~ (^|/)\. {
        return 403;
    }

This block and the location ~* ^/.well-known/ could be safely removed, no ?

By the way, all the 403 should be switch to 404 for security obfuscation.

I wonder if this file should not be put in place by the scaffold process?

johnpitcairn’s picture

By the way, all the 403 should be switch to 404 for security obfuscation.

Only if Drupal does that in .htaccess for Apache as well. It's a misuse of the response code.

Sites are free to modify the defaults for their own purposes, if obfuscation is a requirement.

RoSk0 made their first commit to this issue’s fork.

rosk0’s picture

Status: Needs review » Needs work

Back to NW for the comments on the MR.

solideogloria’s picture

Hide patch as it's very outdated compared with the current recipe at https://www.nginx.com/resources/wiki/start/topics/recipes/drupal/

solideogloria’s picture

As a note, you can also use this Nginx generator to help with getting an "A+" for security on SSL Labs.

https://ssl-config.mozilla.org/#server=nginx

c-logemann’s picture

The old nginx.com recipe is gone. Here is a copy in wayback machine:
https://web.archive.org/web/20240511055421/https://www.nginx.com/resourc...

It seems that it's now more important that we have a kind of official nginx example on d.o maybe not only in code.

andypost’s picture

solideogloria’s picture

It looks like Nginx is moving towards Unit.

Here's the Nginx Unit recipe for Drupal: https://unit.nginx.org/howto/drupal/

I definitely like how readable it looks (prior Nginx was a challenge to read), though I have yet to give it a try.

poker10’s picture

I think that Nginx Unit and Nginx + PHP-FPM are two different solutions and neither one is going away. If anything, we should probably still target classic Nginx + PHP-FPM config, which should be used by majority users these days (but I have not found relevant usage statistics for Nginx Unit).

solideogloria’s picture

Here's a post that compares the performance of PHP-FPM and Nginx Unit: Comparing PHP-FPM, NGINX Unit, and Laravel Octane

c-logemann’s picture

The official wiki repo is still available:
https://github.com/nginxinc/nginx-wiki/blob/master/source/start/topics/r...

Because there so many links to the nginx.com wiki I think it's a bad move to just shut it down.
Maybe they need a little bit help to configure a proper redirect.

@solideogloria I don't know if unit fit's all the things I do with nginx config for now.

@andypost Especially angie seems to be very interesting. I planned to try it ASAP.

solideogloria’s picture

@solideogloria I don't know if unit fit's all the things I do with nginx config for now.

Me neither. I don't use it yet (still using PHP-FPM); I'm just looking at what's available.

andypost’s picture

I'm using following config ATM for Nginx Unit

{
	"access_log": "/dev/stdout",
	"listeners": {
		"*:80": {
			"pass": "routes/main"
		}
	},

	"routes": {
		"main": [
			{
				"match": {
					"uri": [
						"!*/.well-known/*",
						"/vendor/*",
						"/core/profiles/demo_umami/modules/demo_umami_content/default_content/*",
						"*.engine",
						"*.inc",
						"*.install",
						"*.make",
						"*.module",
						"*.po",
						"*.profile",
						"*.sh",
						"*.theme",
						"*.tpl",
						"*.twig",
						"*.xtmpl",
						"*.yml",
						"*/.*",
						"*/Entries*",
						"*/Repository",
						"*/Root",
						"*/Tag",
						"*/Template",
						"*/composer.json",
						"*/composer.lock",
						"*/web.config",
						"*sql",
						"*.bak",
						"*.orig",
						"*.save",
						"*.swo",
						"*.swp",
						"*~"
					]
				},

				"action": {
					"return": 404
				}
			},
			{
				"match": {
					"uri": [
						"/core/authorize.php",
						"/core/install.php",
						"/core/modules/statistics/statistics.php",
						"~^/core/modules/system/tests/https?\\.php",
						"/core/rebuild.php",
						"/update.php",
						"/update.php/*"
					]
				},

				"action": {
					"pass": "applications/drupal/direct"
				}
			},
			{
				"match": {
					"uri": [
						"!/index.php*",
						"*.php"
					]
				},

				"action": {
					"return": 404
				}
			},
			{
				"match": {
					"uri": [
						"~^.*css_[a-zA-Z0-9-_]+\\.css(?:\\?.*)?$",
						"~^.*js_[a-zA-Z0-9-_]+\\.js(?:\\?.*)?$"
					],

					"headers": [
						{
							"Accept-Encoding": "*gzip*"
						}
					]
				},

				"action": {
					"pass": "routes/assets_gz"
				}
			},
			{
				"action": {
					"share": "/var/www/html/web$uri",
					"fallback": {
						"pass": "applications/drupal/index"
					}
				}
			}
		],

		"assets_gz": [
			{
				"action": {
					"share": "/var/www/html/web${uri}.gz",
					"response_headers": {
						"Content-Encoding": "gzip"
					},

					"fallback": {
						"pass": "routes/assets"
					}
				}
			}
		],

		"assets": [
			{
				"action": {
					"share": "/var/www/html/web${uri}",
					"fallback": {
						"pass": "applications/drupal/index"
					}
				}
			}
		]
	},

	"applications": {
		"drupal": {
			"type": "php",
			"stdout": "/dev/stdout",
			"stderr": "/dev/stderr",
			"processes": {
				"max": 4,
				"spare": 2,
				"idle_timeout": 120
			},

			"limits": {
				"timeout": 300,
				"requests": 1500
			},

			"options": {
				"admin": {
					"apc.serializer": "igbinary",
					"memory_limit": "1G",
					"opcache.jit_buffer_size": "20M"
				}
			},

			"targets": {
				"direct": {
					"root": "/var/www/html/web/"
				},

				"index": {
					"root": "/var/www/html/web/",
					"script": "index.php"
				}
			}
		}
	}
}
fred6633’s picture

This code below breaks my site, if I enable aggregate css and javascript. It's a composer install with a web directory. Brave says too many redirects.

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|avif)$ {
        try_files $uri @rewrite;
        expires max;
        log_not_found off;
    }

If I replace "try_files $uri @rewrite;" with "try_files $uri /index.php?$query_string;" it works.

I also have not been able to get this code to work with images that Drupal has converted to webp. Drupal creates a file "filename.png.webp". Drupal also sets "?itok=suhCNess", so the total name is "filename.png.webp?itok=suhCNess" But is has no effect to change the regex to "webp.*". The expires directive still does not work.

The approach that works for me in order to get the expires directive to work with converted webp images is described here: https://linux-audit.com/web/nginx-adding-expires-header-to-improve-caching/

solideogloria’s picture

The nginx.org recipe was outdated compared to Drupal's .htaccess file. It's probably a good thing that Nginx.org removed it.

This one looks better, at least when it comes to the "Protect files and directories from prying eyes" section:

https://github.com/uselagoon/lagoon-images/blob/main/images/nginx-drupal...

solideogloria’s picture

fred6633’s picture

Don't forget this :
https://www.drupal.org/project/drupal/issues/3034643.
I had to add code under comment #15 in order to run update.php.

greggles’s picture

If #2868079: Add a default Content-Security-Policy-header for svg files happens then that should get rolled into these recipes as well.

longwave’s picture

Now we have GitLab CI, we have the possibility of running some tests on nginx, which would be good to ensure both that things work and that we match the protections we provide for Apache.

andypost’s picture

Curious how we can create image containing both nginx and apache

longwave’s picture

@andypost install both in the container, run Apache by default, but in a specific CI job we kill it and run nginx instead before starting tests?

andypost’s picture

Yes, there's server-setup.sh script which can be improved, so only a backend question remains - fpm or what?!

greggles’s picture

The issue summary says:

Nginx is one of the most popular web servers on which Drupal is run.

Does anyone have data about the level of popularity?

solideogloria’s picture

Does anyone have data about the level of popularity?

Here's Nginx compared with Apache. Nginx is more popular than Apache is. Note that this is in general, not only for Drupal sites.

https://trends.builtwith.com/Web-Server/nginx
https://trends.builtwith.com/Web-Server/Apache

Nginx:

Top 1m         40.15%          401,507
Top 100k       50.09%          50,094
Top 10k        53.77%          5,377

Apache:

Top 1m         25.55%          255,529
Top 100k       31.54%          31,538
Top 10k        31.81%          3,181
greggles’s picture

TIL - thanks @solideogloria! It would be ideal to know the number of Drupal sites among those, but I recognize that may not be possible.

solideogloria’s picture

It would be ideal to know the number of Drupal sites among those

For BuiltWith, I think that might require a Pro account. I couldn't figure out how to do it.

alexgreyhead’s picture

Apologies if this has already been covered here, or isn't appropriate to this discussion, but I'm fixing an issue where Nginx is no longer passing requests to generate aggregated JS files to Drupal in D10.

I think the following change is needed to my existing Nginx configuration which might also be applicable for other Nginx users:

Old config - in nginx.conf:

  location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
      expires max;
      log_not_found off;
  }

New config - note both are passed into the rewrite rule, but JS and CSS files have a different expiry header:

  location ~* \.(jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|webp|htc)$ {
      try_files $uri @rewrite;
      expires max;
      log_not_found off;
  }

  location ~* \.(js|css)$ {
      try_files $uri @rewrite;
      expires -1;
      log_not_found off;
  }

Hopefully helpful to someone other than me :o)

/A

solideogloria’s picture

@alexgreyhead Your old config didn't match what the MR has. Does the MR's suggested config work for you?

https://git.drupalcode.org/project/drupal/-/merge_requests/5596/diffs#92...

holo96’s picture

I think it is covered in change record

https://www.drupal.org/node/2888767

alexgreyhead’s picture

Hi @solideogloria, would I be right in thinking the only key difference there is the line to route the request via index.php? (Ignoring the additional extensions) - old:

  location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
      expires max;
      log_not_found off;
  }

New:

  location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|avif)$ {
      try_files $uri @rewrite;
      expires max;
      log_not_found off;
  }

/A

solideogloria’s picture

Yes, that's the main difference.

I will also note that jpg|jpeg can be shortened to jpe?g if desired.

andrii-500’s picture

Version: 11.x-dev » 11.2.x-dev
Assigned: Unassigned » andrii-500

Hello everyone!
This code works fine for me on NGINX:

server {
  listen 80;
  listen [::]:80;
  listen 443 quic;
  listen 443 ssl;
  listen [::]:443 quic;
  listen [::]:443 ssl;
  http2 on;
  http3 off;
  {{ssl_certificate_key}}
  {{ssl_certificate}}
  server_name example.com;
  return 301 https://www.example.com$request_uri;
}

server {
  listen 80;
  listen [::]:80;
  listen 443 quic;
  listen 443 ssl;
  listen [::]:443 quic;
  listen [::]:443 ssl;
  http2 on;
  http3 off;
  {{ssl_certificate_key}}
  {{ssl_certificate}}
  server_name www.example.com www1.example.com;
  {{root}}

  {{nginx_access_log}}
  {{nginx_error_log}}

  if ($scheme != "https") {
    rewrite ^ https://$host$request_uri permanent;
  }

  location ~ /.well-known {
    auth_basic off;
    allow all;
  }
  
  rewrite ^/core/authorize.php/core/authorize.php(.*)$ /core/authorize.php$1;

  location ~ (^|/)\. {
    return 403;
  }

  {{settings}}

  include /etc/nginx/global_settings;

  location ~ ^/sites/.*/files/styles/ {
    try_files $uri @rewrite;
  }

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

  location @rewrite {
    rewrite ^ /index.php;
  }

  location ~ ^(/[a-z\-]+)?/system/files/ { # For Drupal >= 7
    try_files $uri /index.php?$query_string;
  }

  if ($request_uri ~* "^(.*/)index\.php/(.*)") {
    return 307 $1$2;
  }

  index index.php index.html;

  location ~ ^/update.php {
    fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
    try_files $fastcgi_script_name =404;
    include fastcgi_params;
    fastcgi_intercept_errors on;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_read_timeout 3600;
    fastcgi_send_timeout 3600;
    fastcgi_param HTTPS $fastcgi_https;
    fastcgi_pass 127.0.0.1:{{php_fpm_port}};
    fastcgi_param PHP_VALUE "{{php_settings}}";
  }

  location ~ \.php$ {
    include fastcgi_params;
    fastcgi_intercept_errors on;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    try_files $uri =404;
    fastcgi_read_timeout 3600;
    fastcgi_send_timeout 3600;
    fastcgi_param HTTPS $fastcgi_https;
    fastcgi_pass 127.0.0.1:{{php_fpm_port}};
    fastcgi_param PHP_VALUE "{{php_settings}}";
  }

  location ~* ^.+\.(css|js|jpg|jpeg|gif|png|ico|gz|svg|svgz|ttf|otf|ico|woff|woff2|eot|mp4|ogg|ogv|webm|webp|zip|swf)$ {
    add_header Access-Control-Allow-Origin "*";
    add_header alt-svc 'h3=":443"; ma=86400';
    try_files $uri @rewrite;
    expires max;
    access_log off;
  }

  if (-f $request_filename) {
    break;
  }
}
quietone’s picture

Version: 11.2.x-dev » 11.x-dev

Hi, in Drupal core changes are made on on 11.x (our main development branch) first, and are then back ported as needed according to the Core change policies. Thanks.

@andrii-500, this is assigned to you. Are you still working on it?

andrii-500’s picture

Thanks.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

sirclickalot’s picture

@andrii-500

Why are there two separate server { } clauses with the first ending in...

return 301 https://www.example.com$request_uri;?

andrii-500’s picture

There are two `server` blocks because they serve two different purposes.

The first server block only handles the redirect from the non-www domain to the canonical domain:

`example.com → https://www.example.com`

It does not process the site itself (no root, PHP, or Drupal logic). It simply catches requests to `example.com` and immediately returns a 301 redirect to the main domain while preserving the request URI.

The second server block is the one that actually serves the website. It contains the root directive, PHP-FPM configuration, Drupal rewrites, and the rest of the NGINX logic required to run the site.

This separation is a common and recommended NGINX practice because it:

keeps the configuration clean and easy to understand

avoids unnecessary rewrite logic in the main server block

ensures a clear canonical domain for SEO

makes redirects faster since the server immediately returns a 301

In short:
server block 1 = redirect
server block 2 = actual website

ressa’s picture

Issue summary: View changes

I wanted to install Varnish in front of an Apache server, when I was reminded that Nginx adds caching close to Varnish-level by default. So I searched for Drupal/Nginx-documentation, but found only old or fragmented examples ...

It would be really great to get Nginx set up for Drupal documented, so thank you everyone for working on it.

@longwave: Great suggestion about adding an Nginx test in GitLab CI, I have added it to the remaining tasks, and added usage statistics links as well in the Issue Summary.

We could also create a documentation page, which show the basic steps to set it up, perhaps under a new page at https://www.drupal.org/docs/getting-started/system-requirements/nginx-setup, and maybe even include how to set up Let’s Encrypt for Nginx, since HTTPS is required by everyone?

andrii-500’s picture

Status: Needs work » Needs review

I have updated the example.nginx file in the MR with a production-tested configuration. Changes include:

Merged security constraints for sensitive files (composer.json, .git, etc.).

Added support for webp and avif static assets.

Restricted PHP execution to core files only (index.php, update.php, etc.) for better security.

Note: The branch is currently behind main and has merge conflicts, so it will need a rebase, but the NGINX config rules are updated and ready for review.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new97 bytes

The Needs Review Queue Bot tested this issue. The merge request has merge conflicts and cannot be merged. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.