Story / scenario opener
You just deployed a Node.js application on port 3000. It responds perfectly on localhost. You want to expose it to the internet behind a proper domain name. You open the nginx configuration file, paste a server block, and suddenly the service refuses to start. The terminal spits out a cryptic syntax error, and your browser shows a default welcome page instead of your app.
What's actually happening
Nginx does not read configuration files the way a web browser reads HTML. It parses a strict hierarchy of directives before it even thinks about listening for connections. The main file at /etc/nginx/nginx.conf sets global worker processes and includes site-specific files from /etc/nginx/conf.d/. When you drop a new file into that directory, nginx merges it with the global settings. If the syntax is off, or if a port is already bound, the entire daemon refuses to launch.
Think of it like a theater stage manager. The manager checks every prop and cue sheet before the curtain rises. If one cue sheet has a typo, the show does not start. This strict validation protects your server from serving broken configurations to real users. Nginx uses a master-worker architecture. The master process reads the configuration, opens the listening sockets, and spawns worker processes. The workers handle the actual connections. When you change a configuration file, you are telling the master process to re-read its instructions and hand new ones to the workers. The workers do not guess. They follow the exact syntax you provide.
Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/. Fedora follows this convention strictly. Package updates will overwrite /usr/lib/ files and silently destroy your custom routing rules. Keep your site definitions in /etc/nginx/conf.d/ and leave the vendor files alone.
Run nginx -t before you touch systemd. Half the time the symptom is a missing semicolon, not a broken service.
The fix or how-to
Start by creating a dedicated configuration file for your site. Never edit the main nginx.conf directly for site routing. Fedora ships nginx with a clean separation of concerns. Global settings live in the main file. Site routing lives in conf.d/.
Create the file with your editor of choice. I will use nano for this example, but vim or vscode work equally well.
sudo nano /etc/nginx/conf.d/example.com.conf
# WHY: sudo grants root access to write in /etc/nginx/conf.d/
# WHY: conf.d/ is the standard drop-in directory for site configurations
# WHY: naming the file after the domain makes future maintenance obvious
Paste the server block into the file. Adjust the server_name and proxy_pass values to match your actual domain and backend port.
server {
listen 80;
# WHY: binds the server to port 80 for incoming HTTP traffic
server_name example.com;
# WHY: matches incoming Host headers to route requests to this block
location / {
proxy_pass http://localhost:3000;
# WHY: forwards the request to your backend application
proxy_set_header Host $host;
# WHY: preserves the original domain name for the backend
proxy_set_header X-Real-IP $remote_addr;
# WHY: passes the client's actual IP address through the proxy
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WHY: builds a chain of client IPs for upstream logging
proxy_set_header X-Forwarded-Proto $scheme;
# WHY: tells the backend whether the original request was HTTP or HTTPS
}
}
Save the file and exit the editor. Before you restart the service, you must validate the syntax. Nginx will not tell you which file contains the error if you skip this step. It will just fail to start and leave you guessing.
sudo nginx -t
# WHY: parses all configuration files and checks for syntax errors
# WHY: reports the exact line number and file path if something is wrong
# WHY: exits with a non-zero code if the configuration is invalid
If the test passes, you will see a message confirming the configuration is okay and the test is successful. Apply the changes without dropping existing connections.
sudo systemctl reload nginx
# WHY: reloads the configuration without stopping the master process
# WHY: existing client connections finish normally while new ones use the updated rules
# WHY: avoids the brief downtime window that a full restart causes
Reload the service instead of restarting it. A restart kills all active connections. A reload swaps the worker processes in place. Your users will not see a broken pipe error. If you are opening port 80 to the public network, update the firewall rules before you test.
sudo firewall-cmd --permanent --add-service=http
# WHY: adds HTTP to the persistent firewall configuration
sudo firewall-cmd --reload
# WHY: applies the persistent rules to the active runtime firewall
# WHY: prevents divergence between what you configured and what is actually filtering
firewall-cmd --reload after every rule change. Otherwise the runtime config and the persistent config diverge. Fedora's firewall daemon does not auto-sync.
Verify it worked
Point your browser to the domain name. You should see your application responding. If you are testing from the command line, use curl to inspect the response headers and status code.
curl -I http://example.com
# WHY: -I fetches only the HTTP headers, not the full page body
# WHY: confirms nginx is serving the request and proxying correctly
# WHY: shows the backend response code without downloading assets
Check the active connections to ensure nginx is routing traffic to your backend.
sudo ss -tlnp | grep -E ':(80|3000)'
# WHY: lists listening TCP sockets and filters for your ports
# WHY: confirms both nginx and your backend are bound and ready
# WHY: reveals if another process is hijacking the port
If the headers show server: nginx and your app returns a 200 status, the proxy chain is working. Run journalctl -xeu nginx if you need to inspect recent service logs. The x flag adds explanatory context and the e flag jumps to the end of the journal. Most sysadmins type journalctl -xeu <unit> muscle-memory style when debugging service state.
systemctl status <unit> shows recent log lines AND state in one view. Always check status before restart.
Common pitfalls and what the error looks like
The most frequent mistake is a missing semicolon. Nginx directives require a semicolon at the end of every line. If you forget one, the parser will complain about an unexpected token on the next line.
nginx: [emerg] unexpected "}" in /etc/nginx/conf.d/example.com.conf:10
nginx: configuration file /etc/nginx/nginx.conf test failed
The error points to the closing brace, but the actual problem is usually the line immediately before it. Read the line above the reported error. Add the missing semicolon and run nginx -t again.
Another common issue is port conflicts. If Apache or another service is already listening on port 80, nginx will refuse to bind. The system log will show a clear address already in use message.
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Stop the conflicting service or change the nginx listen directive to a different port. Fedora does not ship Apache and nginx enabled by default, but old installations or container leftovers often leave port 80 occupied.
SELinux can also block proxy connections if your backend runs on a non-standard port. Fedora enforces strict network port labeling. If your app runs on port 3000, SELinux expects it to be labeled correctly. You can check for denials in the audit log.
sudo journalctl -t setroubleshoot | tail -n 5
# WHY: filters SELinux denial summaries for recent events
# WHY: shows exactly which process was blocked and why
# WHY: provides a one-line summary before you dig into raw audit logs
If you see a denial related to nginx proxying, add the correct port label instead of disabling SELinux. Trust the package manager. Manual file edits drift, snapshots stay.
Upstream timeouts are another silent killer. If your backend takes longer than 60 seconds to respond, nginx will cut the connection and return a 504 Gateway Timeout. The error log will show a clear upstream timed out message. Adjust the proxy_read_timeout directive if your application performs heavy background tasks. Do not set it to infinity. Set it to a realistic value like 120s and fix the slow query instead.
Read the actual error before guessing. Fabricated fixes break production systems.
When to use this vs alternatives
Use a dedicated conf.d/ file when you are managing a single domain or a small group of related services. Use the main nginx.conf when you are tuning global worker processes, setting gzip compression levels, or defining shared upstream blocks. Use systemctl reload nginx when you are updating routing rules or proxy headers. Use systemctl restart nginx only when you change core daemon settings or upgrade the nginx package itself. Stay on the standard conf.d/ drop-in pattern if you only deviate from the defaults occasionally. Use dnf upgrade --refresh for weekly maintenance and dnf system-upgrade when crossing major Fedora releases. They are different commands. Don't conflate them.