Magento 2 Nginx Configuration: A Production Setup
Magento 2 ships with a sample nginx configuration, and most production setups are built on it. That is a good thing, because the sample handles static file fallbacks, media, security rules and the PHP entry points correctly. The problems come from the parts around it: the server block, TLS, Varnish in the middle, timeouts and buffers. This article walks through an nginx setup for Magento Open Source and Adobe Commerce 2.4.8 and explains the parts people tend to get wrong.
Start from nginx.conf.sample
In the Magento root there is a file called nginx.conf.sample. It is not a full server block. It is meant to be included inside one, and it expects a variable called $MAGE_ROOT and an upstream called fastcgi_backend.
upstream fastcgi_backend {
server unix:/run/php/php8.3-fpm-magento.sock;
}
server {
listen 8080;
server_name shop.example.com;
set $MAGE_ROOT /var/www/magento/current;
include /etc/nginx/magento/magento.conf;
}
Notice the include points to a copy, not to the file inside the Magento root. Copy nginx.conf.sample to your nginx config folder and include the copy. Composer updates replace the sample in the project root, and you do not want an upgrade to change your web server config without review. After each Magento upgrade, diff the new sample against your copy and bring over what changed.
What the sample does
It helps to know what you are including before you change it:
- It sets the document root to
$MAGE_ROOT/pub. Since 2.4,pubis the only supported document root. Serving the project root exposes files that should never be public. - It sends missing files under
/static/tostatic.php, which can generate them on the fly in developer mode. It also strips the version segment from URLs like/static/version1712345678/frontend/..., which is how static content signing works. - It sends missing files under
/media/toget.php, which serves files stored in the database media storage and resizes catalog images on demand. - It blocks access to
/media/customer/,/media/downloadable/,/media/import/, and to hidden files and sensitive folders. - It allows PHP execution only for a fixed list of entry points such as
index.php,get.php,static.phpand the error pages, and denies every other.phpfile. This is an important security rule. An uploaded PHP file in media cannot run. - It sets long cache headers on static and media files and enables gzip for text types.
If you write your own config from scratch, keep all of those behaviours. The PHP allow list in particular has stopped many malware uploads from becoming remote code execution.
A typical layout with Varnish
On a store with Varnish, nginx usually appears twice in the request path:
- nginx on port 443 terminates TLS and proxies to Varnish.
- Varnish on port 6081 serves cached pages or forwards misses to the backend.
- nginx on port 8080 runs the Magento config and passes PHP requests to PHP-FPM.
The TLS front looks something like this:
server {
listen 443 ssl;
http2 on;
server_name shop.example.com;
ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem;
client_max_body_size 64m;
location / {
proxy_pass http://127.0.0.1:6081;
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 https;
proxy_set_header X-Forwarded-Port 443;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
proxy_read_timeout 300s;
}
}
server {
listen 80;
server_name shop.example.com;
return 301 https://$host$request_uri;
}
The http2 on directive is the syntax for nginx 1.25.1 and newer. On older nginx versions you add http2 to the listen line instead.
Telling Magento the request was HTTPS
Behind a proxy, Magento sees plain HTTP coming from Varnish. It uses the X-Forwarded-Proto header to know the original request was secure. If that header is missing, you get redirect loops when secure URLs are enforced, or mixed content where Magento builds http links on an https page. Make sure Varnish passes the header through to the backend.
Large headers
Magento sends a large X-Magento-Tags header on category pages with many products. Varnish removes it before the response leaves, but between the backend and Varnish it can be big. If you see 502 errors only on large category pages, check the Varnish http_resp_hdr_len and http_resp_size parameters and the nginx fastcgi buffer sizes. The Varnish guide covers the Varnish side.
FastCGI buffers and timeouts
The sample config sets FastCGI buffers and long timeouts for the PHP location. It also passes memory_limit and max_execution_time to PHP through fastcgi_param PHP_VALUE. Those values override your php.ini for web requests. If you change the memory limit in php.ini and nothing happens, this is usually why.
Decide whether you want PHP limits set in nginx or in the FPM pool. Keeping them in the pool config with php_admin_value is clearer, because one place controls them. If you move them, remove the PHP_VALUE line from your copy of the sample.
Long timeouts are useful for admin exports and imports run through the browser, but they also let stuck requests hold a worker for minutes. A storefront request that takes 600 seconds is a bug, not something to wait for. Consider a shorter timeout in the storefront server block and a longer one for admin paths if you split them.
Compression
The sample enables gzip for text content types. Brotli gives better compression for JavaScript and CSS, but it needs the nginx brotli module, which is not part of the standard nginx build on most distributions. If it is available:
brotli on;
brotli_comp_level 5;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;
If a CDN sits in front of the store, it often compresses responses itself, and nginx compression matters less for visitors. It still reduces traffic between your origin and the CDN.
Real client IP
When a load balancer, Varnish or a CDN sits in front, nginx sees the proxy IP as the client. Logs, rate limits and Magento features that look at the remote address then use the wrong IP. The realip module fixes this:
set_real_ip_from 127.0.0.1;
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Only list proxies you control. Trusting X-Forwarded-For from anywhere lets any visitor spoof their IP.
Protecting the admin
The admin URL should not be the default /admin. Beyond that, nginx can restrict it by IP when your team works from fixed addresses:
location ~* ^/(index\.php/)?backoffice_x7k2 {
allow 203.0.113.10;
deny all;
try_files $uri $uri/ /index.php$is_args$args;
}
Replace the path with your admin front name. Test carefully, because the admin also loads static assets and makes AJAX calls under its path. This works best combined with the measures in the admin security hardening guide.
Rate limiting bots
Aggressive crawlers hitting layered navigation and search can produce thousands of uncached requests per minute. nginx limit_req can slow them down:
limit_req_zone $binary_remote_addr zone=search:10m rate=2r/s;
location /catalogsearch/ {
limit_req zone=search burst=10 nodelay;
try_files $uri $uri/ /index.php$is_args$args;
}
Apply it to the backend server block, not the TLS front, if Varnish caches some of those URLs. Make sure the real IP setup above is in place first, or every visitor shares the proxy IP and gets limited together.
Testing changes
Always run nginx -t before a reload, and reload instead of restart so open connections finish. After any change to the Magento location rules, check a product page, a category page, a static file with the version segment in the URL, a resized product image that does not exist yet, the admin login and checkout. Those cover the code paths that break most often.
FAQ
Should I edit nginx.conf.sample directly?
No. Copy it into your nginx config folder and include the copy. Upgrades replace the sample, and you should review changes before they reach your server.
Why does Magento show redirect loops behind nginx and Varnish?
Usually the X-Forwarded-Proto header is missing, so Magento thinks the request is plain HTTP and redirects to HTTPS again. Pass the header from the TLS proxy through Varnish to the backend.
Why does my php.ini memory_limit not apply to web requests?
The sample nginx config passes memory_limit through fastcgi_param PHP_VALUE, which overrides php.ini. Change it there or move the setting into the FPM pool.
Is pub the correct document root for Magento 2.4.8?
Yes. Since 2.4 the pub folder is the supported document root. Serving the project root exposes files that must not be public.
I am available for freelance Magento and Adobe Commerce work, including server and hosting setup. If you need help with this, get in touch.
Related articles
Magento 2 Admin Security Hardening Checklist
Harden the Magento 2 admin: custom admin path, IP restriction, 2FA, lockout and session settings, user and role cleanup, API integrations and admin logs.
Magento 2 Admin Two Factor Authentication: Setup and Fixes
Set up Magento 2 admin two factor authentication: providers, forcing a provider, CLI commands to reset users, local development, API tokens and common problems.
Magento 2 Code Review Checklist for Pull Requests
A practical Magento 2 code review checklist: DI and architecture, performance traps, security and escaping, database changes, frontend, tests and deploy impact.
Magento 2 GraphQL Customer Login, Tokens and Security
Customer authentication with Magento 2 GraphQL: login tokens, lifetime, logout, registration, password reset, safe token storage, expiry handling and reCAPTCHA.