نشر حاويات Docker على شبكة Cloudflare الطرفية باستخدام Cloudflare Containers

Noqta Team
بواسطة Noqta Team ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

حاويات Docker حقيقية على حافة الشبكة. تشغّل خدمة Cloudflare Containers صور OCI قياسية في أكثر من 300 موقع حول العالم، وتتولى Workers تنسيق الحركة. لا تحتاج إلى Kubernetes ولا إلى إدارة عناقيد، ولا تواجه أي بدء بارد. تدفع فقط مقابل كل طلب وكل ثانية حوسبة، وعند وصول حركة المرور إلى حاويتك فقط.

ما الذي ستبنيه؟

في هذا الدرس، ستنشر واجهة برمجية لمعالجة الصور مبنية على Node.js و Express داخل حاوية إلى Cloudflare Containers. ستوجّه الطلبات عبر Worker، وستُكبّر الحاويات تلقائياً وفقاً لحركة المرور، وستربط الكلّ بـ KV للتخزين المؤقت. في النهاية ستحصل على خدمة طرفية إنتاجية قادرة على تشغيل أحمال ثقيلة (معالجة الصور، توليد ملفات PDF، استدلال نماذج الذكاء الاصطناعي) قريبة من المستخدمين أينما كانوا.

مميزات الحزمة النهائية:

  • صورة Docker حقيقية تُشغّل واجهة Express (مع مكتبة Sharp لتغيير حجم الصور)
  • Worker يعمل بمثابة بوابة الواجهة، يوجّه الحركة، ويخزّن مؤقتاً، ويحدّ من المعدّل
  • توسعة تلقائية لكل منطقة حسب حجم الطلبات
  • تخزين مؤقت للاستجابات في KV لتقليل استدعاءات الحاويات
  • فحوصات سلامة، وسجلات منظّمة، ورصد عبر لوحات Cloudflare

المتطلبات الأساسية

قبل البدء، تأكد من توفر:

  • Node.js 20+ (رابط التحميل)
  • Docker Desktop يعمل محلياً (تثبيت Docker)
  • حساب Cloudflare على خطة Workers Paid (Containers تتطلبها — حوالي خمسة دولارات شهرياً)
  • Wrangler CLI v4+ — أداة المطور من Cloudflare
  • إلمام بـ أساسيات Docker و TypeScript
  • محرر شيفرات (نوصي بـ VS Code)

أصبحت Cloudflare Containers متاحة بشكل عام منذ بداية 2026. تحصل كل حاوية على ما يصل إلى 4 vCPU و 8 GB من الذاكرة، وتدفع فقط مقابل الثواني التي تخدم فيها الحاوية الطلبات، إضافةً إلى رسم صغير لكل طلب.


الخطوة 1: تثبيت Wrangler والمصادقة

افتح الطرفية وثبّت أحدث إصدار من Wrangler عالمياً:

npm install -g wrangler@latest
wrangler --version
# wrangler 4.x.x

سجّل الدخول إلى حساب Cloudflare:

wrangler login

ستُفتح علامة تبويب في المتصفح للسماح للأداة بالدخول. بعد التفويض، تحقق من الوصول:

wrangler whoami

سترى البريد الإلكتروني للحساب ومعرّف الحساب. انسخ معرّف الحساب — ستحتاجه لاحقاً.


الخطوة 2: إنشاء بنية المشروع

أنشئ مجلداً جديداً وابدأ المشروع:

mkdir image-edge-api
cd image-edge-api
npm init -y

ثبّت اعتماديات تطبيق الحاوية:

npm install express sharp
npm install -D typescript @types/node @types/express tsx

أنشئ هيكل الملفات:

mkdir -p container/src worker/src
touch container/src/index.ts container/Dockerfile
touch worker/src/index.ts wrangler.jsonc
touch tsconfig.json .dockerignore

أضف ملف tsconfig.json بسيطاً:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": ["container/src", "worker/src"]
}

ثم ملف .dockerignore لتقليل حجم الصورة:

node_modules
dist
.git
.env
*.md
worker

الخطوة 3: بناء واجهة برمجية داخل حاوية

افتح ملف container/src/index.ts واكتب تطبيق Express صغيراً يقوم بتغيير حجم الصور:

import express, { Request, Response } from "express";
import sharp from "sharp";
 
const app = express();
const PORT = Number(process.env.PORT) || 8080;
 
app.use(express.raw({ type: "image/*", limit: "10mb" }));
 
app.get("/health", (_req: Request, res: Response) => {
  res.json({ status: "ok", region: process.env.CF_REGION ?? "unknown" });
});
 
app.post("/resize", async (req: Request, res: Response) => {
  const width = Number(req.query.w) || 800;
  const format = (req.query.fmt as string) || "webp";
 
  if (!Buffer.isBuffer(req.body) || req.body.length === 0) {
    return res.status(400).json({ error: "no image body" });
  }
 
  try {
    const output = await sharp(req.body)
      .resize({ width, withoutEnlargement: true })
      .toFormat(format as keyof sharp.FormatEnum)
      .toBuffer();
 
    res.set("Content-Type", `image/${format}`);
    res.set("X-Container-Region", process.env.CF_REGION ?? "unknown");
    res.send(output);
  } catch (err) {
    console.error("resize_failed", err);
    res.status(500).json({ error: "resize failed" });
  }
});
 
app.listen(PORT, () => {
  console.log(`image-edge-api listening on :${PORT}`);
});

لاحظ أننا نقرأ متغيّر CF_REGION من البيئة — تحقن Cloudflare هذه القيمة تلقائياً داخل الحاويات الجارية، مما يساعدنا على تتبع التوجيه الإقليمي.


الخطوة 4: كتابة Dockerfile

تقبل Cloudflare Containers أي صورة OCI قياسية. استخدم صورة Node صغيرة وبناءً متعدد المراحل لإبقاء الصورة النهائية خفيفة.

# container/Dockerfile
FROM node:20-bookworm-slim AS builder
WORKDIR /app
 
COPY package*.json tsconfig.json ./
RUN npm ci
 
COPY container/src ./container/src
RUN npx tsc -p tsconfig.json
 
FROM node:20-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
 
COPY package*.json ./
RUN npm ci --omit=dev
 
COPY --from=builder /app/dist ./dist
 
EXPOSE 8080
CMD ["node", "dist/container/src/index.js"]

ابنها محلياً للتأكد من أن كل شيء يعمل:

docker build -t image-edge-api -f container/Dockerfile .
docker run --rm -p 8080:8080 image-edge-api

في طرفية أخرى، اضرب نقطة الفحص:

curl http://localhost:8080/health
# {"status":"ok","region":"unknown"}

اضغط Ctrl+C لإيقاف الحاوية محلياً.

تتطلب Cloudflare Containers صور linux/amd64. إذا كنت تستخدم Apple Silicon، فابنِ الصورة مع علم المنصة: docker build --platform=linux/amd64 ...


الخطوة 5: تهيئة wrangler.jsonc للحاويات

هذه هي القطعة الجديدة المهمة. تكشف Cloudflare الحاويات كرابط داخل Workers — فالـ Worker هو المنسّق الذي يقرر أي حاوية تخدم كل طلب.

افتح wrangler.jsonc:

{
  "name": "image-edge-api",
  "main": "worker/src/index.ts",
  "compatibility_date": "2026-04-15",
  "containers": [
    {
      "class_name": "ImageContainer",
      "image": "./container/Dockerfile",
      "instance_type": "standard",
      "max_instances": 25
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "name": "IMAGE_CONTAINER",
        "class_name": "ImageContainer"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["ImageContainer"]
    }
  ],
  "kv_namespaces": [
    {
      "binding": "IMAGE_CACHE",
      "id": "REPLACE_WITH_KV_ID"
    }
  ],
  "observability": { "enabled": true }
}

بعض الأمور التي يجدر فهمها هنا:

  • يخبر قسم containers خدمة Cloudflare بأي Dockerfile يجب بناؤه وحجم كل حاوية. أنواع الحاويات هي: dev, basic, standard, enhanced — اختر بحسب احتياجك من الذاكرة والمعالج.
  • تُكشف الحاويات عبر Durable Objects. يُغلّف كل عنصر في صنف Durable Object فتحصل على عزل وتثبيت إقليمي وأطر دورة حياة جاهزة.
  • يحدّد max_instances العدد الأقصى من الحاويات المتزامنة على مستوى العالم.

أنشئ مساحة الأسماء KV واستبدل القيمة المؤقتة:

wrangler kv namespace create IMAGE_CACHE

انسخ المعرّف الذي تظهره الأداة إلى ملف wrangler.jsonc.


الخطوة 6: كتابة منسّق Worker

الـ Worker هو نقطة الدخول العامة. يستقبل كل طلب، يطبّق التخزين المؤقت والتحقق، ثم يحوّل الحركة إلى الحاوية.

افتح worker/src/index.ts:

import { Container, getContainer } from "@cloudflare/containers";
 
export interface Env {
  IMAGE_CONTAINER: DurableObjectNamespace<ImageContainer>;
  IMAGE_CACHE: KVNamespace;
}
 
export class ImageContainer extends Container {
  defaultPort = 8080;
  sleepAfter = "5m";
 
  override onStart() {
    console.log("container_started", { id: this.ctx.id.toString() });
  }
 
  override onError(err: unknown) {
    console.error("container_error", err);
  }
}
 
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
 
    if (url.pathname === "/health") {
      const container = getContainer(env.IMAGE_CONTAINER, "primary");
      return container.fetch(request);
    }
 
    if (url.pathname === "/resize" && request.method === "POST") {
      const cacheKey = await buildCacheKey(request);
      const cached = await env.IMAGE_CACHE.get(cacheKey, "stream");
      if (cached) {
        return new Response(cached, {
          headers: { "Content-Type": "image/webp", "X-Cache": "HIT" },
        });
      }
 
      const container = getContainer(env.IMAGE_CONTAINER, "primary");
      const upstream = await container.fetch(request);
 
      if (upstream.ok) {
        const buf = await upstream.arrayBuffer();
        await env.IMAGE_CACHE.put(cacheKey, buf, { expirationTtl: 86400 });
        return new Response(buf, {
          headers: {
            "Content-Type": upstream.headers.get("Content-Type") ?? "image/webp",
            "X-Cache": "MISS",
          },
        });
      }
      return upstream;
    }
 
    return new Response("Not Found", { status: 404 });
  },
};
 
async function buildCacheKey(request: Request): Promise<string> {
  const url = new URL(request.url);
  const body = await request.clone().arrayBuffer();
  const hash = await crypto.subtle.digest("SHA-256", body);
  const hex = [...new Uint8Array(hash)]
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  return `${url.search}:${hex}`;
}

ثبّت حزمة المساعد لـ Cloudflare Containers:

npm install @cloudflare/containers

ثلاثة سلوكيات يجدر الانتباه إليها:

  1. getContainer(namespace, name) يُرجع وكيلاً مرتبطاً بحاوية محددة. مرّر الاسم نفسه للتوجيه اللاصق، أو ولّد اسماً عشوائياً لتوزيع الحمل.
  2. sleepAfter يخبر Cloudflare أن تُغلق الحاوية بعد خمس دقائق من عدم النشاط. تتوقف عن الدفع عندما لا يستخدمها أحد، وغالباً ما يكون البدء الدافئ في أقل من 300 مللي ثانية.
  3. يتولّى الـ Worker التخزين المؤقت قبل الحاوية. قراءة KV أرخص بكثير من حوسبة الحاوية، فتخزين التحويلات الشائعة يوفّر كثيراً من التكلفة.

الخطوة 7: التطوير المحلي مع Wrangler

تُشغّل بيئة التطوير المحلية من Cloudflare حاويات حقيقية عبر Docker، دون الحاجة إلى محاكاة.

wrangler dev

ستقوم Wrangler بـ:

  1. بناء صورة Docker محلياً
  2. تشغيل حاوية حية
  3. تشغيل الـ Worker على http://localhost:8787
  4. تمرير طلباتك عبر المنسّق

اختبر التدفق الكامل:

curl http://localhost:8787/health
# {"status":"ok","region":"local"}
 
curl -X POST "http://localhost:8787/resize?w=400&fmt=webp" \
  --data-binary "@./sample.jpg" \
  -H "Content-Type: image/jpeg" \
  -o resized.webp

لديك الآن حزمة كاملة تعمل على حاسوبك — Worker وحاوية — وتتصرّف بنفس الطريقة التي ستتصرف بها في الإنتاج.


الخطوة 8: النشر إلى شبكة Cloudflare الطرفية

عندما تكون راضياً عن السلوك المحلي، انشر كل شيء إلى الإنتاج:

wrangler deploy

تبني Wrangler صورة Docker، وترفعها إلى سجل حاويات Cloudflare، ثم تسجّل صنف Durable Object، وتُطلق الـ Worker. النشر الأول قد يستغرق ثلاث إلى خمس دقائق لأن رفع الصورة لا يكون مخزناً مؤقتاً. النشرات اللاحقة عادةً ما تستغرق أقل من دقيقة.

عند انتهاء النشر، سترى شيئاً مثل:

Deployed image-edge-api to:
  https://image-edge-api.<your-subdomain>.workers.dev
Container instance class: ImageContainer (standard, max 25 instances)

اضرب نقطتك الإنتاجية:

curl -X POST "https://image-edge-api.<subdomain>.workers.dev/resize?w=800" \
  --data-binary "@./sample.jpg" \
  -H "Content-Type: image/jpeg" \
  -o output.webp

الطلب الأول يستثير بدءاً بارداً للحاوية، وعادةً ما يكتمل في حدود 600 مللي ثانية. الطلبات اللاحقة تستخدم الحاوية الدافئة وتنتهي خلال عشرات المللي ثوانٍ فقط.


الخطوة 9: تهيئة التوسعة التلقائية والتوجيه الإقليمي

افتراضياً، تحتفظ Cloudflare بحاوية "primary" واحدة في كل منطقة. لحمل عمل حقيقي، ستريد قواعد توسعة صريحة.

حدّث قسم containers في wrangler.jsonc:

"containers": [
  {
    "class_name": "ImageContainer",
    "image": "./container/Dockerfile",
    "instance_type": "standard",
    "max_instances": 50,
    "scale_rules": {
      "concurrent_requests": 30,
      "cool_down_seconds": 60
    },
    "regions": ["wnam", "enam", "weu", "eeu", "apac", "mena"]
  }
]

ما يفعله هذا الإعداد:

  • تشغّل Cloudflare حاوية إضافية في أي منطقة تتجاوز فيها حاوياتها 30 طلباً متزامناً.
  • بعد 60 ثانية من الحمل تحت العتبة، تُغلق الحاويات الخاملة.
  • تقصر مصفوفة regions الحاويات على المناطق المذكورة من شبكة Cloudflare — مفيدة لمتطلبات إقامة البيانات أو لتجنّب خدمة المناطق التي لن تستفيد من خفض الكمون.

للتوجيه اللاصق (مثل أحمال العمل المرتبطة بالجلسة)، مرّر اسماً ثابتاً إلى getContainer. أما للأعمال المتوازية بطبيعتها، فعشوِ الاسم:

const id = crypto.randomUUID();
const container = getContainer(env.IMAGE_CONTAINER, id);

أعد النشر لتطبيق التغييرات:

wrangler deploy

الخطوة 10: فحوصات السلامة والسجلات والرصد

تعيد Cloudflare تلقائياً تشغيل الحاويات المعطوبة، لكنك تتحكم في تعريف "السلامة". وسّع الصنف ImageContainer في worker/src/index.ts:

export class ImageContainer extends Container {
  defaultPort = 8080;
  sleepAfter = "5m";
  healthCheck = {
    path: "/health",
    intervalSeconds: 30,
    timeoutSeconds: 5,
    failureThreshold: 3,
  };
 
  override onHealthCheckFailed(err: unknown) {
    console.error("health_check_failed", err);
  }
}

بالنسبة للسجلات، يُدفق رابط الرصد التابع لـ Worker ("observability": { "enabled": true } في wrangler.jsonc) كل سطر console.log من Worker والحاوية إلى لوحة Cloudflare. تابع السجلات في الزمن الحقيقي:

wrangler tail

سترى أحداثاً منظّمة مع تدفق الحركة:

container_started { id: "abc123..." }
resize ok width=800 region="weu" duration_ms=42

لتحليلات أعمق، ادفع السجلات إلى مخزن خارجي (Datadog, Axiom, S3) باستخدام Logpush — اضبطه مرة واحدة من اللوحة وستتولى Cloudflare التجميع.


الخطوة 11: الأمان والأسرار

لا تضمّن بيانات الاعتماد داخل صورة Docker أبداً. استخدم أسرار Wrangler المشفرة، التي تظهر كمتغيرات بيئة داخل الحاوية:

wrangler secret put IMGBB_API_KEY
# الصق القيمة، اضغط Enter

داخل container/src/index.ts:

const apiKey = process.env.IMGBB_API_KEY;
if (!apiKey) {
  console.error("missing_api_key");
  process.exit(1);
}

بالنسبة للمصادقة الواردة، تحقّق من الطبقة العلوية في الـ Worker قبل استدعاء الحاوية:

const token = request.headers.get("Authorization");
if (token !== `Bearer ${env.SHARED_TOKEN}`) {
  return new Response("Unauthorized", { status: 401 });
}

هذا النمط يوفر المال — الطلبات غير المصادقة لا تصل إلى الحاوية أبداً، فلا تُحاسب على وقت حوسبة لطلب مرفوض.


اختبار التطبيق

نفّذ اختبار حمل صغيراً على نقطتك الإنتاجية باستخدام hey:

hey -n 1000 -c 50 -m POST \
  -H "Content-Type: image/jpeg" \
  -D ./sample.jpg \
  "https://image-edge-api.<subdomain>.workers.dev/resize?w=400"

في لوحة Cloudflare، يفترض أن ترى:

  • إنشاء حاويات متعددة عبر مناطق مختلفة مع ارتفاع التزامن
  • ارتفاع نسبة إصابة الذاكرة المؤقتة مع تكرار الطلبات
  • زمن p50 أقل من 80 مللي ثانية للطلبات الدافئة، و p99 أقل من 500 مللي ثانية

إذا رأيت إخفاقات في التوسعة أو ارتفاعاً في 5xx، راجع wrangler tail ولوحة Containers للاطلاع على عدد إعادات التشغيل وضغط الموارد.


استكشاف الأخطاء

فشل تشغيل الحاوية مع رمز 137. نفاد الذاكرة. ارفع نوع الحاوية إلى enhanced أو حلّل صورتك بـ docker stats.

بدء بارد يتجاوز ثانيتين. صورتك كبيرة. البناء متعدد المراحل، والصور القاعدية الخفيفة، وإزالة اعتماديات التطوير عادةً ما تخفّض الحجم بنسبة 60 إلى 80 بالمئة.

الـ Worker يخطئ بـ "container not bound". لم يُنفّذ ترحيل Durable Object. تأكد أن قسم migrations في wrangler.jsonc يتضمن الصنف، ثم أعد النشر.

wrangler dev يتجمد عند "starting container". Docker Desktop غير عامل، أو الصورة تستهدف معمارية خاطئة. ابنِ صراحة بـ --platform=linux/amd64.

الذاكرة المؤقتة في KV تُرجع نتائج قديمة. ارفع expirationTtl، أو أضف بصمة محتوى إلى مفتاح التخزين كي لا تتصادم المدخلات المختلفة.


الخطوات التالية

أصبح لديك الآن حزمة عاملة من Cloudflare Containers. إليك بعض الاتجاهات لتطويرها:

  • أضف طوابير عبر Cloudflare Queues للمعالجة غير المتزامنة
  • أربط قاعدة بيانات — راجع درس Cloudflare Workers و Hono و D1 لنمط شقيق
  • شغّل نموذج تعلّم آلةtransformers.js أو نموذج ONNX خفيف يعمل بسلاسة على حاوية standard
  • غلّفها ببوابة API بـ Hono لتوجيه أغنى
  • اطلق تطبيقاً متكاملاً — ادمج Containers مع Next.js على Workers

الخلاصة

تملأ Cloudflare Containers الفجوة المربكة بين Workers (المثالية للدوال السريعة عديمة الحالة) وKubernetes التقليدي (مبالَغ فيه لأغلب الفرق). مع Containers تحصل على صور Docker حقيقية، ووقت معالجة حقيقي، وعمليات مستمرة فعلية — كلها تُقدَّم من شبكة Cloudflare الطرفية، فيما تتولى طبقة Worker التوجيه والتخزين المؤقت.

في هذا الدرس أعددت حزمة حاويات طرفية كاملة: صورة Docker مع Express وSharp، ومنسّق Worker مع تخزين KV مؤقت، وتوسعة تلقائية، وتثبيت إقليمي، وفحوصات سلامة، ورصد. الهيكل نفسه يصلح لتوليد PDF، واستدلال نماذج الذكاء الاصطناعي، ومهام المتصفحات بدون واجهة، وأي شيء أثقل من Worker وأخفّ من تخصيص عنقود.

تُحوّل Cloudflare Containers الحافة العالمية إلى مضيف Docker — وحالما تبني خدمة بهذا الأسلوب، ستجد نفسك تلجأ إليها كلما احتجت إلى حوسبة حقيقية قريبة من المستخدمين.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على بناء مهام خلفية للإنتاج باستخدام Trigger.dev v3 و Next.js.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

Docker Compose للمطورين: Next.js مع PostgreSQL و Redis

تعلم كيفية تغليف تطبيق Next.js كامل مع PostgreSQL و Redis باستخدام Docker Compose. يغطي هذا الدليل العملي تنسيق الخدمات المتعددة وسير عمل التطوير وإعادة التحميل الفوري وفحوصات الصحة والإعدادات الجاهزة للإنتاج.

28 د قراءة·