The Complete NGINX on Ubuntu Series: Part 14 – Content Delivery Network (CDN) Setup

The Complete NGINX on Ubuntu Series: Part 14 – Content Delivery Network (CDN) Setup

Welcome to Part 14 of our comprehensive NGINX on Ubuntu series! We’ll transform NGINX into a powerful Content Delivery Network (CDN) edge server, implementing global content distribution, edge caching, and optimized content delivery.

CDN Fundamentals

A Content Delivery Network uses geographically distributed edge servers to deliver content closer to users, reducing latency, improving performance, and offloading traffic from origin servers.

graph TD
    A[Origin Server] --> B[CDN Edge Servers]
    
    B --> C[US East Edge]
    B --> D[US West Edge]
    B --> E[Europe Edge]
    B --> F[Asia Edge]
    
    G[Users] --> H[Nearest Edge Server]
    H --> I{Content Cached?}
    
    I -->|Yes| J[Serve from Cache]
    I -->|No| K[Fetch from Origin]
    K --> L[Cache Content]
    L --> M[Serve to User]
    
    N[CDN Benefits] --> O[Reduced Latency]
    N --> P[Lower Bandwidth Costs]
    N --> Q[Improved Availability]
    N --> R[Global Distribution]
    
    style B fill:#e1f5fe
    style H fill:#e8f5e8
    style I fill:#fff3e0
    style J fill:#e8f5e8
    style N fill:#e3f2fd

CDN Cache Configuration

# Configure CDN cache zones in nginx.conf
sudo nano /etc/nginx/nginx.conf
# Add to http block in nginx.conf
http {
    # CDN cache zones
    proxy_cache_path /var/cache/nginx/cdn-static
                     levels=1:2
                     keys_zone=cdn_static:100m
                     max_size=10g
                     inactive=7d
                     use_temp_path=off;
    
    proxy_cache_path /var/cache/nginx/cdn-dynamic
                     levels=1:2
                     keys_zone=cdn_dynamic:50m
                     max_size=2g
                     inactive=24h
                     use_temp_path=off;
    
    proxy_cache_path /var/cache/nginx/cdn-api
                     levels=1:2
                     keys_zone=cdn_api:25m
                     max_size=1g
                     inactive=1h
                     use_temp_path=off;
    
    # Origin server upstream
    upstream origin_servers {
        server origin1.example.com:80 weight=3;
        server origin2.example.com:80 weight=2;
        server origin3.example.com:80 backup;
        
        keepalive 32;
        keepalive_requests 100;
        keepalive_timeout 60s;
    }
    
    # CDN logging format
    log_format cdn_log '$remote_addr - [$time_local] '
                      '"$request" $status $body_bytes_sent '
                      'cache="$upstream_cache_status" '
                      'origin="$upstream_addr" '
                      'rt=$request_time urt="$upstream_response_time"';
}

Basic CDN Edge Server

# Create CDN edge server configuration
sudo nano /etc/nginx/sites-available/cdn-edge.example.com
server {
    listen 80;
    server_name cdn-edge.example.com *.cdn.example.com;
    
    # CDN logging
    access_log /var/log/nginx/cdn-access.log cdn_log;
    error_log /var/log/nginx/cdn-error.log warn;
    
    # Origin server
    set $origin_server "origin.example.com";
    
    # Static content with long-term caching
    location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
        proxy_pass http://origin_servers;
        
        # Cache configuration
        proxy_cache cdn_static;
        proxy_cache_valid 200 302 7d;
        proxy_cache_valid 404 1h;
        proxy_cache_valid any 1h;
        
        # Cache behavior
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_revalidate on;
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;
        
        # Cache key
        proxy_cache_key "$scheme$request_method$host$request_uri";
        
        # Origin request headers
        proxy_set_header Host $origin_server;
        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;
        proxy_set_header X-CDN-Edge $server_name;
        
        # CDN response headers
        add_header X-Cache-Status $upstream_cache_status always;
        add_header X-Cache-Date $upstream_http_date always;
        add_header X-CDN-Edge $server_name always;
        
        # Client-side caching
        expires 30d;
        add_header Cache-Control "public, immutable";
        add_header Vary "Accept-Encoding";
        
        # Compression
        gzip on;
        gzip_vary on;
        gzip_types image/svg+xml;
    }
    
    # CSS and JavaScript with medium-term caching
    location ~* \.(css|js)$ {
        proxy_pass http://origin_servers;
        
        proxy_cache cdn_static;
        proxy_cache_valid 200 24h;
        proxy_cache_valid 404 1h;
        proxy_cache_use_stale error timeout updating;
        proxy_cache_revalidate on;
        
        proxy_set_header Host $origin_server;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-CDN-Edge $server_name;
        
        add_header X-Cache-Status $upstream_cache_status always;
        add_header X-CDN-Edge $server_name always;
        
        expires 1d;
        add_header Cache-Control "public";
        
        # Enhanced compression for text assets
        gzip on;
        gzip_vary on;
        gzip_min_length 1024;
        gzip_types text/css application/javascript application/json;
    }
    
    # Font files with long-term caching
    location ~* \.(woff|woff2|ttf|eot|otf)$ {
        proxy_pass http://origin_servers;
        
        proxy_cache cdn_static;
        proxy_cache_valid 200 30d;
        proxy_cache_use_stale error timeout updating;
        
        proxy_set_header Host $origin_server;
        proxy_set_header X-CDN-Edge $server_name;
        
        add_header X-Cache-Status $upstream_cache_status always;
        add_header Access-Control-Allow-Origin "*";
        
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
    
    # HTML content with short-term caching
    location ~* \.(html|htm)$ {
        proxy_pass http://origin_servers;
        
        proxy_cache cdn_dynamic;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating;
        proxy_cache_revalidate on;
        
        proxy_set_header Host $origin_server;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-CDN-Edge $server_name;
        
        add_header X-Cache-Status $upstream_cache_status always;
        
        expires 10m;
        add_header Cache-Control "public, must-revalidate";
    }
    
    # API endpoints with minimal caching
    location /api/ {
        proxy_pass http://origin_servers;
        
        proxy_cache cdn_api;
        proxy_cache_valid 200 5m;
        proxy_cache_valid 404 30s;
        proxy_cache_use_stale error timeout;
        
        # Cache bypass for authenticated requests
        proxy_cache_bypass $http_authorization $cookie_session;
        proxy_no_cache $http_authorization $cookie_session;
        
        proxy_set_header Host $origin_server;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Authorization $http_authorization;
        proxy_set_header X-CDN-Edge $server_name;
        
        add_header X-Cache-Status $upstream_cache_status always;
        
        expires 5m;
        add_header Cache-Control "public, max-age=300";
    }
    
    # Default catch-all
    location / {
        proxy_pass http://origin_servers;
        
        proxy_cache cdn_dynamic;
        proxy_cache_valid 200 1h;
        proxy_cache_valid any 1m;
        proxy_cache_use_stale error timeout updating;
        
        proxy_set_header Host $origin_server;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-CDN-Edge $server_name;
        
        add_header X-Cache-Status $upstream_cache_status always;
    }
    
    # Cache purge endpoint (restricted access)
    location ~ /cdn-purge(/.*) {
        allow 127.0.0.1;
        allow 192.168.1.0/24;
        deny all;
        
        proxy_cache_purge cdn_static "$scheme$request_method$host$1";
        proxy_cache_purge cdn_dynamic "$scheme$request_method$host$1";
    }
}

Geographic CDN Configuration

# Create geographic CDN configuration
sudo nano /etc/nginx/sites-available/geo-cdn.example.com
# Geographic origin mapping
geo $nearest_origin {
    default "origin-us.example.com";
    
    # North America
    US "origin-us.example.com";
    CA "origin-us.example.com";
    
    # Europe
    GB "origin-eu.example.com";
    DE "origin-eu.example.com";
    FR "origin-eu.example.com";
    
    # Asia Pacific
    JP "origin-ap.example.com";
    SG "origin-ap.example.com";
    AU "origin-ap.example.com";
}

# Origin server clusters by region
upstream origin_us {
    server origin-us-1.example.com:80;
    server origin-us-2.example.com:80;
    keepalive 32;
}

upstream origin_eu {
    server origin-eu-1.example.com:80;
    server origin-eu-2.example.com:80;
    keepalive 32;
}

upstream origin_ap {
    server origin-ap-1.example.com:80;
    server origin-ap-2.example.com:80;
    keepalive 32;
}

# Map origin hostnames to upstreams
map $nearest_origin $origin_upstream {
    "origin-us.example.com" origin_us;
    "origin-eu.example.com" origin_eu;
    "origin-ap.example.com" origin_ap;
    default origin_us;
}

server {
    listen 80;
    server_name geo-cdn.example.com;
    
    # Enhanced logging with geographic info
    log_format geo_cdn_log '$remote_addr [$time_local] '
                          '"$request" $status $body_bytes_sent '
                          'origin="$nearest_origin" '
                          'cache="$upstream_cache_status" '
                          'rt=$request_time';
    
    access_log /var/log/nginx/geo-cdn.log geo_cdn_log;
    
    # Static content with geographic origin selection
    location ~* \.(jpg|jpeg|png|gif|css|js|woff|woff2)$ {
        proxy_pass http://$origin_upstream;
        
        proxy_cache cdn_static;
        proxy_cache_valid 200 7d;
        proxy_cache_valid 404 1h;
        proxy_cache_use_stale error timeout updating;
        proxy_cache_revalidate on;
        
        proxy_set_header Host $nearest_origin;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-CDN-Edge $server_name;
        proxy_set_header X-Origin-Region $nearest_origin;
        
        add_header X-Cache-Status $upstream_cache_status always;
        add_header X-Origin-Server $nearest_origin always;
        add_header X-CDN-Edge $server_name always;
        
        expires 7d;
        add_header Cache-Control "public, immutable";
    }
    
    # Dynamic content with regional origins
    location / {
        proxy_pass http://$origin_upstream;
        
        proxy_cache cdn_dynamic;
        proxy_cache_valid 200 1h;
        proxy_cache_use_stale error timeout updating;
        
        proxy_set_header Host $nearest_origin;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Origin-Region $nearest_origin;
        
        add_header X-Cache-Status $upstream_cache_status always;
        add_header X-Origin-Server $nearest_origin always;
    }
}

Image Optimization CDN

graph TD
    A[Image Request] --> B[CDN Edge Server]
    B --> C{Image Cached?}
    
    C -->|Yes| D[Serve Cached Image]
    C -->|No| E[Fetch from Origin]
    
    E --> F[Image Optimization]
    F --> G[Format Conversion]
    F --> H[Quality Adjustment]
    F --> I[Resize/Compress]
    
    G --> J[WebP for Modern Browsers]
    G --> K[AVIF for Supported]
    G --> L[JPEG/PNG Fallback]
    
    J --> M[Cache Optimized Image]
    K --> M
    L --> M
    M --> N[Serve to Client]
    
    style B fill:#e1f5fe
    style F fill:#e8f5e8
    style M fill:#fff3e0
# Create image-optimized CDN
sudo nano /etc/nginx/sites-available/image-cdn.example.com
# Image format detection
map $http_accept $webp_suffix {
    default "";
    "~image/webp" ".webp";
}

map $http_accept $avif_suffix {
    default "";
    "~image/avif" ".avif";
}

server {
    listen 80;
    server_name images.example.com img.example.com;
    
    # Original images with format optimization
    location ~* ^/images/(.+)\.(jpg|jpeg|png)$ {
        set $image_path $1;
        set $image_ext $2;
        
        # Try optimized formats first
        try_files $uri$avif_suffix $uri$webp_suffix $uri @origin_image;
        
        expires 30d;
        add_header Cache-Control "public, immutable";
        add_header Vary "Accept";
    }
    
    # Handle original image requests
    location @origin_image {
        proxy_pass http://origin_servers;
        
        proxy_cache cdn_static;
        proxy_cache_valid 200 30d;
        proxy_cache_valid 404 1h;
        proxy_cache_use_stale error timeout updating;
        
        proxy_set_header Host "origin.example.com";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        add_header X-Cache-Status $upstream_cache_status always;
        add_header X-Image-Source "origin" always;
        
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
    
    # Resized images with dynamic sizing
    location ~ ^/resize/(\d+)x(\d+)/(.+\.(jpg|jpeg|png|webp))$ {
        set $width $1;
        set $height $2;
        set $image_path $3;
        
        # Proxy to image processing service
        proxy_pass http://image_processing_service/resize?w=$width&h=$height&src=$image_path;
        
        proxy_cache cdn_static;
        proxy_cache_valid 200 7d;
        proxy_cache_use_stale error timeout updating;
        
        add_header X-Cache-Status $upstream_cache_status always;
        add_header X-Image-Dimensions "${width}x${height}" always;
        
        expires 7d;
        add_header Cache-Control "public";
    }
}

CDN Cache Management

# Create CDN cache management script
sudo nano /usr/local/bin/cdn-manager.sh
#!/bin/bash

# CDN Cache Management Script
CACHE_DIR="/var/cache/nginx"

show_cache_stats() {
    echo "=== CDN Cache Statistics ==="
    echo "Generated: $(date)"
    echo
    
    # Cache directory sizes
    for cache_type in cdn-static cdn-dynamic cdn-api; do
        if [ -d "$CACHE_DIR/$cache_type" ]; then
            local size=$(du -sh "$CACHE_DIR/$cache_type" 2>/dev/null | cut -f1)
            local files=$(find "$CACHE_DIR/$cache_type" -type f 2>/dev/null | wc -l)
            echo "$cache_type cache: $size ($files files)"
        fi
    done
    
    # Cache hit rates from logs
    local access_log="/var/log/nginx/cdn-access.log"
    if [ -f "$access_log" ]; then
        echo
        echo "--- Cache Hit Rates (Last 1000 requests) ---"
        local total=$(tail -1000 "$access_log" | grep 'cache=' | wc -l)
        local hits=$(tail -1000 "$access_log" | grep 'cache="HIT"' | wc -l)
        
        if [ "$total" -gt 0 ]; then
            local hit_rate=$((hits * 100 / total))
            echo "Total requests: $total"
            echo "Cache hits: $hits"
            echo "Hit rate: $hit_rate%"
        fi
    fi
}

purge_cache() {
    local cache_type="$1"
    
    if [ -z "$cache_type" ]; then
        echo "Purging all caches..."
        rm -rf "$CACHE_DIR"/cdn-*/*
        echo "All caches purged"
    else
        echo "Purging $cache_type cache..."
        rm -rf "$CACHE_DIR/cdn-$cache_type"/*
        echo "$cache_type cache purged"
    fi
}

warm_cache() {
    local urls=(
        "http://cdn-edge.example.com/images/logo.png"
        "http://cdn-edge.example.com/css/main.css"
        "http://cdn-edge.example.com/js/app.js"
    )
    
    echo "Starting cache warming..."
    
    for url in "${urls[@]}"; do
        echo "Warming: $url"
        curl -s "$url" > /dev/null
        sleep 1
    done
    
    echo "Cache warming completed"
}

case "${1:-stats}" in
    stats)
        show_cache_stats
        ;;
    purge)
        purge_cache "$2"
        ;;
    warm)
        warm_cache
        ;;
    *)
        echo "Usage: $0 {stats|purge [cache_type]|warm}"
        echo "Cache types: static, dynamic, api"
        ;;
esac

# Make executable: sudo chmod +x /usr/local/bin/cdn-manager.sh

Testing CDN Configuration

# Create CDN cache directories
sudo mkdir -p /var/cache/nginx/{cdn-static,cdn-dynamic,cdn-api}
sudo chown -R www-data:www-data /var/cache/nginx

# Enable CDN sites
sudo ln -s /etc/nginx/sites-available/cdn-edge.example.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/geo-cdn.example.com /etc/nginx/sites-enabled/

# Test configuration
sudo nginx -t

# Reload NGINX
sudo systemctl reload nginx

# Test CDN functionality
curl -H "Host: cdn-edge.example.com" http://localhost/images/test.jpg -v
curl -H "Host: cdn-edge.example.com" http://localhost/css/main.css -v

# Check cache headers
curl -I -H "Host: cdn-edge.example.com" http://localhost/images/logo.png

# Test cache hit (second request)
curl -I -H "Host: cdn-edge.example.com" http://localhost/images/logo.png

# Monitor CDN performance
/usr/local/bin/cdn-manager.sh stats

# Cache warming
/usr/local/bin/cdn-manager.sh warm

# Purge specific cache
/usr/local/bin/cdn-manager.sh purge static

What’s Next?

Excellent! You’ve built a comprehensive CDN solution with NGINX that provides global content distribution, intelligent caching, and optimized content delivery. Your infrastructure now supports edge computing and improved user experience worldwide.

Coming up in Part 15: NGINX High Availability and Clustering

References


This is Part 14 of our 22-part NGINX series. Your server is now a powerful CDN edge node! Next, we’ll implement high availability clustering. Questions? Share them in the comments!

Written by:

373 Posts

View All Posts
Follow Me :
How to whitelist website on AdBlocker?

How to whitelist website on AdBlocker?

  1. 1 Click on the AdBlock Plus icon on the top right corner of your browser
  2. 2 Click on "Enabled on this site" from the AdBlock Plus option
  3. 3 Refresh the page and start browsing the site