نشر تطبيق 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.01. ستكتب 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)2.
- 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 Encrypt3.
خلال هذا الدليل، يسمى التطبيق المثال notes-API. استبدل notes.example.com بنطاقك الحقيقي و 203.0.113.10 بعنوان IP الحقيقي لخادمك.
الخطوة 1 — بناء تطبيق Express API بسيط مع نقطة نهاية لفحص الحالة
يقوم Kamal بنشر كل ما يبنيه Dockerfile الخاص بك، لذا نحتاج أولاً إلى تطبيق حقيقي. أنشئ مجلد مشروع وتطبيق Express 5 API بسيطاً مدعوماً بـ Postgres. نقطة النهاية الوحيدة التي يهتم بها Kamal هي GET /up — يقوم kamal-proxy بفحصها ليقرر متى تكون الحاوية التي بدأت حديثاً جاهزة لاستقبال حركة المرور3.
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:
{
"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 محلي للتطوير:
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 مع المسارات ونقطة نهاية فحص الحالة:
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)2:
# 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 مرة واحدة محلياً إذا لم تكن قد فعلت ذلك بالفعل. تأكد من بناء الصورة قبل إقحام الخادم:
Docker build -t notes-API:test .
ينتهي البناء الناجح بـ naming to Docker.io/library/notes-API:test. إذا فشل هنا، فقم بإصلاحه الآن — يقوم Kamal بتشغيل نفس عملية البناء تماماً، لذا فإن الفشل المحلي يعني فشل النشر.
الخطوة 3 — تثبيت Kamal وتشغيل kamal init
قم بتثبيت Kamal عالمياً من RubyGems4:
gem install kamal
kamal version
يجب أن ترى 2.12.0 (أو أي إصدار 2.11+ — مخطط deploy.yml والأوامر في هذا الدليل مستقرة عبر كامل سلسلة 2.x)1. إذا كنت تفضل عدم تثبيت Ruby، فإن Kamal يتوفر أيضاً كصورة Docker، على الرغم من أن هذا المسار له بعض القيود4.
قم بإنشاء هيكل الإعدادات. من جذر مشروعك:
kamal init
يؤدي هذا إلى إنشاء نموذج إعدادات في config/deploy.yml ونموذج أسرار في مجلد .kamal/5. يستخدم Kamal ملف config/deploy.yml بغض النظر عن اللغة — لا يوجد شيء خاص بـ Rails في هذا المسار. سنقوم باستبدال ملف deploy.yml الناتج بالكامل في الخطوة التالية.
الخطوة 4 — تكوين deploy.yml لتطبيق ليس من نوع Rails
هذا هو جوهر إعدادات deploy.yml في Kamal لتطبيق Node. افتح config/deploy.yml واستبدله بما يلي، مع استبدال اسم الخدمة الخاص بك، ومستودع الصور، وعنوان IP للخادم، والنطاق:
# 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 الخاص بنا، لذا نقوم بضبطه صراحةً3.
ssl: true — يقوم بتفعيل شهادات Let's Encrypt التلقائية. يتطلب خادماً واحداً وقيمة host تشير إلى ذلك الخادم3. سنغطي متطلبات DNS في الخطوة 8.host: notes.example.com — يقوم kamal-proxy بتوجيه الطلبات الخاصة باسم المضيف هذا فقط إلى تطبيقك، مما يسمح لتطبيقات متعددة بمشاركة خادم واحد.builder.arch: amd64 — إذا كنت تقوم بالبناء على جهاز Mac بمعالج Apple Silicon (arm64) وتنشره على خادم x86-64، فيجب عليك البناء العابر (cross-build) لـ amd64 وإلا فلن تبدأ الحاوية (container)4.لاحظ عدم وجود بلوك traefik:. كانت دروس Kamal 1 القديمة تقوم بتهيئة Traefik باستخدام وسوم Docker؛ استبدل Kamal 2 ذلك تماماً بـ kamal-proxy المدمج، والذي يتم تهيئته تحت proxy:3. إذا أرشدك أي دليل لإضافة وسوم traefik:، فهو قديم.
الخطوة 5 — تخزين الأسرار في .kamal/secrets
يجب ألا توضع كلمة مرور السجل (registry) ورابط قاعدة البيانات أبداً في deploy.yml أو Git. يقوم Kamal بتحميل الأسرار من .kamal/secrets باستخدام dotenv، ويدعم استبدال المتغيرات والأوامر6. قم بتحرير .kamal/secrets:
# .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، ولكن تحقق من ذلك)6:
grep -q '.kamal/secrets' .gitignore && echo "ignored ✓" || echo ".kamal/secrets" >> .gitignore
الآن قم بتصدير القيم الحقيقية إلى الشيل الخاص بك قبل أي أمر نشر. استخدم رمز وصول (access token) لـ Docker Hub (وليس كلمة مرور حسابك) للسجل، وقم بتوليد كلمة مرور قوية لقاعدة البيانات:
export KAMAL_REGISTRY_PASSWORD="dckr_pat_your_access_token"
export POSTGRES_PASSWORD="$(openssl rand -hex 24)"
الأسرار المدرجة تحت env: secret: لا يتم دمجها في الصورة — يقوم Kamal بكتابتها في ملف بيئة (env file) على المضيف ويمررها إلى docker run عند التشغيل6، لذا فإن تغيير كلمة المرور يتطلب فقط إعادة نشر.
الخطوة 6 — إضافة ملحق PostgreSQL 18
ملحق Kamal (accessory) هو حاوية داعمة — قاعدة بيانات، ذاكرة تخزين مؤقت (cache)، أو محرك بحث — يقوم Kamal بتشغيلها وإدارتها ولكنه لا يعيد نشرها مع كل عملية دفع للتطبيق7. أضف ملحق Postgres 18 إلى config/deploy.yml:
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— المسار القديم يكسر استمرارية البيانات بصمت ويمكن أن يؤدي لفقدان البيانات عند إعادة إنشاء الحاوية89.port: "127.0.0.1:5432:5432"يربط Postgres بالـ loopback على المضيف، لذا يمكن لتطبيقك الوصول إليه عبر شبكةkamalالخاصة ولكن لا يمكن الوصول إليه أبداً من الإنترنت العام7.- اسم حاوية الملحق هو
notes-docker-db— اسم الخدمة (notes-docker) متصلاً بمفتاح الملحق (db)7. هذا هو بالضبط المضيف فيDATABASE_URLمن الخطوة 5.
كيف يصل التطبيق إلى notes-docker-db؟ يقوم Kamal بربط كل من التطبيق وملحقاته بشبكة جسر (bridge network) من نوع Docker معرفة من قبل المستخدم تسمى kamal افتراضياً7. توفر شبكات الجسر المعرفة من قبل المستخدم دقة DNS تلقائية باسم الحاوية، لذا فإن postgres://notes:...@notes-docker-db:5432/notes يتم حله دون الكشف عن أي منفذ عام.
الخطوة 7 — توجيه DNS إلى خادمك
قبل النشر الأول، تأكد من أن العالم يمكنه الوصول إلى خادمك عبر اسم المضيف الذي قمت بتهيئته:
- في مزود DNS الخاص بك، أنشئ سجل
Aلـnotes.example.comيشير إلى203.0.113.10. - تأكد من انتشاره:
dig +short notes.example.com
# should print 203.0.113.10
- تأكد من أن جدار حماية الخادم يسمح بالدخول عبر المنفذين
80و443. على جهاز Ubuntu جديد باستخدامufw:
ssh root@203.0.113.10 'ufw allow 80 && ufw allow 443 && ufw status'
يجب أن يشير DNS إلى الخادم ويجب أن يكون المنفذ 443 مفتوحاً قبل تفعيل SSL، لأن Let's Encrypt تتحقق من نطاقك من خلال الاتصال بـ kamal-proxy على ذلك الخادم3. لست بحاجة لتثبيت Docker بنفسك — يقوم kamal setup بذلك نيابة عنك عبر SSH4.
الخطوة 8 — النشر الأول باستخدام kamal setup
كل شيء جاهز. قم بتشغيل الإعداد للمرة الأولى من جذر مشروعك:
kamal setup
يقوم kamal setup بإعداد جميع الملحقات، ودفع البيئة، ونشر التطبيق على خوادمك5. بشكل ملموس، يتصل عبر SSH كـ root، ويثبت Docker إذا كان مفقوداً (عبر get.docker.com)، ويسجل الدخول إلى سجلك محلياً وعن بُعد، ويشغل ملحق Postgres، ويبني صورتك من Dockerfile، ويدفعها إلى Docker Hub، ويسحبها على الخادم، ويبدأ kamal-proxy على المنفذين 80 و 443، ويبدأ حاوية تطبيقك، ويوجه حركة المرور إليها بمجرد أن يعيد GET /up استجابة 200 OK4. ولأن الملحقات يتم إعدادها قبل نشر التطبيق، فإن قاعدة بياناتك تكون جاهزة بالفعل لاستقبال الاتصالات بحلول الوقت الذي تبدأ فيه حاوية Express.
عندما ينتهي، يصبح تطبيقك متاحاً. قم بزيارة https://notes.example.com وستشاهد:
{ "message": "notes-docker is live", "notes": 0 }
لكل عملية نشر بعد المرة الأولى، لن تقوم بإعادة تهيئة الخادم — فقط قم بتشغيل:
kamal deploy
كيف يحقق Kamal عمليات نشر بدون وقت توقف
يحقق Kamal ميزة عدم توقف الخدمة (zero downtime) من خلال عدم إيقاف الحاوية القديمة أبدًا حتى يتم التأكد من سلامة الحاوية الجديدة. يعمل kamal-proxy على المنافذ 80/443 ويقوم بتوجيه الطلبات إلى حاوية تطبيقك3. عند كل عملية kamal deploy، يبدأ Kamal حاوية تطبيق جديدة بجانب الحاوية التي تعمل حاليًا، ثم يقوم بفحص GET /up (مرة كل ثانية، مع مهلة 5 ثوانٍ لكل طلب) حتى تستجيب الحاوية الجديدة بـ 200 OK3. عندها فقط يقوم البروكسي بتحويل حركة المرور إلى الحاوية الجديدة وإيقاف القديمة4. وبالتكامل مع معالج SIGTERM من الخطوة 1، يتم إنهاء الطلبات الجارية في الحاوية القديمة بشكل نظيف، بحيث لا يواجه المستخدمون أبدًا انقطاعًا في الاتصال أو خطأ 502.
هناك تنبيه واحد يستحق المعرفة: الملحقات (accessories) مثل Postgres ليست جزءًا من عملية التبديل بدون توقف هذه. يتم تشغيلها مرة واحدة وتترك لتعمل؛ ولا يقوم kamal deploy بإعادة تشغيلها7. ترحيل المخططات (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، وبدون تجديد يدوي3. وبشكل افتراضي، يقوم أيضًا بإعادة توجيه كافة حركة مرور HTTP إلى HTTPS.
هناك سلوكان يجب وضعهما في الاعتبار:
- تغييرات توجيه الترويسات (Header forwarding). مع
ssl: true، يتوقف kamal-proxy عن توجيهX-Forwarded-ForوX-Forwarded-Protoإلى تطبيقك ما لم تقم بإضافةforward_headers: trueصراحةً3. إذا كان تطبيق Express الخاص بك يقرأ عنوان IP العميل عبرreq.ipخلف البروكسي، فقم بضبطforward_headers: trueواستدعِapp.set('trust proxy', true). - تعطيل إعادة التوجيه. إذا كنت تقوم بإنهاء TLS في مكان آخر وتريد تمرير HTTP، فقم بضبط
ssl_redirect: false3.
إذا كنت بحاجة إلى مسار صحة (health path) غير افتراضي، فإن فحص الصحة للبروكسي قابل للتكوين. تطبيقنا يخدم بالفعل /up، وهو المسار الافتراضي، لذا لا يلزم تكوين إضافي3:
proxy:
host: notes.example.com
app_port: 3000
ssl: true
healthcheck:
path: /up
interval: 3
timeout: 3
التحقق
تأكد من أن النظام بالكامل يعمل من البداية إلى النهاية. من جهازك المحلي:
# 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 الخاصة:
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 وأعد النشر4.
2. شهادة Let's Encrypt لا تصدر أبدًا. يحتاج تحدي ACME إلى سجل A الخاص بنطاقك ليشير إلى الخادم وأن يكون المنفذ 443 مفتوحًا3. قم بتشغيل dig +short notes.example.com (يجب أن يساوي IP خادمك) وتأكد من أن ufw status يسمح بـ 443. يتطلب SSL أيضًا النشر على خادم واحد مع ضبط host — فلن يعمل عبر مضيفين متعددين3.
3. التطبيق لا يمكنه الوصول إلى قاعدة البيانات (ECONNREFUSED notes-API-db). يجب أن يكون المضيف في DATABASE_URL هو اسم حاوية الملحق، <service>-<accessory> — هنا notes-API-db7. تأكد من أن كلتا الحاويتين على شبكة kamal باستخدام kamal accessory logs db و Docker network inspect kamal على الخادم. تغطي حلقة إعادة المحاولة في waitForDb() أيضًا النافذة القصيرة التي لا يزال فيها Postgres قيد التشغيل.
4. مهلة فحوصات الصحة تنتهي ويتراجع النشر. يقوم kamal-proxy بفحص GET /up ويتوقف عند انتهاء مهلة النشر3. تأكد من أن app_port يطابق المنفذ الذي يستمع إليه تطبيقك بالفعل (3000 هنا) وأن /up يعيد 200 بدون الحاجة إلى قاعدة البيانات. تتبع kamal app logs لمعرفة سبب خروج الحاوية.
5. بيانات Postgres تختفي بعد إعادة النشر. لقد قمت بتركيب المسار القديم. بالنسبة لـ postgres:18، يجب أن يستهدف المجلد /var/lib/postgresql، وليس /var/lib/postgresql/data — فقد انتقل دليل البيانات في PostgreSQL 1889. قم بإصلاح إدخال directories:، ثم استعد من النسخة الاحتياطية إذا كنت قد فقدت البيانات بالفعل.
للتراجع عن إصدار سيئ تمامًا، يعود kamal rollback إلى الإصدار السابق، ويقوم kamal remove بإزالة البروكسي والتطبيق والملحقات5.
الخطوات التالية ومزيد من القراءة
لديك الآن نشر Node.js على خادم واحد، مع HTTPS، وبدون توقف للخدمة، مع ملحق Postgres مدار — بسعر خادم VPS صغير واحد. من هنا يمكنك إضافة خط أنابيب CI يقوم بتشغيل kamal deploy عند كل دفع للكود، أو نقل الأسرار إلى مدير كلمات مرور باستخدام kamal secrets، أو التوسع من خلال إدراج المزيد من المضيفين تحت servers: وإضافة موازن تحميل.
- نشر Bun + Hono على Fly.io في الإنتاج — البديل المدار (PaaS) عندما تفضل عدم تشغيل خادمك الخاص
- بروكسي عكسي Caddy مع Docker Compose و HTTPS للإنتاج — نهج بروكسي عكسي يدوي لنفس مشكلة HTTPS
- ترقيم الصفحات بالمؤشر (Cursor pagination) مع Postgres و Node.js — قم ببناء
/notesAPI الآن بعد أن أصبحت قاعدة بياناتك مباشرة
المصادر
Footnotes
-
Kamal على RubyGems — الإصدار 2.12.0، الصادر في 18 يونيو 2026 — https://rubygems.org/gems/kamal ↩ ↩2
-
إصدارات Node.js — إصدار Node 24 هو Active LTS (صدر في 06-05-2025، الصيانة 20-10-2026، نهاية الدعم 30-04-2028) — https://nodejs.org/en/about/previous-releases ↩ ↩2
-
Kamal — إعدادات Proxy (
host،app_port،ssl،forward_headers،ssl_redirect، افتراضيات healthcheck) — https://kamal-deploy.org/docs/configuration/proxy/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 -
تثبيت Kamal (gem install،
kamal init، ماذا يفعلkamal setup) — https://kamal-deploy.org/docs/installation/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Kamal — عرض جميع الأوامر (
kamal init،kamal setup،kamal deploy،kamal rollback،kamal remove) — https://kamal-deploy.org/docs/commands/view-all-commands/ ↩ ↩2 ↩3 -
Kamal — متغيرات البيئة و
.kamal/secrets(dotenv،clear/secret، ملف env من جانب المضيف) — https://kamal-deploy.org/docs/configuration/environment-variables/ ↩ ↩2 ↩3 -
Kamal — الملحقات (التسمية
<service>-<accessory>، افتراضي شبكةkamal، ربط المنافذ، لا يوجد zero-downtime للملحقات) — https://kamal-deploy.org/docs/configuration/accessories/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
إطلاق PostgreSQL 18 (25 سبتمبر 2025) — https://www.postgresql.org/about/news/postgresql-18-released-3142/ ↩ ↩2
-
صورة
postgresالرسمية على Docker — تم نقل PGDATA و VOLUME إلى/var/lib/postgresqlفي الإصدار 18+ — https://hub.Docker.com/_/postgres/ ↩ ↩2