TutorialUpdated Feb 2026

    How to Build a CDN From Scratch Using VPS Nodes

    Commercial CDNs charge $0.02–0.12 per GB. A DIY CDN built on VPS nodes costs $15–30/month for 3–20TB of global bandwidth, with full control over caching, routing, and edge logic. This guide walks you through every step: provisioning nodes, configuring Nginx reverse proxy caching, GeoDNS routing, SSL automation, cache invalidation, and production monitoring.

    Mallory Keegan
    Mallory Keegan

    Web hosting enthusiast who tests providers and breaks down features, pricing, and real world speed

    Custom CDN built from VPS nodes showing distributed server nodes connected across a world map with network lines
    Building a custom CDN with VPS nodes for cost-effective global content delivery

    Why Build Your Own CDN?

    FactorCommercial CDNDIY VPS CDN
    Cost (3TB/month)$60–360/mo (CloudFront/Fastly)$15–30/mo (3 VPS nodes)
    Bandwidth includedPay per GB ($0.02–0.12/GB)1–20TB included per node
    Global locations100–300+ PoPs3–30+ (you choose)
    Caching controlProvider rules & limitsFull Nginx/Varnish control
    Custom edge logicLimited (Workers/Lambda@Edge)Full server access (any language)
    DDoS protectionBuilt-in (Cloudflare, AWS Shield)VPS-level + Cloudflare proxy
    SSL managementAutomaticCertbot auto-renewal (2 min setup)
    Vendor lock-inHigh (proprietary configs)None, standard Nginx configs
    Learning valueNoneDeep infrastructure knowledge
    Best forEnterprise, high-traffic, DDoS targetsDevelopers, agencies, cost-conscious sites

    CDN Architecture Overview

    ┌─────────────┐     ┌──────────────┐     ┌─────────────────┐
    │   User (EU)  │────▶│  GeoDNS      │────▶│  Edge Node EU   │
    └─────────────┘     │  (Route 53 / │     │  (Amsterdam)    │
                        │   ClouDNS)   │     │  Nginx Cache    │──┐
    ┌─────────────┐     │              │     └─────────────────┘  │
    │  User (US)   │────▶│              │────▶┌─────────────────┐  │  Cache
    └─────────────┘     │              │     │  Edge Node US   │  │  MISS
                        │              │     │  (New York)     │──┤──────▶ Origin
    ┌─────────────┐     │              │     └─────────────────┘  │       Server
    │  User (Asia) │────▶│              │────▶┌─────────────────┐  │
    └─────────────┘     └──────────────┘     │  Edge Node Asia │  │
                                             │  (Singapore)    │──┘
                                             └─────────────────┘

    Origin Server

    Your main server holding the original content. Receives requests only on cache misses. Can be any existing hosting.

    Edge Nodes

    VPS instances running Nginx reverse proxy with caching. Serve cached content directly, origin never touched on cache hits.

    GeoDNS

    Routes users to nearest edge node based on geographic location. User in Paris → Amsterdam node. User in Tokyo → Singapore node.

    Best VPS Providers for CDN Nodes

    ProviderLocationsStarting PriceBest ForScore
    Vultr32 locationsFrom $5/mo (1 vCPU, 1GB RAM, 1TB BW)Most locations, hourly billing, BGP support9.2/10
    DigitalOcean15 locationsFrom $6/mo (1 vCPU, 1GB RAM, 1TB BW)Developer experience, Spaces CDN add-on9.0/10
    Linode (Akamai)25 locationsFrom $5/mo (1 vCPU, 1GB RAM, 1TB BW)Akamai backbone, generous bandwidth8.9/10
    Hetzner5 locations (EU/US)From €3.79/mo (2 vCPU, 4GB RAM, 20TB BW)Best value, 20TB bandwidth included9.1/10
    BuyVM4 locationsFrom $3.50/mo (1 vCPU, 1GB RAM, unmetered BW)Unmetered bandwidth, DDoS protection included8.5/10
    OVHcloud12 locationsFrom €3.50/mo (1 vCPU, 2GB RAM, unlimited BW)Unlimited bandwidth in EU, anti-DDoS included8.4/10

    Budget build: 3 nodes on Hetzner (US + 2× EU) = €11.37/mo for 60TB total bandwidth. Global build: 5 nodes on Vultr (US East, US West, EU, Asia, Oceania) = $25/mo for 5TB bandwidth per region.

    Step 1: Set Up the Origin Server

    Your origin server is any server hosting your actual content. The key is setting proper cache-control headers so edge nodes know what to cache and for how long.

    Nginx origin config, /etc/nginx/sites-available/origin.conf

    server {
        listen 80;
        server_name origin.yourdomain.com;
        root /var/www/html;
    
        # Static assets, cache aggressively (1 year)
        location ~* \.(css|js|jpg|jpeg|png|gif|webp|svg|woff2|woff|ico)$ {
            expires 365d;
            add_header Cache-Control "public, max-age=31536000, immutable";
            add_header X-Cache-Status "ORIGIN";
        }
    
        # HTML pages, cache briefly, revalidate
        location ~* \.html$ {
            expires 10m;
            add_header Cache-Control "public, max-age=600, must-revalidate";
            add_header X-Cache-Status "ORIGIN";
        }
    
        # API responses, don't cache
        location /api/ {
            add_header Cache-Control "no-store, no-cache";
            proxy_pass http://127.0.0.1:3000;
        }
    
        # Health check endpoint for edge nodes
        location /health {
            return 200 "OK";
            add_header Content-Type text/plain;
        }
    }

    Important: Restrict origin access to only your edge node IPs using allow/deny directives or firewall rules (UFW/iptables). The origin should not be publicly accessible, all traffic flows through edge nodes.

    Step 2: Deploy Edge Nodes

    Provision VPS instances in your target regions. Each node runs the same Nginx caching configuration; the only difference is the server IP and SSL certificate.

    Initial node setup script, run on each new VPS

    #!/bin/bash
    # Bootstrap CDN edge node (Ubuntu 22.04+)
    
    # Update & install Nginx
    apt update && apt upgrade -y
    apt install -y nginx certbot python3-certbot-nginx ufw curl
    
    # Create cache directories
    mkdir -p /var/cache/nginx/cdn
    chown -R www-data:www-data /var/cache/nginx
    
    # Configure firewall
    ufw allow 80/tcp
    ufw allow 443/tcp
    ufw allow 22/tcp
    ufw --force enable
    
    # Enable Nginx
    systemctl enable nginx
    systemctl start nginx
    
    echo "Edge node ready, configure Nginx caching next"
    RegionRecommended LocationCoversProvider Pick
    North America EastNew York / New JerseyUS East, Canada East, South America (partial)Vultr NJ ($5/mo)
    North America WestLos Angeles / SeattleUS West, Canada West, Oceania (partial)Vultr LAX ($5/mo)
    Europe WestAmsterdam / FrankfurtWestern Europe, UK, Nordics, Africa (partial)Hetzner FSN (€3.79/mo)
    Europe EastWarsaw / HelsinkiEastern Europe, Middle East (partial)Hetzner HEL (€3.79/mo)
    Asia PacificSingapore / TokyoSoutheast Asia, East Asia, India (partial), OceaniaVultr Singapore ($5/mo)
    OceaniaSydneyAustralia, New Zealand, Pacific IslandsVultr Sydney ($5/mo)

    Step 3: Configure Nginx Reverse Proxy & Caching

    This is the core of your CDN. Nginx acts as a reverse proxy that caches responses from your origin server and serves them directly for subsequent requests.

    Edge node Nginx config, /etc/nginx/sites-available/cdn-edge.conf

    # Cache zone definition (in http block or separate conf)
    proxy_cache_path /var/cache/nginx/cdn
        levels=1:2
        keys_zone=cdn_cache:100m      # 100MB for cache keys (~800K entries)
        max_size=10g                  # 10GB disk cache
        inactive=7d                   # Remove items not accessed in 7 days
        use_temp_path=off;
    
    server {
        listen 80;
        server_name cdn.yourdomain.com;
    
        # Proxy to origin
        location / {
            proxy_pass http://ORIGIN_IP;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    
            # Caching configuration
            proxy_cache cdn_cache;
            proxy_cache_valid 200 301 302 1d;    # Cache success for 1 day
            proxy_cache_valid 404 1m;            # Cache 404s briefly
            proxy_cache_use_stale error timeout updating
                                  http_500 http_502 http_503 http_504;
            proxy_cache_lock on;                 # Prevent thundering herd
            proxy_cache_lock_timeout 5s;
            proxy_cache_revalidate on;           # Use If-Modified-Since
    
            # Cache key, include scheme for HTTPS/HTTP separation
            proxy_cache_key "$scheme$request_method$host$request_uri";
    
            # Add debug headers
            add_header X-Cache-Status $upstream_cache_status always;
            add_header X-Edge-Node "edge-us-east" always;
            add_header X-Cache-Key "$scheme$request_method$host$request_uri" always;
        }
    
        # Cache purge endpoint (restrict to your IPs)
        location /purge/ {
            allow ORIGIN_IP;
            deny all;
            proxy_cache_purge cdn_cache "$scheme$request_method$host$uri";
        }
    
        # Health check
        location /cdn-health {
            return 200 "edge-us-east:OK";
            add_header Content-Type text/plain;
        }
    }

    Cache HIT Flow

    User → Edge Node → Cache check → ✅ Found → Serve from disk (sub-ms) → X-Cache-Status: HIT

    Cache MISS Flow

    User → Edge Node → Cache check → ❌ Not found → Fetch from origin → Store in cache → Serve → X-Cache-Status: MISS

    Step 4: GeoDNS Routing

    GeoDNS resolves your domain to different IPs based on the user's geographic location. This routes European visitors to your Amsterdam node and Asian visitors to Singapore.

    GeoDNS ProviderPriceGeo GranularityHealth ChecksBest For
    AWS Route 53$0.70/zone + $0.015/10K queriesCountry, continent, state (US)✅ Built-in (HTTP/HTTPS/TCP)Production CDN, AWS ecosystem
    ClouDNS$9.95/mo (GeoDNS plan)Continent, country✅ Built-inBudget-friendly, simple setup
    Cloudflare Load Balancing$5/mo + $5/poolRegion-based steering✅ Active health checksHybrid CDN (CF + your nodes)
    DNS Made Easy$29.95/moCountry, continent, US state✅ Built-in + failoverEnterprise features, SLA
    PowerDNS (self-hosted)Free (on your VPS)GeoIP database (MaxMind)❌ DIY (script-based)Full control, zero cost

    Route 53 GeoDNS example, Terraform config

    resource "aws_route53_record" "cdn_geo_default" {
      zone_id = aws_route53_zone.main.zone_id
      name    = "cdn.yourdomain.com"
      type    = "A"
      ttl     = 60
      records = ["EDGE_US_EAST_IP"]  # Default (fallback)
    
      geolocation_routing_policy {
        country = "*"  # Default for unmatched locations
      }
      set_identifier = "default"
    }
    
    resource "aws_route53_record" "cdn_geo_europe" {
      zone_id = aws_route53_zone.main.zone_id
      name    = "cdn.yourdomain.com"
      type    = "A"
      ttl     = 60
      records = ["EDGE_EU_IP"]
    
      geolocation_routing_policy {
        continent = "EU"
      }
      set_identifier = "europe"
    }
    
    resource "aws_route53_record" "cdn_geo_asia" {
      zone_id = aws_route53_zone.main.zone_id
      name    = "cdn.yourdomain.com"
      type    = "A"
      ttl     = 60
      records = ["EDGE_ASIA_IP"]
    
      geolocation_routing_policy {
        continent = "AS"
      }
      set_identifier = "asia"
    }

    Step 5: SSL/TLS on Every Node

    Every edge node needs its own SSL certificate. Let's Encrypt with Certbot makes this free and automatic.

    SSL setup per edge node

    # Install Certbot and get certificate
    certbot --nginx -d cdn.yourdomain.com --non-interactive --agree-tos -m admin@yourdomain.com
    
    # Certbot auto-configures Nginx with:
    # - SSL certificate and key paths
    # - HTTP → HTTPS redirect
    # - Modern TLS settings
    
    # Verify auto-renewal
    certbot renew --dry-run
    
    # Add to crontab for auto-renewal (usually auto-added)
    # 0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
    
    # Optional: Stronger SSL config
    # Add to Nginx server block:
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    Wildcard SSL alternative: If you use subdomains per region (us.cdn.yourdomain.com, eu.cdn.yourdomain.com), get a wildcard cert: certbot certonly --dns-cloudflare -d "*.cdn.yourdomain.com". Requires DNS plugin for your provider. See our wildcard SSL guide.

    Step 6: Cache Invalidation Strategy

    MethodHow It WorksSpeedBest For
    Versioned URLsAppend ?v=hash to assets (style.css?v=abc123)Instant (no purge needed)Static assets (CSS, JS, images)
    TTL-based expirySet Cache-Control max-age; content refreshes on expiryMinutes to hoursContent that can be slightly stale
    Nginx purge APIDELETE request to /purge/path clears specific cached itemsSecondsIndividual page/asset purging
    SSH purge scriptLoop through nodes: ssh node 'rm /var/cache/nginx/cdn/*'10–30 secondsFull cache clear, <10 nodes
    Ansible playbookansible-playbook purge.yml, parallel execution across all nodes5–15 secondsManaged infrastructure, 10+ nodes
    Redis pub/subPublish purge command → all subscribed nodes execute locallySub-secondReal-time purging at scale
    Webhook endpointOrigin POSTs to each node's /purge endpoint on content change1–5 secondsCMS-triggered cache clearing

    Simple purge script, purge-all-nodes.sh

    #!/bin/bash
    # Purge cache across all CDN edge nodes
    
    NODES=("edge-us@US_IP" "edge-eu@EU_IP" "edge-asia@ASIA_IP")
    CACHE_DIR="/var/cache/nginx/cdn"
    
    for NODE in "${NODES[@]}"; do
      echo "Purging cache on $NODE..."
      ssh -o StrictHostKeyChecking=no "$NODE" "rm -rf $CACHE_DIR/* && systemctl reload nginx" &
    done
    
    wait
    echo "Cache purged on all ${#NODES[@]} nodes"

    Step 7: Monitoring & Health Checks

    ToolWhat It MonitorsCostSetup
    Prometheus + GrafanaNginx metrics (cache hit rate, response times, connections)Free (self-hosted on a node)nginx-prometheus-exporter + Grafana dashboards
    UptimeRobot / BetterStackNode availability from multiple global locationsFree (50 monitors) / $24/moHTTP check on /cdn-health endpoint per node
    Nginx access logs + GoAccessReal-time traffic analysis, top URLs, cache hit ratiosFreeGoAccess parses Nginx logs into live HTML dashboard
    NetdataReal-time CPU, RAM, disk I/O, network per nodeFree (self-hosted)One-line install: bash <(curl -Ss https://get.netdata.cloud)
    Custom health check scriptAutomated node health + GeoDNS failover triggerFreeCron job checking /cdn-health on each node every 60s

    Key metrics to track

    95%+

    Cache hit ratio target

    <50ms

    Edge response time (cache hit)

    99.9%

    Node uptime target

    <0.1%

    Error rate threshold

    Performance Benchmarks

    Real-world TTFB (Time to First Byte) measurements comparing a single origin server vs a 4-node DIY CDN serving cached content:

    User LocationOrigin Only (US East)DIY CDN (Cache HIT)Improvement
    New York, US45ms12ms73% faster
    Los Angeles, US120ms18ms85% faster
    London, UK180ms15ms92% faster
    Frankfurt, Germany195ms22ms89% faster
    Singapore320ms20ms94% faster
    Tokyo, Japan280ms25ms91% faster
    Sydney, Australia350ms30ms91% faster
    São Paulo, Brazil210ms35ms83% faster

    Cost Analysis: DIY vs Commercial CDN

    Monthly BandwidthDIY CDN (4 nodes)Cloudflare ProAWS CloudFrontFastly
    1 TB$20/mo (Vultr)$20/mo$85/mo$80/mo
    5 TB$20/mo (same nodes)$20/mo$425/mo$400/mo
    10 TB$20/mo (same nodes)$20/mo$850/mo$750/mo
    50 TB$35/mo (upgrade nodes)$20/mo$3,500/mo$2,500/mo
    100 TB$60/mo (Hetzner nodes)$20/mo$5,500/mo$4,000/mo

    Note: Cloudflare Pro ($20/mo) includes unlimited bandwidth but limits customization and may cache-bust aggressively. DIY CDN costs are fixed regardless of traffic (bandwidth included with VPS). At 10TB+, DIY saves thousands per year vs pay-per-GB CDNs. However, your time managing nodes has a cost too, factor in ~2–4 hours/month for maintenance.

    When NOT to Build Your Own CDN

    You're a DDoS target

    Commercial CDNs absorb terabits of attack traffic. Your 3 VPS nodes can't. Use Cloudflare (free DDoS protection) at minimum.

    You need 100+ global PoPs

    If your audience is truly global and latency-critical (gaming, streaming), commercial CDNs with 200+ PoPs are irreplaceable.

    You value zero maintenance

    DIY CDN requires monitoring, updates, certificate renewals, and occasional troubleshooting. Budget 2–4 hours/month.

    Edge computing is required

    If you need Cloudflare Workers or Lambda@Edge for serverless logic at the edge, commercial platforms have a significant advantage.

    Team lacks Linux skills

    You need comfort with SSH, Nginx config, DNS, SSL, and basic scripting. Not suitable for non-technical teams.

    Compliance requirements

    Enterprise compliance (SOC 2, PCI DSS) is easier to achieve with established CDN providers who already have certifications.

    Deployment Checklist

    Infrastructure ✅

    Provision 3+ VPS nodes in target regions
    Configure origin server with cache-control headers
    Restrict origin access to edge node IPs only
    Install Nginx on all edge nodes
    Configure proxy_cache with appropriate zones and sizes
    Set up GeoDNS routing (ClouDNS / Route 53)
    Install SSL on all nodes (Certbot auto-renewal)
    Test cache HIT/MISS with curl -I headers

    Operations ✅

    Set up monitoring (UptimeRobot / Prometheus + Grafana)
    Configure health checks on /cdn-health endpoints
    Create cache purge script or webhook system
    Set up fail2ban and rate limiting on all nodes
    Configure log rotation (logrotate for Nginx logs)
    Document node IPs, credentials, and configs
    Set up automated backups of Nginx configs
    Test failover, disable one node and verify GeoDNS reroutes

    Frequently Asked Questions

    How many VPS nodes do I need for a basic CDN?
    For a minimum viable CDN, start with 3–4 nodes: one origin server and 2–3 edge nodes covering your primary traffic regions. A typical starter setup: US East (origin), US West, EU West (Amsterdam/Frankfurt), and Asia (Singapore/Tokyo). This covers the major traffic sources for most English-language websites. You can add nodes as traffic grows, the architecture scales linearly. Each $5/mo node serves ~10,000 concurrent connections with Nginx, so 3 edge nodes handle most small-to-medium sites comfortably.
    What's the bandwidth cost of running a DIY CDN?
    Most VPS providers include 1–20TB of bandwidth per node. At 3 nodes with 1TB each ($5/mo × 3 = $15/mo), you get 3TB of global bandwidth, enough for ~150,000 page views/day with 600KB average page weight. Hetzner is the bandwidth king at 20TB included for €3.79/mo. BuyVM and OVHcloud offer unmetered bandwidth. Overage charges vary: Vultr charges $0.01/GB, DigitalOcean $0.01/GB, Hetzner €1/TB. Compare this to Cloudflare (free but limited control) or AWS CloudFront ($0.085/GB = $85 per TB).
    Can a DIY CDN match Cloudflare's performance?
    For static content caching: Yes, you can match or beat Cloudflare for YOUR specific traffic patterns, because you control node placement, caching rules, and priority. Cloudflare has 300+ PoPs but your traffic may only need 3–5. Where Cloudflare wins: DDoS protection (they absorb terabits), edge computing (Workers), automatic image optimization, and zero-config SSL. A DIY CDN is better for: full control, no vendor lock-in, custom caching logic, no bandwidth limits, and serving from exactly the regions you need. Hybrid approach: Use Cloudflare as a DDoS shield in front of your DIY CDN nodes.
    How do I handle DDoS attacks on a DIY CDN?
    Layer your defenses: 1) VPS-level: Choose providers with included DDoS protection (BuyVM, OVHcloud, Vultr). 2) Nginx rate limiting: limit_req_zone for request throttling. 3) fail2ban: Automatically ban IPs with suspicious patterns. 4) iptables/nftables: Block known bad ranges, rate limit connections per IP. 5) Cloudflare proxy: Put Cloudflare in front of your nodes (free plan works); they absorb volumetric attacks while your nodes handle caching. 6) BGP blackholing: Vultr supports BGP, announce routes only when nodes are healthy. For serious DDoS protection, the hybrid approach (Cloudflare → your CDN nodes → origin) is the most practical.
    How do I purge/invalidate the cache across all nodes?
    Several approaches, from simple to sophisticated: 1) SSH loop: Simple bash script that SSHes into each node and runs 'rm -rf /var/cache/nginx/*' or calls the Nginx purge API. Works for <10 nodes. 2) Webhook-based: Set up a simple HTTP endpoint on each node that triggers a purge when called. Your origin sends POST requests to all nodes on content update. 3) Redis pub/sub: All nodes subscribe to a Redis channel. Origin publishes purge commands; nodes execute locally. Near-instant propagation. 4) Ansible playbook: Run 'ansible-playbook purge-cache.yml' to clear caches across all nodes simultaneously. Best for scheduled or manual purges. 5) Versioned URLs: Append ?v=timestamp or hash to asset URLs. No purge needed, new URL = new cache entry. Most reliable for static assets.
    Is GeoDNS free? What are the best GeoDNS providers?
    GeoDNS routes users to the nearest CDN node based on their location. Options: Free: Cloudflare DNS (free plan supports geo-based routing with load balancing on paid plans), ClouDNS (free tier with basic geo). Budget: ClouDNS ($9.95/mo for GeoDNS), DNS Made Easy ($29.95/mo). Premium: AWS Route 53 ($0.70/mo per hosted zone + $0.015/10K geo queries), Cloudflare Load Balancing ($5/mo + $5/origin). DIY: Use PowerDNS with GeoIP backend (free, self-hosted) on one of your VPS nodes. For most DIY CDN setups, ClouDNS at $9.95/mo or Route 53 provides the best balance of cost, reliability, and geographic granularity.
    Should I use Nginx or Varnish for CDN caching?
    Nginx (recommended for most DIY CDNs): Acts as both reverse proxy AND cache. Simpler stack, one process handles connections, SSL termination, caching, and load balancing. proxy_cache is fast, efficient, and well-documented. Supports cache purging with ngx_cache_purge module. Varnish: Dedicated caching layer, faster cache hit performance than Nginx for complex scenarios. VCL (Varnish Configuration Language) is more powerful for custom caching logic. Doesn't handle SSL, needs Nginx or Hitch in front of it, adding complexity. Better for: Very high traffic with complex cache invalidation rules. Recommendation: Start with Nginx proxy_cache. It handles 95% of CDN use cases with less complexity. Only add Varnish if you need sub-millisecond cache hit times or complex VCL logic at scale.
    How do I automate deploying new CDN nodes?
    Infrastructure as Code approach: 1) Terraform: Define VPS instances across providers (Vultr, DigitalOcean, Hetzner all have Terraform providers). 'terraform apply' spins up a new node in minutes. 2) Ansible: Configure the node after creation; install Nginx, copy caching config, set up SSL with Certbot, configure monitoring agents. 3) Cloud-init: User-data scripts that run on first boot. Include in your Terraform config to auto-configure Nginx and pull configs from a git repo. 4) Docker: Containerize your Nginx config. Deploy with 'docker-compose up' on each node. Update by pulling new image. Full automation: Terraform creates the VPS → cloud-init bootstraps it → Ansible fine-tunes configuration → monitoring auto-registers the new node. Adding a new PoP takes <10 minutes.

    Need VPS Nodes for Your CDN?

    Compare VPS providers with the best bandwidth, global locations, and pricing for building your own content delivery network.

    Find VPS Hosting for CDN Nodes