مجله ایرانیکاسرورآموزش سرور، هاست، وردپرس و شبکه
لینوکس

ساخت CDN شخصی با Nginx؛ فایل موقت، Static و Image CDN

ساخت CDN شخصی و سرویس انتقال فایل با Nginx؛ فایل موقت، Static CDN و Image CDN اگر فایل، تصویر، CSS، JavaScript یا فایل‌های حجیم را مستقیماً از همان سرور اصلی سایت تحویل دهید، با…

ساخت CDN شخصی با Nginx؛ فایل موقت، Static و Image CDN
خلاصه سریع این آموزش

ساخت CDN شخصی و سرویس انتقال فایل با Nginx؛ فایل موقت، Static CDN و Image CDN اگر فایل، تصویر، CSS، JavaScript یا فایل‌های حجیم را مستقیماً از همان سرور اصلی…

۱۸ دقیقه مطالعه ۰ دیدگاه ۱۰ مهر ۱۴۰۵ آخرین به‌روزرسانی

فهرست مطالب

ساخت CDN شخصی و سرویس انتقال فایل با Nginx؛ فایل موقت، Static CDN و Image CDN

اگر فایل، تصویر، CSS، JavaScript یا فایل‌های حجیم را مستقیماً از همان سرور اصلی سایت تحویل دهید، با افزایش کاربران هم مصرف پهنای باند Origin بیشتر می‌شود و هم کاربران دورتر تأخیر بیشتری احساس می‌کنند. یک معماری بهتر این است که Origin، سرویس انتقال فایل، Cache Node و Image CDN را از هم جدا کنیم.

در این آموزش یک پلتفرم شخصی برای تحویل فایل می‌سازیم که سه کاربرد را هم‌زمان پوشش می‌دهد: انتقال فایل موقت با تاریخ انقضا، CDN شخصی برای فایل‌های استاتیک و Image CDN برای Resize و تبدیل تصاویر به WebP یا AVIF. هسته اصلی معماری Nginx است و برای بخش فایل موقت از Python و FastAPI و برای پردازش تصویر از imgproxy استفاده می‌کنیم.

📌 در پایان این پروژه خواهیم داشت:

🔹 سرویس انتقال فایل با لینک موقت و تاریخ انقضا

🔹 Origin Server برای فایل‌های اصلی

🔹 Edge Cache با Nginx

🔹 کش خودکار CSS، JS، فونت و فایل‌های استاتیک

🔹 Image CDN با Resize و Crop لحظه‌ای

🔹 خروجی WebP و AVIF

🔹 URL امضاشده برای جلوگیری از سوءاستفاده از Image Processor

🔹 امکان ساخت چند Edge در ایران و خارج

اول یک نکته مهم؛ یک VPS به‌تنهایی CDN نیست

اگر فقط یک VPS داشته باشید و فایل‌ها را از روی آن تحویل دهید، در واقع یک File Server یا Cache Server دارید؛ نه یک CDN واقعی.

CDN زمانی معنا پیدا می‌کند که فایل اصلی روی Origin قرار داشته باشد و چند Edge Node در موقعیت‌های شبکه‌ای یا جغرافیایی مختلف آن را Cache کنند. کاربر نیز تا حد امکان از Edge نزدیک‌تر پاسخ می‌گیرد.

مثلاً معماری می‌تواند چنین باشد:

                    +----------------+
                    | Origin Server  |
                    |  Files/Images  |
                    +--------+-------+
                             |
             +---------------+---------------+
             |                               |
             v                               v
     +---------------+               +---------------+
     | Edge - Iran   |               | Edge - Europe |
     | Nginx Cache   |               | Nginx Cache   |
     +-------+-------+               +-------+-------+
             |                               |
             v                               v
       Iranian Users                   Other Users

برای توزیع خودکار کاربران میان Edgeها به GeoDNS، Anycast یا مکانیزم Routing دیگری نیاز دارید. در پروژه‌های کوچک‌تر می‌توانید برای هر منطقه Subdomain جدا تعریف کنید یا با DNS Round Robin شروع کنید؛ هرچند Round Robin الزاماً نزدیک‌ترین سرور را انتخاب نمی‌کند.

معماری نهایی پروژه

برای اینکه اجزای پروژه قاطی نشوند، چهار Hostname در نظر می‌گیریم:

Hostname کاربرد
transfer.example.com آپلود و انتقال فایل موقت
origin.example.com Origin فایل‌های استاتیک و تصاویر اصلی
cdn.example.com Static CDN و Edge Cache
img.example.com Image CDN و پردازش تصویر

برای این پروژه چه سروری نیاز داریم؟

برای شروع می‌توانید Origin، FastAPI و imgproxy را روی یک VPS لینوکس قرار دهید و بعد Edgeها را روی VPSهای جدا اضافه کنید.

نقش CPU پیشنهادی شروع RAM نکته مهم
Origin + Transfer 2 vCPU 2GB+ Storage و Traffic
Edge Cache 1 تا 2 vCPU 1 تا 2GB پورت شبکه و Disk Cache
Image CDN 2 تا 4 vCPU 2 تا 4GB CPU برای Resize و Encoding

ساخت CDN شخصی با چند VPS ایرانیکاسرور

برای شروع می‌توانید Origin را روی یک سرور مجازی ایران قرار دهید و در صورت نیاز Edgeهای دیگر را روی VPSهای جداگانه بسازید. اگر کاربران شما هم داخل و هم خارج ایران هستند، جدا کردن Edge ایران و خارج می‌تواند Architecture مناسبی برای آزمایش CDN شخصی باشد.

مرحله اول: آماده‌سازی Origin روی Ubuntu 24.04

sudo apt update
sudo apt upgrade -y

sudo apt install 
  nginx 
  python3 
  python3-venv 
  python3-pip 
  sqlite3 
  curl 
  ca-certificates 
  -y

مسیرهای اصلی پروژه را ایجاد کنید:

sudo mkdir -p /srv/file-platform/static
sudo mkdir -p /srv/file-platform/images
sudo mkdir -p /srv/file-platform/transfers
sudo mkdir -p /opt/file-platform

بخش اول: ساخت سرویس انتقال فایل موقت

سرویس انتقال فایل ما شبیه یک نسخه شخصی و ساده از سرویس‌های Temporary File Sharing عمل می‌کند. فایل Upload می‌شود، Token تصادفی ساخته می‌شود و لینک تا زمان مشخصی معتبر می‌ماند.

در این بخش لینک یک‌بارمصرف نیست؛ تا پایان زمان اعتبار می‌تواند چند بار Download شود. برای لینک کاملاً یک‌بارمصرف باید وضعیت مصرف Token نیز ثبت شود.

ساخت Virtual Environment

cd /opt/file-platform

python3 -m venv venv

./venv/bin/pip install 
  fastapi 
  "uvicorn[standard]" 
  python-multipart

کد FastAPI

فایل زیر را بسازید:

nano /opt/file-platform/app.py
import hashlib
import os
import secrets
import sqlite3
import time
import uuid

from pathlib import Path
from urllib.parse import quote

from fastapi import (
    FastAPI,
    File,
    Form,
    Header,
    HTTPException,
    UploadFile,
)

from fastapi.responses import Response


app = FastAPI()

BASE_URL = os.getenv(
    "BASE_URL",
    "https://transfer.example.com"
).rstrip("/")

ADMIN_KEY = os.getenv(
    "ADMIN_KEY",
    "CHANGE-ME"
)

FILE_ROOT = Path(
    "/srv/file-platform/transfers"
).resolve()

DB_PATH = Path(
    "/opt/file-platform/transfers.db"
)

MAX_BYTES = 5 * 1024 * 1024 * 1024


def db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    conn = db()

    conn.execute("""
        CREATE TABLE IF NOT EXISTS transfers (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            token_hash TEXT UNIQUE NOT NULL,
            stored_name TEXT NOT NULL,
            original_name TEXT NOT NULL,
            expires_at INTEGER NOT NULL,
            created_at INTEGER NOT NULL
        )
    """)

    conn.commit()
    conn.close()


def token_hash(token):
    return hashlib.sha256(
        token.encode()
    ).hexdigest()


init_db()


@app.post("/api/upload")
async def upload_file(
    file: UploadFile = File(...),
    ttl_minutes: int = Form(60),
    x_admin_key: str | None = Header(None),
):
    if not secrets.compare_digest(
        x_admin_key or "",
        ADMIN_KEY
    ):
        raise HTTPException(
            status_code=401,
            detail="Unauthorized"
        )

    if ttl_minutes < 1 or ttl_minutes > 10080:
        raise HTTPException(
            status_code=400,
            detail="Invalid TTL"
        )

    original = Path(
        file.filename or "file.bin"
    ).name

    suffix = Path(original).suffix[:16]

    stored = (
        uuid.uuid4().hex
        + suffix
    )

    temp_path = FILE_ROOT / (
        stored + ".part"
    )

    final_path = FILE_ROOT / stored

    total = 0

    try:
        with temp_path.open("wb") as output:
            while True:
                chunk = await file.read(
                    1024 * 1024
                )

                if not chunk:
                    break

                total += len(chunk)

                if total > MAX_BYTES:
                    raise HTTPException(
                        status_code=413,
                        detail="File too large"
                    )

                output.write(chunk)

        temp_path.rename(final_path)

    except Exception:
        temp_path.unlink(
            missing_ok=True
        )
        raise

    token = secrets.token_urlsafe(32)
    digest = token_hash(token)

    now = int(time.time())

    expires = (
        now
        + ttl_minutes * 60
    )

    conn = db()

    conn.execute("""
        INSERT INTO transfers (
            token_hash,
            stored_name,
            original_name,
            expires_at,
            created_at
        )
        VALUES (?, ?, ?, ?, ?)
    """, (
        digest,
        stored,
        original,
        expires,
        now
    ))

    conn.commit()
    conn.close()

    return {
        "url": f"{BASE_URL}/t/{token}",
        "expires_at": expires,
        "size": total,
    }


@app.get("/t/{token}")
def download(token: str):
    digest = token_hash(token)
    now = int(time.time())

    conn = db()

    row = conn.execute("""
        SELECT
            stored_name,
            original_name,
            expires_at
        FROM transfers
        WHERE token_hash = ?
    """, (digest,)).fetchone()

    conn.close()

    if row is None:
        raise HTTPException(
            status_code=404
        )

    if row["expires_at"] <= now:
        raise HTTPException(
            status_code=410,
            detail="Link expired"
        )

    response = Response(
        status_code=200
    )

    response.headers[
        "X-Accel-Redirect"
    ] = (
        "/_transfer/"
        + quote(row["stored_name"])
    )

    response.headers[
        "Content-Disposition"
    ] = (
        "attachment; "
        "filename*=UTF-8''"
        + quote(row["original_name"])
    )

    response.headers[
        "Cache-Control"
    ] = "private, no-store"

    response.headers[
        "X-Robots-Tag"
    ] = "noindex, nofollow, noarchive"

    return response

در این کد Token خام داخل دیتابیس ذخیره نمی‌شود و فقط SHA-256 آن نگه‌داری می‌شود. فایل نیز با نام تصادفی روی Disk ذخیره می‌شود تا نام اصلی فایل مسیر واقعی Storage را تعیین نکند.

اجرای FastAPI با systemd

یک کلید مدیریتی تصادفی بسازید:

openssl rand -hex 32

سپس Unit بسازید:

sudo nano \
/etc/systemd/system/file-transfer.service
[Unit]
Description=Temporary File Transfer API
After=network.target

[Service]
WorkingDirectory=/opt/file-platform

Environment="BASE_URL=https://transfer.example.com"
Environment="ADMIN_KEY=YOUR_RANDOM_ADMIN_KEY"

ExecStart=/opt/file-platform/venv/bin/uvicorn 
    app:app 
    --host 127.0.0.1 
    --port 9000 
    --workers 2

Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now file-transfer
sudo systemctl status file-transfer

تنظیم Nginx برای سرویس انتقال فایل

server {
    listen 80;
    server_name transfer.example.com;

    client_max_body_size 5g;

    location /api/ {
        proxy_request_buffering off;

        proxy_pass http://127.0.0.1:9000;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /t/ {
        proxy_pass http://127.0.0.1:9000;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /_transfer/ {
        internal;

        alias /srv/file-platform/transfers/;

        add_header X-Content-Type-Options nosniff always;
    }
}

Location با نام /_transfer/ عمداً internal است. کاربر نمی‌تواند مسیر فایل واقعی را مستقیم باز کند؛ فقط FastAPI بعد از تأیید Token با هدر X-Accel-Redirect اجازه تحویل فایل را صادر می‌کند.

تست سرویس انتقال فایل

مثلاً برای آپلود فایل با اعتبار 120 دقیقه:

curl -X POST \
  -H "X-Admin-Key: YOUR_RANDOM_ADMIN_KEY" \
  -F "ttl_minutes=120" \
  -F "file=@backup.zip" \
  https://transfer.example.com/api/upload

پاسخ نمونه:

{
  "url": "https://transfer.example.com/t/....",
  "expires_at": 1790960000,
  "size": 52428800
}

پاک کردن خودکار فایل‌های منقضی‌شده

اگر فایل‌های موقت پاک نشوند، Disk به‌مرور پر می‌شود. بهتر است یک Cleanup Job دوره‌ای براساس expires_at داشته باشید.

منطق Cleanup باید رکوردهای منقضی‌شده را از SQLite بخواند، فایل stored_name مربوط به هر رکورد را حذف کند و سپس رکورد دیتابیس را پاک کند. برای محیط کوچک اجرای این Job هر 15 یا 30 دقیقه کافی است.

بخش دوم: ساخت CDN شخصی برای فایل‌های استاتیک

حالا سراغ فایل‌هایی می‌رویم که موقت نیستند؛ مانند CSS، JavaScript، فونت، ZIP، PDF یا Assetهای سایت.

فایل اصلی فقط روی Origin قرار می‌گیرد و Edgeها اولین درخواست را از Origin دریافت می‌کنند. درخواست‌های بعدی تا پایان TTL مستقیماً از Cache همان Edge پاسخ داده می‌شوند.

کانفیگ Origin

server {
    listen 80;
    server_name origin.example.com;

    location /static/ {
        alias /srv/file-platform/static/;

        autoindex off;

        expires 7d;

        add_header Cache-Control \
            "public, max-age=604800, immutable";
    }

    location /images/ {
        alias /srv/file-platform/images/;

        autoindex off;

        expires 30d;

        add_header Cache-Control \
            "public, max-age=2592000, immutable";
    }
}

برای Cache طولانی حتماً فایل‌ها را Version کنید

اگر فایل app.css را با Cache یک‌هفته‌ای منتشر کنید و بعد محتوای همان URL را تغییر دهید، برخی کاربران تا پایان TTL نسخه قبلی را خواهند دید.

راه بهتر استفاده از نام نسخه‌دار یا Hash است:

app.4f87c2.css
app.v17.js
logo.20261002.svg

در این مدل می‌توانید Cache-Control طولانی‌تری بدهید؛ چون هر تغییر فایل یک URL جدید ایجاد می‌کند.

ساخت اولین Edge Node با Nginx Proxy Cache

روی VPS دوم Nginx نصب کنید:

sudo apt update
sudo apt install nginx -y

در بخش http فایل nginx.conf یک Cache Zone ایجاد کنید:

proxy_cache_path \
  /var/cache/nginx/static \
  levels=1:2 \
  keys_zone=STATIC_CACHE:50m \
  max_size=20g \
  inactive=7d \
  use_temp_path=off;

حالا Virtual Host مربوط به Edge:

server {
    listen 80;
    server_name cdn.example.com;

    location / {
        proxy_cache STATIC_CACHE;

        proxy_cache_key \
            "$scheme$proxy_host$request_uri";

        proxy_cache_lock on;

        proxy_cache_valid 200 301 302 7d;
        proxy_cache_valid 404 1m;

        proxy_cache_use_stale \
            error \
            timeout \
            updating \
            http_500 \
            http_502 \
            http_503 \
            http_504;

        proxy_pass \
            https://origin.example.com;

        proxy_set_header \
            Host origin.example.com;

        proxy_ssl_server_name on;

        add_header \
            X-Cache-Status \
            $upstream_cache_status \
            always;
    }
}

چطور بفهمیم CDN واقعاً Cache می‌کند؟

یک فایل را دو بار درخواست کنید:

curl -I \
https://cdn.example.com/static/app.v1.js

اولین درخواست معمولاً:

X-Cache-Status: MISS

درخواست بعدی باید در شرایط عادی چیزی شبیه این باشد:

X-Cache-Status: HIT

چرا proxy_cache_lock مهم است؟

فرض کنید یک فایل جدید منتشر شده و در یک لحظه صدها کاربر آن را درخواست کنند. اگر Cache خالی باشد، بدون Lock ممکن است تعداد زیادی درخواست هم‌زمان به Origin برسد.

با proxy_cache_lock فقط یک Request Cache را پر می‌کند و Requestهای دیگر منتظر نتیجه می‌مانند؛ این رفتار فشار ناگهانی روی Origin را کمتر می‌کند.

اگر Origin قطع شود چه می‌شود؟

دستور proxy_cache_use_stale اجازه می‌دهد در برخی خطاهای Origin نسخه Cacheشده قبلی همچنان به کاربر تحویل داده شود.

این یکی از مزیت‌های مهم Edge Cache است؛ قطعی کوتاه Origin الزاماً به معنی قطع فوری تمام Assetهای Cacheشده نیست.

Cache فایل‌های بسیار بزرگ

اگر قرار است فایل‌های چندگیگابایتی را Cache کنید، Byte Range اهمیت پیدا می‌کند. Nginx ماژولی به نام slice دارد که Response بزرگ را به بخش‌های کوچک‌تر تقسیم می‌کند تا Cache کردن Rangeها مؤثرتر شود.

اما این Module در همه Buildهای Nginx به‌صورت پیش‌فرض فعال نیست؛ قبل از استفاده باید Build فعلی را بررسی کنید.

چطور CDN را چندنقطه‌ای کنیم؟

بعد از اینکه اولین Edge کار کرد، می‌توانید همان Configuration را روی VPSهای دیگری Deploy کنید.

برای نمونه:

ir-edge.example.com
eu-edge.example.com
cdn.example.com

اگر Routing هوشمند ندارید، می‌توانید ابتدا Application را طوری تنظیم کنید که براساس موقعیت یا نوع کاربر Hostname مناسب را انتخاب کند. راه حرفه‌ای‌تر استفاده از GeoDNS یا Anycast است.

بخش سوم: ساخت Image CDN شخصی

Static CDN فقط همان فایل اصلی را Cache می‌کند، اما Image CDN یک کار اضافه انجام می‌دهد: تصویر اصلی را در لحظه Resize، Crop یا Convert می‌کند و خروجی تولیدشده را Cache می‌کند.

مثلاً فقط یک تصویر 4000×3000 روی Origin دارید، اما سایت به چند نسخه نیاز دارد:

120x120   avatar
400x300   card
800x600   article
1600x900  hero

به‌جای ساخت دستی همه نسخه‌ها، imgproxy آن‌ها را On-Demand تولید می‌کند.

imgproxy چیست؟

imgproxy یک سرویس مستقل برای Resize، Crop، تبدیل و Optimize تصویر است که روی libvips کار می‌کند. نسخه متن‌باز آن از فرمت‌هایی مانند JPEG، PNG، WebP، AVIF و GIF پشتیبانی می‌کند.

مزیت آن این است که Application اصلی شما دیگر درگیر پردازش تصویر نمی‌شود.

نصب Docker روی Ubuntu

Docker را از Repository رسمی نصب کنید:

sudo apt update
sudo apt install ca-certificates curl -y

sudo install -m 0755 
  -d /etc/apt/keyrings

sudo curl -fsSL 
  https://download.docker.com/linux/ubuntu/gpg 
  -o /etc/apt/keyrings/docker.asc

sudo chmod a+r 
  /etc/apt/keyrings/docker.asc

sudo tee 
/etc/apt/sources.list.d/docker.sources 
>/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update

sudo apt install 
  docker-ce 
  docker-ce-cli 
  containerd.io 
  docker-buildx-plugin 
  docker-compose-plugin 
  -y

اجرای imgproxy

کلید و Salt تولید کنید:

openssl rand -hex 32
openssl rand -hex 32

یک پوشه بسازید:

sudo mkdir -p /opt/imgproxy
cd /opt/imgproxy

فایل .env:

IMGPROXY_KEY=YOUR_HEX_KEY
IMGPROXY_SALT=YOUR_HEX_SALT
IMGPROXY_SIGNATURE_SIZE=32
IMGPROXY_AUTO_WEBP=true
IMGPROXY_AUTO_AVIF=true

Docker Compose:

services:
  imgproxy:
    image: ghcr.io/imgproxy/imgproxy:latest

    restart: unless-stopped

    ports:
      - "127.0.0.1:8080:8080"

    env_file:
      - .env
sudo docker compose up -d

sudo docker compose ps

قرار دادن تصاویر اصلی روی Origin

مثلاً تصویر زیر را در Origin قرار می‌دهیم:

/srv/file-platform/images/product.jpg

و از طریق این URL قابل دسترسی است:

https://origin.example.com/images/product.jpg

تست Resize تصویر

در محیط آزمایشی و بدون Signature، ساختار URL imgproxy شبیه این است:

/SIGNATURE/rs:fill:800:600/plain/SOURCE_URL@webp

اما در Production نباید Endpoint را با Signature غیرفعال یا insecure در اینترنت قرار دهید؛ چون هر فرد می‌تواند هزاران سایز مختلف درخواست کند و CPU سرور را مصرف کند.

ساخت URL امضاشده برای Image CDN

imgproxy از HMAC برای امضای مسیر URL استفاده می‌کند. یک Generator ساده Python:

import base64
import hashlib
import hmac


KEY = bytes.fromhex(
    "YOUR_HEX_KEY"
)

SALT = bytes.fromhex(
    "YOUR_HEX_SALT"
)


source = (
    "https://origin.example.com"
    "/images/product.jpg"
)

encoded_source = (
    base64.urlsafe_b64encode(
        source.encode()
    )
    .rstrip(b"=")
    .decode()
)

path = (
    "/rs:fill:800:600:0"
    f"/{encoded_source}.webp"
)

digest = hmac.new(
    KEY,
    SALT + path.encode(),
    hashlib.sha256
).digest()

signature = (
    base64.urlsafe_b64encode(
        digest
    )
    .rstrip(b"=")
    .decode()
)

print(
    "https://img.example.com/"
    + signature
    + path
)

هر بار که کاربر سایز 800×600 را درخواست کند، همان URL تولید می‌شود؛ بنابراین نتیجه به‌خوبی قابل Cache شدن است.

Cache کردن خروجی‌های Image CDN

اگر imgproxy هر بار تصویر را دوباره Resize کند، CPU بیهوده مصرف می‌شود. بهترین حالت این است که خروجی‌های پردازش‌شده در Nginx Cache شوند.

در nginx.conf:

proxy_cache_path \
  /var/cache/nginx/images \
  levels=1:2 \
  keys_zone=IMAGE_CACHE:100m \
  max_size=30g \
  inactive=30d \
  use_temp_path=off;

Virtual Host:

server {
    listen 80;
    server_name img.example.com;

    location / {
        proxy_cache IMAGE_CACHE;

        proxy_cache_key \
            "$scheme$host$request_uri";

        proxy_cache_lock on;

        proxy_cache_valid 200 30d;
        proxy_cache_valid 404 1m;

        proxy_cache_use_stale \
            error \
            timeout \
            updating;

        proxy_pass \
            http://127.0.0.1:8080;

        add_header \
            X-Image-Cache \
            $upstream_cache_status \
            always;
    }
}

WebP بهتر است یا AVIF؟

imgproxy می‌تواند خروجی‌های WebP و AVIF تولید کند. AVIF در بسیاری از تصاویر حجم کمتری ایجاد می‌کند، اما Encoding آن معمولاً CPU بیشتری می‌طلبد.

برای شروع می‌توانید URLهای مشخص و پایدار تولید کنید:

product-800x600.webp
product-800x600.avif

اگر انتخاب Format را براساس Header مرورگر انجام می‌دهید، Cache Key باید تفاوت Accept Header را هم درست مدیریت کند. برای معماری ساده، URL مستقل برای هر Format قابل پیش‌بینی‌تر است.

استفاده از Image CDN در HTML سایت

بعد از ساخت URLهای امضاشده می‌توانید srcset ایجاد کنید:

<img
  src="IMAGE_800_URL"
  srcset="
    IMAGE_400_URL 400w,
    IMAGE_800_URL 800w,
    IMAGE_1200_URL 1200w
  "
  sizes="(max-width: 600px) 100vw, 800px"
  loading="lazy"
  alt="Product image"
/>

مرورگر متناسب با عرض نمایشگر و Device Pixel Ratio نسخه مناسب‌تر را انتخاب می‌کند.

امنیت CDN شخصی

CDN شخصی یعنی خودتان مسئول امنیت Origin، Edge و Image Processor هستید.

✅ Origin را تا حد امکان فقط برای Edgeها باز کنید.

✅ HTTPS را روی Origin و Edge فعال کنید.

✅ Upload API را بدون Authentication عمومی نکنید.

✅ Tokenهای انتقال فایل را طولانی و تصادفی بسازید.

✅ URLهای imgproxy را Sign کنید.

✅ فایل‌های موقت منقضی‌شده را خودکار پاک کنید.

✅ روی Cache Disk فضای آزاد را مانیتور کنید.

✅ Docker Container را مستقیماً روی IP عمومی Publish نکنید.

چطور Origin را پنهان کنیم؟

اگر کاربران بتوانند مستقیماً Origin را باز کنند، ممکن است Cache و Edge را دور بزنند.

برای معماری حرفه‌ای‌تر می‌توانید Origin را فقط برای IP Edge Nodeها Allow کنید. یک روش دیگر قرار دادن Origin در Private Network میان VPSها است.

اگر Edge و Origin در دیتاسنترهایی هستند که شبکه خصوصی بین آن‌ها در دسترس نیست، حداقل Firewall Origin را روی IPهای مشخص Edge محدود کنید.

چطور Cache را به‌روزرسانی کنیم؟

در نسخه متن‌باز Nginx بهترین استراتژی برای Assetهای استاتیک این است که به‌جای Purge مداوم، URL فایل را Version کنید.

یعنی به‌جای:

/static/app.js

نسخه جدید را با URL جدید منتشر کنید:

/static/app.92db18.js

فایل قبلی تا پایان TTL در Cache می‌ماند، اما دیگر در صفحات جدید Reference نمی‌شود.

چه چیزهایی را روی CDN مانیتور کنیم؟

فقط Up/Down بودن Nginx کافی نیست. چند شاخص مهم عبارت‌اند از:

🔹 Cache HIT Ratio

🔹 مصرف Disk Cache

🔹 Traffic خروجی Edge

🔹 Traffic ورودی به Origin

🔹 Response Time

🔹 خطاهای 4xx و 5xx

🔹 CPU مصرفی imgproxy

🔹 تعداد فایل‌های موقت منقضی‌شده

آیا CDN شخصی از CDN تجاری ارزان‌تر است؟

همیشه نه. اگر ترافیک کم باشد، چند VPS ممکن است از نظر هزینه قابل قبول باشند؛ اما با افزایش تعداد کشورها، Traffic، Storage و نیاز به DDoS Protection، مدیریت شبکه شخصی پیچیده‌تر می‌شود.

مزیت CDN شخصی بیشتر در کنترل کامل، Privacy، سناریوهای داخلی، فایل‌های خاص، Lab، دانلودسنتر اختصاصی و معماری Hybrid دیده می‌شود.

برای وب‌سایت جهانی با میلیون‌ها کاربر، استفاده از CDN تخصصی همچنان می‌تواند منطقی‌تر باشد.

این معماری برای چه پروژه‌هایی مناسب است؟

🔹 فایل‌های دانلود مشتریان

🔹 انتقال فایل موقت میان تیم‌ها

🔹 فایل‌های CSS و JavaScript سایت

🔹 فونت و Assetهای استاتیک

🔹 تصاویر فروشگاه اینترنتی

🔹 تصاویر وبلاگ یا اپلیکیشن

🔹 دانلودسنتر نرم‌افزار

🔹 سرویس داخلی سازمان

رفع مشکلات رایج

X-Cache-Status همیشه MISS است

Cache-Control، Set-Cookie، Vary Header، Cache Key و Permission مسیر /var/cache/nginx را بررسی کنید. برخی Responseها طبق Header مبدا قابل Cache نیستند.

Edge فایل قدیمی نمایش می‌دهد

URL Asset را Version کنید. تغییر محتوا زیر URL ثابت همراه با TTL طولانی طراحی مناسبی برای CDN نیست.

Image CDN CPU زیادی مصرف می‌کند

بررسی کنید خروجی imgproxy Cache می‌شود یا نه. اگر هر Request دوباره پردازش شود، CPU به‌سرعت بالا می‌رود.

فایل موقت Download نمی‌شود

زمان انقضای Token، Permission فایل، Location داخلی Nginx و X-Accel-Redirect را بررسی کنید.

Upload فایل بزرگ با 413 قطع می‌شود

client_max_body_size در Nginx و MAX_BYTES در Application را بررسی کنید. هر دو باید با سیاست Upload شما هماهنگ باشند.

imgproxy خطای Signature می‌دهد

KEY و SALT برنامه تولید URL باید دقیقاً با Environment Container یکسان باشند. همچنین Signature باید روی همان Path دقیق URL محاسبه شود.

سؤالات متداول درباره CDN شخصی و سرویس انتقال فایل

آیا با یک VPS می‌توان CDN ساخت؟

با یک VPS می‌توانید Origin یا Cache Proxy بسازید، اما CDN واقعی معمولاً چند Edge Node در نقاط مختلف و مکانیزم مسیریابی کاربران میان آن‌ها دارد.

CDN شخصی با Nginx چگونه کار می‌کند؟

Nginx Edge اولین درخواست فایل را از Origin می‌گیرد و در Cache محلی ذخیره می‌کند. درخواست‌های بعدی همان URL مستقیماً از Edge پاسخ داده می‌شوند.

سرویس انتقال فایل موقت چگونه فایل را حذف می‌کند؟

برای هر فایل زمان انقضا در دیتابیس ذخیره می‌شود و یک Cleanup Job دوره‌ای فایل و رکوردهای منقضی‌شده را حذف می‌کند.

Image CDN چه تفاوتی با Static CDN دارد؟

Static CDN فایل موجود را Cache می‌کند، اما Image CDN می‌تواند قبل از Cache کردن تصویر را Resize، Crop، Optimize یا به WebP و AVIF تبدیل کند.

imgproxy چیست؟

imgproxy یک سرویس مستقل پردازش تصویر است که می‌تواند تصویر Source را دریافت کند و براساس پارامترهای URL نسخه Resize یا تبدیل‌شده تحویل دهد.

چرا URLهای imgproxy باید Sign شوند؟

اگر URL آزاد باشد، مهاجم می‌تواند تعداد بسیار زیادی سایز و تبدیل مختلف درخواست کند و CPU سرور پردازش تصویر را مصرف کند. Signature فقط URLهای تولیدشده توسط Application شما را معتبر می‌کند.

آیا Nginx می‌تواند هنگام خرابی Origin فایل Cacheشده را نمایش دهد؟

بله. proxy_cache_use_stale می‌تواند در برخی خطاها یا Timeoutهای Origin نسخه Cacheشده قبلی را تحویل دهد.

بهترین سیستم‌عامل برای CDN شخصی چیست؟

برای این معماری Ubuntu Server یا Debian انتخاب‌های ساده‌ای هستند؛ Nginx، Python، Docker و ابزارهای مورد نیاز روی هر دو در دسترس‌اند.

برای ساخت Origin، Edge یا دانلودسنتر به VPS نیاز دارید؟

می‌توانید Origin را روی VPS ایران قرار دهید و برای تست معماری Multi-Edge از سرورهای مجازی دیگر استفاده کنید. اگر در انتخاب منابع، Nginx Cache، Docker یا شبکه میان Edgeها مشکل دارید، امکان درخواست کانفیگ نیز وجود دارد.

📌 تماس با پشتیبانی:
021-91302460
| ایرانیکاسرور

جمع‌بندی

با ترکیب Nginx، Python و imgproxy می‌توان یک زیرساخت شخصی برای تحویل فایل ساخت که سه نیاز متفاوت را پوشش دهد: فایل‌های موقت، Assetهای استاتیک و تصاویر بهینه‌شده.

FastAPI مسئول صدور لینک‌های موقت است، Nginx فایل‌های استاتیک را روی Edge Cache می‌کند و imgproxy نسخه‌های مختلف تصویر را فقط هنگام نیاز می‌سازد. با Cache کردن خروجی imgproxy نیز پردازش تکراری تصویر به حداقل می‌رسد.

اگر فقط یک Node داشته باشید، معماری شما هنوز بیشتر شبیه Origin + Cache Server است. با اضافه کردن Edge در چند نقطه و مسیریابی کاربران به Edge مناسب، این ساختار به یک CDN شخصی واقعی نزدیک می‌شود.

برای پروژه‌های داخلی، سرویس‌های سازمانی، دانلودسنتر، فروش فایل یا سایت‌هایی که کنترل کامل روی داده و Cache برایشان مهم است، این معماری می‌تواند جایگزین جذابی برای قرار دادن تمام فایل‌ها روی همان وب‌سرور اصلی باشد.

اشتراک‌گذاری مقاله
گفت‌وگو درباره آموزش

دیدگاه‌ها (۰)

هنوز دیدگاهی ثبت نشده است.

دیدگاه یا سؤال خود را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *