نشر Bun + Hono على Fly.io: دليل الإنتاج ٢٠٢٦
٢١ مايو ٢٠٢٦
يتطلب نشر تطبيق Bun + Hono على Fly.io ثلاثة أشياء: ملف Dockerfile متعدد المراحل، وملف fly.toml يحدد "ماكينة" (Machine) واحدة ذاتية الإيقاف، وأمر fly deploy. يبني هذا الدليل API Hono بمواصفات الإنتاج ويقوم بتشغيل Hono على Fly.io كماكينة واحدة تتقلص إلى الصفر (scale to zero) بين الطلبات.
ملخص
ستقوم ببناء API Hono صغير ولكنه مصمم للإنتاج على بيئة تشغيل Bun، ووضعه في حاوية (containerize) باستخدام ملف Dockerfile متعدد المراحل يعمل كمستخدم ليس له صلاحيات جذور (non-root)، ونشره على ماكينة Fly.io واحدة. تم ضبط ملف fly.toml ليناسب العالم الحقيقي: حدود التزامن القائمة على الطلبات، وفحص صحة HTTP، وإشارة إغلاق آمن (graceful-shutdown)، وخاصية auto_stop_machines = "stop" حتى لا تكلّف الماكينة شيئاً أثناء الخمول. الأدوات المستخدمة: Bun 1.3.14، Hono 4.12.21، Docker، و flyctl. يستغرق الأمر حوالي 25 دقيقة من البداية للنهاية، وبحلول النهاية سيكون لديك Hono على Fly.io يخدم حركة مرور HTTPS مباشرة.
ما ستتعلمه
- تأسيس مشروع Bun + Hono بهيكل ملفات نظيف وقابل للاختبار
- بناء API Hono مع تسجيل السجلات (logging)، ومعرفات الطلبات، وترويسات الأمان، ونقطة نهاية للصحة (health endpoint)
- إضافة نقطة دخول
Bun.serveصريحة مع إغلاق آمن عند استلام إشارة SIGTERM - كتابة ملف Bun Dockerfile متعدد المراحل يعمل كمستخدم غير جذري (non-root)
- اختبار الحاوية محلياً قبل التعامل مع Fly.io
- إنشاء تطبيق Fly.io باستخدام
fly launchوكتابة ملفfly.tomlجاهز للإنتاج - ضبط الإيقاف التلقائي للماكينات، وحدود التزامن، وفحوصات الصحة لكي يتقلص التطبيق إلى الصفر
- تخزين الأسرار (secrets) باستخدام
fly secretsوشحن التطبيق باستخدامfly deploy
المتطلبات الأساسية
تحتاج إلى تثبيت أربعة أشياء محلياً:
- Bun 1.3.14 — ثبته باستخدام
curl -fsSL https://bun.com/install | bash، أوbrew install oven-sh/bun/bun. تحقق منه باستخدامbun --version.1 - Docker — أي نسخة حديثة من Docker Desktop أو Docker Engine. BuildKit هو المنشئ الافتراضي، لذا لا حاجة لأعلام إضافية. ستحتاجه فقط لاختبار الحاوية محلياً في الخطوة 5.
- flyctl — واجهة سطر أوامر Fly.io. ثبته من التعليمات الرسمية وقم بتشغيل
fly auth login. تم التحقق من هذا الدليل باستخدام flyctl v0.4.49. - حساب Fly.io مع وسيلة دفع. لم يعد لدى Fly.io باقة مجانية دائمة — تحتاج كل منظمة إلى بطاقة مسجلة، على الرغم من أن الحسابات الجديدة تحصل على رصيد تجريبي.2 الماكينة في هذا الدليل هي
shared-cpu-1xمع 256 ميجابايت من الرام، وتكلف حوالي 1.94 دولار شهرياً إذا عملت باستمرار — وأقل بكثير هنا، لأنها تتوقف عند الخمول.2
تساعد المعرفة العملية بـ TypeScript و HTTP، لكنك لا تحتاج إلى خبرة سابقة في Bun أو Hono أو Fly.io.
الخطوة 1 — تأسيس مشروع Bun + Hono
أنشئ مجلداً فارغاً وابدأ مشروع TypeScript جديداً. يقبل العلم -y كل الإعدادات الافتراضية، لذا يعمل bun init بدون مطالبات.3
mkdir fly-hono-edge && cd fly-hono-edge
bun init -y
يقوم bun init بكتابة package.json، و tsconfig.json، وملف index.ts في الجذر، و .gitignore. أضف Hono — يقوم bun add بتثبيت الحزمة وكتابة ملف القفل النصي bun.lock، وهو مرساة إعادة الإنتاج لبناء Docker لاحقاً:
bun add hono
سنحتفظ بكود الخادم تحت مجلد src/، لذا قم بإزالة نقطة الدخول المؤقتة وأنشئ مجلد المصدر:
rm index.ts
mkdir src
افتح package.json واستبدل كتلة scripts. يضيف bun init حزمة @types/bun إلى devDependencies — قم بتثبيتها على إصدار صريح حتى تظل عمليات التثبيت قابلة لإعادة الإنتاج:
{
"name": "fly-hono-edge",
"module": "src/index.ts",
"type": "module",
"private": true,
"scripts": {
"dev": "bun run --hot src/index.ts",
"start": "bun run src/index.ts",
"test": "bun test"
},
"dependencies": {
"hono": "^4.12.21"
},
"devDependencies": {
"@types/bun": "^1.3.14"
}
}
ملف bun.lock — وليس نطاقات العلامة (caret ranges) — هو ما يضمن شجرة تبعيات متطابقة بالبايت، لأن بناء Docker يتم تثبيته باستخدام --frozen-lockfile. قم بإرسال (commit) ملف bun.lock إلى نظام التحكم في الإصدار.
الخطوة 2 — بناء API Hono: المسارات، والبرمجيات الوسيطة، ونقطة نهاية الصحة
Hono هو إطار عمل ويب صغير مبني على معايير الويب (Request/Response)، ويعمل بشكل أصلي على Bun.4 سنحتفظ بتطبيق Hono في وحدته الخاصة بحيث يمكن استيراده من قبل كل من نقطة دخول الخادم وملف الاختبار.
أنشئ src/app.ts:
import { Hono from 'hono'
import type { RequestIdVariables from 'hono/request-id'
import { logger from 'hono/logger'
import { requestId from 'hono/request-id'
import { secureHeaders from 'hono/secure-headers'
import { bearerAuth from 'hono/bearer-auth'
import { HTTPException from 'hono/http-exception'
const startedAt = Date.now()
const region = process.env.FLY_REGION ?? 'local'
const apiToken = process.env.API_TOKEN
// Typing the app with RequestIdVariables makes c.get('requestId') type-safe.
export const app = new Hono<{ Variables: RequestIdVariables >()
// --- Global middleware ---
app.use('*', logger())
app.use('*', requestId())
app.use('*', secureHeaders())
// --- Public routes ---
app.get('/', (c) =>
c.json({
name: 'fly-hono-edge',
message: 'Bun + Hono running on Fly.io',
region,
requestId: c.get('requestId'),
}),
)
// Health endpoint — wired to the fly.toml health check in Step 7.
app.get('/health', (c) =>
c.json({
status: 'ok',
region,
uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
}),
)
app.get('/API/hello/:name', (c) => {
const name = c.req.param('name')
return c.json({ greeting: `Hello, ${name}!`, region )
})
// --- Protected route — the token is supplied via `fly secrets` ---
const admin = new Hono<{ Variables: RequestIdVariables >()
admin.use('*', (c, next) => {
if (!apiToken) {
return c.json({ error: 'API_TOKEN secret is not configured' , 503)
}
return bearerAuth({ token: apiToken )(c, next)
})
admin.get('/stats', (c) =>
c.json({
bunVersion: Bun.version,
pid: process.pid,
rssMB: Math.round(process.memoryUsage().rss / 1024 / 1024),
region,
}),
)
app.route('/API/admin', admin)
// --- Error handling ---
app.notFound((c) => c.json({ error: 'Not Found', path: c.req.path , 404))
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
console.error(`[${c.get('requestId')}]`, err)
return c.json({ error: 'Internal Server Error' , 500)
})
هناك بعض تفاصيل الإنتاج التي تستحق الذكر. تقوم البرمجية الوسيطة requestId بتمييز كل طلب بمعرف UUID (وتعيد استخدام ترويسة X-Request-Id الواردة إذا أرسلها العميل) وتكشفها من خلال c.get('requestId').5 تكتب البرمجية الوسيطة logger سطراً واحداً لكل طلب إلى stdout — على Fly.io يصبح هذا التدفق هو fly logs، لذا تحصل على سجلات الطلبات مجاناً. تضيف secureHeaders مجموعة معقولة من ترويسات الاستجابة مثل X-Frame-Options و Strict-Transport-Security.
مسار /health رخيص التكلفة عمداً: فهو يعيد HTTP 200 بدون عمليات إدخال/إخراج (I/O)، وهو بالضبط ما يريده فحص صحة موازن الحمل. يقرأ المسار المحمي /API/admin/stats رمزاً مميزاً (bearer token) من process.env.API_TOKEN. إذا كان هذا السر مفقوداً، يعيد المسار 503 بدلاً من العمل بصمت بدون مصادقة — وهي خاصية أمان صغيرة ولكنها حقيقية. تأتي region من FLY_REGION، وهو رمز منطقة مكون من ثلاثة أحرف يحقنه Fly.io في بيئة تشغيل كل ماكينة.6
الآن أضف ملف اختبار حتى تتمكن من التحقق من السلوك دون تشغيل خادم. توفر تطبيقات Hono طريقة app.request() تأخذ مساراً وتعيد Response. أنشئ src/app.test.ts:
import { describe, expect, it from 'bun:test'
import { app from './app'
describe('fly-hono-edge', () => {
it('GET /health returns ok', async () => {
const res = await app.request('/health')
expect(res.status).toBe(200)
expect(await res.json()).toMatchObject({ status: 'ok' )
})
it('GET /API/hello/:name echoes the name', async () => {
const res = await app.request('/API/hello/ada')
expect(res.status).toBe(200)
expect(await res.json()).toMatchObject({ greeting: 'Hello, ada!' )
})
it('protected route is 503 when API_TOKEN is unset', async () => {
const res = await app.request('/API/admin/stats')
expect(res.status).toBe(503)
})
})
قم بتشغيله باستخدام مشغل الاختبارات المدمج في Bun — لا حاجة لتبعيات إضافية:
bun test
3 pass
0 fail
الخطوة 3 — إضافة نقطة دخول Bun.serve صريحة مع إغلاق آمن
سيقوم Bun تلقائياً بخدمة أي وحدة تحتوي على تصدير default مع معالج fetch، لكن هذا الاختصار لا يمنحك تحكماً في الخادم المشغل. بالنسبة للإنتاج، ستحتاج إلى كائن الخادم حتى تتمكن من إغلاقه بشكل نظيف. أنشئ src/index.ts:
import { app from './app'
const port = Number(process.env.PORT ?? 8080)
const server = Bun.serve({
port,
fetch: app.fetch,
idleTimeout: 30,
)
console.log(`fly-hono-edge listening on ${server.url}`)
let shuttingDown = false
async function shutdown(signal: string): Promise<void> {
if (shuttingDown) return
shuttingDown = true
console.log(`Received ${signal - draining in-flight requests...`)
await server.stop() // resolves once in-flight requests finish
console.log('Server stopped cleanly')
process.exit(0)
}
process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))
يرتبط Bun.serve بالعنوان 0.0.0.0 افتراضياً، وهو أمر مطلوب على Fly.io — حيث يصل وكيل المنصة (platform proxy) إلى تطبيقك عبر شبكة خاصة، لذا فإن الارتباط بـ 127.0.0.1 سيجعل الماكينة غير قابلة للوصول.7 المنفذ port يستخدم افتراضياً متغير البيئة PORT، لكن تمريره بشكل صريح يجعل القصد واضحاً. idleTimeout بالثواني (الافتراضي 10، والحد الأقصى 255)؛ 30 ثانية مريحة لـ API JSON.7
منطق الإغلاق هو الجزء الذي تتجاهله معظم الشروحات التعليمية. server.stop() بدون وسائط يتوقف عن قبول اتصالات جديدة وينتهي بمجرد اكتمال الطلبات الجارية؛ أما تمرير true فسيؤدي إلى إنهاء الاتصالات فوراً.7 تسجيل معالجات لكل من SIGTERM و SIGINT يعني أن التطبيق يفرغ حمولته بنظافة سواء تم إيقافه بواسطة Fly.io، أو بواسطة Docker، أو عن طريق Ctrl+C أثناء التطوير المحلي.
ابدأ تشغيل الخادم محلياً للتأكد من أنه يعمل:
bun run dev
fly-hono-edge listening on http://localhost:8080/
في نافذة أوامر أخرى، curl http://localhost:8080/health يعيد JSON مثل {"status":"ok","region":"local","uptimeSeconds":3}. اضغط على Ctrl+C لإيقاف الخادم — ستشاهد تسلسل الإغلاق التدريجي الكامل يعمل داخل الحاوية في الخطوة 5.
الخطوة 4 — كتابة Dockerfile متعدد المراحل لـ Bun
يقوم Fly.io بنشر صور حاويات OCI، لذا يحتاج التطبيق إلى Dockerfile. يحافظ البناء متعدد المراحل على صغر حجم الصورة النهائية عن طريق فصل تثبيت التبعيات عن طبقة وقت التشغيل.8 أنشئ Dockerfile:
# syntax=Docker/dockerfile:1
# ---------- base ----------
FROM oven/bun:1.3.14-slim AS base
WORKDIR /app
# ---------- dependencies (production only) ----------
FROM base AS deps
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
# ---------- runtime ----------
FROM base AS release
ENV NODE_ENV=production
COPY /app/node_modules ./node_modules
COPY package.json bun.lock ./
COPY src ./src
USER bun
EXPOSE 8080
ENTRYPOINT ["bun", "run", "src/index.ts"]
الصورة الأساسية هي oven/bun:1.3.14-slim — وهي نسخة Debian-slim من صورة Bun الرسمية. تنشر Bun نسخ debian، و slim، و alpine، و distroless لكل إصدار؛ وتعتبر slim هي الخيار الافتراضي العملي لأنها تحتفظ بـ shell لتصحيح الأخطاء مع بقائها صغيرة الحجم. يمكنك التأكد من التاج (tag) الدقيق عبر hub.Docker.com/r/oven/bun/tags.
bun install --frozen-lockfile --production يثبت بالضبط ما يسجله bun.lock ويتخطى devDependencies — لذا لن يتم شحن @types/bun أبداً إلى الإنتاج.8 يتم تخزين مرحلة deps مؤقتاً بشكل مستقل عن كود المصدر الخاص بك، لذا فإن تعديل مسار (route) لا يؤدي إلى إعادة تشغيل bun install.
USER bun أمر مهم: تشحن صور oven/bun مستخدماً غير جذري (non-root) باسم bun، والتشغيل باستخدامه يحد من نطاق الضرر في حال تم اختراق العملية.8 EXPOSE 8080 هو توثيق يقرأه Fly.io أثناء fly launch لضبط المنفذ الداخلي. يقوم ENTRYPOINT بتشغيل ملف الخادم مباشرة.
أضف ملف .dockerignore ليبقى سياق البناء خفيفاً ولا تصل ملفات الاختبار أبداً إلى الصورة:
node_modules
.git
.gitignore
Dockerfile
.dockerignore
fly.toml
README.md
**/*.test.ts
.env
.env.*
أبقِ bun.lock خارج .dockerignore — فمرحلة deps تقوم بنسخه، وسيفشل البناء بدونه.
الخطوة 5 — اختبار الحاوية محلياً
لا تجعل Fly.io أبداً المكان الأول الذي تعمل فيه الحاوية. قم ببنائها وتشغيلها محلياً أولاً:
Docker build -t fly-hono-edge .
Docker run --rm -p 8080:8080 fly-hono-edge
يجب أن ترى fly-hono-edge listening on http://localhost:8080/. جرب المسارات العامة:
curl localhost:8080/
curl localhost:8080/API/hello/ada
curl localhost:8080/health
يعيد المسار المحمي 503 لأنه لم يتم ضبط API_TOKEN داخل الحاوية بعد:
curl -s localhost:8080/API/admin/stats
# {"error":"API_TOKEN secret is not configured"}
هذا هو السلوك المتوقع والآمن — يرفض المسار الخدمة حتى يتوفر السر (secret). أوقف الحاوية باستخدام Ctrl+C؛ سيسجل معالج الإغلاق التدريجي Server stopped cleanly قبل الخروج. إذا تصرفت الحاوية بشكل صحيح هنا، فستتصرف بشكل صحيح على Fly.io، لأن Fly.io يشغل نفس الصورة تماماً.
الخطوة 6 — إنشاء تطبيق Fly.io باستخدام fly launch
يقوم fly launch بفحص المشروع، وإنشاء تطبيق Fly.io، وكتابة ملف fly.toml. نظراً لوجود Dockerfile بالفعل في المجلد، سيستخدمه fly launch بدلاً من إنشاء واحد جديد. مرر --no-deploy حتى تتمكن من مراجعة وتعديل fly.toml قبل النشر الأول:
fly launch --no-deploy
سيطلب منك flyctl اسماً للتطبيق ومنطقة أساسية، وسيسألك عما إذا كنت تريد تعديل الإعدادات. اقبل اسماً (يجب أن يكون فريداً عالمياً، لذا قد يضيف Fly.io لاحقة) واختر منطقة قريبة من مستخدميك — يستخدم هذا الدليل ord (شيكاغو). ارفض أي عرض لإضافة قاعدة بيانات؛ فهذا التطبيق لا يحتاج إليها.
عند انتهاء الأمر، سيكون لديك ملف fly.toml. يحتوي بالفعل على كتلة [http_service] مع internal_port = 8080 — قرأ Fly.io ذلك من سطر EXPOSE في Dockerfile الخاص بك. الخطوة التالية هي استبدال هذا الملف بنسخة محسنة للإنتاج.
الخطوة 7 — تهيئة fly.toml للإنتاج: الإيقاف التلقائي، التزامن، وفحوصات الحالة
هنا يتحول النشر التجريبي إلى نشر إنتاجي. افتح fly.toml واجعله يطابق ما يلي — احتفظ بقيمة app التي أنشأها لك fly launch:
app = "fly-hono-edge"
primary_region = "ord"
# Fly sends this signal on stop/deploy; the app drains on SIGTERM.
kill_signal = "SIGTERM"
# Seconds to wait for a graceful exit before a hard kill (default 5, max 300).
kill_timeout = 10
[build]
# Optional: flyctl already looks for ./Dockerfile by default.
dockerfile = "Dockerfile"
[env]
NODE_ENV = "production"
PORT = "8080"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[http_service.concurrency]
type = "requests"
soft_limit = 200
hard_limit = 250
[[http_service.checks]]
grace_period = "3s"
interval = "15s"
method = "GET"
timeout = "4s"
path = "/health"
[[vm]]
size = "shared-cpu-1x"
memory = "256mb"
كل كتلة لها دورها المحدد:
التحجيم إلى الصفر. auto_stop_machines = "stop" يخبر Fly Proxy بإيقاف الآلة (Machine) عندما يكون التطبيق خاملاً، و auto_start_machines = true يعيد تشغيلها عند الطلب التالي. مع min_machines_running = 0، تتقلص الآلة فعلياً إلى الصفر — الآلة المتوقفة تُحاسب فقط على نظام ملفات الجذر الخاص بها بسعر 0.15 دولار لكل جيجابايت كل 30 يوماً، وليس على المعالج أو الرام.92 لاحظ أن auto_stop_machines يأخذ القيم النصية "off"، أو "stop"، أو "suspend" — القيمة المنطقية القديمة true التي ستجدها في التدوينات القديمة لم تعد الصيغة الموثقة، و "suspend" يستأنف العمل أسرع من "stop" على حساب الاحتفاظ بمزيد من الحالة (state).10
التزامن. يُنصح بـ type = "requests" لتطبيقات HTTP لأن Fly Proxy يمكنه تجميع وإعادة استخدام الاتصالات؛ بينما يحسب الخيار الافتراضي "connections" مقابس TCP الخام بدلاً من ذلك.10 soft_limit هو الحد الذي يبدأ عنده البروكسي في توجيه حركة المرور إلى آلات أخرى (وهو الرقم الذي يستخدمه ليقرر أن الآلة خاملة)؛ أما hard_limit فهو الحد الذي يتوقف عنده تماماً عن إرسال حركة مرور جديدة.10
فحوصات الحالة. تجعل كتلة [[http_service.checks]] الـ Fly Proxy يقوم بطلب GET /health كل 15 ثانية ويتوقع استجابة 2xx.10 grace_period = "3s" يمنح الآلة وقتاً للإقلاع قبل الفحص الأول — يبدأ Bun و Hono في أقل من ثانية بكثير، لذا فإن ثلاث ثوانٍ تعتبر مدة سخية. تنبيه واحد من وثائق Fly.io: فحص HTTP لا يتبع تحويلات 301/302، لذا إذا كان تطبيقك يحول /health إجبارياً إلى HTTPS فسيفشل الفحص. هذا التطبيق يعيد 200 مباشرة، لذا لا داعي للقلق.10
الإغلاق الآمن (Graceful shutdown). القيمة الافتراضية لـ kill_signal في المستوى العلوي هي SIGINT، والتي تشير وثائق Fly.io إلى أنها تسبب إغلاقاً حاداً (hard shutdown) في معظم التطبيقات؛ لذا فإن تغييرها إلى SIGTERM يمنح التطبيق إغلاقاً أكثر سلاسة وأقل إزعاجاً، وهو ما صُمم هذا التطبيق من أجله.10 يقترن ذلك مع معالج SIGTERM من الخطوة 3. تمنح القيمة kill_timeout = 10 التطبيق عشر ثوانٍ لإنهاء الطلبات الجارية قبل أن يقوم Fly.io بإيقاف الـ Machine إجبارياً.10
حجم الـ Machine. تقوم كتلة [[vm]] بتثبيت Machine من نوع shared-cpu-1x بذاكرة وصول عشوائي (RAM) سعة 256 ميجابايت. تثبيت الحجم مهم لضمان سلوك متوقع عند التوسع من الصفر (scale-from-zero) — فبدون قسم [[vm]]، لن يفرض أمر fly deploy حجماً معيناً.10
الخطوة 8 — تخزين الأسرار باستخدام fly secrets
كتلة [env] في ملف fly.toml مخصصة للقيم غير الحساسة فقط — حيث يتم رفعها إلى git كنص عادي. أما رمز API فيجب أن يوضع في مخزن الأسرار المشفر الخاص بـ Fly.io بدلاً من ذلك. قم بتعيينه باستخدام fly secrets set:
fly secrets set API_TOKEN="$(openssl rand -hex 24)"
تُعرض الأسرار للتطبيق كمتغيرات بيئة، تماماً مثل قيم [env]، لكنها تكون مشفرة أثناء التخزين ولا يتم طباعتها أبداً. يعرض أمر fly secrets list أسماء الأسرار وملخصاتها (digests)، ولا يعرض قيمها أبداً. إذا كان التطبيق قيد التشغيل بالفعل، فإن تعيين سر سيؤدي إلى إعادة تشغيل تدريجية (rolling restart) حتى تدخل القيمة الجديدة حيز التنفيذ؛ وبما أنه لم يتم نشر أي شيء بعد، فإن Fly.io يقوم ببساطة بتجهيزها لعملية النشر الأولى.11
إذا كنت تريد التحقق من المسار المحمي بعد النشر، فاحتفظ بنسخة من الرمز الذي تم إنشاؤه — حيث يطبعه أمر openssl rand -hex 24 مرة واحدة فقط. يمكنك دائماً تغييره لاحقاً عن طريق تشغيل fly secrets set مرة أخرى.
الخطوة 9 — النشر باستخدام fly deploy
كل شيء جاهز الآن. لنقم بالنشر:
fly deploy
يقوم أمر fly deploy ببناء صورة Docker (باستخدام أداة بناء عن بُعد افتراضياً)، ويدفعها، ثم ينقل Fly Machine إلى الإصدار الجديد.12 عملية البناء الأولى تقوم بتنزيل الصورة الأساسية oven/bun، لذا توقع أن تستغرق دقيقة أو اثنتين؛ أما عمليات النشر اللاحقة فتعيد استخدام الطبقات المخزنة مؤقتاً (cached layers) وتكون أسرع بكثير.
عند الانتهاء، سيقوم flyctl بطباعة رابط التطبيق. افتحه في المتصفح:
fly apps open
التحقق
تأكد من نجاح النشر من سطر الأوامر. استبدل fly-hono-edge باسم تطبيقك الفعلي:
curl https://fly-hono-edge.fly.dev/health
{"status":"ok","region":"ord","uptimeSeconds":2}
وجود قيمة في حقل region تطابق الـ primary_region الخاصة بك يثبت أن الطلب تمت خدمته بواسطة Fly Machine، وليس بواسطة شيء محلي. تحقق من المسارات الأخرى:
curl https://fly-hono-edge.fly.dev/
curl https://fly-hono-edge.fly.dev/API/hello/ada
الآن اختبر السر. بدون الرمز، سيعيد المسار المحمي الخطأ 401؛ ومع الرمز الذي حفظته في الخطوة 8، سيعيد الحالة 200 وبيانات الإحصائيات:
# 401 Unauthorized - no credentials
curl -s -o /dev/null -w '%{http_code}\n' https://fly-hono-edge.fly.dev/API/admin/stats
# 200 OK - authorized
curl -H "Authorization: Bearer YOUR_TOKEN_HERE" \
https://fly-hono-edge.fly.dev/API/admin/stats
افحص الـ Machine وتابع سجلاتها مباشرة:
fly status
fly logs
في fly logs، سترى سطراً واحداً لكل طلب من برمجية Hono الوسيطة logger. اترك التطبيق دون لمس لبضع دقائق، وسيعرض fly status الـ Machine في حالة stopped — وهذا يعني أن خاصية auto_stop_machines تقوم بعملها. طلب curl التالي سيوقظها مرة أخرى، وستظهر سجلات fly logs البداية الباردة (cold start) متبوعة بطلبك. التطبيق الآن متاح، ولا يكلف شيئاً أثناء الخمول، ويغلق بشكل نظيف مع كل عملية نشر.
الأخطاء الشائعة
ينجح النشر ولكن الـ Machine لا تصبح "سليمة" (healthy) أبداً. غالباً ما يكون السبب هو أن التطبيق لا يستمع إلى المنفذ الذي يتوقعه Fly.io. تأكد من أن internal_port في ملف fly.toml، ومتغير البيئة PORT، والمنفذ الذي يمرره الكود الخاص بك إلى Bun.serve جميعها لها نفس القيمة (8080 هنا). تأكد أيضاً من أن التطبيق يرتبط بالعنوان 0.0.0.0 — يقوم Bun.serve بذلك افتراضياً، لذا لا تقم بتغيير hostname إلى localhost.
فشل فحوصات السلامة (Health checks) باستمرار. قم بتشغيل curl https://APP.fly.dev/health مباشرة. إذا أعاد أي شيء بخلاف حالة 2xx، فسيفشل الفحص. السبب الشائع الآخر هو قصر فترة grace_period: إذا تم الفحص قبل أن ينتهي التطبيق من بدء التشغيل، فسيتم تسجيل فشل. ثلاث ثوانٍ كافية جداً لـ Bun + Hono، لكن التطبيقات الأثقل قد تحتاج إلى المزيد.
فشل bun install --frozen-lockfile داخل بناء Docker. هذا يعني أن ملف bun.lock غير متوافق مع package.json، أو أنه مفقود من سياق البناء. قم بتشغيل bun install محلياً لإعادة إنشاء ملف القفل، ثم ارفعه، وتأكد من أن bun.lock ليس مدرجاً في ملف .dockerignore.
الـ Machine لا تتوقف أبداً، أو لا تبدأ أبداً. إذا لم تتوقف أبداً، فتأكد من أن auto_stop_machines هي السلسلة النصية "stop" — وليست القيمة المنطقية true المهجورة من الأدلة القديمة — وأن min_machines_running هي 0. إذا لم تبدأ أبداً، فتأكد من أن auto_start_machines = true. توصي وثائق Fly.io بإبقاء التوقف التلقائي والبدء التلقائي إما مفعلين معاً أو معطلين معاً.9
الطلب الأول بعد الخمول بطيء. هذه هي التكلفة المتوقعة للتوسع إلى الصفر — حيث يجب على Fly.io تشغيل الـ Machine المتوقفة قبل خدمة الطلب. لاستيقاظ أسرع، قم بتغيير auto_stop_machines إلى "suspend"، والتي تستأنف العمل من حالة محفوظة. إذا كنت لا تستطيع تحمل أي بداية باردة، فقم بتعيين min_machines_running = 1؛ سيؤدي ذلك إلى إبقاء Machine واحدة قيد التشغيل بتكلفة تقريبية تبلغ 1.94 دولاراً شهرياً.2
الخطوات التالية ومزيد من القراءة
لديك الآن نمط قابل للتكرار لتشغيل Hono على Fly.io: تطبيق Bun + Hono داخل حاوية، ملف fly.toml جاهز للإنتاج، ونشر بأمر واحد. من هنا يمكنك التوسع إلى مناطق متعددة عن طريق إضافة Machines باستخدام fly scale count، أو ربط قاعدة بيانات Postgres مدارة أو Upstash Redis من كتالوج إضافات Fly.io، أو تقليص حجم الصورة أكثر. يوضح دليلنا حول صور حاويات distroless لبناء الإنتاج نفس أسلوب المراحل المتعددة ولكن بخطوة إضافية — حيث تتخلى oven/bun:1.3.14-distroless عن الـ shell تماماً لتقليل مساحة الهجوم.
من أجل قابلية المراقبة (observability)، قم بربط سجلات الطلبات المنظمة بجهة خلفية بدلاً من قراءة fly logs يدوياً — التقنيات المذكورة في دليلنا حول قابلية المراقبة عند الحافة باستخدام السجلات المنظمة و Sentry يمكن نقلها بسهولة إلى تطبيق Fly.io. وإذا كان الشيء التالي الذي ستبنيه هو API تتوثق ضده خدمات أخرى، فإن دليلنا حول خادم TypeScript للإنتاج مع OAuth و Streamable HTTP هو تكملة طبيعية لنمط الـ bearer-token الموضح هنا.
المراجع الموثوقة لكل شيء في هذا الدليل هي مرجع تكوين تطبيقات Fly.io، و وثائق خادم Bun HTTP، و وثائق Hono.
Footnotes
-
تثبيت Bun والإصدار الحالي. Bun 1.3.14 هو أحدث إصدار مستقر (npm
bundist-taglatest، تم نشره في 13-05-2026). https://bun.com/docs/installation ↩ -
تسعير موارد Fly.io — تكلفة تشغيل ماكينة
shared-cpu-1xبذاكرة 256 ميجابايت هي 0.0027 دولار/ساعة (حوالي 1.94 دولار/شهرياً)؛ الماكينات المتوقفة تُحاسب فقط على مساحة تخزين نظام الملفات الجذري (root filesystem) بسعر 0.15 دولار لكل جيجابايت كل 30 يوماً؛ تتطلب جميع المؤسسات وسيلة دفع. https://fly.io/docs/about/pricing/ ↩ ↩2 ↩3 ↩4 -
مرجع
bun init— الخيار-yيقبل الإعدادات الافتراضية وينشئ هيكل مشروع TypeScript فارغ بشكل غير تفاعلي. https://bun.com/docs/runtime/templating/init ↩ -
Hono على Bun — دليل البدء. Hono 4.12.21 هو الإصدار الحالي. https://hono.dev/docs/getting-started/bun ↩
-
برمجية Hono Request ID الوسيطة — تولد UUID لكل طلب عبر
crypto.randomUUID()، وتعيد استخدام رأسX-Request-Idالوارد، وتكشف القيمة من خلالc.get('requestId'). https://hono.dev/docs/middleware/builtin/request-id ↩ -
Fly.io — بيئة تشغيل الماكينة (Machine Runtime Environment). يتم تخزين رمز المنطقة المكون من ثلاثة أحرف في متغير البيئة
FLY_REGIONفي كل ماكينة يتم تشغيلها. https://fly.io/docs/machines/runtime-environment/ ↩ -
خادم Bun HTTP (
Bun.serve) — القيمة الافتراضية لـhostnameهي0.0.0.0، والقيمة الافتراضية لـportهي متغير البيئةPORT، وidleTimeoutبالثواني (الافتراضي 10، والحد الأقصى 255)، وserver.stop()يقوم بإنهاء الطلبات الجارية بينماserver.stop(true)يغلق الاتصالات قسرياً. https://bun.com/docs/runtime/http/server ↩ ↩2 ↩3 -
وضع تطبيق Bun في حاوية باستخدام Docker — بناء متعدد المراحل،
bun install --frozen-lockfile --production، والتشغيل كمستخدمbunغير جذري (non-root). https://bun.com/guides/ecosystem/Docker ↩ ↩2 ↩3 -
Fly.io — إيقاف وتشغيل الماكينات تلقائياً. يقبل
auto_stop_machinesالقيم"off"أو"stop"أو"suspend". https://fly.io/docs/launch/autostop-autostart/ ↩ ↩2 -
مرجع إعدادات تطبيق Fly.io (
fly.toml) —[http_service]،[http_service.concurrency]،[[http_service.checks]]،[[vm]]،kill_signal، وkill_timeout. https://fly.io/docs/reference/configuration/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Fly.io — الأسرار (Secrets). متغيرات بيئة مشفرة؛ يؤدي تعيين سر في تطبيق قيد التشغيل إلى إعادة تشغيله. https://fly.io/docs/apps/secrets/ ↩
-
Fly.io — نشر تطبيق و
fly launch. الخيارfly launch --no-deployينشئ التطبيق وملفfly.tomlدون نشر؛ بينما يقومfly deployبالبناء والإصدار. https://fly.io/docs/launch/deploy/ ↩