
Nginx is one of those tools that quietly becomes essential once a server starts doing real work. It can serve static files with very little overhead, terminate TLS, reverse proxy an application, load-balance upstream services, or act as the public edge for a site behind a CDN. On Fedora 45, installing it is straightforward, but a production-ready setup involves more than running dnf install nginx.
Fedora uses DNF as its package manager, so the package installation itself is quick and dependency resolution is handled automatically. The important work begins afterward: opening the correct firewall services, enabling the service at boot, validating the configuration, choosing sensible worker settings, and ensuring SELinux does not silently block a custom document root or upstream connection. Fedora’s documentation identifies DNF as the default package manager and notes that it installs packages while resolving dependencies. [26]
This guide walks through a complete, production-oriented Nginx installation on Fedora 45. It covers the base install, firewall configuration, virtual hosts, reverse proxying, HTTPS, SELinux, logging, performance tuning, and common failure modes. The commands are written for a standard Fedora Server or Fedora Cloud instance with sudo access.
Prerequisites
Before starting, confirm that you have:
- A Fedora 45 system with root or sudo privileges.
- A public DNS record pointing to the server, if you plan to serve a website.
- Ports 80 and 443 reachable from the internet, if this is a public web server.
- SSH access and a working firewall policy that does not lock you out.
- Basic familiarity with
systemctl,dnf, and text editors such asvimornano.
If this is a fresh cloud server, resist the temptation to install every package “just in case.” A lean base system is easier to secure, patch, and troubleshoot. Start with Nginx, the modules you actually need, and the application stack behind it.
Check the operating system and available resources first:
cat /etc/fedora-release
uname -r
nproc
free -h
df -h
The nproc output matters later. Nginx is efficient, but its worker-process model still benefits from matching worker count to available CPU capacity rather than blindly assigning a large fixed number.
Install Nginx on Fedora 45
Fedora 45 provides Nginx through its standard repositories. The Fedora package ecosystem also carries Nginx-related modules, and recent Fedora update notices show Nginx 1.30.6 packages for Fedora 45, which indicates that the distribution package is actively maintained. [11][14]
Update the package metadata and installed packages first:
sudo dnf upgrade --refresh -y
Then install Nginx:
sudo dnf install nginx -y
If you want to inspect what will be installed before committing, use:
sudo dnf install nginx
DNF will show the package, version, repository, and dependencies. Press y to continue.
For a typical web or reverse-proxy server, this base package is usually enough. Add modules only when your use case requires them. For example, Brotli compression support may be available as a separate Fedora module package, and Fedora update notices list nginx-mod-brotli among the Nginx-related packages. [14]
Install it only if you need it:
sudo dnf install nginx-mod-brotli -y
Avoid installing modules speculatively. Each additional module increases the maintenance surface, and a module that is useful for one workload may be irrelevant for another.
Start and Enable Nginx
After installation, start the service and enable it to launch automatically at boot:
sudo systemctl enable --now nginx
Check that it is active:
sudo systemctl status nginx --no-pager
A healthy result should show active (running). If the service fails immediately, do not start changing random configuration values. Read the journal first:
sudo journalctl -u nginx -b --no-pager
Then test the configuration:
sudo nginx -t
This command parses the configuration and reports syntax or inclusion errors before Nginx attempts to reload. It is one of the most valuable habits in day-to-day server administration.
Verify that Nginx is listening:
sudo ss -ltnp | grep nginx
You should see Nginx listening on port 80, and later on 443 once TLS is configured.
Open HTTP and HTTPS in Firewalld
Fedora commonly uses firewalld, a dynamically managed firewall that organizes rules through zones and services. [10][24] A fresh server may have SSH allowed but HTTP and HTTPS blocked, which produces a confusing situation: Nginx runs locally, curl works from the server itself, yet the site is unreachable from outside.
Allow the standard web services permanently:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
Confirm the active rules:
sudo firewall-cmd --list-all
For a public-facing server, the output should include http and https in the services list. The http service opens TCP port 80, while https opens TCP port 443. Fedora’s own web-server documentation uses the same permanent firewalld service approach for HTTP and HTTPS. [19]
If firewalld is not installed or running, install and activate it:
sudo dnf install firewalld -y
sudo systemctl enable --now firewalld
Then apply the rules again.
One practical caution: if you are connected over SSH, do not remove or replace the SSH rule without confirming that port 22 remains permitted. A firewall mistake on a remote server can turn a five-minute change into an emergency console-session recovery.
Verify the Default Nginx Page
From the server itself:
curl -I http://localhost
You should receive an HTTP response, commonly 200 OK for Fedora’s default Nginx landing page.
From another machine, test using the server’s public IP or domain:
curl -I http://your-server-ip
curl -I http://example.com
If the local test succeeds but the external test fails, the issue is usually one of the following:
- Firewalld is blocking ports 80 or 443.
- A cloud-provider security group or network ACL is blocking traffic.
- DNS points to the wrong IP address.
- Another service already owns port 80 or 443.
- Nginx is bound only to localhost rather than a public interface.
Check listening sockets with:
sudo ss -ltnp | grep -E ':(80|443)\b'
If Apache is installed and running, it may already hold port 80. Fedora’s default and fully supported web server is Apache, so it is not unusual to encounter it on an existing Fedora system. [19] In that case, either disable Apache or migrate its configuration deliberately:
sudo systemctl disable --now httpd
Do not disable Apache blindly on a production server. First inspect its sites, certificates, logs, and reverse-proxy rules. A rushed port takeover can break an existing application.
Understand the Fedora Nginx Layout
Fedora follows a conventional layout that keeps configuration modular:
| Path | Purpose |
|---|---|
/etc/nginx/nginx.conf |
Main Nginx configuration file |
/etc/nginx/conf.d/ |
Drop-in HTTP configuration files |
/etc/nginx/nginx.conf.d/ |
Additional configuration location, depending on package layout and local conventions |
/etc/nginx/mime.types |
File-extension to MIME-type mappings |
/var/log/nginx/access.log |
Access logs |
/var/log/nginx/error.log |
Error logs |
/usr/share/nginx/html/ |
Default web root |
/var/lib/nginx/ |
Runtime and cache-related directories |
The most important habit is to keep global changes limited. Put site-specific configuration in /etc/nginx/conf.d/ using descriptive filenames such as example.com.conf. This makes configuration review, automation, and rollback much easier.
Avoid editing the main nginx.conf file for every virtual host. A clean separation between global server settings and per-site configuration prevents a small change for one domain from accidentally affecting every site on the server.
Create a Basic Website
Create a document root for a test site:
sudo mkdir -p /var/www/example.com/html
sudo tee /var/www/example.com/html/index.html >/dev/null <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nginx on Fedora 45</title>
</head>
<body>
<h1>Nginx is running</h1>
<p>This page is served by Nginx on Fedora 45.</p>
</body>
</html>
EOF
Create a virtual-host configuration:
sudo tee /etc/nginx/conf.d/example.com.conf >/dev/null <<'EOF'
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log warn;
location / {
try_files $uri $uri/ =404;
}
}
EOF
Validate and reload:
sudo nginx -t
sudo systemctl reload nginx
A reload is preferable to a full restart for most configuration changes because it asks Nginx to re-read configuration without dropping active connections. Use a restart when the service is stopped, misbehaving, or when a change specifically requires a full process restart.
Test the site:
curl -I http://example.com
If DNS has not propagated yet, test using the server IP with a Host header:
curl -I -H "Host: example.com" http://your-server-ip
Configure SELinux Correctly
SELinux is not an obstacle to work around by disabling it. It is a mandatory access-control layer that restricts what processes can do, even if a process is compromised. Fedora’s documentation explains that SELinux policy rules define interactions between processes and resources, and that web-server processes commonly run in the httpd_t domain. [17]
The default Nginx web root is already labeled appropriately. Custom directories under /var/www/ usually inherit the correct context, but a non-standard location such as /srv/myweb/ may not. Fedora’s SELinux troubleshooting documentation specifically notes that non-standard directories can receive labels that the web server cannot access. [29]
If you use a custom document root, label it for web content:
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/example.com(/.*)?"
sudo restorecon -R -v /var/www/example.com
If policycoreutils-python-utils is not installed, install it:
sudo dnf install policycoreutils-python-utils -y
Check the current SELinux mode:
getenforce
For a production server, Enforcing is the appropriate target. Do not use setenforce 0 as a permanent fix. It may reveal whether SELinux is involved, but disabling enforcement broadly removes an important layer of protection.
If Nginx needs to connect to an application running on a nonstandard TCP port, SELinux may block the connection. First identify the denial:
sudo ausearch -m avc -ts recent
Then use a narrowly scoped solution. For example, allowing Nginx to connect to a backend on port 8080 may require an SELinux port label:
sudo semanage port -a -t http_port_t -p tcp 8080
Only do this when the port is genuinely part of the web-service architecture. Avoid broad boolean changes unless you understand their effect.
Reverse Proxy an Application
Most modern Nginx deployments do not serve an application directly. Instead, Nginx accepts public traffic and forwards it to an application listening on a private port, such as Node.js on port 3000, Gunicorn on a Unix socket, or a containerized service on port 8080.
Create a reverse-proxy configuration:
sudo tee /etc/nginx/conf.d/app.example.com.conf >/dev/null <<'EOF'
upstream app_backend {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name app.example.com;
access_log /var/log/nginx/app.example.com.access.log;
error_log /var/log/nginx/app.example.com.error.log warn;
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
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 $scheme;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
EOF
Validate and reload:
sudo nginx -t
sudo systemctl reload nginx
The X-Forwarded-For and X-Forwarded-Proto headers matter because many frameworks use them to determine the client IP and whether the original request used HTTPS. Without them, an application may generate incorrect absolute URLs, reject secure-cookie requests, or log the proxy IP instead of the real visitor IP.
For a local Unix socket, which is often preferable for Python applications using Gunicorn or uWSGI, use:
upstream app_backend {
server unix:/run/myapp/gunicorn.sock;
}
Unix sockets avoid TCP overhead on the same host and can simplify permissions, but they require the Nginx worker user to have read and write access to the socket path.
Add HTTPS With Let’s Encrypt
Plain HTTP is fine for local testing, but public sites should use HTTPS. TLS protects login sessions, form submissions, API tokens, and browser privacy. It also prevents certain types of content injection and improves user trust.
Install Certbot:
sudo dnf install certbot python3-certbot-nginx -y
Issue and install a certificate:
sudo certbot --nginx -d example.com -d www.example.com
Certbot can modify the Nginx configuration to redirect HTTP traffic to HTTPS and install the certificate paths. Review the resulting configuration afterward rather than treating it as a black box.
A typical HTTPS server block will contain something similar to:
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
Test renewal without actually renewing:
sudo certbot renew --dry-run
Automate renewals with a systemd timer if Certbot did not already create one:
sudo systemctl list-timers | grep certbot
Certificates should renew automatically. The operational risk is not usually the initial issuance; it is discovering months later that renewal silently failed because DNS changed, port 80 became blocked, or a configuration error prevented the renewal challenge.
Tune Nginx for Performance
Nginx uses a master process and one or more worker processes. The master reads configuration and manages workers, while workers handle client connections. Nginx’s documentation describes worker_processes auto as a way to automatically match the worker count to available CPU cores. [3][2]
A sensible baseline for nginx.conf is:
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
events {
worker_connections 4096;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 5;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
application/rss+xml
image/svg+xml;
include /etc/nginx/conf.d/*.conf;
}
The worker_connections directive controls the maximum simultaneous connections per worker. Nginx documentation notes that this includes upstream connections, not only client connections, and that the effective limit also depends on open-file limits. [2] That distinction matters: a reverse proxy with many upstream calls consumes connection slots differently from a static-file server.
Do not copy an aggressive configuration from a benchmarking blog. A worker_connections value of 150,000 may be appropriate for a heavily tuned dedicated edge node, but it can be meaningless if the operating-system file-descriptor limit, memory, CPU, or upstream application cannot sustain that load. Nginx’s own configuration guidance lists high values such as worker_rlimit_nofile 524288 and worker_connections 150000 for certain optimized deployments, but those are deployment-specific recommendations rather than universal defaults. [6][7]
CPU and worker tuning
Start with:
worker_processes auto;
This is usually the right choice. Fixed worker counts make sense only when you have a specific reason, such as CPU pinning, NUMA awareness, or an unusual container CPU quota.
For a small VPS with two vCPUs, auto will generally create two workers. For a 32-core proxy node, it will create more, but that does not automatically mean better throughput. Measure before and after changes.
Useful monitoring commands include:
top
htop
mpstat 1
pidstat -u -p $(pgrep -d',' nginx) 1
If CPU saturation is caused by TLS handshakes, consider terminating TLS at a CDN or load balancer, enabling session resumption where appropriate, or using hardware acceleration if the platform supports it.
Memory and disk I/O
Nginx itself is memory-efficient, but caching, logging, and large file transfers change the picture. On a small server, avoid oversized proxy caches. A cache that constantly evicts useful objects wastes disk I/O and provides little benefit.
For static content, enable sendfile and ensure the underlying filesystem has enough free space. For high-traffic media sites, consider serving assets from object storage or a CDN rather than making the origin server handle every image, video, or download request.
Check disk pressure with:
iostat -xz 1
vmstat 1
High await, high utilization, or a persistently full disk queue indicates that the bottleneck is storage, not Nginx. In that case, tuning Nginx buffers will not solve the underlying problem.
Compression and caching
Gzip is useful, but it is not free. Compressing every response on every request consumes CPU. For dynamic responses, compression can still be worthwhile, but set a minimum response size and avoid compressing already-compressed formats.
For static assets, prefer explicit cache headers:
location ~* \.(?:css|js|jpg|jpeg|png|gif|webp|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
try_files $uri =404;
}
Use versioned filenames for deployable assets, such as app.8f3a2c.js. That allows long cache lifetimes without users receiving stale files after a deployment.
Harden the Nginx Installation
Security is not a final step. It is a set of decisions made throughout the deployment.
Hide unnecessary information
Nginx does not need to advertise its version in response headers. Add:
server_tokens off;
This does not make a server secure by itself, but it removes one easy piece of reconnaissance information.
Restrict methods and paths
Do not expose administrative paths, backup files, version-control directories, or development artifacts:
location ~ /\.(?!well-known) {
deny all;
}
location ~* \.(?:bak|config|sql|ini|log|sh|swp)$ {
deny all;
}
The well-known exception matters because ACME certificate validation may need access to /.well-known/acme-challenge/.
Use modern TLS settings
Avoid weak protocols and outdated cipher choices. A practical starting point is:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
Use a current certificate authority and keep certificates renewed. TLS configuration should be tested with a tool such as openssl s_client or an external SSL scanner after deployment.
Limit request abuse
Rate limiting is useful for login endpoints, API routes, and public form submissions. A simple example:
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=10r/m;
server {
location /login {
limit_req zone=login_limit burst=20 nodelay;
proxy_pass http://app_backend;
}
}
This will not stop a determined attacker, but it reduces the impact of credential-stuffing attempts and accidental retry loops. Combine it with application-level lockouts, logging, and monitoring.
Keep the system patched
Run updates regularly:
sudo dnf upgrade --refresh -y
sudo needs-restarting -r
Fedora moves quickly, which is excellent for current hardware support and security updates but requires a disciplined maintenance process. Schedule reboots and service restarts rather than assuming that a package update alone applies every change.
Logging and Monitoring
Logs are only useful if someone looks at them. At minimum, monitor:
- HTTP 5xx response rates
- Upstream connection failures
- TLS handshake errors
- Disk usage in
/var/log - Nginx worker CPU and memory use
- Backend latency and error rates
Useful commands:
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
sudo journalctl -u nginx -f
For a production environment, forward logs to a central system such as Loki, Elasticsearch, Graylog, or a managed logging service. Local logs are useful during an incident, but centralized logs make it possible to correlate Nginx errors with application logs and system metrics.
A simple log-rotation check is worthwhile:
sudo logrotate -d /etc/logrotate.conf
On a busy site, access logs can grow quickly. Ensure rotation, compression, and retention policies are in place before disk usage becomes an incident.
Troubleshooting Common Nginx Issues
nginx: [emerg] bind() to 0.0.0.0:80 failed
Another process is using port 80.
Identify it:
sudo ss -ltnp | grep ':80'
sudo lsof -i :80
If Apache is running, stop or disable it:
sudo systemctl status httpd
sudo systemctl disable --now httpd
Then reload Nginx:
sudo systemctl restart nginx
nginx: [emerg] open() "/etc/nginx/..." failed
This usually means a missing file, incorrect path, or permission problem.
Check the referenced file:
sudo nginx -T | grep -n "ssl_certificate"
sudo ls -l /etc/letsencrypt/live/example.com/
Do not copy certificates into random directories. Keep them where Certbot manages them, and ensure Nginx can read the certificate chain and private key.
403 Forbidden
A 403 often comes from file permissions or SELinux.
Check permissions:
namei -l /var/www/example.com/html/index.html
ls -lZ /var/www/example.com/html/index.html
Check SELinux denials:
sudo ausearch -m avc -ts recent
If the document root is outside the standard web location, apply the proper context:
sudo semanage fcontext -a -t httpd_sys_content_t "/path/to/site(/.*)?"
sudo restorecon -R -v /path/to/site
Fedora’s documentation identifies httpd_sys_content_t as the context associated with files normally found in /var/www/html/. [17]
502 Bad Gateway
Nginx is running, but it cannot reach the backend.
Check whether the application is listening:
sudo ss -ltnp | grep ':3000'
curl -I http://127.0.0.1:3000
Then inspect Nginx errors:
sudo tail -n 100 /var/log/nginx/error.log
Common causes include:
- The application crashed or is not running.
- The upstream port is wrong.
- The application listens only on
127.0.0.1while Nginx runs in a different network namespace or container. - SELinux prevents Nginx from connecting to the backend port.
- A Unix socket has incorrect ownership or permissions.
504 Gateway Timeout
The backend accepted the connection but did not respond in time.
Increase timeouts cautiously:
proxy_connect_timeout 10s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
Do not treat a longer timeout as a solution. A slow backend often needs database indexes, caching, queueing, connection pooling, or horizontal scaling. Nginx can hide a performance problem for a while, but it cannot fix it.
Site works locally but not externally
Run through the network path:
curl -I http://localhost
sudo firewall-cmd --list-all
sudo ss -ltnp | grep -E ':(80|443)\b'
dig +short example.com
If everything looks correct on the server, inspect the cloud provider’s security group, load balancer, DNS record, and any upstream CDN configuration. The most common production issue is not a broken Nginx configuration; it is a network path that was never opened correctly.