---
title: 'نشر Bun + Hono على Fly.io: دليل الإنتاج ٢٠٢٦'
description: 'نشر تطبيق Bun + Hono جاهز للإنتاج على Fly.io: ملف Dockerfile لـ Bun متعدد المراحل، وملف fly.toml مع خاصية الإيقاف التلقائي للأجهزة وفحوصات الحالة، والأسرار، والإغلاق الآمن.'
url: https://nerdleveltech.com/ar/deploy-bun-hono-fly-io-production-tutorial
language: ar
date: '2026-05-21'
category: cloud-devops
tags:
  - bun
  - hono
  - fly.io
  - deployment
  - docker
  - edge
  - typescript
  - devops
  - cloud
markdown_url: https://nerdleveltech.com/ar/deploy-bun-hono-fly-io-production-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/deploy-bun-hono-fly-io-production-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# نشر 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`.[^bun]
- **Docker** — أي نسخة حديثة من Docker Desktop أو Docker Engine. BuildKit هو المنشئ الافتراضي، لذا لا حاجة لأعلام إضافية. ستحتاجه فقط لاختبار الحاوية محلياً في الخطوة 5.
- **flyctl** — واجهة سطر أوامر Fly.io. ثبته من [التعليمات الرسمية](https://fly.io/docs/flyctl/install/) وقم بتشغيل `fly auth login`. تم التحقق من هذا الدليل باستخدام flyctl v0.4.49.
- **حساب Fly.io مع وسيلة دفع.** لم يعد لدى Fly.io باقة مجانية دائمة — تحتاج كل منظمة إلى بطاقة مسجلة، على الرغم من أن الحسابات الجديدة تحصل على رصيد تجريبي.[^pricing] الماكينة في هذا الدليل هي `shared-cpu-1x` مع 256 ميجابايت من الرام، وتكلف حوالي **1.94 دولار شهرياً** إذا عملت باستمرار — وأقل بكثير هنا، لأنها تتوقف عند الخمول.[^pricing]

تساعد المعرفة العملية بـ TypeScript و HTTP، لكنك لا تحتاج إلى خبرة سابقة في Bun أو Hono أو Fly.io.

## الخطوة 1 — تأسيس مشروع Bun + Hono

أنشئ مجلداً فارغاً وابدأ مشروع TypeScript جديداً. يقبل العلم `-y` كل الإعدادات الافتراضية، لذا يعمل `bun init` بدون مطالبات.[^buninit]

```bash
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 لاحقاً:

```bash
bun add hono
```

سنحتفظ بكود الخادم تحت مجلد `src/`، لذا قم بإزالة نقطة الدخول المؤقتة وأنشئ مجلد المصدر:

```bash
rm index.ts
mkdir src
```

افتح `package.json` واستبدل كتلة `scripts`. يضيف `bun init` حزمة `@types/bun` إلى `devDependencies` — قم بتثبيتها على إصدار صريح حتى تظل عمليات التثبيت قابلة لإعادة الإنتاج:

```json
{
  "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.[^hono] سنحتفظ بتطبيق Hono في وحدته الخاصة بحيث يمكن استيراده من قبل كل من نقطة دخول الخادم وملف الاختبار.

أنشئ `src/app.ts`:

```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')`.[^requestid] تكتب البرمجية الوسيطة `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 في بيئة تشغيل كل ماكينة.[^flyruntime]

الآن أضف ملف اختبار حتى تتمكن من التحقق من السلوك دون تشغيل خادم. توفر تطبيقات Hono طريقة `app.request()` تأخذ مساراً وتعيد `Response`. أنشئ `src/app.test.ts`:

```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 — لا حاجة لتبعيات إضافية:

```bash
bun test
```

```
 3 pass
 0 fail
```

## الخطوة 3 — إضافة نقطة دخول Bun.serve صريحة مع إغلاق آمن

سيقوم Bun تلقائياً بخدمة أي وحدة تحتوي على تصدير `default` مع معالج `fetch`، لكن هذا الاختصار لا يمنحك تحكماً في الخادم المشغل. بالنسبة للإنتاج، ستحتاج إلى كائن الخادم حتى تتمكن من إغلاقه بشكل نظيف. أنشئ `src/index.ts`:

```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` سيجعل الماكينة غير قابلة للوصول.[^bunserve] المنفذ `port` يستخدم افتراضياً متغير البيئة `PORT`، لكن تمريره بشكل صريح يجعل القصد واضحاً. `idleTimeout` بالثواني (الافتراضي 10، والحد الأقصى 255)؛ 30 ثانية مريحة لـ API JSON.[^bunserve]

منطق الإغلاق هو الجزء الذي تتجاهله معظم الشروحات التعليمية. `server.stop()` بدون وسائط يتوقف عن قبول اتصالات جديدة وينتهي بمجرد اكتمال الطلبات الجارية؛ أما تمرير `true` فسيؤدي إلى إنهاء الاتصالات فوراً.[^bunserve] تسجيل معالجات لكل من `SIGTERM` و `SIGINT` يعني أن التطبيق يفرغ حمولته بنظافة سواء تم إيقافه بواسطة Fly.io، أو بواسطة Docker، أو عن طريق Ctrl+C أثناء التطوير المحلي.

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

```bash
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. يحافظ البناء متعدد المراحل على صغر حجم الصورة النهائية عن طريق فصل تثبيت التبعيات عن طبقة وقت التشغيل.[^bundocker] أنشئ `Dockerfile`:

```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 --from=deps /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](https://hub.Docker.com/r/oven/bun/tags).

`bun install --frozen-lockfile --production` يثبت بالضبط ما يسجله `bun.lock` ويتخطى `devDependencies` — لذا لن يتم شحن `@types/bun` أبداً إلى الإنتاج.[^bundocker] يتم تخزين مرحلة `deps` مؤقتاً بشكل مستقل عن كود المصدر الخاص بك، لذا فإن تعديل مسار (route) لا يؤدي إلى إعادة تشغيل `bun install`.

`USER bun` أمر مهم: تشحن صور `oven/bun` مستخدماً غير جذري (non-root) باسم `bun`، والتشغيل باستخدامه يحد من نطاق الضرر في حال تم اختراق العملية.[^bundocker] `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 أبداً المكان الأول الذي تعمل فيه الحاوية. قم ببنائها وتشغيلها محلياً أولاً:

```bash
Docker build -t fly-hono-edge .
Docker run --rm -p 8080:8080 fly-hono-edge
```

يجب أن ترى `fly-hono-edge listening on http://localhost:8080/`. جرب المسارات العامة:

```bash
curl localhost:8080/
curl localhost:8080/API/hello/ada
curl localhost:8080/health
```

يعيد المسار المحمي `503` لأنه لم يتم ضبط `API_TOKEN` داخل الحاوية بعد:

```bash
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` قبل النشر الأول:

```bash
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`:

```toml
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 يوماً، وليس على المعالج أو الرام.[^autostop][^pricing] لاحظ أن `auto_stop_machines` يأخذ القيم النصية `"off"`، أو `"stop"`، أو `"suspend"` — القيمة المنطقية القديمة `true` التي ستجدها في التدوينات القديمة لم تعد الصيغة الموثقة، و `"suspend"` يستأنف العمل أسرع من `"stop"` على حساب الاحتفاظ بمزيد من الحالة (state).[^flycfg]

**التزامن.** يُنصح بـ `type = "requests"` لتطبيقات HTTP لأن Fly Proxy يمكنه تجميع وإعادة استخدام الاتصالات؛ بينما يحسب الخيار الافتراضي `"connections"` مقابس TCP الخام بدلاً من ذلك.[^flycfg] `soft_limit` هو الحد الذي يبدأ عنده البروكسي في توجيه حركة المرور إلى آلات أخرى (وهو الرقم الذي يستخدمه ليقرر أن الآلة خاملة)؛ أما `hard_limit` فهو الحد الذي يتوقف عنده تماماً عن إرسال حركة مرور جديدة.[^flycfg]

**فحوصات الحالة.** تجعل كتلة `[[http_service.checks]]` الـ Fly Proxy يقوم بطلب `GET /health` كل 15 ثانية ويتوقع استجابة 2xx.[^flycfg] `grace_period = "3s"` يمنح الآلة وقتاً للإقلاع قبل الفحص الأول — يبدأ Bun و Hono في أقل من ثانية بكثير، لذا فإن ثلاث ثوانٍ تعتبر مدة سخية. تنبيه واحد من وثائق Fly.io: فحص HTTP لا يتبع تحويلات 301/302، لذا إذا كان تطبيقك يحول `/health` إجبارياً إلى HTTPS فسيفشل الفحص. هذا التطبيق يعيد 200 مباشرة، لذا لا داعي للقلق.[^flycfg]

**الإغلاق الآمن (Graceful shutdown).** القيمة الافتراضية لـ `kill_signal` في المستوى العلوي هي `SIGINT`، والتي تشير وثائق Fly.io إلى أنها تسبب إغلاقاً حاداً (hard shutdown) في معظم التطبيقات؛ لذا فإن تغييرها إلى `SIGTERM` يمنح التطبيق إغلاقاً أكثر سلاسة وأقل إزعاجاً، وهو ما صُمم هذا التطبيق من أجله.[^flycfg] يقترن ذلك مع معالج `SIGTERM` من الخطوة 3. تمنح القيمة `kill_timeout = 10` التطبيق عشر ثوانٍ لإنهاء الطلبات الجارية قبل أن يقوم Fly.io بإيقاف الـ Machine إجبارياً.[^flycfg]

**حجم الـ Machine.** تقوم كتلة `[[vm]]` بتثبيت Machine من نوع `shared-cpu-1x` بذاكرة وصول عشوائي (RAM) سعة 256 ميجابايت. تثبيت الحجم مهم لضمان سلوك متوقع عند التوسع من الصفر (scale-from-zero) — فبدون قسم `[[vm]]`، لن يفرض أمر `fly deploy` حجماً معيناً.[^flycfg]

## الخطوة 8 — تخزين الأسرار باستخدام fly secrets

كتلة `[env]` في ملف `fly.toml` مخصصة للقيم غير الحساسة فقط — حيث يتم رفعها إلى git كنص عادي. أما رمز API فيجب أن يوضع في مخزن الأسرار المشفر الخاص بـ Fly.io بدلاً من ذلك. قم بتعيينه باستخدام `fly secrets set`:

```bash
fly secrets set API_TOKEN="$(openssl rand -hex 24)"
```

تُعرض الأسرار للتطبيق كمتغيرات بيئة، تماماً مثل قيم `[env]`، لكنها تكون مشفرة أثناء التخزين ولا يتم طباعتها أبداً. يعرض أمر `fly secrets list` *أسماء* الأسرار وملخصاتها (digests)، ولا يعرض قيمها أبداً. إذا كان التطبيق قيد التشغيل بالفعل، فإن تعيين سر سيؤدي إلى إعادة تشغيل تدريجية (rolling restart) حتى تدخل القيمة الجديدة حيز التنفيذ؛ وبما أنه لم يتم نشر أي شيء بعد، فإن Fly.io يقوم ببساطة بتجهيزها لعملية النشر الأولى.[^flysecrets]

إذا كنت تريد التحقق من المسار المحمي بعد النشر، فاحتفظ بنسخة من الرمز الذي تم إنشاؤه — حيث يطبعه أمر `openssl rand -hex 24` مرة واحدة فقط. يمكنك دائماً تغييره لاحقاً عن طريق تشغيل `fly secrets set` مرة أخرى.

## الخطوة 9 — النشر باستخدام fly deploy

كل شيء جاهز الآن. لنقم بالنشر:

```bash
fly deploy
```

يقوم أمر `fly deploy` ببناء صورة Docker (باستخدام أداة بناء عن بُعد افتراضياً)، ويدفعها، ثم ينقل Fly Machine إلى الإصدار الجديد.[^flylaunch] عملية البناء الأولى تقوم بتنزيل الصورة الأساسية `oven/bun`، لذا توقع أن تستغرق دقيقة أو اثنتين؛ أما عمليات النشر اللاحقة فتعيد استخدام الطبقات المخزنة مؤقتاً (cached layers) وتكون أسرع بكثير.

عند الانتهاء، سيقوم `flyctl` بطباعة رابط التطبيق. افتحه في المتصفح:

```bash
fly apps open
```

## التحقق

تأكد من نجاح النشر من سطر الأوامر. استبدل `fly-hono-edge` باسم تطبيقك الفعلي:

```bash
curl https://fly-hono-edge.fly.dev/health
```

```json
{"status":"ok","region":"ord","uptimeSeconds":2}
```

وجود قيمة في حقل `region` تطابق الـ `primary_region` الخاصة بك يثبت أن الطلب تمت خدمته بواسطة Fly Machine، وليس بواسطة شيء محلي. تحقق من المسارات الأخرى:

```bash
curl https://fly-hono-edge.fly.dev/
curl https://fly-hono-edge.fly.dev/API/hello/ada
```

الآن اختبر السر. بدون الرمز، سيعيد المسار المحمي الخطأ `401`؛ ومع الرمز الذي حفظته في الخطوة 8، سيعيد الحالة `200` وبيانات الإحصائيات:

```bash
# 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 وتابع سجلاتها مباشرة:

```bash
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 بإبقاء التوقف التلقائي والبدء التلقائي إما مفعلين معاً أو معطلين معاً.[^autostop]

**الطلب الأول بعد الخمول بطيء.** هذه هي التكلفة المتوقعة للتوسع إلى الصفر — حيث يجب على Fly.io تشغيل الـ Machine المتوقفة قبل خدمة الطلب. لاستيقاظ أسرع، قم بتغيير `auto_stop_machines` إلى `"suspend"`، والتي تستأنف العمل من حالة محفوظة. إذا كنت لا تستطيع تحمل أي بداية باردة، فقم بتعيين `min_machines_running = 1`؛ سيؤدي ذلك إلى إبقاء Machine واحدة قيد التشغيل بتكلفة تقريبية تبلغ 1.94 دولاراً شهرياً.[^pricing]

## الخطوات التالية ومزيد من القراءة

لديك الآن نمط قابل للتكرار لتشغيل Hono على Fly.io: تطبيق Bun + Hono داخل حاوية، ملف `fly.toml` جاهز للإنتاج، ونشر بأمر واحد. من هنا يمكنك التوسع إلى مناطق متعددة عن طريق إضافة Machines باستخدام `fly scale count`، أو ربط قاعدة بيانات Postgres مدارة أو Upstash Redis من كتالوج إضافات Fly.io، أو تقليص حجم الصورة أكثر. يوضح دليلنا حول [صور حاويات distroless لبناء الإنتاج](https://nerdleveltech.com/distroless-python-containers-with-uv-tutorial) نفس أسلوب المراحل المتعددة ولكن بخطوة إضافية — حيث تتخلى `oven/bun:1.3.14-distroless` عن الـ shell تماماً لتقليل مساحة الهجوم.

من أجل قابلية المراقبة (observability)، قم بربط سجلات الطلبات المنظمة بجهة خلفية بدلاً من قراءة `fly logs` يدوياً — التقنيات المذكورة في دليلنا حول [قابلية المراقبة عند الحافة باستخدام السجلات المنظمة و Sentry](https://nerdleveltech.com/cloudflare-workers-observability-workers-logs-sentry-tutorial) يمكن نقلها بسهولة إلى تطبيق Fly.io. وإذا كان الشيء التالي الذي ستبنيه هو API تتوثق ضده خدمات أخرى، فإن دليلنا حول [خادم TypeScript للإنتاج مع OAuth و Streamable HTTP](https://nerdleveltech.com/mcp-server-TypeScript-oauth-streamable-http-production-tutorial) هو تكملة طبيعية لنمط الـ bearer-token الموضح هنا.

المراجع الموثوقة لكل شيء في هذا الدليل هي [مرجع تكوين تطبيقات Fly.io](https://fly.io/docs/reference/configuration/)، و [وثائق خادم Bun HTTP](https://bun.com/docs/runtime/http/server)، و [وثائق Hono](https://hono.dev/docs/).

[^bun]: تثبيت Bun والإصدار الحالي. Bun 1.3.14 هو أحدث إصدار مستقر (npm `bun` dist-tag `latest`، تم نشره في 13-05-2026). <https://bun.com/docs/installation>

[^pricing]: تسعير موارد Fly.io — تكلفة تشغيل ماكينة `shared-cpu-1x` بذاكرة 256 ميجابايت هي 0.0027 دولار/ساعة (حوالي 1.94 دولار/شهرياً)؛ الماكينات المتوقفة تُحاسب فقط على مساحة تخزين نظام الملفات الجذري (root filesystem) بسعر 0.15 دولار لكل جيجابايت كل 30 يوماً؛ تتطلب جميع المؤسسات وسيلة دفع. <https://fly.io/docs/about/pricing/>

[^buninit]: مرجع `bun init` — الخيار `-y` يقبل الإعدادات الافتراضية وينشئ هيكل مشروع TypeScript فارغ بشكل غير تفاعلي. <https://bun.com/docs/runtime/templating/init>

[^hono]: Hono على Bun — دليل البدء. Hono 4.12.21 هو الإصدار الحالي. <https://hono.dev/docs/getting-started/bun>

[^requestid]: برمجية Hono Request ID الوسيطة — تولد UUID لكل طلب عبر `crypto.randomUUID()`، وتعيد استخدام رأس `X-Request-Id` الوارد، وتكشف القيمة من خلال `c.get('requestId')`. <https://hono.dev/docs/middleware/builtin/request-id>

[^flyruntime]: Fly.io — بيئة تشغيل الماكينة (Machine Runtime Environment). يتم تخزين رمز المنطقة المكون من ثلاثة أحرف في متغير البيئة `FLY_REGION` في كل ماكينة يتم تشغيلها. <https://fly.io/docs/machines/runtime-environment/>

[^bunserve]: خادم 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>

[^bundocker]: وضع تطبيق Bun في حاوية باستخدام Docker — بناء متعدد المراحل، `bun install --frozen-lockfile --production`، والتشغيل كمستخدم `bun` غير جذري (non-root). <https://bun.com/guides/ecosystem/Docker>

[^autostop]: Fly.io — إيقاف وتشغيل الماكينات تلقائياً. يقبل `auto_stop_machines` القيم `"off"` أو `"stop"` أو `"suspend"`. <https://fly.io/docs/launch/autostop-autostart/>

[^flycfg]: مرجع إعدادات تطبيق Fly.io (`fly.toml`) — `[http_service]`، `[http_service.concurrency]`، `[[http_service.checks]]`، `[[vm]]`، `kill_signal`، و `kill_timeout`. <https://fly.io/docs/reference/configuration/>

[^flysecrets]: Fly.io — الأسرار (Secrets). متغيرات بيئة مشفرة؛ يؤدي تعيين سر في تطبيق قيد التشغيل إلى إعادة تشغيله. <https://fly.io/docs/apps/secrets/>

[^flylaunch]: Fly.io — نشر تطبيق و `fly launch`. الخيار `fly launch --no-deploy` ينشئ التطبيق وملف `fly.toml` دون نشر؛ بينما يقوم `fly deploy` بالبناء والإصدار. <https://fly.io/docs/launch/deploy/>
