DEV Community

Adetayo Akinsanya
Adetayo Akinsanya

Posted on

Part 2: Web Server Mastery - Serving Static Content Like a Pro

Nginx Logo

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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)