استضافة Chatwoot على VPS باستخدام Docker
ثبّت Chatwoot على VPS باستخدام Docker Compose وTraefik، مع وسوم إصدارات ثابتة، وإعداد SMTP يعمل فعلاً، ونسخ Postgres والملفات المرفوعة وترقيات آمنة.
ما الذي ستبنيه
لاستضافة Chatwoot ذاتياً على VPS، ستشغّل أربع حاويات: عملية Rails للويب، وعامل Sidekiq للمهام الخلفية، وPostgreSQL مع إضافة pgvector، وRedis. Chatwoot هو مكتب دعم عملاء مفتوح المصدر، لذلك ستحصل على صندوق وارد مشترك للفريق وأداة محادثة لموقع الويب على خادم تتحكم فيه. يستغرق التثبيت نحو عشرين دقيقة. لكن ما يأتي بعد ذلك، مثل تسليم البريد والنسخ الاحتياطية والترقيات وتحديد الموارد، هو ما يحدد ما إذا كان النظام سيظل يعمل بعد عام.
لكل حاوية مهمة واحدة. يقدّم Rails لوحة تحكم الموظفين وواجهة API لأداة المحادثة. ينفّذ Sidekiq المهام البطيئة: إرسال البريد الإلكتروني، والاستعلام الدوري عن القنوات المتصلة، وتشغيل قواعد الأتمتة، وإنشاء التقارير. يخزّن Postgres المحادثات وجهات الاتصال وحسابات الموظفين وكل إعداد تغيّره في لوحة التحكم. يخزّن Redis قوائم Sidekiq وقناة النشر/الاشتراك في ActionCable، التي تدفع الرسالة الجديدة إلى لوحة تحكم مفتوحة من دون إعادة تحميل الصفحة. Redis ليس ذاكرة تخزين مؤقت مؤقتة هنا، لأن فقدانه يعني فقدان المهام الموجودة في قائمة الانتظار.
صورة Postgres في ملف compose الأساسي من upstream هي pgvector/pgvector:pg16 بدلاً من صورة postgres القياسية، لأن مخطط Chatwoot يفعّل الإضافة vector لميزات الذكاء الاصطناعي. إذا استبدلتها بصورة Postgres القياسية، فسيتوقف أول تشغيل لقاعدة البيانات مع ERROR: extension "vector" is not available، لأن ملف التحكم الخاص بالإضافة غير موجود في تلك الصورة. استخدم الصورة التي يوفّرها upstream.
يفترض هذا الدليل أن Docker وreverse proxy يعملان مسبقاً على الخادم. إذا لم يكونا كذلك، فابدأ بـ Docker Compose على VPS ثم عُد إلى هنا.
ما مقدار موارد VPS التي يحتاج إليها Chatwoot المستضاف ذاتياً؟
اعتباراً من August 2026، تطلب صفحة المتطلبات الرسمية 4 GB من RAM و4 CPU cores كحد أدنى، وتقدّر أن ذلك يكفي لما يصل إلى 10,000 محادثة يومياً. كما تذكر أن 8 GB و8 cores تكفي لما يصل إلى 20,000 محادثة يومياً. وتطلب أيضاً 1 GB على الأقل من swap، وتذكر السبب مباشرة: حتى لا تنفد ذاكرة الجهاز أثناء الترقية. خصّص من 5 GB إلى 10 GB على القرص لـPostgres قبل احتساب مساحة تحميل الملفات.
أما الجزء المباشر فهو التالي. سيُقلع Chatwoot على VPS بسعة 2 GB، وسيبدو مستقراً مع وكيلين وصندوق وارد هادئ. لكنه يتوقف في حالتين. الأولى هي Sidekiq، إذ تقيسه الجهة الرسمية بأكثر من 1 GB على خادم مشغول. لذلك تدفع دفعة من رسائل البريد أو مهمة تقرير الجهاز إلى تجاوز الذاكرة المتاحة قبل أن تحصل Rails وPostgres وRedis على حصتها. والثانية هي الترقية، لأن db:chatwoot_prepare يشغّل عملية Rails جديدة لتطبيق migrations، ويستهلك إقلاع Rails على هذه الصورة مئات الميغابايت قبل أن ينفذ أي عمل مفيد.
لن تتلقى تحذيراً واضحاً أولاً. يرسل out of memory killer في النواة الإشارة SIGKILL إلى أكبر عملية، ويرى Docker أن الحاوية توقفت، ثم يعيد restart: always تشغيلها. يعرض docker compose ps بعد ذلك حاوية تعود باستمرار إلى Exited (137)، حيث يعني 137 أنها قُتلت بالإشارة 9. أكّد ذلك باستخدام sudo dmesg -T | grep -i "killed process"، الذي يذكر العملية التي اختارتها النواة.
إذا كانت 4 GB تتجاوز ميزانيتك، فشغّل جهازاً بسعة 2 GB مع 2 GB من swap، وتقبّل أن أزمنة الاستجابة ستزداد تحت الحمل بدلاً من توقف الخدمة بالكامل. ومن المفيد في جميع الأحوال وضع حد أقصى صارم للذاكرة لكل خدمة، حتى لا يتمكن العامل من إسقاط قاعدة البيانات معه. راجع حدود الذاكرة في Docker Compose.
تحميلات الملفات هي الجزء الذي ينمو من دون حد تضبطه. كل لقطة شاشة يرفقها العميل تُحفظ في وحدة التخزين وتبقى فيها، لذلك راقب docker system df -v بدلاً من افتراض أن قاعدة البيانات هي التي ملأت القرص.
احصل على ملف Compose وثبّت وسم الإصدار
mkdir -p ~/chatwoot && cd ~/chatwoot
wget -O .env https://raw.githubusercontent.com/chatwoot/chatwoot/develop/.env.example
wget -O docker-compose.yaml https://raw.githubusercontent.com/chatwoot/chatwoot/develop/docker-compose.production.yaml
chmod 600 .envيشير الملف الذي نزّلته للتو إلى image: chatwoot/chatwoot:latest. غيّر ذلك قبل تنفيذ أي إجراء آخر.
services:
base: &base
image: chatwoot/chatwoot:v4.16.2
env_file: .env
volumes:
- storage_data:/app/storageيعني latest أن docker compose pull التالي سيحصل على أي إصدار نُشر في ذلك الصباح، وقد يكون إصداراً رئيسياً يتضمن عمليات ترحيل لم تقرأ عنها مطلقاً. عملياً، لا يمكن التراجع عن عمليات ترحيل Chatwoot، لذلك فإن الانتقال غير المقصود يعني الاستعادة من نسخة احتياطية، وليس التراجع عن التغيير. ثبّت الوسم وغيّره بشكل مقصود. كان v4.16.2 هو الإصدار الحالي في August 2026؛ راجع صفحة الإصدارات لمعرفة الوسم الذي ينبغي تثبيته اليوم.
خدمة base هي مرساة YAML التي تدمجها كل من rails وsidekiq، لذلك يؤدي تغيير الوسم في موضع واحد إلى تغييره لكليهما. أثناء وجودك في الملف، احذف السطر version: '3' الموجود في الأعلى. يتجاهله Compose الحديث ويطبع the attribute 'version' is obsolete, it will be ignored مع كل أمر.
املأ ملف .env
أنشئ السر أولاً. يطلب Upstream قيمة أبجدية رقمية، لأن المحارف الخاصة قد تتشوّه عندما تمرّ القيمة عبر shell أو محلّل YAML.
head /dev/urandom | tr -dc A-Za-z0-9 | head -c 63 ; echo ''عيّن هذه المفاتيح في .env.
SECRET_KEY_BASE=<the 63 characters you just generated>
FRONTEND_URL=https://support.example.com
FORCE_SSL=true
DEFAULT_LOCALE=en
ENABLE_ACCOUNT_SIGNUP=true
POSTGRES_HOST=postgres
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=<long random string>
POSTGRES_DATABASE=chatwoot
REDIS_URL=redis://redis:6379
REDIS_PASSWORD=<a different long random string>
RAILS_ENV=production
INSTALLATION_ENV=docker
ACTIVE_STORAGE_SERVICE=localPOSTGRES_HOST=postgres وredis://redis:6379 هما اسما خدمتي Compose، ويُحلّان عبر الشبكة الافتراضية للمشروع. أما FRONTEND_URL فليس للزينة. ينشئ Chatwoot منه عنوان URL لبرنامج widget، وكل رابط داخل رسالة بريد إلكتروني صادرة. لذلك تؤدي القيمة الخاطئة إلى إنشاء روابط لإعادة تعيين كلمة المرور تشير إلى مضيف لا يستجيب.
إليك موضع الخطأ في ملف Upstream. لا تقرأ خدمة postgres المتغير .env. بل تحتوي على كتلة environment خاصة بها، مع ترك POSTGRES_PASSWORD= فارغاً. لذلك يؤدي ضبط كلمة المرور في .env وحده إلى ترك قاعدة البيانات بلا كلمة مرور، مع تزويد التطبيق بكلمة مرور. اربط الخدمة بالمتغير نفسه:
postgres:
image: pgvector/pgvector:pg16
restart: always
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=chatwoot
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}يقرأ Compose .env من دليل المشروع لإجراء استبدال ${...}، وبذلك يحصل الطرفان على السلسلة نفسها. إذا أخطأت في ذلك، يتوقف Rails مع PG::ConnectionBad: FATAL: password authentication failed for user "postgres".
يفاجئ سلوك واحد معظم المستخدمين: لا تطبّق صورة Postgres POSTGRES_PASSWORD إلا عند تهيئة دليل بيانات فارغ. لا يؤثر تغيير القيمة لاحقاً، لأن initdb لا يعمل مرة ثانية. إذا كنت قد بدأت المكدس مرة واحدة، فغيّرها داخل قاعدة البيانات بدلاً من ذلك.
docker compose exec postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD 'the-new-password';"تُستخدم ENABLE_ACCOUNT_SIGNUP=true مؤقتاً. فهي تفتح نموذج التسجيل العام كي تنشئ الحساب الأول. عيّنها إلى false وشغّل docker compose up -d مرة أخرى فور إنشاء حسابك، وإلا فبإمكان أي شخص يعثر على عنوان URL التسجيل في مكتب الدعم لديك. بعد ذلك، يصل الوكلاء عبر الدعوات، وتُخزَّن كلمات مرورهم داخل هذا التطبيق وحده. وهذا مناسب إلى أن تشغّل نحو نصف دزينة من الخدمات وتتعب من وجود قائمة حسابات منفصلة في كل خدمة، وعندها يحل موفّر هوية مستضاف ذاتياً مثل Authentik محل هذه القوائم.
يحتوي .env الآن على كل أسرار هذا المكدس بنص واضح، لذلك اضبط صلاحياته على mode 600 وأبقِه خارج git. يشرح كيفية قراءة Compose لملفات env والمواضع التي تتسرّب فيها الأسرار النقاط الحساسة، بما في ذلك الفرق بين env_file وenvironment.
ضع Chatwoot خلف Traefik الموجود لديك
لا تنشئ Reverse Proxy ثانياً لتطبيق واحد. إذا كان Traefik ينهي TLS (أمان طبقة النقل) مسبقاً للحاويات الأخرى على هذا الخادم، فأضف Chatwoot إليه باستخدام كتلة labels. إذا لم تكن قد أعددت ذلك بعد، فأعدّه مرة واحدة باتباع Traefik أمام عدة تطبيقات Docker Compose، ثم عد إلى هنا.
أبقِ docker-compose.yaml الخاص بالمشروع الأصلي قريباً من الإعداد الافتراضي، حتى تتمكن لاحقاً من مقارنته بنسخة أحدث، وضع تغييراتك في ملف override. يدمج Compose docker-compose.override.yaml تلقائياً، وتشرح تقسيم Compose عبر عدة ملفات قواعد الدمج.
services:
rails:
networks:
- default
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.chatwoot.rule=Host(`support.example.com`)"
- "traefik.http.routers.chatwoot.entrypoints=websecure"
- "traefik.http.routers.chatwoot.tls.certresolver=letsencrypt"
- "traefik.http.services.chatwoot.loadbalancer.server.port=3000"
networks:
proxy:
external: trueاستخدم أسماء entrypoint وcertresolver الخاصة بك. يجب أن تكون الحاوية على شبكة Docker نفسها التي يستخدمها Traefik، وهذا ما يفعله الإدخال proxy، كما يجب أن تبقى على default أيضاً، وإلا فقدت الاتصال بـPostgres وRedis. هذا هو السطر الثاني الذي ينساه الناس.
اترك كتلة ports: كما هي. يربطها المشروع الأصلي بـ127.0.0.1:3000، وهو loopback فقط، لذلك لا يمكن الوصول إليها من الإنترنت، وتبقى مفيدة للاختبار من داخل الخادم باستخدام curl -I http://127.0.0.1:3000.
تُبقي لوحة تحكم الوكيل اتصال websocket مفتوحاً إلى /cable لتسليم الرسائل مباشرة. يمرّر Traefik طلب HTTP upgrade دون إعداد إضافي، لذلك لا تضف شيئاً. إذا وضعت لاحقاً CDN أو Proxy آخر أمام Traefik، فاسمح باتصالات websocket هناك، لأن العَرَض سيكون تحميل لوحة التحكم بصورة طبيعية، بينما لا تظهر الرسائل الجديدة إلا بعد تحديث يدوي.
تهيئة قاعدة البيانات وتشغيل الحزمة
شغّل خدمات البيانات أولاً، وانتظر حتى يكمل Postgres تشغيله الأول.
docker compose up -d postgres redis
docker compose logs postgres | tail -n 5انتظر حتى database system is ready to accept connections. ثم أنشئ المخطط.
docker compose run --rm rails bundle exec rails db:chatwoot_prepareينشئ هذا الأمر قاعدة البيانات إذا لم تكن موجودة، ثم يحمّل المخطط وبيانات التهيئة الافتراضية. ويطبع أسطر الترحيل ثم ينتهي بنجاح. إذا ظل يطبع postgres:5432 - no response، فهذا يعني أن entrypoint ينتظر قاعدة بيانات لا تقبل الاتصالات بعد. في التشغيل الأول، يعني ذلك عادةً أن initdb ما زال يعمل. انتظر، واقرأ سجلات Postgres، ثم شغّل الأمر مرة أخرى. إذا توقف عند إضافة vector، فقد استبدلت صورة pgvector بصورة Postgres القياسية.
docker compose up -d
docker compose ps
docker compose logs --tail 30 railsيجب أن تعرض الحاويات الأربع الحالة Up، ويجب أن ينتهي سجل rails بسطر Puma يستمع على http://0.0.0.0:3000. ثم تحقق من المسار العام:
curl -sI https://support.example.com | head -n 1تعني HTTP/2 200 أن السلسلة كاملة تعمل. يشير ظهور 404 من Traefik إلى أن قاعدة الموجّه لم تتطابق، ويكون السبب عادةً خطأً إملائياً في اسم المضيف. يشير ظهور 502 إلى أن Traefik طابق الموجّه، لكنه لم يتمكن من الوصول إلى الحاوية. ويكون السبب في الغالب هو غياب شبكة proxy أو استخدام loadbalancer.server.port لا تساوي 3000.
افتح URL، وأنشئ حسابك في /app/auth/signup، ثم عيّن ENABLE_ACCOUNT_SIGNUP=false وشغّل docker compose up -d لإغلاق النموذج.
لماذا تفشل إعادة تعيين كلمات المرور ومحادثات البريد الإلكتروني من دون SMTP
يصبح Chatwoot، عند عدم ضبط إعدادات SMTP (بروتوكول نقل البريد البسيط)، مكتب دعم لا يستطيع إرسال البريد، وهذا يعطّل أكثر من مجرد الإشعارات. تتوقف إعادة تعيين كلمات المرور، لذلك يبقى المسؤول الذي فُقد وصوله مقفلاً خارج النظام. وتتوقف دعوات الوكلاء، لأن الدعوة عبارة عن رسالة بريد إلكتروني. كما يتوقف الرد على العميل ضمن محادثة بريد إلكتروني، فتسير المحادثة في اتجاه واحد فقط. هذه هي الخطوة التي يتجاوزها الناس، ثم يكتشفونها خلال أسوأ أسابيعهم.
الآلية واضحة. عند غياب إعدادات SMTP، يحتفظ ActionMailer بإعداد التسليم الافتراضي إلى localhost عبر المنفذ 25. لا يوجد خادم بريد داخل حاوية Rails، لذلك ترفع مهمة التسليم Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 25. يُرسل البريد من مهمة تعمل في الخلفية، لذلك يظهر هذا السطر في سجل Sidekiq ولا يظهر في سجل Rails. في الوقت نفسه، يرى الشخص الذي ينقر على "نسيت كلمة المرور" رسالة تأكيد مطمئنة، لكنه لا يتلقى شيئاً.
MAILER_SENDER_EMAIL=Support <support@example.com>
SMTP_DOMAIN=example.com
SMTP_ADDRESS=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=support@example.com
SMTP_PASSWORD=<the relay password>
SMTP_AUTHENTICATION=plain
SMTP_ENABLE_STARTTLS_AUTO=trueاستخدم المنفذ 587 مع STARTTLS. يفتح هذا البروتوكول الاتصال بنص واضح، ثم يرقّيه إلى اتصال مشفّر قبل المصادقة. يحظر معظم موفري VPS الاتصالات الصادرة عبر المنفذ 25 للحد من الرسائل المزعجة، لذلك يكون الترحيل عبر المنفذ 587 عادةً الخيار الوحيد الذي يمكنه الاتصال فعلياً. يشير SMTP_DOMAIN إلى النطاق الذي يعلنه خادمك أثناء محادثة SMTP، وقد ترفض بعض خوادم الترحيل عدم تطابقه.
طبّق الإعدادات وراقب العامل:
docker compose up -d rails sidekiq
docker compose logs -f sidekiqابدأ إعادة تعيين كلمة المرور من صفحة تسجيل الدخول. يدل التسليم الناجح على اكتمال مهمة mailer بشكل طبيعي في سجل Sidekiq. أما الفشل فيعرض فئة الاستثناء، ثم يعيد Sidekiq المحاولة مع تزايد فترة الانتظار، ولهذا ينتج خادم الترحيل المعطّل الخطأ نفسه كل بضع دقائق ولساعات.
تشيع حالتا رفض، ولا تمثل أي منهما خطأً في Chatwoot. تعني 535 Authentication failed أن اسم المستخدم أو كلمة المرور غير صحيحين لخادم الترحيل، ويريد كثير من موفري الخدمة كلمة مرور للتطبيق بدلاً من كلمة مرور الحساب. تعني 550 Sender address rejected أن MAILER_SENDER_EMAIL هو عنوان لن يسمح خادم الترحيل بالإرسال منه، لذلك يجب أن يكون صندوق بريد أو نطاقاً تحققت منه لدى موفّر الخدمة.
يُعد استلام البريد الإلكتروني ضمن محادثة مهمة منفصلة. يحتاج إلى MAILER_INBOUND_EMAIL_DOMAIN وRAILS_INBOUND_EMAIL_SERVICE، إضافةً إلى خادم بريد يسلّم الرسائل الواردة إلى Chatwoot. استئجار خادم ترحيل هو المسار الأسرع. وإذا كنت تفضّل امتلاك مسار البريد بالكامل، فراجع تشغيل خادم بريدك الخاص باستخدام Mailcow لمعرفة ما يتطلبه هذا الالتزام فعلياً.
ما الذي يجب نسخه احتياطياً، وكيف تثبت نجاح الاستعادة
تتكوّن نسخة Chatwoot الاحتياطية من أربعة أجزاء. يؤدي تخطي أي جزء منها إلى تحويل الاستعادة إلى إعادة بناء.
- قاعدة بيانات Postgres، التي تحتوي على المحادثات وجهات الاتصال وحسابات الوكلاء وجميع الإعدادات.
- وحدة التخزين
storage_data، لأنACTIVE_STORAGE_SERVICE=localيكتب الملفات المرفوعة على القرص ويحتفظ بصف مرجعي لها في Postgres فقط. - ملف
.env، لأنه يحتوي علىSECRET_KEY_BASEومفاتيحACTIVE_RECORD_ENCRYPTION_*. - ملفات compose، لأنها تسجل وسم الصورة المطابق تماماً لمخطط قاعدة البيانات.
إذا استعدت قاعدة البيانات وحدها، فستعود كل المحادثات مع مرفقات معطّلة، لأن الصفوف تشير إلى ملفات لم تعد موجودة على القرص.
cd ~/chatwoot
docker compose exec -T postgres pg_dump -U postgres -Fc chatwoot > db-$(date +%F).dumpتُعد -T مهمة. من دونها، يخصّص Compose طرفية زائفة تعيد كتابة بايتات الأسطر الجديدة في الدفق، فتحصل على ملف تفريغ ترفضه pg_restore. أما -Fc فهي الصيغة المخصصة، التي تضغط البيانات وتتيح لـpg_restore العمل بشكل انتقائي.
docker run --rm -v chatwoot_storage_data:/data:ro -v "$PWD":/backup alpine \
tar czf /backup/storage-$(date +%F).tgz -C /data .يتكوّن اسم وحدة التخزين من اسم دليل مشروعك إضافة إلى _storage_data. أكّده باستخدام docker volume ls | grep storage_data قبل الوثوق بهذا الأمر، لأن Docker ينشئ وحدة تخزين فارغة بدلاً من الفشل عند تسمية وحدة غير موجودة. ستحصل على أرشيف صالح وفارغ، من دون أي خطأ. تحقق من الحجم بعد ذلك باستخدام ls -lh storage-*.tgz.
يوجد كلا الملفين الآن على القرص نفسه الذي توجد عليه البيانات التي يحميانها، وهذا لا يحميك من شيء. انقلهما إلى خارج الخادم وشفرهما، لأن تفريغ قاعدة البيانات يحتوي على كل رسائل العملاء بنص واضح. يشرح النسخ الاحتياطية المشفرة خارج الموقع باستخدام restic جانبَي الجدولة والاحتفاظ.
اختبار الاستعادة، نفّذه قبل أن تحتاج إليه
استعد النسخة على VPS ثانٍ، وليس على الخادم المباشر. انسخ .env وملفات compose والأرشيفين إلى الخادم الثاني، ثم نفّذ:
docker compose up -d postgres
docker compose exec -T postgres pg_restore -U postgres -d chatwoot --clean --if-exists < db-2026-08-10.dump
docker run --rm -v chatwoot_storage_data:/data -v "$PWD":/backup alpine \
sh -c 'rm -rf /data/* && tar xzf /backup/storage-2026-08-10.tgz -C /data'
docker compose up -dيحذف --clean --if-exists الكائنات الموجودة قبل التحميل، لذلك لا توجّهه إلا إلى قاعدة بيانات مستعد لفقدانها. بعد ذلك، سجّل الدخول وافتح محادثة تحتوي على مرفق. إذا ظهرت قائمة الرسائل ونُزّل الملف، فالنسخة الاحتياطية حقيقية.
تؤدي الاستعادة باستخدام SECRET_KEY_BASE مختلف إلى إبطال كل ملفات تعريف ارتباط الجلسات، لذلك سيُسجَّل خروج الجميع. أما الاستعادة باستخدام مفاتيح ACTIVE_RECORD_ENCRYPTION_* مختلفة فهي أسوأ: لن يتمكن Chatwoot من فك تشفير الأعمدة التي تحتوي على بيانات اعتماد القنوات، وسيُصدر ActiveRecord::Encryption::Errors::Decryption. لهذا السبب يوجد .env ضمن قائمة النسخ الاحتياطية.
كيفية ترقية Chatwoot إلى tag جديد
ترتيب الخطوات أهم من الأوامر نفسها.
- اقرأ ملاحظات الإصدار بين tag الحالي وtag الهدف، وابحث عن الخطوات اليدوية المطلوبة.
- أنشئ تفريغاً حديثاً لقاعدة البيانات وأرشيفاً للتخزين، وتحقق من أن حجمي الملفين منطقيان.
- عدّل tag الصورة في الخدمة
baseداخلdocker-compose.yaml. - اسحب الصورة الجديدة، وأوقف الـstack، وشغّل عمليات الترحيل، ثم ابدأه من جديد.
docker compose pull
docker compose down
docker compose run --rm rails bundle exec rails db:chatwoot_prepare
docker compose up -d
docker compose imagesاسحب الصورة قبل تشغيل عملية الترحيل، لأن عملية الترحيل يجب أن تعمل من الصورة الجديدة. فالصورة القديمة لا تحتوي على ملفات الترحيل الجديدة. أوقف الـstack قبل تشغيل عملية الترحيل، لأن الشيفرة القديمة والمخطط الجديد غير متوافقين. وقد ترفع عملية Rails القديمة العاملة أخطاءً أو تكتب صفوفاً لن يقبلها المخطط الجديد. كما يؤدي الإيقاف إلى تحرير الذاكرة التي تحتاج إليها عملية الترحيل. وهذا هو سبب طلب upstream إضافة swap.
يعرض docker compose images الـtag الذي تعمل به كل حاوية فعلياً. وهذا يكشف حالة تعديل الـtag ثم نسيان سحب الصورة.
لا تنتقل عبر عدة إصدارات دفعة واحدة. تنصح upstream تثبيتاً قديماً بالمرور عبر tags وسيطة، لأن عمليات الترحيل تُزال بعد دمجها في المخطط الأساسي. لذلك قد تصل قاعدة بيانات قديمة جداً إلى حالة لا يتوفر فيها مسار للترقية. انتقل إصداراً فرعياً واحداً في كل مرة، وشغّل خطوة التحضير بعد كل إصدار.
إذا بدأت Rails قبل تشغيل عملية الترحيل، ترفض تقديم الخدمة وتسجل ActiveRecord::PendingMigrationError: Migrations are pending. وعند ضبط restart: always، تدخل الحاوية في دورة إعادة تشغيل، ولذلك يعرض docker compose ps مدة تشغيل تُعاد تهيئتها كل بضع ثوانٍ. شغّل خطوة التحضير، وستُزال المشكلة.
تعني العودة إلى إصدار سابق إعادة tag القديم واستعادة التفريغ. لا يوجد مسار عكسي لعمليات الترحيل يمكن الاعتماد عليه، ولهذا تحتاج إلى الخطوة 2.
أوضاع الفشل والعبارات التي ستظهر
502 Bad Gateway من Traefik. طابق الموجّه الطلب، لكن الخدمة الخلفية لم تستجب. تحقّق من أن docker compose ps يعرض rails على أنه Up، ثم شغّل docker network inspect proxy وتأكد من ظهور حاوية rails في قائمة حاوياته. تكون الحاوية غير المرتبطة غير مرئية لـTraefik، لذلك يطابق الطلب موجّهاً ثم لا يصل إلى أي مكان.
تُحمَّل لوحة المعلومات، لكن الرسائل الجديدة تتطلب تحديث الصفحة. لم تمرر websocket إلى /cable، أو أن FRONTEND_URL لا يطابق العنوان الظاهر في شريط المتصفح. يعني عدم التطابق أن الصفحة تحاول فتح websocket إلى origin مختلف، ويحظر المتصفح ذلك.
FATAL: password authentication failed for user "postgres". تختلف كلمة المرور في .env عن كلمة المرور المضمّنة في volume بيانات Postgres. أصلح ذلك باستخدام ALTER USER داخل الحاوية قيد التشغيل، لأن تعديل .env مرة أخرى لن يغيّر قاعدة بيانات تمت تهيئتها مسبقاً.
NOAUTH Authentication required. يعمل Redis باستخدام --requirepass، لكن التطبيق اتصل من دون كلمة مرور، لذلك فإن REDIS_PASSWORD مفقود من .env أو لم يتم تطبيقه. اختبر ذلك مباشرة باستخدام docker compose exec redis redis-cli -a "$REDIS_PASSWORD" ping، وينبغي أن يعرض PONG.
تخرج الحاويات بالرمز 137. هذا هو SIGKILL، وفي الخوادم الصغيرة يكون سببه عادةً قاتل نفاد الذاكرة في النواة. أضف swap، وحدد حدود الذاكرة لكل خدمة، أو انتقل إلى خطة أكبر.
FAQ
كم يحتاج Chatwoot المستضاف ذاتياً من RAM على VPS؟
اعتباراً من August 2026، يطلب المشروع الأساسي 4 GB من RAM و4 أنوية CPU كحد أدنى، مع قدرة تصل إلى 10,000 محادثة يومياً، و8 GB مع 8 أنوية لما يصل إلى 20,000 محادثة. أضف 1 GB من swap على الأقل، لأن الترقية تشغّل عملية Rails ثانية لتطبيق عمليات الترحيل، وعندها تنفد الذاكرة من الخوادم الصغيرة. يبدأ VPS بسعة 2 GB ويعمل مع وكيلين لبضع محادثات، لكن Sidekiq وحده قد يتجاوز 1 GB تحت الحمل. لذلك توقّع إيقاف الحاويات مع exit code 137 خلال فترات الانشغال وأثناء الترقية.
لماذا لا تصل رسائل إعادة تعيين كلمة مرور Chatwoot أبداً؟
لأن إعدادات SMTP غير مهيّأة، ولذلك يحاول ActionMailer التسليم إلى localhost على المنفذ 25، ولا يوجد خادم بريد داخل الحاوية. تفشل المهمة في Sidekiq مع Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 25، بينما يعرض المتصفح رسالة نجاح. اضبط SMTP_ADDRESS وSMTP_PORT وSMTP_USERNAME وSMTP_PASSWORD وMAILER_SENDER_EMAIL في .env، ثم أعد تشغيل خدمتي rails وsidekiq، وراقب docker compose logs -f sidekiq أثناء طلب إعادة تعيين.
ما الذي أحتاج إلى نسخه احتياطياً لاستعادة Chatwoot؟
قاعدة بيانات Postgres، ووحدة تخزين Docker storage_data، وملف .env، وملفات compose. لا تكفي قاعدة البيانات وحدها، لأن الملفات المرفوعة موجودة في وحدة التخزين، بينما يحتفظ Postgres بمراجع إليها فقط. لذلك تؤدي استعادة قاعدة البيانات وحدها إلى ظهور محادثات ذات مرفقات معطّلة. يهم .env لأن تغيير SECRET_KEY_BASE يؤدي إلى تسجيل خروج جميع المستخدمين، كما أن اختلاف مفاتيح ACTIVE_RECORD_ENCRYPTION_* يجعل الأعمدة المشفّرة غير قابلة للقراءة.
كيف أرقّي Chatwoot من دون إتلاف قاعدة البيانات؟
أنشئ نسخة احتياطية، وغيّر وسم image في ملف compose، ثم شغّل docker compose pull وdocker compose down وdocker compose run --rm rails bundle exec rails db:chatwoot_prepare وdocker compose up -d. نفّذ السحب أولاً، لأن عمليات الترحيل يجب أن تعمل من image الجديدة. أوقف المكدس أولاً أيضاً، لأن تشغيل الكود القديم مع مخطط جديد يسبب أخطاء. في التثبيتات القديمة، انتقل إصداراً فرعياً واحداً في كل مرة، لأن عمليات الترحيل تُزال بعد دمجها في المخطط الأساسي.
هل يمكنني استخدام image postgres القياسية بدلاً من pgvector؟
لا. يفعّل مخطط Chatwoot الامتداد vector، ولذلك تفشل image postgres القياسية أثناء db:chatwoot_prepare مع ERROR: extension "vector" is not available، لأن ملف التحكم الخاص بالامتداد غير موجود في تلك image. احتفظ بـpgvector/pgvector:pg16 من ملف compose الأساسي، أو استخدم image أخرى تتضمن pgvector لإصدار Postgres الرئيسي لديك.