How to Install and Configure nginx
Install nginx, configure server blocks and document roots, set up a reverse proxy to a local application, and verify everything with nginx -t and curl.
Before you start
- ▸A Linux server with sudo or root access
- ▸A working package manager connection to the internet
- ▸Basic familiarity with a terminal text editor (nano, vim)
- ▸An application listening on a local port if configuring the reverse proxy section
nginx is a high-performance web server and reverse proxy used on a significant share of the internet's busiest sites. Installing it takes seconds; configuring it correctly—server blocks, document roots, SSL-aware reverse proxy passes—takes a clear mental model. This guide covers a full working setup: default server block, a named virtual host, a reverse proxy to an upstream application, and validating every change before reloading.
Installation
Debian / Ubuntu
sudo apt update
sudo apt install -y nginx
Fedora / RHEL 9 / Rocky Linux
sudo dnf install -y nginx
Arch Linux
sudo pacman -S nginx
Enable and start the service with systemd on all distros:
sudo systemctl enable --now nginx
Confirm it is running:
systemctl status nginx
You should see active (running). Hit http://localhost in a browser or with curl and you will get the default nginx welcome page.
Understanding the Directory Layout
nginx's layout differs slightly between distros, but the logical structure is the same:
| Path | Purpose | Distro note |
|---|---|---|
/etc/nginx/nginx.conf | Main config, sets global directives and includes site configs | All |
/etc/nginx/sites-available/ | Server block files (not yet active) | Debian/Ubuntu |
/etc/nginx/sites-enabled/ | Symlinks to active server blocks | Debian/Ubuntu |
/etc/nginx/conf.d/ | Drop-in config files (all loaded automatically) | Fedora/RHEL/Arch |
/var/www/html/ | Default document root | Debian/Ubuntu |
/usr/share/nginx/html/ | Default document root | Fedora/RHEL/Arch |
On Fedora/RHEL/Arch you can adopt the sites-available / sites-enabled pattern by adding the appropriate include lines to nginx.conf, or simply drop files into conf.d/. This guide uses conf.d/ as a common denominator and notes the Debian symlink step where it differs.
Creating a Server Block (Virtual Host)
A server block tells nginx which hostname and port to answer on, and where to find files. Create a document root and a minimal HTML page first:
sudo mkdir -p /var/www/example.com/html
echo '<h1>It works</h1>' | sudo tee /var/www/example.com/html/index.html
sudo chown -R www-data:www-data /var/www/example.com # Debian/Ubuntu
# On Fedora/RHEL/Arch the web user is 'nginx':
# sudo chown -R nginx:nginx /var/www/example.com
Now write the server block. On Debian/Ubuntu use /etc/nginx/sites-available/example.com; on Fedora/RHEL/Arch use /etc/nginx/conf.d/example.com.conf:
sudo nano /etc/nginx/conf.d/example.com.conf
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html index.htm;
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;
}
}
On Debian/Ubuntu, activate the block with a symlink:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
# then remove or disable the default symlink if you want this to be the only site:
sudo rm /etc/nginx/sites-enabled/default
Testing the Configuration
Always test before reloading. nginx -t parses every included file and reports syntax errors with file names and line numbers:
sudo nginx -t
Expected output (will vary slightly):
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If the test passes, reload without dropping connections:
sudo systemctl reload nginx
Never use restart in production for config changes. reload sends SIGHUP, which gracefully reopens files and applies the new config while existing connections finish.
Configuring a Reverse Proxy
A common pattern is nginx sitting in front of an application server (Node.js, Gunicorn, a Java service, etc.) running on a local port. nginx handles TLS termination, compression, and static file serving; the application handles dynamic requests.
Assume your application listens on 127.0.0.1:3000. Create a new server block:
sudo nano /etc/nginx/conf.d/app.example.com.conf
upstream app_backend {
server 127.0.0.1:3000;
# Add more servers here for basic round-robin load balancing:
# server 127.0.0.1:3001;
}
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;
# Optional: serve static assets directly from disk
location /static/ {
root /var/www/app.example.com;
expires 30d;
add_header Cache-Control "public, no-transform";
}
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
# Pass real client info to the application
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;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
The upstream block is optional for a single backend, but it makes adding servers later trivial and lets you name the backend clearly in logs. The Upgrade and Connection headers are required for WebSocket proxying and harmless otherwise.
Test and reload again:
sudo nginx -t && sudo systemctl reload nginx
Opening the Firewall
If a firewall is active, allow HTTP (and HTTPS when you add TLS).
ufw (Debian / Ubuntu)
sudo ufw allow 'Nginx Full'
sudo ufw reload
firewalld (Fedora / RHEL / Rocky)
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload
nftables (Arch and anywhere running nftables directly)
# Add to your nftables ruleset, e.g. /etc/nftables.conf
# tcp dport { 80, 443 } accept
Verification
Confirm nginx is serving the right content for each hostname. If you do not have DNS set up yet, pass the Host header manually:
curl -H 'Host: example.com' http://127.0.0.1/
curl -H 'Host: app.example.com' http://127.0.0.1/
Check that the access log for each site is receiving entries:
sudo tail -f /var/log/nginx/example.com.access.log
Verify the full config parse including all included files:
sudo nginx -T | less
-T (uppercase) dumps the parsed, concatenated config to stdout—useful for auditing exactly what nginx sees at runtime.
Troubleshooting
- 502 Bad Gateway from the reverse proxy: The upstream application is not listening. Confirm with
ss -tlnp | grep 3000. Check the application's own logs. - 403 Forbidden on static files: Almost always a file-system permission problem. nginx worker processes run as
www-data(Debian/Ubuntu) ornginx(Fedora/RHEL/Arch). The document root and all parent directories must be executable by that user:sudo chmod o+x /var/www/example.com. - Changes not taking effect: You may have edited the wrong file. Run
sudo nginx -Tand search for the relevantserver_nameto confirm which file is actually loaded. - SELinux blocking connections (Fedora/RHEL/Rocky): If nginx cannot connect to the upstream, SELinux may be enforcing. Check with
sudo ausearch -m avc -ts recentand allow withsudo setsebool -P httpd_can_network_connect 1. - Port 80 already in use: Run
sudo ss -tlnp | grep ':80'to find the occupying process. Apache (apache2/httpd) is the usual culprit—stop and disable it before starting nginx.
Frequently asked questions
- What is the difference between nginx reload and nginx restart?
- reload sends SIGHUP to the master process, which re-reads config and gracefully hands off to new worker processes while existing connections finish. restart kills all processes immediately, dropping active connections—avoid it in production for config changes.
- Can I run multiple sites on the same server with nginx?
- Yes. Create one server block file per site, each with a unique server_name. nginx matches incoming requests to the correct block based on the Host header. If no block matches, nginx falls back to the default_server block.
- How do I add HTTPS / TLS to a server block?
- The recommended approach is Certbot with the nginx plugin (sudo certbot --nginx -d example.com), which obtains a Let's Encrypt certificate and rewrites your server block automatically. It adds a listen 443 ssl block and configures renewal.
- Why do I get a 502 Bad Gateway when using the reverse proxy?
- nginx reached the upstream address but got no response. The most common causes are: the backend application is not running, it is listening on a different port than configured, or on Fedora/RHEL SELinux is blocking the outbound connection (fix with setsebool -P httpd_can_network_connect 1).
- What does the try_files directive actually do?
- try_files $uri $uri/ =404 tells nginx to first look for an exact file match, then a directory with an index file, and return a 404 if neither exists. Without it, nginx may pass requests for missing static files to an index script unintentionally.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.