SELinux blocking Nginx start on RHEL 9 — fix in 60 seconds
If Nginx won't start on RHEL 9 or Fedora and logs just say 'permission denied', SELinux is almost certainly the culprit. The fix is a single command to set the right context.
You've spent 20 minutes checking permissions, re-reading config files, and Nginx still refuses to start with a vague 'permission denied'. It's SELinux. Every time. Let's fix it now.
The Fix
- Check if SELinux is the problem: look at the audit log for denials.
If you see something likesudo ausearch -m avc -ts recentdenied { open } for pid=... comm="nginx", we're on the right track. - Find the offending file context. Usually it's your web root —
/var/www/htmlor wherever you pointrootin your Nginx config. Run:
If you seels -Z /var/www/html/index.htmluser_home_torvar_tinstead ofhttpd_sys_content_t, that's the problem. - Fix the file context recursively:
Then restart Nginx:sudo restorecon -R /var/www/html
It should come right up.sudo systemctl restart nginx
If it still doesn't work after that, check your Nginx error log at /var/log/nginx/error.log. If you see connect() to unix:/var/run/php-fpm.sock failed, that's a socket context issue — see the variations section below.
Why This Works
What's actually happening here is that SELinux enforces a type-enforcement policy on every file and process. When you copy files into /var/www/html from a home directory, a USB stick, or a tarball, those files inherit the context of their source — often user_home_t or var_t. Nginx runs under the httpd_t domain (its SELinux security context). The policy says: processes in httpd_t can only read files labeled httpd_sys_content_t (or a few others like httpd_sys_rw_content_t for write access). Any other label results in a denial, logged to audit.log, and Nginx gracefully fails to serve that file — or outright fails to start if its own config file is mislabeled.
restorecon resets the SELinux context of a file to the default for its path. The default for /var/www/html is httpd_sys_content_t, because the SELinux policy defines that path as a web content directory. Running restorecon -R applies that rule recursively. After that, Nginx can read everything inside.
The reason step 3 works is that restorecon doesn't care about Linux file permissions — it only touches the SELinux labels. So if you already fixed Unix permissions (e.g., chmod 755 or chown nginx:nginx) and it still fails, you were missing this step.
Less Common Variations
Nginx won't start after changing its config file location
If you moved nginx.conf to /etc/nginx/custom/, SELinux may not allow Nginx to read it. The context should be etc_t. Run:
sudo restorecon -R /etc/nginx/custom/
Or, if you need a custom context, use semanage fcontext, but that's overkill for most setups.
PHP-FPM socket permission denied
If Nginx starts but returns 502 Bad Gateway, and the error log mentions a Unix socket like /var/run/php-fpm.sock, SELinux may be blocking the connection. PHP-FPM runs under httpd_sys_script_t (if you're using the default policy). The socket needs the httpd_sys_content_t label or a custom one. Quick check:
ls -Z /var/run/php-fpm.sock
If it shows var_run_t, run:
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/run/php-fpm\.sock"
sudo restorecon -v /var/run/php-fpm.sock
sudo systemctl restart php-fpm nginx
This tells SELinux that the socket file is a writable web content object, so httpd_t can connect to it through the Unix domain socket mechanism.
Custom web root on a different filesystem (like /data/web)
If your web root is on a separate mount, restorecon might not apply the default context because the path doesn't match any predefined SELinux file context. You'll get no label change. In that case, you need to add a new mapping:
sudo semanage fcontext -a -t httpd_sys_content_t "/data/web(/.*)?"
sudo restorecon -R /data/web
This permanently tells SELinux: anything under /data/web should get the httpd_sys_content_t label. After that, Nginx can read files there.
Prevention
Stop copying files manually into web directories. Use a deployment tool (rsync, ansible, or even a simple script) that runs restorecon after copying. Better yet, set up a cron job that runs restorecon -R /var/www/html every hour — overkill, but it works.
Really though, the cleanest way is to add a restorecon call to your CI/CD pipeline or post-receive hook. For example, in a Git post-receive hook on the server:
#!/bin/bash
GIT_WORK_TREE=/var/www/html git checkout -f
restorecon -R /var/www/html
That way every deploy resets contexts automatically. You won't see this error again.
If you find yourself constantly fighting SELinux, read the audit2why output — it literally tells you the exact command to fix the denial. Run sudo audit2why -a after a failure. That tool was written by someone who knew you'd be annoyed.
Was this solution helpful?