Skip to main content

Configuring Nginx for a Website on VDS

VDS / VPS · 20.02.2025 · 5 min read
Illustration for “Configuring Nginx for a Website on VDS”

An Nginx virtual host is a server block that ties a domain name, a site root, and the PHP handler together into one config. On a VDS with several sites, these blocks are exactly what decides which request goes where.

Nginx Virtual Host Configuration

Create a separate config file for the site — do not edit default directly once there is more than one site:

nano /etc/nginx/sites-available/yourdomain.com

File contents — a minimal working block for a PHP site:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/yourdomain.com;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}

Enable the config with a symlink into sites-enabled — Nginx reads configs from that folder specifically, while sites-available is just storage:

ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Several Sites on One IP

Nginx picks the right block based on the Host header from the request, matching it against the list in server_name. If no block matches, the block with default_server on its listen directive fires — without it, the first config read by file order becomes the implicit default, which can occasionally produce an unexpected result when a new site is added.

server {
    listen 80 default_server;
    server_name _;
    return 444;
}

A stub block like this with return 444; drops the connection for any request with a domain that is not explicitly configured — a useful defense against bots that scan IP addresses directly, bypassing DNS.

Gzip Compression

gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
gzip_comp_level 5;

gzip_min_length skips compressing very small files, where the gain is smaller than the overhead of packing them, while gzip_comp_level in the 5–6 range is a reasonable balance between saving bandwidth and CPU load; level 9 barely improves the size but is noticeably more expensive on the processor.

Browser Caching for Static Assets

location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}

Common Errors Explained

The error nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use) means port 80 is already taken by another process — most often Apache left running, or a second Nginx instance. Check the ports in use with ss -tulpn | grep :80 and stop the extra process.

502 Bad Gateway — Nginx did not get a response from the backend named in fastcgi_pass or proxy_pass. The exact cause is always in /var/log/nginx/error.log: a line like connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied) points to socket access permissions, while (111: Connection refused) means PHP-FPM simply is not running.

404 on every page except the home page in projects with "pretty" URLs usually means the try_files directive is not set up to fall through to index.php — Nginx honestly looks for a file on disk matching the route name and does not find one.

Comparing Routing Approaches in Nginx

ApproachWhere it is setFlexibilityTypical case
try_filesInside location /Medium, order is fixedServing static files with a fallback to index.php
rewriteInside server or locationHigh, regular expressionsLegacy links, pretty URLs without a framework
returnInside server or locationLow, a direct responseRedirects, dropping the connection (444)
proxy_passInside locationHigh, full forwarding to a backendNode.js, Python, reverse proxying

A deeper look at location block priority in Nginx, and which block wins when rules overlap, is in the article on location priority in Nginx (it also walks through examples of configuration conflicts).

When a Bare Nginx Config Is Not Enough

  • The site needs HTTPS — the certificate and the HTTP-to-HTTPS redirect are added separately, covered in the article on installing SSL with Certbot.
  • Many identical sites run on one server — instead of copying the config for every domain, it is more convenient to move shared blocks into include files.
  • You need protection against brute-forcing or DDoS at the request level — a plain server block will not help here, you need limit_req, limit_conn, or an external WAF.
  • The backend is not PHP-FPM but a separate application on its own port (Node.js, Python, Go) — instead of fastcgi_pass, you use proxy_pass with its own set of headers.

Verifying the Result

  1. Check the syntax before every change: nginx -t should return syntax is ok and test is successful.
  2. Open the site by domain and by IP separately — if it works by domain, but the IP shows someone else's site, server_name is set up correctly; if the IP shows this same site without a default_server, it is worth checking why.
  3. Check the response headers: curl -I http://yourdomain.com — a Content-Encoding: gzip header confirms compression is actually working, not just enabled in the config.
  4. Watch the access log for 404s and 502s right after deployment: tail -f /var/log/nginx/access.log shows real user requests, not just test ones from a terminal.
Was this article helpful?
← Back to Knowledge Base Ask Support