Building on our foundation from Part 1, let's transform your basic Nginx setup into a high-performance web server that can handle multiple sites, optimize content delivery, and provide an excellent user experience.
What We'll Accomplish Today
By the end of this part, you'll have:
- Multiple websites running on a single server
- Lightning-fast static file serving with caching
- Automatic compression for faster loading
- Professional error pages that don't break user experience
- Smart redirects and URL rewriting
- Performance optimizations that make your sites fly
The Multi-Site Challenge
Remember our simple single-site setup from Part 1? In the real world, you'll often need to serve multiple websites from one server. Let's see how Nginx makes this incredibly straightforward.
Understanding Server Blocks
Think of server blocks as separate apartments in a building. Each has its own address (domain name), its own space (directory), and its own rules, but they all share the same building infrastructure (your server).
Setting Up Multiple Sites
Let's create three different websites to demonstrate various scenarios:
Site 1: Personal Portfolio (Static HTML/CSS/JS)
Create /etc/nginx/sites-available/portfolio.conf
:
server {
listen 80;
server_name portfolio.local www.portfolio.local;
root /var/www/portfolio;
index index.html index.htm;
# Enable efficient file serving
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# Main location block
location / {
try_files $uri $uri/ =404;
# Add security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
}
# Optimize static assets
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary Accept-Encoding;
# Enable compression for text-based assets
gzip_static on;
}
# Handle favicon requests gracefully
location = /favicon.ico {
log_not_found off;
access_log off;
expires 1y;
}
# Custom error pages
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
Site 2: Blog (Static Site Generator - Jekyll/Hugo style)
Create /etc/nginx/sites-available/blog.conf
:
server {
listen 80;
server_name blog.local www.blog.local;
root /var/www/blog;
index index.html;
# Handle clean URLs (no .html extension)
location / {
try_files $uri $uri.html $uri/ =404;
}
# Blog-specific optimizations
location ~* \.(css|js)$ {
expires 30d;
add_header Cache-Control "public";
gzip_static on;
}
# Images with longer cache
location ~* \.(png|jpg|jpeg|gif|webp|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# RSS and sitemap
location ~* \.(xml|rss)$ {
expires 1h;
add_header Content-Type application/xml;
}
# Deny access to admin/private files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Pretty URLs for blog posts
location ~* ^/posts/(.*)$ {
try_files /posts/$1.html /posts/$1/index.html =404;
}
}
Site 3: Single Page Application (React/Vue/Angular)
Create /etc/nginx/sites-available/app.conf
:
server {
listen 80;
server_name app.local www.app.local;
root /var/www/app/dist;
index index.html;
# Handle client-side routing
location / {
try_files $uri $uri/ /index.html;
# Don't cache the main HTML file
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Static assets with hash-based filenames
location ~* \.(js|css)$ {
expires 1y;
add_header Cache-Control "public, immutable";
gzip_static on;
}
# Images and fonts
location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API proxy (if your SPA needs to call APIs)
location /api/ {
proxy_pass http://localhost:3001/;
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;
}
# Health check for load balancers
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
Enabling Your Sites
# Create symbolic links to enable sites
sudo ln -s /etc/nginx/sites-available/portfolio.conf /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/blog.conf /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/
# Create directory structure
sudo mkdir -p /var/www/{portfolio,blog,app/dist}
# Test configuration
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
Performance Optimization: Making Your Sites Lightning Fast β‘
1. Compression Configuration
Create /etc/nginx/conf.d/gzip.conf
:
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
# Specify which file types to compress
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml
font/truetype
font/opentype
application/font-woff
application/font-woff2;
# Don't compress already compressed files
gzip_disable "MSIE [1-6]\.";
# Serve pre-compressed files if available
gzip_static on;
2. Advanced Caching Strategy
Create /etc/nginx/conf.d/cache.conf
:
# Browser caching for different file types
map $sent_http_content_type $expires {
default off;
text/html 1h;
text/css 1y;
application/javascript 1y;
application/json 1h;
image/png 1y;
image/jpg 1y;
image/jpeg 1y;
image/gif 1y;
image/svg+xml 1y;
image/x-icon 1y;
font/woff 1y;
font/woff2 1y;
font/ttf 1y;
font/otf 1y;
application/pdf 1M;
video/mp4 1M;
video/webm 1M;
}
expires $expires;
# Add cache control headers
add_header Cache-Control "public" always;
# Conditional requests support
if_modified_since before;
3. File Type Optimization
Update your main nginx.conf
to include:
http {
# ... existing configuration ...
# Optimize file serving
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# Increase buffer sizes for better performance
client_body_buffer_size 128k;
client_max_body_size 50m;
client_header_buffer_size 1k;
large_client_header_buffers 4 4k;
# Output compression
output_buffers 1 32k;
postpone_output 1460;
# Include our optimization configs
include /etc/nginx/conf.d/gzip.conf;
include /etc/nginx/conf.d/cache.conf;
}
Creating Professional Error Pages π¨
Default error pages are boring and unprofessional. Let's create custom ones that match your brand.
Custom 404 Page
Create /var/www/errors/404.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Not Found - 404</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.error-container {
text-align: center;
max-width: 600px;
padding: 2rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.error-code {
font-size: 8rem;
font-weight: bold;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.error-message {
font-size: 1.5rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.error-description {
font-size: 1rem;
margin-bottom: 2rem;
opacity: 0.8;
line-height: 1.6;
}
.home-button {
display: inline-block;
padding: 12px 30px;
background: rgba(255, 255, 255, 0.2);
color: white;
text-decoration: none;
border-radius: 25px;
transition: all 0.3s ease;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.home-button:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.search-box {
margin: 2rem 0;
}
.search-input {
padding: 12px 20px;
border: none;
border-radius: 25px;
width: 300px;
max-width: 100%;
background: rgba(255, 255, 255, 0.9);
color: #333;
font-size: 1rem;
}
.search-button {
padding: 12px 20px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 25px;
margin-left: 10px;
cursor: pointer;
transition: background 0.3s ease;
}
.search-button:hover {
background: #ff5252;
}
.helpful-links {
margin-top: 2rem;
}
.helpful-links a {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
margin: 0 15px;
transition: color 0.3s ease;
}
.helpful-links a:hover {
color: white;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-code">404</div>
<h1 class="error-message">Oops! Page Not Found</h1>
<p class="error-description">
The page you're looking for seems to have wandered off into the digital wilderness.
Don't worry though, even the best explorers sometimes take a wrong turn!
</p>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search for what you need..." id="searchInput">
<button class="search-button" onclick="performSearch()">Search</button>
</div>
<a href="/" class="home-button">π Take Me Home</a>
<div class="helpful-links">
<a href="/about">About</a>
<a href="/contact">Contact</a>
<a href="/blog">Blog</a>
<a href="/sitemap.xml">Sitemap</a>
</div>
</div>
<script>
function performSearch() {
const query = document.getElementById('searchInput').value;
if (query.trim()) {
window.location.href = `/search?q=${encodeURIComponent(query)}`;
}
}
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
</script>
</body>
</html>
Custom 50x Error Page
Create /var/www/errors/50x.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Error - We'll Be Right Back</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #ff7b7b 0%, #ff416c 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.error-container {
text-align: center;
max-width: 600px;
padding: 2rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.robot {
font-size: 4rem;
margin-bottom: 1rem;
animation: shake 2s infinite;
}
@keyframes shake {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-5deg); }
75% { transform: rotate(5deg); }
}
.error-message {
font-size: 1.8rem;
margin-bottom: 1rem;
font-weight: 600;
}
.error-description {
font-size: 1rem;
margin-bottom: 2rem;
opacity: 0.9;
line-height: 1.6;
}
.status-info {
background: rgba(255, 255, 255, 0.1);
padding: 1rem;
border-radius: 10px;
margin: 1rem 0;
font-family: monospace;
}
.retry-button {
display: inline-block;
padding: 12px 30px;
background: rgba(255, 255, 255, 0.2);
color: white;
text-decoration: none;
border-radius: 25px;
transition: all 0.3s ease;
border: 2px solid rgba(255, 255, 255, 0.3);
margin: 0 10px;
}
.retry-button:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
</style>
</head>
<body>
<div class="error-container">
<div class="robot">π€</div>
<h1 class="error-message">Our Servers Are Taking a Coffee Break</h1>
<p class="error-description">
We're experiencing some technical difficulties. Our team of digital mechanics
is working hard to get everything running smoothly again.
</p>
<div class="status-info">
Error Code: 500-503<br>
Estimated Fix Time: A few minutes<br>
Status: Engineers Deployed β
</div>
<a href="javascript:location.reload()" class="retry-button"> Try Again</a>
<a href="/" class="retry-button">Go Home</a>
</div>
<script>
// Auto-refresh every 30 seconds
setTimeout(() => {
location.reload();
}, 30000);
</script>
</body>
</html>
Update Your Sites to Use Custom Error Pages
Add this to each of your site configurations:
# Custom error pages
error_page 404 /errors/404.html;
error_page 500 502 503 504 /errors/50x.html;
location = /errors/404.html {
root /var/www;
internal;
}
location = /errors/50x.html {
root /var/www;
internal;
}
Smart Redirects and URL Rewriting π
Common Redirect Scenarios
server {
listen 80;
server_name example.com www.example.com;
# Redirect old blog structure to new one
location ~ ^/blog/(\d{4})/(\d{2})/(.+)$ {
return 301 /posts/$1-$2-$3;
}
# Redirect old file extensions
location ~ ^(.+)\.php$ {
return 301 $1;
}
# Redirect trailing slashes for files
location ~ ^(.+)/$ {
return 301 $1;
}
# Force HTTPS (when you have SSL)
if ($scheme != "https") {
return 301 https://$server_name$request_uri;
}
# Redirect www to non-www (or vice versa)
if ($host = 'www.example.com') {
return 301 $scheme://example.com$request_uri;
}
# Handle common typos in domain
if ($host ~ ^(.*\.)?exampl\.com$) {
return 301 $scheme://example.com$request_uri;
}
}
Advanced URL Rewriting
# Pretty URLs for a PHP application
location / {
# Try exact file, then directory, then rewrite
try_files $uri $uri/ @rewrite;
}
location @rewrite {
# Convert /category/subcategory/item to /index.php?cat=category&sub=subcategory&item=item
rewrite ^/([^/]+)/([^/]+)/([^/]+)/?$ /index.php?cat=$1&sub=$2&item=$3 last;
# Convert /category/item to /index.php?cat=category&item=item
rewrite ^/([^/]+)/([^/]+)/?$ /index.php?cat=$1&item=$2 last;
# Convert /item to /index.php?item=item
rewrite ^/([^/]+)/?$ /index.php?item=$1 last;
}
# API versioning
location ~ ^/api/v(\d+)/(.*)$ {
proxy_pass http://backend/api/$2;
proxy_set_header X-API-Version $1;
proxy_set_header Host $host;
}
Performance Monitoring and Optimization
Enable Status Monitoring
Add this to your main nginx.conf:
server {
listen 8080;
server_name localhost;
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
}
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
Performance Testing Commands
# Test your site performance
curl -H "Accept-Encoding: gzip" -H "Cache-Control: no-cache" -w "@curl-format.txt" -o /dev/null -s "http://localhost/"
# Create curl-format.txt for detailed timing
echo 'time_namelookup: %{time_namelookup}s
time_connect: %{time_connect}s
time_appconnect: %{time_appconnect}s
time_pretransfer: %{time_pretransfer}s
time_redirect: %{time_redirect}s
time_starttransfer: %{time_starttransfer}s
time_total: %{time_total}s
http_code: %{http_code}
size_download: %{size_download}bytes
speed_download: %{speed_download}bytes/sec' > curl-format.txt
# Check compression
curl -H "Accept-Encoding: gzip,deflate" -v http://localhost/style.css | head
# Monitor connections
watch -n 1 'ss -tuln | grep :80'
Real-World Performance Tips π
1. Preload Critical Resources
Add to your HTML head:
<!-- Preload critical CSS -->
<link rel="preload" href="/css/critical.css" as="style">
<!-- Preload important fonts -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<!-- DNS prefetch for external domains -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//cdn.example.com">
2. Optimize Images
# WebP support with fallback
location ~* \.(png|jpg|jpeg)$ {
# Try WebP version first
try_files $uri.webp $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Serve different image sizes
location ~ ^/images/(.+)_(small|medium|large)\.(jpg|png)$ {
alias /var/www/images/$1_$2.$3;
expires 1y;
}
3. Security Headers for Production
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;
# HSTS (when using HTTPS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Troubleshooting Common Issues π§
Problem: CSS/JS Not Loading
# Check file permissions
sudo chown -R www-data:www-data /var/www/
sudo chmod -R 755 /var/www/
# Check MIME types
grep "text/css" /etc/nginx/mime.types
grep "application/javascript" /etc/nginx/mime.types
Problem: Pages Not Updating
# Clear any caches
sudo rm -rf /var/cache/nginx/*
# Check if files are actually updated
ls -la /var/www/yoursite/
# Force reload without cache
curl -H "Cache-Control: no-cache" http://localhost/
Problem: Slow Performance
# Check access logs for slow requests
sudo tail -f /var/log/nginx/access.log | grep -E " [5-9][0-9]{3} "
# Monitor server resources
htop
iotop -o
What's Next? π―
Congratulations! You now have a professional, high-performance web server setup that can:
- β Serve multiple websites efficiently
- β Optimize content delivery with compression and caching
- β Handle errors gracefully with custom pages
- β Redirect and rewrite URLs intelligently
- β Monitor performance and troubleshoot issues
In Part 3: Reverse Proxy Magic, we'll dive into:
- Connecting your frontend to backend APIs
- Handling WebSocket connections
- Setting up development vs production environments
- Load balancing basics
- SSL termination and security
Practice Exercises π
Before moving to Part 3, try these:
Exercise 1: Multi-Site Setup
Create three different websites with different purposes and optimize each one differently.
Exercise 2: Performance Audit
Use tools like Google PageSpeed Insights or GTmetrix to test your sites and implement their suggestions.
Exercise 3: Custom Error Pages
Design error pages that match your brand and include helpful navigation.
Exercise 4: URL Restructuring
Implement a redirect strategy for an old site structure to a new one.
Quick Reference Commands π
# Test configuration
sudo nginx -t
# Reload configuration
sudo systemctl reload nginx
# Check active sites
ls -la /etc/nginx/sites-enabled/
# Monitor access logs
sudo tail -f /var/log/nginx/access.log
# Check server status
curl http://localhost:8080/nginx_status
# Performance test
ab -n 1000 -c 10 http://localhost/
You're building real expertise here! Each part of this series builds practical skills that you'll use every day as a developer. In Part 3, we'll connect all these optimized frontend sites to powerful backend services. Keep experimenting!
Top comments (0)