Installing and configuring nginx

Installing nginx, where server blocks live on Debian and RHEL, serving a static site, proxying to an app port, and reading the logs.

Linux5 min read

On this page
  1. Installing
  2. Opening the ports
  3. Where the configuration lives
  4. Serving a static site
  5. Reverse proxy to an application port
  6. Test before you reload
  7. Where the logs are
  8. When it will not start or will not serve
  9. Related

nginx serves files and passes requests to applications running behind it. Installing it is one command on any of the distributions below. The part worth reading is where each family expects your configuration to go, because that differs, and copying the wrong layout is the usual first mistake.

Installing

On Debian and Ubuntu:

Shell
apt update
apt install -y nginx

On RHEL, Alma, and Rocky:

Shell
dnf install -y nginx

Then start it and have it come back after a reboot:

Shell
systemctl enable --now nginx
systemctl status nginx

Confirm it answers on the machine itself before you involve the network:

Shell
curl -I http://127.0.0.1/

Opening the ports

A working nginx that nobody can reach is a firewall problem, and it needs two things, not one.

In the host firewall, with ufw:

Shell
ufw allow 80/tcp
ufw allow 443/tcp

or with firewalld:

Shell
firewall-cmd --permanent --add-service=http --add-service=https
firewall-cmd --reload

Then add inbound rules in the panel for TCP 80 and TCP 443. See Port rules, and Using a firewall on your Linux server for how the two relate.

Where the configuration lives

Debian and UbuntuRHEL, Alma, and Rocky
Main file/etc/nginx/nginx.conf/etc/nginx/nginx.conf
Where you add a site/etc/nginx/sites-available/, then symlink into /etc/nginx/sites-enabled//etc/nginx/conf.d/, one .conf file per site
Default site/etc/nginx/sites-available/defaultA server block inside nginx.conf itself
Default document root/var/www/html/usr/share/nginx/html
Worker userwww-datanginx

The default site answers any request that does not match one of your server_name values, so it can quietly shadow a site you thought you had configured. On Debian and Ubuntu, remove the symlink once your own site works:

Shell
rm /etc/nginx/sites-enabled/default

On the RHEL family the default lives in nginx.conf, so comment out that server block rather than deleting the file.

Serving a static site

Create the directory and put your files in it:

Shell
mkdir -p /var/www/example.com
chmod -R a+rX /var/www/example.com

The worker user needs to read the files and traverse every directory above them. That is what a+rX gives you: read on files, and execute only on directories.

Write the server block. On Debian and Ubuntu the file is /etc/nginx/sites-available/example.com; on the RHEL family it is /etc/nginx/conf.d/example.com.conf. The contents are the same:

nginx config
server {
    listen 80;
    server_name example.com www.example.com;
 
    root /var/www/example.com;
    index index.html;
 
    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;
 
    location / {
        try_files $uri $uri/ =404;
    }
}

Add listen [::]:80; only if your server has IPv6. On a machine without it, nginx fails to start on that line.

On Debian and Ubuntu, enable the site by linking it:

Shell
ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

On the RHEL family, files in conf.d are active as soon as they exist. Either way, test and reload:

Shell
nginx -t
systemctl reload nginx

Reverse proxy to an application port

Run your application on loopback so it is only reachable through nginx, then proxy to it:

nginx config
server {
    listen 80;
    server_name app.example.com;
 
    location / {
        proxy_pass http://127.0.0.1:3000;
        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_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";
        proxy_read_timeout 300s;
    }
}

The Upgrade and Connection headers are what let WebSocket connections through. Without them a WebSocket handshake gets a plain HTTP response and the application appears to connect and then go silent.

Check the application is listening where you think it is:

Shell
ss -tlnp | grep 3000

If it shows 0.0.0.0:3000, the application is reachable directly from the internet as well as through nginx. Bind it to 127.0.0.1 in its own configuration, and do not rely on the firewall to hide it.

Your server, 203.0.113.10Bound to 127.0.0.1:3000ss shows 127.0.0.1:3000A visitor on the internetTries app.example.com, and 203.0.113.10:3000port 80port 3000nginxProxies to127.0.0.1:3000Nothing thereThe app is not onthe public addressYour app on port 3000Reachable only through nginxYour server, 203.0.113.10Bound to 0.0.0.0:3000ss shows 0.0.0.0:3000A visitor on the internetTries app.example.com, and 203.0.113.10:3000port 80port 3000nginxProxies to127.0.0.1:3000Port 3000 answersThe app, reacheddirectly, skippingnginxYour app on port 3000Reachable through nginx, and directlyYour server, 203.0.113.10Bound to 127.0.0.1:3000ss shows 127.0.0.1:3000A visitor on the internetTries app.example.com, and 203.0.113.10:3000port 80port 3000nginxProxies to127.0.0.1:3000Nothing thereThe app is not onthe public addressYour app on port 3000Reachable only through nginxYour server, 203.0.113.10Bound to 0.0.0.0:3000ss shows 0.0.0.0:3000A visitor on the internetTries app.example.com, and 203.0.113.10:3000port 80port 3000nginxProxies to127.0.0.1:3000Port 3000 answersThe app, reacheddirectly, skippingnginxYour app on port 3000Reachable through nginx, and directly
The same app, bound two ways. On 127.0.0.1 the only way in is through nginx, and a visitor trying port 3000 finds nothing; on 0.0.0.0 the app answers on port 3000 too, and everything nginx does in front of it is skipped. Illustration.

Test before you reload

Shell
nginx -t

That parses the whole configuration, including every included file, and checks that the certificates and roots it references exist. It prints the file and line of the first problem. Only when it passes:

Shell
systemctl reload nginx

Reload starts new workers with the new configuration and lets the old ones finish what they were doing, so no request is dropped. Use restart only when you have changed something a reload cannot pick up, such as the worker user, and know that it drops connections in progress.

nginx -t does not check anything that only exists at request time. A wrong proxy_pass port, an application that is not running, or a DNS name that does not resolve all pass the test and fail on the first request.

Where the logs are

The defaults are /var/log/nginx/access.log and /var/log/nginx/error.log. Per site, use the access_log and error_log directives shown above, which keeps one site's traffic out of another's file.

Shell
tail -f /var/log/nginx/error.log

If nginx will not start at all, the reason is often not in the error log yet, because it failed before opening it. Look at the service instead, as described in Managing services with systemd:

Shell
journalctl -u nginx -n 50 --no-pager

When it will not start or will not serve

Address already in use. Something else holds port 80, often another web server installed with a control panel:

Shell
ss -tlnp | grep ':80'

403 on files you can read as root. Every directory in the path needs execute permission for the worker user, and a file under a home directory usually fails that. Serve from /var/www and set a+rX as above.

403 on the RHEL family with correct permissions. SELinux is refusing a document root outside the default. Label it:

Shell
dnf install -y policycoreutils-python-utils
semanage fcontext -a -t httpd_sys_content_t "/var/www/example.com(/.*)?"
restorecon -Rv /var/www/example.com

A duplicate server_name warning from nginx -t. Two server blocks claim the same name on the same port, usually because a site is both in conf.d and in nginx.conf, or linked twice. The first one loaded wins and the other is ignored.

Need a server, or a hand with one?

Pick a plan to deploy a server, or open a ticket in the panel. Support runs 24/7/365.