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!