f in x
Nginx from scratch: Installation, basic configuration and virtual hosts — Practical guide for developers
> cd .. / HUB_EDITORIALE
Analisi dei dati e metriche

Nginx from scratch: Installation, basic configuration and virtual hosts — Practical guide for developers

[2026-06-02] Author: Ing. Calogero Bono

The web server is the front door to your site. If it's misconfigured, everything else — perfect code, optimized database, CDN — is wasted. We see it every day from developers who bring us projects on a VPS: Nginx installed with Ubuntu defaults, a single virtual host serving everything, wrong permissions, disabled logs. Then they wonder why the site is slow or crashes with 50 concurrent visitors.

We, at Meteora Web, have been working with Nginx since 2017. We use it for high-traffic WordPress sites, Laravel platforms, WooCommerce stores, and manage every single parameter from the terminal. In this guide, we get straight to the point: clean installation, solid base configuration, virtual hosts (server blocks) to serve multiple domains. No fluff. Just what you need to put a professional web server into production.

Why Nginx and not Apache (or something else)

Nginx was built to solve the C10K problem — handling ten thousand concurrent connections with minimal resource consumption. Unlike Apache, which spawns a thread or process per connection, Nginx uses an asynchronous, event-driven model. Result: with the same RAM, Nginx serves 2 to 5 times more requests. For us, with clients that have seasonal traffic spikes (Black Friday, end-of-season sales), this difference means servers that don't crash and revenue that doesn't get lost.

Also, Nginx integrates seamlessly with PHP-FPM (for WordPress, Laravel, any CMS), acts as a reverse proxy for Node.js or Python apps, and handles SSL/TLS efficiently. It's the de facto standard for modern sites.

Clean installation on Ubuntu/Debian

Let's start from scratch. Assume a fresh VPS (Ubuntu 22.04 or 24.04, Debian 12). First step: update packages and install Nginx from the official repository.

sudo apt update
sudo apt upgrade -y
sudo apt install nginx -y

After installation, Nginx starts automatically. Check:

sudo systemctl status nginx

You should see active (running). If not, check the logs with journalctl -u nginx.

Firewall: open the right ports

Before moving on, make sure the firewall (UFW) allows HTTP and HTTPS traffic:

sudo ufw allow 'Nginx Full'

This opens ports 80 and 443. If you only need HTTP for testing, use sudo ufw allow 'Nginx HTTP'.

At this point, visiting the server's public IP in your browser should show the default Nginx page. If not, check that your cloud provider doesn't have an external firewall (e.g., security group on AWS, firewall on OVH).

Basic nginx.conf configuration

The main configuration file is /etc/nginx/nginx.conf. We open it and immediately adjust a few parameters for typical workloads.

sudo nano /etc/nginx/nginx.conf

Here is a solid starting configuration for a server hosting one or more websites (not for pure reverse proxy):

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
    # multi_accept on; # uncomment if you have many keepalives
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Log format with response times
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    'rt=$request_time uct=$upstream_connect_time uht=$upstream_header_time urt=$upstream_response_time';

    access_log /var/log/nginx/access.log main buffer=32k flush=5s;
    error_log /var/log/nginx/error.log warn;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # Limit upload size to 50MB (useful for file uploads)
    client_max_body_size 50M;

    # Gzip to save bandwidth
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript image/svg+xml;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Key choices explained:

  • worker_processes auto: Nginx sets the number of worker processes equal to the number of CPU cores. For most VPS (1-4 cores), this is perfect.
  • worker_connections 1024: max connections per worker. Total server can handle worker_processes * worker_connections = 4 * 1024 = 4096 concurrent connections. Enough to start.
  • gzip: compresses text before sending to browser. Reduces resource weight by 50-70%.
  • client_max_body_size 50M: prevents 413 errors when uploading images or backups via web. We've seen clients whose contact form couldn't accept attachments because the default was 1M.

After editing, test syntax:

sudo nginx -t

If OK, reload configuration without downtime:

sudo systemctl reload nginx

Virtual Host (Server Block): serving multiple sites with one Nginx

The real power of Nginx lies in server blocks — the equivalent of Apache's VirtualHost. Each server block defines how to respond to a specific domain. We organize files in /etc/nginx/sites-available/ (available configurations) and create symlinks in /etc/nginx/sites-enabled/ to activate them.

Standard structure

For each domain we host, we create a file like this:

server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    root /var/www/example.com/public;
    index index.php index.html index.htm;

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

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
    }

    location ~ /\.ht {
        deny all;
    }

    # Log for this site
    access_log /var/log/nginx/example.com_access.log main buffer=32k flush=5s;
    error_log /var/log/nginx/example.com_error.log warn;
}

Let's break down each directive:

  • server_name: the domains this block serves. You can use wildcards like *.example.com.
  • root: directory from which to serve files. Important: Nginx does not follow symlinks by default (good for security).
  • location /: tries to serve the exact file ($uri) or directory ($uri/), otherwise rewrites everything to index.php. This is the standard pattern for WordPress, Laravel, and Symfony.
  • location ~ \.php$: passes PHP files to PHP-FPM via Unix socket. Faster than TCP port.
  • location ~ /\.ht: blocks access to .htaccess files (Nginx doesn't read them, but it's good practice to hide them).

Activating a site

Save the file as example.com in /etc/nginx/sites-available/. Then:

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

To disable a site, just remove the symlink: sudo rm /etc/nginx/sites-enabled/example.com and reload.

Practical example: two sites on the same server

Imagine you have two projects: an e-commerce (shop.com) and a blog (blog.com). Create two separate files in sites-available. The first:

# /etc/nginx/sites-available/shop.com
server {
    listen 80;
    server_name shop.com www.shop.com;
    root /var/www/shop.com;
    index index.php index.html;
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        include snippets/fastcgi-php.conf;
    }
    access_log /var/log/nginx/shop.com_access.log;
    error_log /var/log/nginx/shop.com_error.log;
}

The second:

# /etc/nginx/sites-available/blog.com
server {
    listen 80;
    server_name blog.com www.blog.com;
    root /var/www/blog.com/public;
    index index.php index.html;
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        include snippets/fastcgi-php.conf;
    }
    access_log /var/log/nginx/blog.com_access.log;
    error_log /var/log/nginx/blog.com_error.log;
}

Activate both:

sudo ln -s /etc/nginx/sites-available/shop.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/blog.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Now, when a browser calls shop.com, Nginx responds with content from /var/www/shop.com; for blog.com with /var/www/blog.com/public. Clean and isolated.

Common errors and how to fix them

Wrong permissions on root

One of the most frequent errors: the server block points to a directory that Nginx (user www-data) cannot read. Result: 403 Forbidden. Solution: ensure www-data has at least read and execute permissions on the directory and files. We usually do:

sudo chown -R www-data:www-data /var/www/mysite
sudo find /var/www/mysite -type d -exec chmod 755 {} \;
sudo find /var/www/mysite -type f -exec chmod 644 {} \;

If the site writes files (e.g., WordPress uploads), give write permissions only to the necessary directory:

sudo chmod 775 /var/www/mysite/wp-content/uploads

Server block not activated

You created the file in sites-available but forgot the symlink in sites-enabled. Nginx ignores the file. Always check with ls -la /etc/nginx/sites-enabled/.

Server name not resolving

If you visit the domain and land on the default Nginx page (or another site), the server_name probably doesn't match what you're calling. This can happen if you have a default server block without a server_name that catches everything. We recommend disabling the default:

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

The alternative is to create a catch-all server block that returns 444 (no response):

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

PHP files downloaded instead of executed

This happens when the PHP location block is missing, or the PHP-FPM socket is not active. Check that php8.3-fpm.sock exists (ls /var/run/php/) and the service is running (sudo systemctl status php8.3-fpm).

In summary — what to do now

You now have the basics to put an Nginx server into production with a solid configuration and multiple virtual hosts. Don't stop here: the next steps are SSL with Let's Encrypt (certbot), hardening, static resource caching, and monitoring.

  1. Install Nginx on a test VPS (DigitalOcean, Hetzner, or locally with Vagrant).
  2. Apply the basic configuration we gave you, test with nginx -t.
  3. Create two server blocks for two domains (even fake ones, editing your local hosts file). Verify they respond correctly.
  4. Enable per-site logs to diagnose specific issues.
  5. Set correct permissions for site directories.

If you work with microservices or complex architectures, our article on Microservices vs Monolith will help you understand when Nginx as a reverse proxy is the right choice. For deeper monitoring, read the guide to the three pillars of observability.

A well-configured web server is like an organized warehouse: everything is easy to find, nothing breaks, and you work twice as fast. As we always say: a server is not a black box; it's the foundation of your digital business. Treat it as such.

Sponsored Protocol

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Co-founder di Meteora Web. Ingegnere informatico, sviluppo ecosistemi digitali ad alte prestazioni. AI, automazione, SEO tecnica e infrastrutture web. Scrivo di tecnologia per rendere complesso… semplice.

[ Read Full Dossier ]

Hai bisogno di applicare questa strategia?

Esegui il protocollo di contatto per iniziare un progetto con noi.

> INIZIA_PROGETTO

Sponsored

> MW_JOURNAL

> READ_ALL()