The scenario
You built a Node.js app, a Python FastAPI service, or a Go backend. It runs fine on localhost:3000. You want to expose it to the internet on port 80 or 443. You could open port 3000 on your firewall and point a domain directly at it, but that exposes your application framework to unfiltered web traffic. You need a reverse proxy. Nginx is the standard choice on Fedora. This guide shows you how to install it, write the proxy block, handle SELinux and firewalld, and verify the routing without breaking your backend.
What a reverse proxy actually does
A reverse proxy sits between the internet and your application. Clients talk to the proxy. The proxy talks to your app. The proxy handles TLS termination, connection pooling, static file serving, and request buffering. Your app only sees traffic from 127.0.0.1. Think of it like a receptionist. Visitors hand their requests to the receptionist. The receptionist checks the directory, forwards the request to the right office, and hands the response back. Your application never deals with the outside world directly.
On Fedora, Nginx ships as a standard package. It uses a modular configuration layout. The main config lives in /etc/nginx/nginx.conf. You never edit that file directly. You drop server blocks into /etc/nginx/conf.d/. Nginx includes everything in that directory automatically. This keeps your custom rules separate from package updates. Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/.
Run journalctl -xe when troubleshooting services. The x flag adds explanatory text and the e flag jumps to the end. Most sysadmins type journalctl -xeu nginx muscle-memory style to isolate daemon logs quickly.
Install and configure Nginx
Install the package first. Fedora's default repositories contain a well-maintained Nginx build. Use dnf upgrade --refresh for weekly maintenance. Use dnf system-upgrade only when crossing major Fedora releases. They are different commands. Don't conflate them.
sudo dnf install nginx -y
# -y skips the confirmation prompt. The package manager resolves dependencies and places binaries in /usr/bin.
Enable the service so it starts on boot and runs immediately.
sudo systemctl enable --now nginx
# enable creates the systemd symlink for boot persistence. --now starts the unit right away.
Check the default state before adding your proxy block.
systemctl status nginx
# Shows the active state, recent journal lines, and the main PID. Always verify the unit is running before editing configs.
Create the proxy configuration file. Use a descriptive name so you can find it later.
sudo tee /etc/nginx/conf.d/reverse-proxy.conf > /dev/null <<'EOF'
server {
listen 80;
server_name example.com;
# server_name restricts this block to your domain. Omit it to catch all traffic on port 80.
location / {
proxy_pass http://127.0.0.1:3000;
# Forwards the request to your backend. Use 127.0.0.1 instead of localhost to avoid IPv6 resolution delays.
proxy_set_header Host $host;
# Passes the original Host header so your app knows which domain was requested.
proxy_set_header X-Real-IP $remote_addr;
# Tells your app the actual client IP instead of the proxy IP.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Appends the client IP to the forwarded chain for logging and rate limiting.
proxy_set_header X-Forwarded-Proto $scheme;
# Informs the backend whether the original request used HTTP or HTTPS.
proxy_buffering on;
# Buffers the backend response before sending it to the client. Prevents slow clients from tying up backend workers.
proxy_connect_timeout 60s;
# Sets the maximum time to wait for a backend connection. Adjust if your app initializes slowly.
}
}
EOF
# tee writes the heredoc to the file as root. The > /dev/null suppresses the duplicate terminal output.
Test the configuration syntax before reloading. Nginx will refuse to apply broken rules.
sudo nginx -t
# Checks for syntax errors and missing includes. Returns "syntax is ok" and "test is successful" on a clean run.
Reload the daemon to apply the new block. A reload keeps existing connections alive while spawning new worker processes with the updated config.
sudo systemctl reload nginx
# Reload is safer than restart. It avoids dropping active client connections during the transition.
Open the firewall for HTTP traffic. Fedora enables firewalld by default. If you skip this step, external clients will time out.
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reload
# --permanent writes to the persistent zone file. --reload applies the change to the running kernel rules.
Always run firewall-cmd --reload after every rule change. Otherwise the runtime config and the persistent config diverge. SELinux handles process-to-port mapping. Nginx is allowed to bind to port 80 out of the box. If you change the listen port to something non-standard, you will need to adjust the SELinux port type. For standard HTTP, no extra policy is required.
Run nginx -t before every config change. A syntax error in production drops all traffic until you fix it.
Verify the proxy is routing correctly
Test the routing from the command line. Use curl to hit the proxy and inspect the response headers.
curl -I http://localhost
# -I requests only the headers. Look for HTTP/1.1 200 OK and the Server: nginx line.
Check the Nginx access log to confirm the proxy forwarded the request.
sudo tail -n 5 /var/log/nginx/access.log
# Shows the last five requests. Verify the upstream address matches your backend port.
Verify your backend sees the correct headers. Your application should log the X-Real-IP and X-Forwarded-Proto values. If your app still reports 127.0.0.1 as the client IP, the proxy headers are not reaching it. Check your app's trust proxy configuration. Frameworks like Express, FastAPI, and Django require explicit header trust settings to accept forwarded IPs.
Run a quick connectivity test from an external machine or a different network. If the proxy works locally but fails externally, the issue is almost always the firewall or a missing DNS record. Check your domain's A record points to the server's public IP. Verify the firewall allows inbound HTTP. Test with curl -v http://your-domain.com from a remote host.
Check the error log if the access log shows 502 or 504 responses. The error log contains upstream connection failures and timeout details. Read the actual error before guessing.
Common pitfalls and error messages
Nginx will refuse to start if the configuration contains a syntax error or a duplicate listen directive. You will see this in the journal:
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Another process is already holding port 80. Run sudo ss -tlnp | grep :80 to find the culprit. Apache, another Nginx instance, or a container might be occupying the port. Stop the conflicting service before reloading Nginx.
If your backend returns a 502 Bad Gateway, the proxy cannot connect to the upstream address. The error log will show:
connect() failed (111: Connection refused) while connecting to upstream
Your application is not listening on the expected port, or it is bound to 127.0.0.1 but the proxy is trying to reach a different interface. Verify the backend is running and accepting connections on 127.0.0.1:3000. Check your app's bind address configuration. Many frameworks default to 0.0.0.0 in production but 127.0.0.1 in development.
SELinux denials appear when you point the proxy to a non-standard port or a custom socket path. The audit log will contain:
type=AVC msg=audit(...): avc: denied { name_connect } for pid=... comm="nginx" dest=... scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket
Read the one-line summary in journalctl -t setroubleshoot. It will tell you exactly which port needs a label. Run sudo semanage port -a -t http_port_t -p tcp 8080 to allow Nginx to reach port 8080. Never disable SELinux to fix a proxy routing issue. Adjust the policy instead.
Configuration drift happens when you edit files in /usr/lib/nginx/. Those files belong to the package. Updates will overwrite them. Always place custom blocks in /etc/nginx/conf.d/. Trust the package manager. Manual file edits drift, snapshots stay.
Reboot before you debug. Half the time the symptom is gone.
When to use Nginx versus alternatives
Use Nginx when you need high concurrency, low memory footprint, and robust static file serving. Use Apache when you require heavy .htaccess usage, complex per-directory overrides, or legacy PHP-FPM setups that rely on mod_php. Use Caddy when you want automatic HTTPS and zero-config TLS management out of the box. Use Traefik when you are running Docker or Kubernetes and need dynamic service discovery. Use HAProxy when you are building a load balancer for thousands of backend instances and need advanced health checks.
Pick the tool that matches your deployment model. Do not force a proxy into a role it was not designed for.