A web server has one core job: listen on a port, receive HTTP requests, and send back responses. That response could be a static HTML file, an image, or a proxied response from a backend application. Every website on the internet has a web server somewhere in the chain.
What Web Servers Do
At the most basic level, a web server:
- Listens on a port (usually 80 for HTTP, 443 for HTTPS)
- Receives an HTTP request (like
GET /index.html) - Decides what to do — serve a file, forward to a backend, return an error
- Responds with the content and a status code
Beyond that, modern web servers handle SSL/TLS, compression, caching, rate limiting, URL rewrites, and reverse proxying — all before our application code even runs.
Nginx vs Apache
These are the two biggest web servers. Nginx now dominates, but it’s worth knowing the difference.
Apache (1995) — the original. Uses a process-per-request model (or thread-per-request). Each incoming connection gets its own process or thread. This works fine for moderate traffic, but under heavy load, spawning thousands of processes eats memory fast.
Nginx (2004) — built to fix Apache’s scalability problem. Uses an event-driven, async architecture. A single worker process handles thousands of connections using an event loop (similar idea to Node.js). This makes Nginx incredibly efficient under high concurrency.
| Nginx | Apache | |
|---|---|---|
| Architecture | Event-driven, async | Process/thread per request |
| Memory usage | Low (handles 10k+ connections per worker) | Higher under load |
| Static files | Extremely fast | Good |
| Config | Declarative blocks | .htaccess files + config |
| Best for | Reverse proxy, static serving, high concurrency | PHP apps, legacy setups, .htaccess flexibility |
For most modern applications, Nginx is the default choice. Apache still shines in shared hosting environments where .htaccess files give per-directory configuration without restarting the server.
Nginx Config Structure
Nginx config follows a nested block structure: http contains server blocks, which contain location blocks.
# /etc/nginx/nginx.conf (simplified)
http {
# Global settings for all virtual hosts
gzip on; # enable compression
gzip_types text/plain text/css application/json application/javascript;
# Include per-site configs (convention)
include /etc/nginx/conf.d/*.conf;
}
Each site gets its own config file inside conf.d/ or sites-enabled/.
Serving a Static Site
The most basic use case — serving HTML, CSS, JS, and images from a directory.
# /etc/nginx/conf.d/portfolio.conf
server {
listen 80;
server_name pman47.cc;
root /var/www/portfolio/dist; # where our built files live
index index.html;
# Try the exact file, then directory, then fall back to index.html
# (critical for single-page apps with client-side routing)
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets aggressively
location ~* \.(css|js|png|jpg|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
The try_files directive is one of the most important directives. Without it, refreshing /about on an SPA would return a 404 because there’s no about file on disk — try_files catches that and serves index.html instead, letting the JavaScript router handle the path.
Reverse Proxying to a Backend App
This is how we run a Node.js, Python, or Go app behind Nginx. Nginx handles SSL, static files, and compression. Our app just handles business logic.
# /etc/nginx/conf.d/api.conf
server {
listen 80;
server_name api.pman47.cc;
# API requests go to our Node.js app
location / {
proxy_pass http://127.0.0.1:3000;
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;
# WebSocket support (if needed)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Common Nginx Directives
# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
# SSL termination
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.pem;
ssl_certificate_key /etc/ssl/private/example.key;
}
Useful Nginx Commands
nginx -t # test config for syntax errors (always do this!)
systemctl reload nginx # reload config without dropping connections
systemctl restart nginx # full restart (drops active connections)
tail -f /var/log/nginx/error.log # watch error logs in real-time
Always run nginx -t before reloading. A bad config on reload can take our entire site down.
When We Don’t Need a Traditional Web Server
Not every project needs Nginx or Apache. Modern deployment platforms handle the web server layer for us:
- Vercel / Netlify / Cloudflare Pages — static sites and serverless functions, no web server config needed
- Docker + Caddy — Caddy auto-provisions SSL certificates and has a much simpler config than Nginx
- Serverless (AWS Lambda, Cloud Functions) — the cloud provider handles HTTP routing entirely
But the moment we’re managing our own VPS or running Docker containers in production, understanding Nginx becomes essential. It’s the glue between the internet and our application.
In simple language, a web server listens for HTTP requests and figures out what to send back — Nginx does this with an event-driven approach that handles massive traffic with minimal resources, and it’s the go-to choice for reverse proxying, static serving, and SSL termination.