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

حاويات 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ثلاثة سلوكيات يجدر الانتباه إليها:
getContainer(namespace, name)يُرجع وكيلاً مرتبطاً بحاوية محددة. مرّر الاسم نفسه للتوجيه اللاصق، أو ولّد اسماً عشوائياً لتوزيع الحمل.sleepAfterيخبر Cloudflare أن تُغلق الحاوية بعد خمس دقائق من عدم النشاط. تتوقف عن الدفع عندما لا يستخدمها أحد، وغالباً ما يكون البدء الدافئ في أقل من 300 مللي ثانية.- يتولّى الـ Worker التخزين المؤقت قبل الحاوية. قراءة KV أرخص بكثير من حوسبة الحاوية، فتخزين التحويلات الشائعة يوفّر كثيراً من التكلفة.
الخطوة 7: التطوير المحلي مع Wrangler
تُشغّل بيئة التطوير المحلية من Cloudflare حاويات حقيقية عبر Docker، دون الحاجة إلى محاكاة.
wrangler devستقوم Wrangler بـ:
- بناء صورة Docker محلياً
- تشغيل حاوية حية
- تشغيل الـ Worker على http://localhost:8787
- تمرير طلباتك عبر المنسّق
اختبر التدفق الكامل:
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 — وحالما تبني خدمة بهذا الأسلوب، ستجد نفسك تلجأ إليها كلما احتجت إلى حوسبة حقيقية قريبة من المستخدمين.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

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

بناء واجهات REST API جاهزة للإنتاج باستخدام FastAPI و PostgreSQL و Docker
تعلّم كيف تبني وتختبر وتنشر واجهة REST API احترافية باستخدام إطار FastAPI في بايثون مع PostgreSQL و SQLAlchemy و Alembic و Docker Compose — من الصفر حتى النشر.

بناء ونشر واجهة برمجية بدون خوادم باستخدام Cloudflare Workers و Hono و D1
تعلّم كيف تبني واجهة برمجية REST جاهزة للإنتاج باستخدام Cloudflare Workers وإطار Hono وقاعدة بيانات D1 — من إعداد المشروع حتى النشر العالمي.