---
title: نشر تطبيق Node.js على VPS باستخدام Kamal 2 (2026)
description: 'ارفع تطبيق Node.js على VPS باستخدام Kamal 2: إعداد Docker من الصفر مع عمليات نشر بدون توقف، وشهادة Let''s Encrypt SSL تلقائية، وإضافة Postgres.'
url: https://nerdleveltech.com/ar/deploy-nodejs-app-vps-kamal-2-tutorial
language: ar
date: '2026-06-21'
category: cloud-devops
tags:
  - kamal
  - node.js
  - docker
  - vps
  - devops
  - postgres
  - cloud-devops
markdown_url: https://nerdleveltech.com/ar/deploy-nodejs-app-vps-kamal-2-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/deploy-nodejs-app-vps-kamal-2-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# نشر تطبيق Node.js على VPS باستخدام Kamal 2 (2026)

لنشر تطبيق Node.js على VPS باستخدام Kamal 2، قم بتحويله إلى حاوية (containerize) باستخدام Dockerfile، وصِف خادمك وصورتك في `config/deploy.yml`، ثم قم بتشغيل `kamal setup`. يقوم Kamal ببناء الصورة، ورفعها إلى سجل (registry)، وسحبها إلى خادمك عبر SSH، وتبديل حركة المرور من خلال kamal-proxy دون أي توقف (zero downtime).

## ملخص

يأخذك هذا الدليل العملي من تطبيق **Express 5** API بسيط في مجلد فارغ إلى خادم حي ومؤمن بـ HTTPS باستخدام **Kamal 2.12.0**[^kamal]. ستكتب Dockerfile صغيراً يعتمد على صورة **Node.js 24 LTS** الأساسية، وتنشئ هيكل إعدادات Kamal باستخدام `kamal init`، وتضبط kamal-proxy لتطبيق ليس من نوع Rails يعمل على المنفذ 3000، وتخزن بيانات اعتماد السجل وقاعدة البيانات في `.kamal/secrets`، وتشغل **PostgreSQL 18** كملحق (accessory) لـ Kamal، وتشحن أول **نشر بدون توقف (zero-downtime deploy)**. إن Kamal مستقل عن إطارات العمل (framework-agnostic) — لا يوجد شيء هنا خاص بـ Rails. خصص حوالي 45 دقيقة بالإضافة إلى تكلفة خادم VPS صغير واحد.

## ما ستتعلمه

- بناء تطبيق Express 5 API بسيط ولكنه حقيقي مع نقطة نهاية لفحص الحالة `/up` ومسار مدعوم بقاعدة بيانات Postgres
- كتابة **Node.js Dockerfile** صغير وجاهز للإنتاج يعتمد على صورة Node 24 LTS
- إنشاء هيكل إعدادات Kamal باستخدام `kamal init` وفهم مخطط `deploy.yml`
- تكوين **kamal-proxy لتطبيق ليس من نوع Rails** (`app_port: 3000`، المضيف، فحص الحالة)
- إدارة أسرار السجل وقاعدة البيانات بأمان باستخدام `.kamal/secrets`
- تشغيل **PostgreSQL 18 كملحق (accessory) لـ Kamal** والاتصال به عبر شبكة `kamal`
- شحن أول **نشر بدون توقف** باستخدام `kamal setup` وفهم كيفية عمل عملية التبديل
- تفعيل **HTTPS التلقائي من Let's Encrypt** بسطر إعدادات واحد
- التحقق من النشر المباشر وإصلاح أكثر خمسة أخطاء شائعة في أول عملية نشر

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

ستحتاج إلى الإصدارات والموارد المحددة التالية:

- **Ruby 3.2+** على جهازك المحلي (يتم توزيع Kamal كـ Ruby gem). تحقق باستخدام `ruby -v`.
- **Docker** يعمل محلياً (Docker Desktop 4.x أو Docker Engine 27+) حتى يتمكن Kamal من بناء صورتك. تحقق باستخدام `Docker version`.
- **Node.js 24** محلياً إذا كنت تريد تشغيل التطبيق قبل النشر. Node 24 هو خط الدعم طويل الأمد النشط (Active LTS) اعتباراً من يونيو 2026 (مدعوم حتى أبريل 2028)[^node].
- **VPS** يعمل بنظام Ubuntu 24.04 مع عنوان IP عام ووصول عبر مفتاح SSH للمستخدم root. أي مزود خدمة سيفي بالغرض — DigitalOcean، Hetzner، Linode/Akamai، Vultr. نسخة بذاكرة 1 جيجابايت ومعالج واحد (1 vCPU) كافية لهذا الدليل.
- **اسم نطاق (Domain name)** مع سجل `A` يشير إلى عنوان IP الخاص بخادمك. ستحتاج إلى هذا من أجل SSL التلقائي.
- **حساب Docker Hub** (مجاني) لاستضافة صورتك. أي سجل يعمل؛ Docker Hub هو الافتراضي.
- المنافذ **80 و 443 مفتوحة** في جدار حماية الخادم. يستمع kamal-proxy على كليهما، ويجب أن يكون المنفذ 443 قابلاً للوصول لنجاح تحدي Let's Encrypt[^proxy].

خلال هذا الدليل، يسمى التطبيق المثال `notes-API`. استبدل `notes.example.com` بنطاقك الحقيقي و `203.0.113.10` بعنوان IP الحقيقي لخادمك.

## الخطوة 1 — بناء تطبيق Express API بسيط مع نقطة نهاية لفحص الحالة

يقوم Kamal بنشر كل ما يبنيه Dockerfile الخاص بك، لذا نحتاج أولاً إلى تطبيق حقيقي. أنشئ مجلد مشروع وتطبيق Express 5 API بسيطاً مدعوماً بـ Postgres. نقطة النهاية الوحيدة التي يهتم بها Kamal هي `GET /up` — يقوم kamal-proxy بفحصها ليقرر متى تكون الحاوية التي بدأت حديثاً جاهزة لاستقبال حركة المرور[^proxy].

```bash
mkdir notes-API && cd notes-API
npm init -y
npm install express@5.2.1 pg@8.21.0
```

اضبط المشروع لاستخدام وحدات ES (ES modules) وسكريبت `start`. قم بتحرير `package.json`:

```json
{
  "name": "notes-API",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": { "node": ">=22" },
  "scripts": { "start": "node src/server.js" },
  "dependencies": {
    "express": "5.2.1",
    "pg": "8.21.0"
  }
}
```

أنشئ `src/db.js`. يقوم ببناء مجمع اتصالات (connection pool) `pg` من `DATABASE_URL` (الذي سيقوم Kamal بحقنه في بيئة الإنتاج) ويعود إلى Postgres محلي للتطوير:

```js
import pg from 'pg';

const { Pool } = pg;

export const pool = new Pool({
  connectionString:
    process.env.DATABASE_URL ??
    'postgres://notes:notes@localhost:5432/notes',
});

// Create the table on boot. For a tutorial this is fine;
// in a larger app use a real migration tool instead.
export async function initSchema() {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS notes (
      id         SERIAL PRIMARY KEY,
      body       TEXT NOT NULL,
      created_at TIMESTAMPTZ NOT NULL DEFAULT now()
    );
  `);
}

// Postgres in a freshly booted accessory may need a moment to
// accept connections. Retry a few times before giving up.
export async function waitForDb(retries = 10, delayMs = 1500) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      await pool.query('SELECT 1');
      return;
    } catch (err) {
      if (attempt === retries) throw err;
      console.log(`db not ready (attempt ${attempt}), retrying...`);
      await new Promise((r) => setTimeout(r, delayMs));
    }
  }
}
```

الآن أنشئ `src/server.js` مع المسارات ونقطة نهاية فحص الحالة:

```js
import express from 'express';
import { pool, initSchema, waitForDb } from './db.js';

const app = express();
app.use(express.json());

const PORT = Number(process.env.PORT ?? 3000);

// kamal-proxy hits GET /up to decide when this container is live.
app.get('/up', (_req, res) => res.status(200).send('OK'));

app.get('/', async (_req, res) => {
  const { rows } = await pool.query('SELECT count(*)::int AS count FROM notes');
  res.json({ message: 'notes-API is live', notes: rows[0].count });
});

app.get('/notes', async (_req, res) => {
  const { rows } = await pool.query(
    'SELECT id, body, created_at FROM notes ORDER BY id DESC LIMIT 100',
  );
  res.json(rows);
});

app.post('/notes', async (req, res) => {
  const body = (req.body?.body ?? '').toString().trim();
  if (!body) return res.status(422).json({ error: 'body is required' });
  const { rows } = await pool.query(
    'INSERT INTO notes (body) VALUES ($1) RETURNING id, body, created_at',
    [body],
  );
  res.status(201).json(rows[0]);
});

await waitForDb();
await initSchema();

const server = app.listen(PORT, () => {
  console.log(`notes-API listening on :${PORT}`);
});

// Finish in-flight requests when Kamal stops the old container.
for (const signal of ['SIGTERM', 'SIGINT']) {
  process.on(signal, () => {
    server.close(() => pool.end().then(() => process.exit(0)));
  });
}
```

هناك خياران في التصميم يهمان عملية النشر. أولاً، يستمع التطبيق على `PORT` (3000 افتراضياً) — سنخبر kamal-proxy بهذا المنفذ في الخطوة 4. ثانياً، يقوم معالج `SIGTERM` بإغلاق خادم HTTP بشكل سلس، وهو ما يجعل تبديل حركة المرور سلساً حقاً: تنتهي الطلبات الجارية في الحاوية القديمة بدلاً من قطعها.

## الخطوة 2 — كتابة Node.js Dockerfile للإنتاج

يجب أن يكون Dockerfile الخاص بـ Node.js لـ Kamal 2 صغيراً، ويعمل كمستخدم غير جذري (non-root)، ويثبت فقط تبعات الإنتاج. أنشئ `Dockerfile` في جذر المشروع بناءً على صورة **`node:24-slim`** الأساسية (متغير Debian-slim من خط Node 24 LTS)[^node]:

```dockerfile
# syntax=Docker/dockerfile:1
FROM node:24-slim

ENV NODE_ENV=production
WORKDIR /app

# Install production dependencies first for better layer caching.
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Copy the application source.
COPY . .

# Run as the unprivileged "node" user that ships with the image.
USER node

EXPOSE 3000
CMD ["node", "src/server.js"]
```

أضف ملف `.dockerignore` حتى لا تشحن الملفات المحلية غير الضرورية أو الأسرار إلى الصورة:

```
node_modules
npm-debug.log
.git
.kamal
.env
```

قم بإنشاء ملف `package-lock.json` الذي يتطلبه أمر `npm ci` عن طريق تشغيل `npm install` مرة واحدة محلياً إذا لم تكن قد فعلت ذلك بالفعل. تأكد من بناء الصورة قبل إقحام الخادم:

```bash
Docker build -t notes-API:test .
```

ينتهي البناء الناجح بـ `naming to Docker.io/library/notes-API:test`. إذا فشل هنا، فقم بإصلاحه الآن — يقوم Kamal بتشغيل نفس عملية البناء تماماً، لذا فإن الفشل المحلي يعني فشل النشر.

## الخطوة 3 — تثبيت Kamal وتشغيل kamal init

قم بتثبيت Kamal عالمياً من RubyGems[^install]:

```bash
gem install kamal
kamal version
```

يجب أن ترى `2.12.0` (أو أي إصدار 2.11+ — مخطط `deploy.yml` والأوامر في هذا الدليل مستقرة عبر كامل سلسلة 2.x)[^kamal]. إذا كنت تفضل عدم تثبيت Ruby، فإن Kamal يتوفر أيضاً كصورة Docker، على الرغم من أن هذا المسار له بعض القيود[^install].

قم بإنشاء هيكل الإعدادات. من جذر مشروعك:

```bash
kamal init
```

يؤدي هذا إلى إنشاء نموذج إعدادات في `config/deploy.yml` ونموذج أسرار في مجلد `.kamal/`[^commands]. يستخدم Kamal ملف `config/deploy.yml` بغض النظر عن اللغة — لا يوجد شيء خاص بـ Rails في هذا المسار. سنقوم باستبدال ملف `deploy.yml` الناتج بالكامل في الخطوة التالية.

## الخطوة 4 — تكوين deploy.yml لتطبيق ليس من نوع Rails

هذا هو جوهر **إعدادات deploy.yml في Kamal لتطبيق Node**. افتح `config/deploy.yml` واستبدله بما يلي، مع استبدال اسم الخدمة الخاص بك، ومستودع الصور، وعنوان IP للخادم، والنطاق:

```yaml
# config/deploy.yml
service: notes-API

# Your image on Docker Hub: <username>/<repo>
image: your-dockerhub-user/notes-API

# The server(s) to deploy to.
servers:
  web:
    - 203.0.113.10

# kamal-proxy settings for THIS app (not Rails-specific).
proxy:
  host: notes.example.com
  app_port: 3000   # our Express app listens on 3000, not the default 80
  ssl: true        # automatic HTTPS via Let's Encrypt (see Step 8)

# Docker Hub is the default registry.
registry:
  username: your-dockerhub-user
  password:
    - KAMAL_REGISTRY_PASSWORD

# Build an amd64 image (most VPSes are x86-64).
builder:
  arch: amd64

# Environment passed to the app container.
env:
  clear:
    PORT: 3000
  secret:
    - DATABASE_URL
```

الأسطر الأربعة التي تسبب ارتباكاً للأشخاص القادمين من دروس Rails موجودة في كتلة `proxy:`:

- **`app_port: 3000`** — المنفذ الذي يعرضه تطبيقك. القيمة الافتراضية هي `80`، وهو أمر خاطئ لتطبيق Express الخاص بنا، لذا نقوم بضبطه صراحةً[^proxy].

* **`ssl: true`** — يقوم بتفعيل شهادات Let's Encrypt التلقائية. يتطلب خادماً واحداً وقيمة `host` تشير إلى ذلك الخادم[^proxy]. سنغطي متطلبات DNS في الخطوة 8.
* **`host: notes.example.com`** — يقوم kamal-proxy بتوجيه الطلبات الخاصة باسم المضيف هذا فقط إلى تطبيقك، مما يسمح لتطبيقات متعددة بمشاركة خادم واحد.
* **`builder.arch: amd64`** — إذا كنت تقوم بالبناء على جهاز Mac بمعالج Apple Silicon (arm64) وتنشره على خادم x86-64، فيجب عليك البناء العابر (cross-build) لـ `amd64` وإلا فلن تبدأ الحاوية (container)[^install].

لاحظ عدم وجود بلوك `traefik:`. كانت دروس Kamal 1 القديمة تقوم بتهيئة Traefik باستخدام وسوم Docker؛ استبدل Kamal 2 ذلك تماماً بـ kamal-proxy المدمج، والذي يتم تهيئته تحت `proxy:`[^proxy]. إذا أرشدك أي دليل لإضافة وسوم `traefik:`، فهو قديم.

## الخطوة 5 — تخزين الأسرار في .kamal/secrets

يجب ألا توضع كلمة مرور السجل (registry) ورابط قاعدة البيانات أبداً في `deploy.yml` أو Git. يقوم Kamal بتحميل الأسرار من `.kamal/secrets` باستخدام dotenv، ويدعم استبدال المتغيرات والأوامر[^env]. قم بتحرير `.kamal/secrets`:

```bash
# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
DATABASE_URL=postgres://notes:$POSTGRES_PASSWORD@notes-docker-db:5432/notes
```

يقرأ هذا `KAMAL_REGISTRY_PASSWORD` و `POSTGRES_PASSWORD` من بيئة الشيل (shell) الخاصة بك، ثم يركب `DATABASE_URL` يعيد استخدام نفس كلمة المرور. مضيف قاعدة البيانات هو **`notes-docker-db`** — هذا هو اسم حاوية الملحقات (accessory container) التي سننشئها في الخطوة التالية (`<service>-<accessory>`).

تأكد من أن `.kamal/secrets` متجاهل في git (هيكل `kamal init` يضيفه إلى `.gitignore`، ولكن تحقق من ذلك)[^env]:

```bash
grep -q '.kamal/secrets' .gitignore && echo "ignored ✓" || echo ".kamal/secrets" >> .gitignore
```

الآن قم بتصدير القيم الحقيقية إلى الشيل الخاص بك قبل أي أمر نشر. استخدم **رمز وصول (access token)** لـ Docker Hub (وليس كلمة مرور حسابك) للسجل، وقم بتوليد كلمة مرور قوية لقاعدة البيانات:

```bash
export KAMAL_REGISTRY_PASSWORD="dckr_pat_your_access_token"
export POSTGRES_PASSWORD="$(openssl rand -hex 24)"
```

الأسرار المدرجة تحت `env: secret:` لا يتم دمجها في الصورة — يقوم Kamal بكتابتها في ملف بيئة (env file) على المضيف ويمررها إلى `docker run` عند التشغيل[^env]، لذا فإن تغيير كلمة المرور يتطلب فقط إعادة نشر.

## الخطوة 6 — إضافة ملحق PostgreSQL 18

**ملحق Kamal (accessory)** هو حاوية داعمة — قاعدة بيانات، ذاكرة تخزين مؤقت (cache)، أو محرك بحث — يقوم Kamal بتشغيلها وإدارتها ولكنه لا يعيد نشرها مع كل عملية دفع للتطبيق[^accessories]. أضف ملحق Postgres 18 إلى `config/deploy.yml`:

```yaml
accessories:
  db:
    image: postgres:18
    host: 203.0.113.10
    # Bind to localhost only so Postgres is never exposed publicly.
    port: "127.0.0.1:5432:5432"
    env:
      clear:
        POSTGRES_USER: notes
        POSTGRES_DB: notes
      secret:
        - POSTGRES_PASSWORD
    directories:
      - pgdata:/var/lib/postgresql
```

هناك ثلاث تفاصيل يسهل الوقوع في الخطأ فيها:

- **`postgres:18` قام بتغيير دليل البيانات الخاص به.** بدءاً من PostgreSQL 18، انتقل المجلد المعلن للصورة إلى `/var/lib/postgresql` (وأصبح `PGDATA` الآن خاصاً بالإصدار في `/var/lib/postgresql/18/docker`). قم بربط وحدة التخزين الدائمة (persistent volume) الخاصة بك بـ **`/var/lib/postgresql`**، وليس المسار القديم `/var/lib/postgresql/data` — المسار القديم يكسر استمرارية البيانات بصمت ويمكن أن يؤدي لفقدان البيانات عند إعادة إنشاء الحاوية[^pg18][^pgvol].
- **`port: "127.0.0.1:5432:5432"`** يربط Postgres بالـ loopback على المضيف، لذا يمكن لتطبيقك الوصول إليه عبر شبكة `kamal` الخاصة ولكن لا يمكن الوصول إليه أبداً من الإنترنت العام[^accessories].
- اسم حاوية الملحق هو **`notes-docker-db`** — اسم الخدمة (`notes-docker`) متصلاً بمفتاح الملحق (`db`)[^accessories]. هذا هو بالضبط المضيف في `DATABASE_URL` من الخطوة 5.

كيف يصل التطبيق إلى `notes-docker-db`؟ يقوم Kamal بربط كل من التطبيق وملحقاته بشبكة جسر (bridge network) من نوع Docker معرفة من قبل المستخدم تسمى `kamal` افتراضياً[^accessories]. توفر شبكات الجسر المعرفة من قبل المستخدم دقة DNS تلقائية باسم الحاوية، لذا فإن `postgres://notes:...@notes-docker-db:5432/notes` يتم حله دون الكشف عن أي منفذ عام.

## الخطوة 7 — توجيه DNS إلى خادمك

قبل النشر الأول، تأكد من أن العالم يمكنه الوصول إلى خادمك عبر اسم المضيف الذي قمت بتهيئته:

1. في مزود DNS الخاص بك، أنشئ سجل `A` لـ `notes.example.com` يشير إلى `203.0.113.10`.
2. تأكد من انتشاره:

```bash
dig +short notes.example.com
# should print 203.0.113.10
```

3. تأكد من أن جدار حماية الخادم يسمح بالدخول عبر المنفذين `80` و `443`. على جهاز Ubuntu جديد باستخدام `ufw`:

```bash
ssh root@203.0.113.10 'ufw allow 80 && ufw allow 443 && ufw status'
```

يجب أن يشير DNS إلى الخادم ويجب أن يكون المنفذ 443 مفتوحاً قبل تفعيل SSL، لأن Let's Encrypt تتحقق من نطاقك من خلال الاتصال بـ kamal-proxy على ذلك الخادم[^proxy]. **لست** بحاجة لتثبيت Docker بنفسك — يقوم `kamal setup` بذلك نيابة عنك عبر SSH[^install].

## الخطوة 8 — النشر الأول باستخدام kamal setup

كل شيء جاهز. قم بتشغيل الإعداد للمرة الأولى من جذر مشروعك:

```bash
kamal setup
```

يقوم `kamal setup` بإعداد جميع الملحقات، ودفع البيئة، ونشر التطبيق على خوادمك[^commands]. بشكل ملموس، يتصل عبر SSH كـ root، ويثبت Docker إذا كان مفقوداً (عبر `get.docker.com`)، ويسجل الدخول إلى سجلك محلياً وعن بُعد، ويشغل ملحق Postgres، ويبني صورتك من Dockerfile، ويدفعها إلى Docker Hub، ويسحبها على الخادم، ويبدأ kamal-proxy على المنفذين 80 و 443، ويبدأ حاوية تطبيقك، ويوجه حركة المرور إليها بمجرد أن يعيد `GET /up` استجابة `200 OK`[^install]. ولأن الملحقات يتم إعدادها قبل نشر التطبيق، فإن قاعدة بياناتك تكون جاهزة بالفعل لاستقبال الاتصالات بحلول الوقت الذي تبدأ فيه حاوية Express.

عندما ينتهي، يصبح تطبيقك متاحاً. قم بزيارة `https://notes.example.com` وستشاهد:

```json
{ "message": "notes-docker is live", "notes": 0 }
```

لكل عملية نشر بعد المرة الأولى، لن تقوم بإعادة تهيئة الخادم — فقط قم بتشغيل:

```bash
kamal deploy
```

### كيف يحقق Kamal عمليات نشر بدون وقت توقف

يحقق Kamal ميزة عدم توقف الخدمة (zero downtime) من خلال عدم إيقاف الحاوية القديمة أبدًا حتى يتم التأكد من سلامة الحاوية الجديدة. يعمل kamal-proxy على المنافذ 80/443 ويقوم بتوجيه الطلبات إلى حاوية تطبيقك[^proxy]. عند كل عملية `kamal deploy`، يبدأ Kamal حاوية تطبيق *جديدة* بجانب الحاوية التي تعمل حاليًا، ثم يقوم بفحص `GET /up` (مرة كل ثانية، مع مهلة 5 ثوانٍ لكل طلب) حتى تستجيب الحاوية الجديدة بـ `200 OK`[^proxy]. عندها فقط يقوم البروكسي بتحويل حركة المرور إلى الحاوية الجديدة وإيقاف القديمة[^install]. وبالتكامل مع معالج `SIGTERM` من الخطوة 1، يتم إنهاء الطلبات الجارية في الحاوية القديمة بشكل نظيف، بحيث لا يواجه المستخدمون أبدًا انقطاعًا في الاتصال أو خطأ 502.

هناك تنبيه واحد يستحق المعرفة: الملحقات (accessories) مثل Postgres **ليست** جزءًا من عملية التبديل بدون توقف هذه. يتم تشغيلها مرة واحدة وتترك لتعمل؛ ولا يقوم `kamal deploy` بإعادة تشغيلها[^accessories]. ترحيل المخططات (Schema migrations) هي مسؤوليتك — قم بتشغيلها كخطاف نشر (deploy hook) أو كأمر `kamal app exec` لمرة واحدة.

## الخطوة 9 — تفعيل SSL التلقائي من Let's Encrypt

لقد قمت بالفعل بتفعيل HTTPS في الخطوة 4 بسطر واحد — `ssl: true` تحت `proxy:`. عند ضبط هذا العلم، يقوم kamal-proxy تلقائيًا بطلب وتجديد شهادة Let's Encrypt لـ `host` الخاص بك، بدون certbot، وبدون مهام cron، وبدون تجديد يدوي[^proxy]. وبشكل افتراضي، يقوم أيضًا بإعادة توجيه كافة حركة مرور HTTP إلى HTTPS.

هناك سلوكان يجب وضعهما في الاعتبار:

- **تغييرات توجيه الترويسات (Header forwarding).** مع `ssl: true`، يتوقف kamal-proxy عن توجيه `X-Forwarded-For` و `X-Forwarded-Proto` إلى تطبيقك ما لم تقم بإضافة `forward_headers: true` صراحةً[^proxy]. إذا كان تطبيق Express الخاص بك يقرأ عنوان IP العميل عبر `req.ip` خلف البروكسي، فقم بضبط `forward_headers: true` واستدعِ `app.set('trust proxy', true)`.
- **تعطيل إعادة التوجيه.** إذا كنت تقوم بإنهاء TLS في مكان آخر وتريد تمرير HTTP، فقم بضبط `ssl_redirect: false`[^proxy].

إذا كنت بحاجة إلى مسار صحة (health path) غير افتراضي، فإن فحص الصحة للبروكسي قابل للتكوين. تطبيقنا يخدم بالفعل `/up`، وهو المسار الافتراضي، لذا لا يلزم تكوين إضافي[^proxy]:

```yaml
proxy:
  host: notes.example.com
  app_port: 3000
  ssl: true
  healthcheck:
    path: /up
    interval: 3
    timeout: 3
```

## التحقق

تأكد من أن النظام بالكامل يعمل من البداية إلى النهاية. من جهازك المحلي:

```bash
# 1. Health endpoint should return 200 over HTTPS.
curl -i https://notes.example.com/up
# HTTP/2 200 ... OK

# 2. Create a note (exercises the Postgres accessory).
curl -s -X POST https://notes.example.com/notes \
  -H 'Content-Type: application/json' \
  -d '{"body":"deployed with Kamal"}'
# {"id":1,"body":"deployed with Kamal","created_at":"2026-06-21T..."}

# 3. The root route should now report one note.
curl -s https://notes.example.com/
# {"message":"notes-API is live","notes":1}
```

ثم تحقق من العرض من جانب الخادم باستخدام أدوات Kamal الخاصة:

```bash
kamal details          # show running app + accessory containers
kamal app logs -f      # tail the app logs
kamal accessory logs db   # check Postgres logs
```

إذا أظهر `kamal details` أن حاوية تطبيقك تعمل وأعاد `curl https://notes.example.com/up` القيمة `200`، فقد نجح النشر. للتحقق من الشهادة، استخدم `curl -vI https://notes.example.com` وتأكد من أن المصدر هو `Let's Encrypt`.

## الأخطاء الشائعة

هذه هي الإخفاقات الخمسة التي يواجهها الأشخاص غالبًا في أول عملية نشر باستخدام Kamal، والمستمدة من وثائق Kamal ومناقشات المجتمع.

**1. `exec format error` عند بدء الحاوية.** لقد قمت ببناء صورة arm64 (Apple Silicon) ونشرتها على خادم x86-64. قم بضبط `builder: { arch: amd64 }` في `deploy.yml` وأعد النشر[^install].

**2. شهادة Let's Encrypt لا تصدر أبدًا.** يحتاج تحدي ACME إلى سجل `A` الخاص بنطاقك ليشير إلى الخادم وأن يكون المنفذ 443 مفتوحًا[^proxy]. قم بتشغيل `dig +short notes.example.com` (يجب أن يساوي IP خادمك) وتأكد من أن `ufw status` يسمح بـ 443. يتطلب SSL أيضًا النشر على خادم واحد مع ضبط `host` — فلن يعمل عبر مضيفين متعددين[^proxy].

**3. التطبيق لا يمكنه الوصول إلى قاعدة البيانات (`ECONNREFUSED notes-API-db`).** يجب أن يكون المضيف في `DATABASE_URL` هو اسم حاوية الملحق، `<service>-<accessory>` — هنا `notes-API-db`[^accessories]. تأكد من أن كلتا الحاويتين على شبكة `kamal` باستخدام `kamal accessory logs db` و `Docker network inspect kamal` على الخادم. تغطي حلقة إعادة المحاولة في `waitForDb()` أيضًا النافذة القصيرة التي لا يزال فيها Postgres قيد التشغيل.

**4. مهلة فحوصات الصحة تنتهي ويتراجع النشر.** يقوم kamal-proxy بفحص `GET /up` ويتوقف عند انتهاء مهلة النشر[^proxy]. تأكد من أن `app_port` يطابق المنفذ الذي يستمع إليه تطبيقك بالفعل (3000 هنا) وأن `/up` يعيد `200` *بدون* الحاجة إلى قاعدة البيانات. تتبع `kamal app logs` لمعرفة سبب خروج الحاوية.

**5. بيانات Postgres تختفي بعد إعادة النشر.** لقد قمت بتركيب المسار القديم. بالنسبة لـ `postgres:18`، يجب أن يستهدف المجلد `/var/lib/postgresql`، وليس `/var/lib/postgresql/data` — فقد انتقل دليل البيانات في PostgreSQL 18[^pg18][^pgvol]. قم بإصلاح إدخال `directories:`، ثم استعد من النسخة الاحتياطية إذا كنت قد فقدت البيانات بالفعل.

للتراجع عن إصدار سيئ تمامًا، يعود `kamal rollback` إلى الإصدار السابق، ويقوم `kamal remove` بإزالة البروكسي والتطبيق والملحقات[^commands].

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

لديك الآن نشر Node.js على خادم واحد، مع HTTPS، وبدون توقف للخدمة، مع ملحق Postgres مدار — بسعر خادم VPS صغير واحد. من هنا يمكنك إضافة خط أنابيب CI يقوم بتشغيل `kamal deploy` عند كل دفع للكود، أو نقل الأسرار إلى مدير كلمات مرور باستخدام `kamal secrets`، أو التوسع من خلال إدراج المزيد من المضيفين تحت `servers:` وإضافة موازن تحميل.

- [نشر Bun + Hono على Fly.io في الإنتاج](https://nerdleveltech.com/deploy-bun-hono-fly-io-production-tutorial) — البديل المدار (PaaS) عندما تفضل عدم تشغيل خادمك الخاص
- [بروكسي عكسي Caddy مع Docker Compose و HTTPS للإنتاج](https://nerdleveltech.com/caddy-reverse-proxy-Docker-compose-production-https-tutorial) — نهج بروكسي عكسي يدوي لنفس مشكلة HTTPS
- [ترقيم الصفحات بالمؤشر (Cursor pagination) مع Postgres و Node.js](https://nerdleveltech.com/cursor-pagination-postgres-nodejs-tutorial) — قم ببناء `/notes` API الآن بعد أن أصبحت قاعدة بياناتك مباشرة

## المصادر

[^kamal]: Kamal على RubyGems — الإصدار 2.12.0، الصادر في 18 يونيو 2026 — <https://rubygems.org/gems/kamal>

[^node]: إصدارات Node.js — إصدار Node 24 هو Active LTS (صدر في 06-05-2025، الصيانة 20-10-2026، نهاية الدعم 30-04-2028) — <https://nodejs.org/en/about/previous-releases>

[^proxy]: Kamal — إعدادات Proxy (`host`، `app_port`، `ssl`، `forward_headers`، `ssl_redirect`، افتراضيات healthcheck) — <https://kamal-deploy.org/docs/configuration/proxy/>

[^install]: تثبيت Kamal (gem install، `kamal init`، ماذا يفعل `kamal setup`) — <https://kamal-deploy.org/docs/installation/>

[^commands]: Kamal — عرض جميع الأوامر (`kamal init`، `kamal setup`، `kamal deploy`، `kamal rollback`، `kamal remove`) — <https://kamal-deploy.org/docs/commands/view-all-commands/>

[^env]: Kamal — متغيرات البيئة و `.kamal/secrets` (dotenv، `clear`/`secret`، ملف env من جانب المضيف) — <https://kamal-deploy.org/docs/configuration/environment-variables/>

[^accessories]: Kamal — الملحقات (التسمية `<service>-<accessory>`، افتراضي شبكة `kamal`، ربط المنافذ، لا يوجد zero-downtime للملحقات) — <https://kamal-deploy.org/docs/configuration/accessories/>

[^pg18]: إطلاق PostgreSQL 18 (25 سبتمبر 2025) — <https://www.postgresql.org/about/news/postgresql-18-released-3142/>

[^pgvol]: صورة `postgres` الرسمية على Docker — تم نقل PGDATA و VOLUME إلى `/var/lib/postgresql` في الإصدار 18+ — <https://hub.Docker.com/_/postgres/>
