The zone file example.local that we created in Chapter 9 already stores the record www IN A 192.168.1.20, so that domain name is already pointing to the correct address. The problem is, a correct DNS does not automatically mean there is a service running at that address. Prove it yourself by running the following command from another computer on the same network.
curl http://192.168.1.20The result is not a web page, but a message:
curl: (7) Failed to connect to 192.168.1.20 port 80: Connection refusedThe DNS is indeed correct, but there is no process listening on port 80 at that IP yet. Our next task is to start the software that actually responds to HTTP requests at that address, and Nginx is the answer. This chapter opens Part IV of the course, a section that specifically discusses web services.
We will start from the installation and configuration directory structure of Nginx, then create a virtual host (server block) for the example.local domain already registered in the DNS. After that, we will practice how to serve static content and the basics of reverse proxy to backend applications, before closing the chapter by learning how to read access logs and error logs for daily verification and troubleshooting needs.
12.1 Installation and Configuration Structure of Nginx
Nginx (pronounced "engine-x") is a web server as well as a reverse proxy originally written by Igor Sysoev to solve the C10K problem, which is the challenge of serving tens of thousands of concurrent connections without excessively overloading server resources. Unlike Apache, which traditionally creates a single process or thread for every connection, Nginx uses an event-driven architecture that is far more memory efficient when handling many connections simultaneously. This characteristic is what makes Nginx a popular choice, both as a pure web server and as a reverse proxy in front of backend applications, a topic we will delve deeper into in Chapter 16.
12.1.1 Installing Nginx from the Ubuntu Repository
Ubuntu Server provides Nginx directly in the official repository, so its installation does not differ from other packages we have installed in previous chapters.
Practical Steps
- Update the package list, then install Nginx.
This command actually installssudo apt update sudo apt install nginxnginxas a metapackage that pulls one of the variants, generallynginx-corewhich is lightweight and sufficient for standard web server needs. Other variants such asnginx-fullornginx-extrasare only relevant if we need additional modules that are not included in the core build. - Unlike
chronyin Chapter 11 which was installed from the start, the Debian/Ubuntu package for Nginx automatically enables and starts its service as soon as the installation completes. Confirm its status.systemctl status nginx.service - Open HTTP port access in UFW so visitors from outside the server can reach Nginx.
sudo ufw allow 80/tcp - Test from another computer on the same network, this time pointing to the IP of the server where Nginx is running.
If successful, the response will be a simple HTML page titled "Welcome to nginx!", a sign that Nginx is already serving requests with its default configuration.curl http://192.168.1.20
Verification and Troubleshooting
- Check the version of Nginx that is actually installed, because the exact version may differ depending on when the Ubuntu repository was last synchronized.
nginx -v - If
systemctl statusshows that the service failed to start, the most common cause is that port 80 is already used by another process. Check with:
This case often happens if Apache (Chapter 13) was previously installed on the same server. Both web servers cannot run simultaneously on the same port without a special configuration.sudo ss -tlnp | grep :80 - If curl from another computer still fails even though the service is
active (running), recheck the UFW rules.sudo ufw status
12.1.2 Anatomy of Nginx Configuration Directory
Before creating our own configuration, it is important to first understand how Nginx configuration files are interconnected. A misunderstanding about this structure is one of the most frequent sources of troubleshooting in the field, especially when saved changes turn out to have never actually been read by Nginx.
Practical Steps
- View the contents of the main configuration file.
The contents are more or less as follows, although details such ascat /etc/nginx/nginx.confworker_connectionsmight slightly differ depending on the installed package version.
The last twouser www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 768; } http { include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; gzip on; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; }includelines in thehttpblock are the key to understanding this structure. Those lines tell Nginx to read every file in/etc/nginx/conf.d/and every file in/etc/nginx/sites-enabled/as part of the active configuration. - View the contents of both directories.
ls -la /etc/nginx/sites-available/ ls -la /etc/nginx/sites-enabled/
/etc/nginx/sites-available/ contains all virtual host configuration files we have ever created, active or not, whereas /etc/nginx/sites-enabled/ only contains symbolic links pointing back to specific files in sites-available. This two-directory convention is actually not a built-in Nginx feature, but a typical Debian and Ubuntu packaging convention to make it easier for us to enable or disable a site simply by adding or removing a symlink, without needing to delete the configuration file at all. The default server block named default is already active from the start; that is what answered our curl earlier with the "Welcome to nginx!" page, and its contents are served from /var/www/html.
Verification and Troubleshooting
- Every time you change the Nginx configuration, make it a habit to test its syntax first before applying it. We will continue using this habit throughout all sections of this chapter.
A healthy output showssudo nginx -tsyntax is okandtest is successful. If there is an incorrect line, the error message usually specifies the file name and line number involved, so there is no need to guess. - In the field, the most common mistake made by beginners is editing a file in
sites-availablebut forgetting to create its symlink insites-enabled, so changes are never read even thoughnginx -tstill reports valid syntax.
12.2 Virtual Host (Server Block)
In Nginx, a single configuration block defining how a domain is served is called a server block, a term equivalent to virtual host in Apache (Chapter 13). A server block allows a single physical server or IP address to serve multiple domains simultaneously, each with a different document root, configuration, or even backend application. Its mechanism relies on the Host header sent by the browser in every HTTP request; that is how Nginx knows which domain the visitor is requesting even though everything points to the same IP address.
12.2.1 Creating a Server Block for example.local
Now we will replace the default built-in default server block with our own server block, which actually serves pages for the example.local and www.example.local domains according to the DNS records we registered in Chapter 9.
Practical Steps
- Create a dedicated document root directory for this domain, separate from
/var/www/htmlbelonging to the default server block.sudo mkdir -p /var/www/example.local/html - Create a simple index page for testing.
echo "<h1>The example.local page is running on Nginx</h1>" | sudo tee /var/www/example.local/html/index.html - Set the directory ownership to match the user used by Nginx worker processes, which is
www-data, according to theuser www-data;line we saw innginx.conf.sudo chown -R www-data:www-data /var/www/example.local - Create a new server block file in
sites-available.sudo nano /etc/nginx/sites-available/example.local - Fill it with the following configuration.
Theserver { listen 80; listen [::]:80; server_name example.local www.example.local; root /var/www/example.local/html; index index.html; location / { try_files $uri $uri/ =404; } }server_namedirective determines which domains are handled by this server block,rootpoints to its document root, andtry_filesin thelocation /block instructs Nginx to search for files matching the requested$uri, trying as a directory if the file is not found, and returning404if both still do not exist. - Enable this server block by creating a symlink to
sites-enabled.sudo ln -s /etc/nginx/sites-available/example.local /etc/nginx/sites-enabled/ - Disable the
defaultserver block so it no longer acts as a confusing fallback when we add other domains later.
The original file insudo rm /etc/nginx/sites-enabled/defaultsites-available/defaultis kept as a reference; only its active symlink is removed. - Test the syntax, then apply the changes.
Usesudo nginx -t sudo systemctl reload nginxreloadinstead ofrestartfor routine configuration changes like this.reloadcauses Nginx to re-read the configuration without interrupting ongoing connections, whereasrestartshuts down all processes before starting them again, which risks causing a brief outage on production services. - Test from another computer on the same network, this time via the domain name because the internal DNS from Chapter 9 already recognizes it.
curl http://www.example.local
Verification and Troubleshooting
- If the error
[emerg] "server" directive is not allowed hereappears duringnginx -t, the most common cause is unbalanced curly braces in the configuration file. - If curl still returns the "Welcome to nginx!" page instead of the example.local page, most likely the symlink to
sites-enabled/defaulthas not been completely removed, or a reload has not been executed after the change. - For clients that have not synchronized their DNS cache yet, test directly to the IP with a manual
Hostheader so you do not have to wait for propagation.curl -H "Host: www.example.local" http://192.168.1.20
12.2.2 Multiple Domain Name-Based Virtual Hosts
The true power of server blocks is seen when a single server serves more than one domain. This scheme is called name-based virtual hosting, because Nginx distinguishes each domain purely based on the Host header, not based on different IP addresses or port numbers.
Practical Steps
- Prepare the document root for the second domain, for example
blog.example.local.sudo mkdir -p /var/www/blog.example.local/html echo "<h1>Blog example.local</h1>" | sudo tee /var/www/blog.example.local/html/index.html sudo chown -R www-data:www-data /var/www/blog.example.local - Create a new server block for this domain.
sudo nano /etc/nginx/sites-available/blog.example.localserver { listen 80; listen [::]:80; server_name blog.example.local; root /var/www/blog.example.local/html; index index.html; location / { try_files $uri $uri/ =404; } } - Enable, test, and apply as before.
sudo ln -s /etc/nginx/sites-available/blog.example.local /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx - The domain
blog.example.localdoes not have a record in the zone file from Chapter 9 yet, so add a newArecord following the same pattern as the existingwwwrecord, then increment the Serial number in theSOAand reload BIND9.
For quick testing without touching the DNS server at all, curl also supports manual name resolution via theblog IN A 192.168.1.20--resolveflag, suitable for when we just want to verify the server block without modifying the zone file first.curl --resolve blog.example.local:80:192.168.1.20 http://blog.example.local/
Verification and Troubleshooting
- Compare the results of
curl http://www.example.localandcurl http://blog.example.localfrom the exact same server IP. Different content on both domains proves that Nginx really chooses the server block based on theHostheader, rather than merely serving the same single configuration for all requests. - If two server blocks accidentally have the same
server_name, Nginx still runs without errors duringnginx -t, but only the server block defined first (based on file reading order) is actually used. This is a classic trap that often goes unnoticed because it produces no error messages whatsoever.
12.3 Serving Static Content and Basic Reverse Proxy
The server blocks we have created so far serve static files as they are. This section deepens slightly into how Nginx serves static content, and then moves into the capability that makes Nginx so popular in modern architecture: acting as a reverse proxy in front of backend applications.
12.3.1 Serving Static Content: root, alias, and Custom Error Pages
Two directives frequently confused by beginners are root and alias. The root directive appends the location path behind the filesystem path, whereas alias replaces it completely. Choosing incorrectly between the two is a common cause of confusing 404 errors even though the file actually exists on disk.
Practical Steps
- Create a simple custom error page for status
404.echo "<h1>Page not found at example.local</h1>" | sudo tee /var/www/example.local/html/404.html - Add the
error_pagedirective into theexample.localserver block.sudo nano /etc/nginx/sites-available/example.local
Theerror_page 404 /404.html; location = /404.html { internal; }internaldirective ensures this page can only be reached through Nginx's own internal redirect mechanism, rather than accessed directly by visitors via the URL/404.html. - Apply and test.
sudo nginx -t sudo systemctl reload nginx curl -i http://www.example.local/non-existent-page
Verification and Troubleshooting
- On the output of
curl -i, pay attention to the first status line. That line should still showHTTP/1.1 404 Not Foundeven though the page content is already a custom error page, rather than the default plain404page from Nginx. - In the field, a fairly common error is placing a different
rootdirective inside a specificlocationblock when intending to usealias. The combinationroot /var/www/example.local/html; location /assets { root /var/www/shared; }still appends/assetsbehind itsrootpath, so the file actually searched for is/var/www/shared/assets/..., not/var/www/shared/...as is often assumed.
12.3.2 Reverse Proxy to Backend Applications
As a reverse proxy, Nginx receives requests from visitors and forwards them to a backend application running on another port, then passes the response back to the visitor as if Nginx itself answered. This pattern is very commonly used by developers for Node.js, Python, or Java applications that have their own application server, but are not designed to face public traffic directly. Chapter 16 will later expand this topic to load balancing multiple backends simultaneously, whereas here we focus first on its basic mechanism with just a single backend.
Practical Steps
- To test the reverse proxy mechanism without needing to deploy a real application, run Python's built-in simple HTTP server as a dummy backend listening only on
localhost.
Let this command run in a separate terminal session or tmux (Chapter 3), because the process runs in the foreground.mkdir -p ~/demo-backend && cd ~/demo-backend echo "Hello from backend port 8080" > index.html python3 -m http.server 8080 --bind 127.0.0.1 - In another terminal, add a new
locationblock to theexample.localserver block that forwards requests to that backend.sudo nano /etc/nginx/sites-available/example.locallocation /app/ { proxy_pass http://127.0.0.1:8080/; 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; } - Apply and test.
The response should display the contents ofsudo nginx -t sudo systemctl reload nginx curl http://www.example.local/app/index.htmlfrom the Python backend earlier, even though the visitor only communicates with Nginx on port 80.
The three proxy_set_header directives above are not just a formality. Without Host, the backend receives Host: 127.0.0.1 instead of the original domain name requested by the visitor, which can break applications that construct absolute URLs based on this header. Without X-Real-IP and X-Forwarded-For, the backend application logs will only record Nginx's IP itself (127.0.0.1) as the source of all requests, rather than the actual visitor IP, a problem that only becomes noticeable when sysadmins need to trace suspicious traffic sources or brute-force attacks on the backend.
Verification and Troubleshooting
- A
502 Bad Gatewayerror almost always means Nginx cannot reach the backend specified inproxy_pass. Ensure that the backend process is still running.ss -tlnp | grep 8080 - Pay attention to the trailing slash (
/) at the end of the path inlocation /app/andproxy_pass http://127.0.0.1:8080/. The combination of slashes on both sides causes Nginx to strip the/app/prefix before forwarding the request to the backend. If one of the slashes is omitted, the prefix gets carried over to the backend, often resulting in a confusing404because it looks correct in configuration but acts wrong in practice.
12.4 Access and Error Logs
Two log files are the main sources of information when we need to verify traffic or trace issues on Nginx, much faster than guessing from client-side symptoms.
12.4.1 Reading Access Logs and Error Logs
Access log records every request successfully received by Nginx, while error log records events outside normal conditions, ranging from mild warnings to proxy failures to the backend. By default, both are stored in /var/log/nginx/access.log and /var/log/nginx/error.log, according to the access_log and error_log directives we saw in nginx.conf in Section 12.1.2.
Practical Steps
- Monitor the access log in real-time in one terminal session.
sudo tail -f /var/log/nginx/access.log - From another terminal, send several requests to the server, then observe the new lines appearing in the access log.
Each line of the log is formatted more or less as follows, following thecurl http://www.example.local curl http://www.example.local/non-existent-pagecombinedlog format which is the de facto standard for many web servers.
The order of fields is client IP, remote user identity (usually192.168.1.5 - - [26/Aug/2026:10:15:03 +0700] "GET / HTTP/1.1" 200 512 "-" "curl/8.5.0"-because it is rarely used), timestamp, complete request line, HTTP status code, response size in bytes, referer, and finally user agent. - Press
Ctrl+Cto stoptail -f, then view the error log for comparison.sudo tail -n 20 /var/log/nginx/error.log
Verification and Troubleshooting
- The status code column in the access log is the quickest starting point to assess overall traffic health roughly. A sudden stream of
5xxusually indicates server-side or backend issues, while a surge of4xx, especially404, often indicates broken links or an attacker randomly scanning common paths. - If you want to separate logs per domain to make analysis easier, add dedicated
access_loganderror_logdirectives inside a specific server block, overriding global settings innginx.conf.access_log /var/log/nginx/example.local.access.log; error_log /var/log/nginx/example.local.error.log;
12.4.2 Error Log Levels and Log Rotation
The error_log directive accepts an optional level parameter that controls how detailed the recorded messages are, ordered from least severe to most critical: debug, info, notice, warn, error, crit, alert, and emerg. The default level is error, sufficient for daily operations, but too minimal when tracing bugs that are difficult to reproduce.
Practical Steps
- Temporarily raise the error log level to
debugwhen maximum detail is needed for troubleshooting, for example on a problematicexample.localserver block.
Theerror_log /var/log/nginx/example.local.error.log debug;debuglevel only works if the Nginx binary was compiled with the--with-debugoption. Check first withnginx -Vbefore relying on this level, and do not forget to restore it towarnorerrorwhen finished, because thedebuglevel generates a very high volume of logs and quickly fills up disk space on production servers. - Ensure log rotation is already running automatically, because without rotation,
access.loganderror.logwill grow indefinitely.cat /etc/logrotate.d/nginx
Verification and Troubleshooting
- The
nginxpackage from the Ubuntu repository includes thislogrotateconfiguration by default, generally running daily and keeping several days of history before old files are deleted or compressed. A deeper discussion on log retention strategies across multiple servers will be covered in Chapter 39. - If server disk space suddenly runs full and suspicion points to Nginx logs, check their size first before blaming traffic volume.
sudo du -sh /var/log/nginx/*
Up to this point, Nginx is already running as a web server that actively answers the example.local and blog.example.local domains, complete with static content, basic reverse proxying to a backend, as well as access logs and error logs ready to be read whenever needed. Chapter 13 will discuss Apache as an alternative web server still widely used across various infrastructures, while comparing it with Nginx so we have a clear basis to choose one according to our needs.

