RHEL in Practice #1: Running a Web Server — nginx, systemd, SELinux Policy
If you worked through the RHEL operations track and learned systemd, SELinux, firewalld, and storage one by one across basics, intermediate, and advanced, it is now time to combine those pieces into one full cycle of bringing up a single service. Rather than listing individual features, the RHEL in Practice track follows scenarios you actually meet in operations from start to finish. We open it with the most common task of all: running a web server.
Bringing up nginx on RHEL does not end with installing a single package. The page only opens from the outside once three beats line up: registering the service with systemd, clearing what SELinux blocks, and opening the port with firewalld. We will pinpoint exactly where things stall when those three beats fall out of step.
Package install and service registration #
RHEL’s default repositories include nginx. Install it with dnf, then register it with systemd.
# install
sudo dnf install -y nginx
# auto-start on boot + start immediately (in one shot)
sudo systemctl enable --now nginx
# check status
systemctl status nginxenable --now handles both enable (auto-start on boot) and start (start immediately) at once. It is the form you reach for most often when bringing up a service on RHEL. Right after install, let’s confirm the response locally first.
curl -I http://localhostUp to here SELinux and firewalld do not get involved, so it usually works in one go. The trouble starts when you connect from the outside and when you step off the defaults.
Opening the port with firewalld #
A stock RHEL has firewalld active, so http (80) and https (443) are blocked. Open them permanently at the service level.
# permanently allow the http and https services
sudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --add-service=https --permanent
# apply
sudo firewall-cmd --reload
# verify
sudo firewall-cmd --list-allLeave out --permanent and the rule vanishes after a reboot. Creating a permanent rule with --permanent and then applying it to the runtime with --reload — these two steps are the basic flow of RHEL firewall work.
Where SELinux blocks you #
RHEL’s SELinux defaults to enforcing. As long as nginx runs from its standard location (/usr/share/nginx/html) on standard ports (80, 443), SELinux stays quiet. But the moment you move the document root or use a non-standard port, it blocks you instantly. These two are the SELinux incidents you hit most often in the field.
Opening a non-standard port #
If you change the config to run nginx on port 8080, the service will not come up even though the config itself is correct. That is because SELinux does not recognize 8080 as a web server port.
# check the ports currently registered under http_port_t
sudo semanage port -l | grep http_port_t
# permanently register 8080 as a web port
sudo semanage port -a -t http_port_t -p tcp 8080If semanage is missing, install the policycoreutils-python-utils package. Once the port is registered, restart nginx and it comes up normally.
Moving the document root #
If you move content to a non-standard path like /srv/www, that directory’s SELinux context is not the web-content type, so nginx cannot read it. Assign the context permanently and apply it.
# permanently register everything under /srv/www as the web-content type
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
# apply to the actual labels on disk
sudo restorecon -Rv /srv/wwwThe two key steps are adding a rule to the policy with semanage fcontext and applying it to the actual files with restorecon. You can also change the context with chcon, but chcon is temporary and reverts on relabel or reboot, so for permanent work use semanage fcontext + restorecon.
Allowing behavior with a boolean #
When nginx needs to forward requests to another server (reverse proxy) or read content on NFS/CIFS, you must turn on the relevant SELinux boolean.
# allow network connections (reverse proxy, etc.)
sudo setsebool -P httpd_can_network_connect onYou need -P for it to persist across reboots. To find which boolean you need, narrow down the candidates with getsebool -a | grep httpd, and when something is blocked, trace the cause from the audit log (/var/log/audit/audit.log) using ausearch or sealert.
Diagnosis order when blocked #
When a web server will not open, narrowing it down in the following order reveals the cause most of the time.
- Is the service up?
systemctl status nginx, and on failurejournalctl -u nginx - Does it open locally? Use
curl -I http://localhostto separate an nginx-side problem from an external-access problem - Is the firewall open? Are http and https in
firewall-cmd --list-all? - Is SELinux blocking? Is there a denied entry in
sudo ausearch -m AVC -ts recentorjournalctl? If it works after a temporarysetenforce 0, SELinux is the cause
In step 4 especially, flipping to permissive briefly with setenforce 0 tells you in one second whether SELinux is the culprit. But use it for confirmation only, and once you have found the cause, the right operational move is to return to setenforce 1 and solve it through policy.
Wrap-up #
What we nailed down in this post:
- Running a web server is three beats. systemd service registration (
enable --now), firewalld port opening (--permanent+--reload), and SELinux policy - The two spots SELinux blocks. Non-standard ports (
semanage port -a), non-standard document roots (semanage fcontext+restorecon) - Booleans. For reverse proxy and remote content,
setsebool -P httpd_can_network_connect on - Diagnosis order. Service → local curl → firewall → SELinux.
setenforce 0for confirmation only
Next: Running a Database #
With the web layer up, we move to the data layer that backs it.
In #2 Running a Database — PostgreSQL on RHEL, we organize one full cycle: installing and initializing PostgreSQL via a RHEL AppStream module, the data directory and its SELinux context, the configuration and firewalld for remote access, and finally backups and service operations.