Lab 02 — Static Site With Nginx
Learner Orientation
Lab Metadata
| Item | Value |
|---|---|
| Difficulty | Beginner |
| Estimated time | 45-75 minutes |
| VMs | 1 |
| Minimum VM RAM | 1024 MB |
| SSH ports | 2202 |
| Internet required | Yes |
Before You Start, You Should Be Able To
- Yeast installed on a Linux/KVM host
- Comfort opening a terminal and changing directories
- Ability to run
yeast up,yeast ssh <instance>, andyeast destroy - Comfort using forwarded Yeast host URLs from
ACCESS.mdfor browser-based tools
Where Commands Run
- Run
yeastcommands from this lab folder on your laptop. - Run Linux service commands only after you SSH into the target VM.
- When a command says "from your laptop", leave the VM shell first with
exit. - When a browser URL uses
localhost, check whether Yeast already forwarded that port for you.
Expected Checkpoints
- After
yeast up,yeast statusshould show the expected VM or VMs as running. - After the main setup steps, the service, tool, or workflow introduced by the lab should respond to the verification commands.
- After
bash assets/validate.sh, the script should report all checks passed. - After
yeast destroy, the lab should be cleaned up before you start the next one.
Common Mistakes To Avoid
- Running a VM command on your laptop, or a laptop command inside the VM.
- Ignoring the forwarded port shown by
yeast uporyeast status. - Skipping validation because the final page or command "looked fine".
- Forgetting to run
yeast destroybefore moving to the next lab.
The Story
Your team needs an internal page online. Nothing fancy — a static site with some HTML. The infrastructure request is simple: "Put up a web server. Point it at some files. Make it reachable on port 80."
You have a clean server from Lab 01. Now you need to install Nginx, configure it to serve files, place your HTML, and verify the whole chain works — from the server process, to the port, to an actual HTTP response in a browser.
And when it breaks — and it will break, because that is part of the lab — you will read logs, trace the failure, and fix it. That skill is more valuable than knowing how to set it up correctly in the first place.
Before You Start — Understanding The Concepts
What Is A Web Server?
A web server is a program that listens for HTTP requests on a network port and responds with content — usually HTML files, images, or data.
When you type a URL in your browser, your browser: 1. Resolves the domain name to an IP address 2. Opens a TCP connection to that IP on port 80 (HTTP) or 443 (HTTPS) 3. Sends an HTTP request: "GET /index.html" 4. The web server reads that request, finds the file, and sends it back 5. Your browser renders the response
A web server does not know or care what is in the files. It just maps URL paths to files on disk and sends them back. That is the fundamental job.
What Is Nginx?
Nginx (pronounced "engine-x") is one of the two most popular web servers in the world (the other is Apache). It is fast, memory-efficient, and extremely configurable.
Nginx can do several things: - Serve static files (HTML, CSS, JS, images) directly from disk — this is what we do in this lab - Proxy requests to a backend application — Lab 03 - Terminate TLS (HTTPS) — later labs - Load balance across multiple backend servers — Lab 08
For this lab we are using the simplest use case: serve static files from a directory.
What Is A Port?
A port is a number (0–65535) that identifies a specific service running on a machine.
When a program wants to receive network connections, it "binds" to a port — it tells the operating system "give me all traffic arriving on port 80." The operating system keeps a table of which process owns which port.
Standard port assignments: - 22 — SSH - 80 — HTTP (unencrypted web) - 443 — HTTPS (encrypted web) - 5432 — PostgreSQL - 3306 — MySQL/MariaDB - 6379 — Redis
You can run any service on any port — these are conventions, not rules. But browsers automatically try port 80 for http:// and port 443 for https://, so web servers almost always use those.
In this lab, Nginx listens on port 80 inside the VM. Yeast forwards that HTTP service to your laptop on port 8080, so you can test it both inside the VM and from your laptop browser.
What Is HTTP?
HTTP (HyperText Transfer Protocol) is the language browsers and web servers use to talk to each other.
A request looks like:
A response looks like:
The number after HTTP/1.1 in the response is the status code:
- 200 — OK, the request succeeded
- 301/302 — Redirect, the resource moved
- 403 — Forbidden, you do not have permission
- 404 — Not Found, the file does not exist
- 500 — Internal Server Error, something broke on the server side
You will see all of these in logs throughout this course.
What Is A Virtual Host?
Nginx can serve multiple websites from the same server. It distinguishes between them using "virtual hosts" (also called server blocks in Nginx terms).
Each virtual host is a configuration block that says: "for requests to this domain/IP on this port, serve files from this directory."
For this lab you have one virtual host: serve files from /var/www/baseline-site on port 80.
What Is /var/www?
Linux has a standard directory layout. /var is for variable data — things that change during normal operation. /var/www is the conventional location for web server content. By default, Nginx serves files from /var/www/html. We will create our own directory at /var/www/baseline-site.
What You Are Building
Your Laptop
│
│ HTTP port 8080 → VM port 80
│ SSH port 2202 → VM port 22
▼
┌──────────────────────────────────────────┐
│ web (Ubuntu 22.04) │
│ │
│ Nginx listening on :80 │
│ └── serves /var/www/baseline-site/ │
│ │
│ ufw: allows SSH + Nginx (ports 22, 80) │
└──────────────────────────────────────────┘
The site files live on the VM's disk. Nginx reads them and sends them to your browser when you visit http://localhost:8080.
The Config File — assets/yeast.yaml
version: 1
instances:
- name: web
hostname: web
image: ubuntu-22.04
memory: 1024
cpus: 2
disk_size: 20G
ssh_port: 2202
user: ubuntu
sudo: nopasswd
provision:
packages:
- nginx
- curl
- vim
shell:
- hostnamectl set-hostname web
- timedatectl set-timezone UTC
- ufw allow OpenSSH
- ufw allow 'Nginx Full'
- ufw --force enable
- systemctl enable --now nginx
New fields compared to Lab 01:
ssh_port: 2202 — We use 2202 here to avoid conflicts with Lab 01 if you ever run both at the same time. Each lab uses a different SSH port.
nginx in packages — This installs Nginx from Ubuntu's package repository.
ufw allow 'Nginx Full' — Nginx Full is a UFW application profile that allows both port 80 (HTTP) and port 443 (HTTPS). Nginx installs these profiles automatically.
systemctl enable --now nginx — Enables Nginx to start on boot and starts it right now.
Starting The Lab
Once Yeast finishes, SSH in:
Verify Nginx Is Running
Before touching anything else, verify that Nginx started correctly:
This command shows the full status of the nginx service. Look for:
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since ...
The important parts:
- enabled — Nginx will start automatically after a reboot
- active (running) — Nginx is running right now
If it says failed or inactive, check the error:
journalctl is the command for reading systemd's log. -u nginx means "show logs for the nginx unit." --no-pager prevents it from opening in a pager (which requires key presses to scroll). -n 20 shows the last 20 lines.
Check what port Nginx is listening on
ss is the socket statistics tool — it shows network connections and listening ports. The flags:
- -t — TCP only
- -l — listening sockets only
- -n — show port numbers, not service names
- -p — show the process that owns the socket
You should see something like:
This means: Nginx is listening on all interfaces (0.0.0.0) on port 80, and the process ID is 1234.
Make a quick HTTP request from inside the VM
curl makes an HTTP request. -I means "HEAD request only" — fetch the headers but not the body. You should see:
The 200 OK means Nginx received the request and responded successfully. Right now it is serving Nginx's default page. We are about to replace that with our own content.
Create Your Site
Create the directory where your site files will live:
mkdir -p creates a directory and any parent directories that do not exist. The -p flag means "no error if it already exists."
Create a simple HTML page:
vim is a terminal text editor. When it opens:
1. Press i to enter "insert mode" (you can now type)
2. Type your HTML
3. Press Esc to exit insert mode
4. Type :wq and press Enter to save and quit
Type this content:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Baseline Site</title>
</head>
<body>
<h1>Welcome to the Baseline Site</h1>
<p>This is Lab 02 of the DevOps Bootcamp.</p>
<p>Server: web</p>
</body>
</html>
Now set the correct permissions so Nginx can read the file:
chown changes the owner of a file or directory. www-data is the user that Nginx runs as. By setting the owner to www-data, you ensure Nginx has permission to read the files.
chmod 755 sets the permissions:
- 7 (owner) = read + write + execute
- 5 (group) = read + execute
- 5 (others) = read + execute
For directories, "execute" means "can enter the directory." For files, it means "can run as a program." HTML files do not need execute permission, but 755 is the standard for web-served content.
Configure Nginx To Serve Your Site
Nginx's configuration lives in /etc/nginx/. The main config is /etc/nginx/nginx.conf, but individual site configs go in /etc/nginx/sites-available/ and are enabled by creating a symlink in /etc/nginx/sites-enabled/.
This two-directory pattern lets you have multiple site configs ready but only enable the ones you want active.
Create a config for your site:
Enter this content:
server {
listen 80;
server_name _;
root /var/www/baseline-site;
index index.html;
location / {
try_files $uri $uri/ =404;
}
access_log /var/log/nginx/baseline-site.access.log;
error_log /var/log/nginx/baseline-site.error.log;
}
Let's read this:
listen 80 — this server block handles requests on port 80.
server_name _ — the underscore is a catch-all. It matches any hostname. Since we are not using a real domain name, this is fine.
root /var/www/baseline-site — the base directory where files are served from.
index index.html — when a request comes in for a directory (like /), serve index.html from it.
location / — this block handles all URL paths. try_files $uri $uri/ =404 means: try to find a file matching the URL path, then try it as a directory, and if neither exists return a 404.
access_log and error_log — write logs to these specific files rather than the default Nginx log. This keeps your site's logs separate from other sites or the default Nginx page.
Enable the site
ln -s creates a symbolic link (symlink) — a pointer from one path to another. The sites-enabled directory just contains symlinks into sites-available. Disabling a site is as simple as removing the symlink.
Disable the default site
The default Nginx site is enabled and would compete for port 80:
Test the config
Before reloading Nginx, test that the config file has no syntax errors:
Expected output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If you see errors, Nginx tells you the file and line number. Fix the error and test again. Never reload Nginx with a broken config — it will fail to start.
Reload Nginx
reload tells Nginx to re-read its config without stopping. It is safer than restart because Nginx keeps serving existing connections while loading the new config. In production, always use reload when changing config.
Verify The Site
Test from inside the VM:
You should see your HTML:
To test from your laptop browser, first create an SSH tunnel from a second terminal on your laptop:
Keep that tunnel terminal open. Then open a browser and go to:
The tunnel maps port 8080 on your laptop to port 80 inside the VM. You should see your page rendered in the browser.
You can also test from the command line on your laptop:
Reading Logs
Logs are how you understand what a server is doing. Get comfortable reading them now — they are your primary debugging tool for every lab in this course.
Access log
Every HTTP request Nginx receives is written to the access log:
tail -f follows a file — it prints new lines as they are added. Leave this running, keep the SSH tunnel open, then make a request from your browser to http://localhost:8080. You will see a line appear:
Reading the fields:
- 127.0.0.1 — IP address of the client that made the request
- [15/Jun/2026:12:00:00 +0000] — timestamp in UTC
- "GET / HTTP/1.1" — the HTTP method, path, and protocol version
- 200 — the response status code
- 312 — the number of bytes in the response body
- "-" — the Referer header (which page linked to this one — empty here)
- "curl/7.81.0" — the User-Agent (what made the request)
Press Ctrl+C to stop following the log.
Error log
This file records errors — permission problems, missing files, config issues. Right now it should be empty or have very few lines. We will see it in action in the next section.
Break Something On Purpose
Break 1: Wrong file permissions
Change the permissions on your site directory so Nginx cannot read it:
Now make a request:
You get:
403 Forbidden. Nginx can reach the directory but does not have permission to read the files. Check the error log:
You will see something like:
The log tells you exactly what went wrong: permission denied on the specific file. This is how real debugging works — you read the log, it tells you the problem.
Fix it:
200 OK again.
Break 2: Nginx config syntax error
Edit the Nginx config and introduce a deliberate syntax error:
Remove the semicolon from the end of the listen 80 line so it reads listen 80 with no semicolon. Save and exit.
Now test the config:
nginx: [emerg] invalid parameter "server_name" in /etc/nginx/sites-available/baseline-site:2
nginx: configuration file /etc/nginx/nginx.conf test failed
Nginx caught the error before you could reload. This is why you always run nginx -t before reloading. If you had reloaded, Nginx would have failed to apply the new config (or in the case of restart, failed to start entirely).
Fix it — put the semicolon back — then:
Break 3: Missing index file
Rename the index file:
Make a request:
You get a 404. Check the error log:
The access log also records the 404:
Fix it:
sudo mv /var/www/baseline-site/index.html.bak /var/www/baseline-site/index.html
curl http://localhost
The Debugging Pattern
Every time you debugged a failure in this lab, you followed the same pattern:
- Observe the symptom — wrong status code, connection refused, empty response
- Check the service status —
systemctl status nginx - Read the error log —
tail -f /var/log/nginx/error.log - Check the access log — what did the server actually receive and respond?
- Fix the root cause — not the symptom, the cause
- Verify the fix — run the same request that failed and confirm it succeeds
This pattern works for every web server, every application, every service. The tools change. The pattern does not.
Take A Snapshot
Validate Your Work
Note: the validate script connects to the VM over SSH and checks Nginx from inside the VM. You do not need an HTTP tunnel for validation.
Clean Up
Quick Recap
In Lab 02 — Static Site With Nginx, you moved from explanation to a working lab environment, verified the result, and practiced the operational habit that matters most: do the work, prove it works, then clean it up.
Keep this pattern for every lab:
- Build the thing.
- Verify it from the right place.
- Read the logs or status when it fails.
- Run the validation script.
- Destroy the lab before moving on.
What You Learned
- What a web server does: listen on a port, map URLs to files, send responses
- What Nginx is and the difference between static serving and proxying
- How ports work and why port 80 is the standard for HTTP
- How to read HTTP status codes: 200 OK, 403 Forbidden, 404 Not Found
- Nginx configuration:
sites-available,sites-enabled, symlinks, server blocks - File permissions and
chown: whywww-dataowns web files nginx -t: always test config before reloadingsystemctl reloadvsrestart: reload is safer for config changes- How to read access and error logs: every field, what it means
- The debugging pattern: symptom → service status → error log → fix → verify
What Is Next
Lab 03 — Reverse Proxy To Backend App
You have a web server. Now we add complexity: two VMs, one acting as a reverse proxy and the other running a backend application. You will configure Nginx to forward requests from the internet-facing server to the internal backend, and learn how traffic actually flows through a modern web architecture.