استضافة 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 المستضاف ذاتياً؟
اعتباراً من أكتوبر 2026، تطلب صفحة المتطلبات الرسمية 4 GB من RAM و4 أنوية CPU كحد أدنى، وتقدّر أن ذلك يكفي لما يصل إلى 10,000 محادثة يومياً. كما تذكر أن 8 GB و8 أنوية تكفي لما يصل إلى 20,000 محادثة يومياً. وتطلب أيضاً 1 GB على الأقل من swap، وتذكر السبب مباشرة: حتى لا تنفد ذاكرة الجهاز أثناء الترقية. خصص من 5 GB إلى 10 GB على القرص لـPostgres قبل احتساب الملفات المرفوعة.
والآن الجزء المباشر. سيُقلع Chatwoot على VPS بسعة 2 GB، وقد يبدو مستقراً مع وكيلين وصندوق وارد قليل النشاط. لكنه يفشل في حالتين. الأولى هي Sidekiq، إذ تقيسه الصفحة الرسمية بأكثر من 1 GB على خادم مزدحم. لذلك قد تؤدي دفعة من رسائل البريد أو مهمة تقرير إلى تجاوز الذاكرة المتاحة قبل أن تحصل Rails وPostgres وRedis على حصصها. والثانية هي الترقية، لأن db:chatwoot_prepare يقلع عملية Rails جديدة لتطبيق عمليات الترحيل، ويستهلك إقلاع Rails على هذه الصورة مئات الميغابايت قبل تنفيذ أي عمل مفيد.
لن تحصل أولاً على تحذير واضح. يرسل قاتل نفاد الذاكرة في kernel الإشارة SIGKILL إلى العملية التي يختارها، وعادةً ما تكون الأكبر، ثم يكتشف Docker توقف الحاوية ويعيد restart: always تشغيلها. يعرض docker compose ps --all بعد ذلك حاوية تستمر في إعادة التشغيل أو تبقى عند Exited (137)، حيث يعني 137 أنها قُتلت بالإشارة 9. إذا حذفت --all، فلن تُدرج الحاوية المتوقفة أصلاً. ولا يثبت رمز الخروج 137 وحده أن السبب هو الذاكرة، لأن docker kill يرسل الإشارة نفسها. لذلك أكّد السبب باستخدام sudo dmesg -T | grep -i "killed process"، الذي يذكر العملية التي اختارها kernel.
إذا كانت سعة 4 GB خارج ميزانيتك، فشغّل جهازاً بسعة 2 GB مع 2 GB من swap، وتقبّل أن أزمنة الاستجابة ستزداد سوءاً تحت الحمل. تعمل swap كاحتياطي عند اقتراب نفاد الذاكرة، لكنها ليست ضماناً. لذلك قد تؤدي دفعة كبيرة بما يكفي إلى قتل الحاوية. ومن المفيد في كل الأحوال تعيين حد أقصى ثابت للذاكرة لكل خدمة، حتى لا يتمكن العامل من إسقاط قاعدة البيانات معه. راجع حدود الذاكرة في Docker Compose.
عمليات رفع الملفات هي الجزء الذي ينمو من دون حد تحدده. تُحفظ كل لقطة شاشة يرفقها العميل في volume التخزين وتبقى فيه. لذلك راقب 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.18.0
env_file: .env
volumes:
- storage_data:/app/storageيعني latest أن docker compose pull التالي سيجلب لك كل ما نُشر في ذلك الصباح، وقد يكون إصداراً رئيسياً يتضمن عمليات ترحيل لم تقرأ عنها. عملياً، لا يمكن التراجع عن عمليات ترحيل Chatwoot، لذلك فإن الانتقال غير المقصود يعني الاستعادة من نسخة احتياطية، وليس التراجع عن التغيير. ثبّت الوسم وغيّره عن قصد. كان v4.18.0 هو الإصدار الحالي في أكتوبر 2026، وتوصي ملاحظات إصداره باستخدامه لإصلاحات الأمان؛ راجع صفحة الإصدارات لمعرفة الوسم الذي ينبغي أن تثبّته اليوم. وينطبق الحذر نفسه على كل ما يدير قاعدة بيانات، ولذلك يبدأ الاستضافة الذاتية لمتتبع تمارين openGym بتثبيت وسم git، رغم أن هذا التطبيق أصغر بكثير من التطبيق الحالي.
خدمة 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_ADDRESS، يبدّل Chatwoot إعداد ActionMailer إلى التسليم عبر sendmail. توثّق المنبع هذا الإعداد ضمن إعداد Postfix، وهو الإعداد الافتراضي منذ v2.12.0، وتثبّت تعليماته Postfix على الخادم باستخدام sudo apt install -y postfix. هذا الإعداد المستند إلى Docker لا يفعل ذلك، لذلك لا يوجد خادم بريد مُعدّ يمكن لآلية التسليم هذه تسليمه البريد. يُرسل البريد من مهمة تعمل في الخلفية، لذلك يظهر أي خطأ تنتجه آلية التسليم في سجل 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 للحد من الرسائل المزعجة، لذلك يكون relay على المنفذ 587 عادةً الخيار الوحيد الذي يمكنه الاتصال أصلاً. يشير SMTP_DOMAIN إلى النطاق الذي يعلنه خادمك أثناء محادثة SMTP، وترفض بعض خدمات relay عدم تطابقه.
طبّق الإعدادات وراقب العامل:
docker compose up -d rails sidekiq
docker compose logs -f sidekiqشغّل إعادة تعيين كلمة مرور من صفحة تسجيل الدخول. يظهر نجاح التسليم عند انتهاء مهمة mailer بصورة طبيعية في سجل Sidekiq. ويظهر الفشل على شكل فئة الاستثناء، ثم يعيد Sidekiq المحاولة مع تأخير متزايد بين المحاولات. لذلك ينتج relay المعطّل الخطأ نفسه كل بضع دقائق ولساعات.
تتكرر حالتا رفض، ولا تُعد أيٌّ منهما خطأً في Chatwoot. يعني 535 Authentication failed أن اسم المستخدم أو كلمة المرور غير صحيحين لهذا الـrelay، ويريد كثير من المزوّدين كلمة مرور تطبيق بدلاً من كلمة مرور الحساب. يعني 550 Sender address rejected أن MAILER_SENDER_EMAIL عنوان لا يسمح الـrelay بالإرسال منه، ولذلك يجب أن يكون صندوق بريد أو نطاقاً تحققت من ملكيته لدى المزوّد.
استقبال البريد الإلكتروني في محادثة مهمة منفصلة. يتطلب ذلك MAILER_INBOUND_EMAIL_DOMAIN وRAILS_INBOUND_EMAIL_SERVICE، إضافة إلى خادم بريد يسلّم الرسائل الواردة إلى Chatwoot. استئجار relay هو المسار الأسرع. وإذا كنت تفضّل إدارة مسار البريد بالكامل بنفسك، فتشرح إدارة خادم البريد الخاص بك باستخدام 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 عن كلمة المرور المضمّنة في وحدة تخزين بيانات Postgres. أصلح ذلك باستخدام ALTER USER داخل الحاوية قيد التشغيل، لأن تعديل .env مرة أخرى لن يغيّر قاعدة بيانات تمت تهيئتها مسبقاً.
NOAUTH Authentication required. يعمل Redis باستخدام --requirepass، لكن التطبيق اتصل من دون كلمة مرور، لذلك فإن REDIS_PASSWORD مفقود من .env أو لم يُلتقط. اختبر كلمة المرور التي يتوقعها Redis مباشرةً باستخدام docker compose exec redis sh -c 'redis-cli -a "$REDIS_PASSWORD" ping'، ومن المفترض أن يجيب بـPONG. علامات الاقتباس المفردة مهمة: فهي تجعل المتغير يتوسع داخل الحاوية التي تقرأ .env، بدلاً من توسعه في الصدفة الخاصة بك، حيث يكون فارغاً عادةً.
خروج الحاويات بالرمز 137. هذا يعني SIGKILL. في الخادم الصغير، يكون المرسل المعتاد هو قاتل العمليات بسبب نفاد ذاكرة النواة، لكن docker kill يرسل الإشارة نفسها، لذلك تحقّق من sudo dmesg -T | grep -i "killed process" قبل أن تلقي اللوم على الذاكرة. إذا كانت المشكلة في الذاكرة، فأضف swap، وحدد حدود الذاكرة لكل خدمة، أو انتقل إلى خطة أكبر.
FAQ
ما مقدار RAM الذي يحتاج إليه Chatwoot مستضاف ذاتياً على VPS؟
اعتباراً من October 2026، يطلب المشروع upstream مقدار 4 GB من RAM و4 أنوية CPU كحد أدنى، مع قدرة تصل إلى 10,000 محادثة يومياً، و8 GB مع 8 أنوية لما يصل إلى 20,000 محادثة. أضف 1 GB على الأقل من swap، لأن الترقية تشغّل عملية Rails ثانية لتطبيق migrations، وعندها تنفد الذاكرة في الخوادم الصغيرة. يعمل VPS بسعة 2 GB ويقلع بشكل طبيعي لعدد قليل من الوكلاء، لكن Sidekiq وحده قد يتجاوز 1 GB تحت الحمل. لذلك توقّع إيقاف containers برمز الخروج 137 خلال فترات الانشغال وأثناء الترقية.
لماذا لا تصل رسائل إعادة تعيين كلمة مرور Chatwoot مطلقاً؟
لأنه لم يتم ضبط SMTP_ADDRESS، لذلك يعود Chatwoot إلى التسليم عبر sendmail. ويفترض upstream وجود خادم Postfix مثبّت على المضيف، لكن إعداد Docker هذا لا يحتوي عليه. تعمل المهمة في Sidekiq، لذلك يظهر أي خطأ في التسليم في سجل Sidekiq، بينما يعرض المتصفح رسالة نجاح. اضبط 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 tag في ملف compose، ثم شغّل docker compose pull وdocker compose down وdocker compose run --rm rails bundle exec rails db:chatwoot_prepare وdocker compose up -d. نفّذ عملية pull أولاً، لأن migrations يجب أن تعمل من image الجديدة. أوقف stack أولاً، لأن تشغيل الكود القديم مع schema جديدة يسبب أخطاء. في التثبيتات القديمة، انتقل من minor version إلى التي تليها في كل مرة، لأن migrations تُحذف بعد دمجها في base schema.
هل يمكنني استخدام image postgres القياسية بدلاً من pgvector؟
لا. يفعّل schema الخاص بـChatwoot إضافة vector، لذلك تفشل image postgres القياسية أثناء db:chatwoot_prepare مع ERROR: extension "vector" is not available، لأن ملف التحكم الخاص بالإضافة غير موجود في تلك image. أبقِ pgvector/pgvector:pg16 من ملف compose المقدم من upstream، أو استخدم image أخرى توفر pgvector لإصدار Postgres major المستخدم لديك.