نشر Laravel على Render مع Neon: دليل Docker خطوة بخطوة

دليل عملي لنشر تطبيق Laravel على Render بـ Docker مع قاعدة Neon المجانية، مع شرح كل سطر في الـ Dockerfile وأخطاء النشر الحقيقية وحلولها.

في هذا الدليل سننشر تطبيق Laravel على Render باستخدام Docker، ونربطه بقاعدة بيانات PostgreSQL مجانية على Neon (أو على Render نفسه كبديل)، مع شرح كل سطر في الـ Dockerfile ولماذا كُتب بهذه الطريقة. الأمثلة مأخوذة من مشروع حقيقي نشرناه بهذه الطريقة بالضبط، بما في ذلك الأخطاء التي واجهتنا وكيف حللناها.

ما الذي سنبنيه؟

المتصفح  ──HTTPS──▶  Render (يُنهي TLS ويمرّر الطلب إلى الحاوية)
                         │
                         ▼
              حاوية Docker  (PHP 8.3 + php artisan serve)
                         │
        ┌────────────────┼──────────────────┐
        ▼                ▼                  ▼
   Neon Postgres     S3 / R2 للملفات     Resend للبريد
   (البيانات)        (لأن قرص Render      (عبر HTTP API)
                      مؤقت)

الفكرة الأساسية: الحاوية على Render بلا حالة (stateless). كل شيء يجب حفظه (بيانات، ملفات مرفوعة، جلسات) يذهب خارج الحاوية، لأنها قد تُعاد أو تُوقَف في أي لحظة.

أولاً: ما الذي تعطيك إياه الخطة المجانية فعلاً؟

قبل أي خطوة، افهم القيود حتى لا تُفاجأ لاحقاً. من صفحة Render الرسمية للخطة المجانية:

  • السكون بعد الخمول: الخدمة المجانية تتوقف بعد 15 دقيقة بلا أي طلب وارد، ويحتاج إيقاظها نحو دقيقة (يعرض Render صفحة تحميل للزائر خلالها). أول زيارة بعد الخمول بطيئة، وهذا طبيعي.
  • 750 ساعة مجانية شهرياً لكل workspace. الخدمات النائمة لا تستهلك الساعات، وإذا انتهت الساعات تُعلَّق الخدمات المجانية حتى بداية الشهر التالي.
  • لا تخزين دائم: أي ملف تكتبه على قرص الحاوية (ملفات مرفوعة، SQLite، ملفات log) يضيع عند كل نشر أو إعادة تشغيل أو سكون. لذلك لن نعتمد على القرص أبداً.
  • قاعدة Postgres المجانية على Render تنتهي صلاحيتها بعد 30 يوماً من إنشائها، مع مهلة 14 يوماً للترقية قبل أن تُحذف مع بياناتها، وحجمها 1GB، وبلا نسخ احتياطية (التفاصيل). هذا هو السبب الرئيسي لاختيارنا Neon.

ثانياً: Neon أم Postgres على Render؟

Neon (مجاني)Postgres على Render (مجاني)
المدةلا يوجد تاريخ انتهاء معلن للخطة المجانية (تحقق من صفحة الأسعار)تنتهي بعد 30 يوماً
السعة0.5GB لكل مشروع1GB
الحوسبة100 CU-hour شهرياً، ويتوقف الـ compute بعد 5 دقائق خمول (قيمة ثابتة في الخطة المجانية)قاعدة واحدة فقط لكل workspace، وقد يعيد Render تشغيلها للصيانة
النسخ الاحتياطيحسب الخطة (راجع لوحة Neon)غير متوفر في الخطة المجانية
الاتصالعبر الإنترنت (يلزم SSL)شبكة داخلية خاصة داخل نفس الـ region

المصدر: Neon: حدود الخطة المجانية وRender.

توصيتي: استخدم Neon لأي مشروع تريده أن يبقى أكثر من شهر. استخدم Postgres على Render للتجارب القصيرة فقط، أو إذا كنت ستترقى لخطة مدفوعة لاحقاً. وللتنويه: تأخير الاستيقاظ في Neon (cold start) عادة أقل من ثانية، لكنه يضاف على أول طلب بعد 5 دقائق خمول.

ثالثاً: تجهيز تطبيق Laravel قبل النشر

هذه التعديلات ضرورية لأن Render يضع خدمتك خلف proxy ويشغّل حاوية مؤقتة.

1) الثقة بالـ Proxy وفرض HTTPS

Render ينهي اتصال TLS عند الـ proxy الخاص به ثم يرسل الطلب لحاويتك بـ HTTP عادي. إن لم تخبر Laravel بذلك فسيولّد روابط http:// لملفاتك (CSS وJS)، فيحجبها المتصفح بخطأ Mixed Content. وهذا بالضبط ما حصل معنا (الصفحة HTTPS وطلبات الأصول HTTP).

في bootstrap/app.php (Laravel 11 وما بعده):

->withMiddleware(function (Middleware $middleware): void {
    // Render يضع الطلب خلف proxy: نثق بترويسات X-Forwarded-*
    // حتى يرى Laravel الـ scheme (https) والـ host الأصليين.
    $middleware->trustProxies(at: '*');
})

وفي app/Providers/AppServiceProvider.php:

use Illuminate\Support\Facades\URL;

public function boot(): void
{
    if (app()->environment('production')) {
        URL::forceScheme('https');
    }
}

الأول يجعل Laravel يقرأ الـ scheme من الترويسات، والثاني ضمان إضافي لأن كل روابط التطبيق ستكون https في الإنتاج.

2) السجلات (Logs) إلى stderr

لأن القرص مؤقت، لا فائدة من storage/logs/laravel.log. اجعل السجلات تذهب إلى stderr فتظهر في تبويب Logs في Render:

LOG_CHANNEL=stderr

3) مسار فحص الصحة (Health check)

Laravel 11+ يوفّر /up تلقائياً (معرَّف في withRouting(health: '/up')). سنعطيه لـ Render حتى يعرف أن الخدمة جاهزة قبل أن يوجّه لها الزيارات.

4) الجلسات والكاش والـ Queue

كل هذه لا يجوز أن تعتمد على القرص المحلي:

الإعدادالقيمة المقترحةالسبب
SESSION_DRIVERdatabase أو cookieجلسات file تضيع عند كل إعادة تشغيل فيُسجَّل خروج المستخدمين
CACHE_STOREfileالكاش يمكن فقدانه، والأهم أن كاش قاعدة البيانات سبّب لنا خطأ (انظر قسم الأخطاء)
QUEUE_CONNECTIONsyncلا يوجد worker منفصل في الخطة المجانية، فتُنفَّذ المهام داخل الطلب
FILESYSTEM_DISKs3الملفات المرفوعة تحتاج تخزيناً دائماً (S3 أو Cloudflare R2 أو أي خدمة متوافقة)

إذا كان تطبيقك يرفع صوراً (مثل Spatie Media Library) فاضبط القرصين local وpublic على S3، وإلا ستختفي الصور عند أول إعادة نشر.

5) البريد

اعتمد على مزود بريد عبر HTTP API (مثل Resend) بدل SMTP. وتأكد أن نطاق الإرسال موثّق (SPF وDKIM) عند المزود.

رابعاً: شرح الـ Dockerfile سطراً بسطر

هذا الـ Dockerfile مبني على ما شغّلناه فعلاً على Render، مع شرح السبب خلف كل قرار. اقرأه كاملاً ثم نفصّله.

ملاحظة: النسخة التي نشغّلها فعلاً في مشروعنا تنتهي بسطر CMD واحد يضع الأوامر مباشرة (ستجده في قسم "إن واجهتك مشكلة" أدناه). استخدمنا هنا ملف entrypoint.sh لأنه أوضح للشرح وأسهل في الصيانة، ولأن الخيارين يؤديان العمل نفسه.

# ───── المرحلة 1: تثبيت حزم Composer ─────
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader \
    --no-interaction --prefer-dist --ignore-platform-reqs

# ───── المرحلة 2: بناء الأصول (Vite) ─────
FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY vite.config.js postcss.config.js tailwind.config.js ./
COPY resources ./resources
COPY public ./public
COPY app ./app
COPY --from=vendor /app/vendor ./vendor
RUN npm run build

# ───── المرحلة 3: الصورة النهائية ─────
FROM php:8.3-cli-alpine

RUN apk add --no-cache \
        postgresql-dev libzip-dev libpng-dev oniguruma-dev \
        icu-dev file-dev curl-dev \
    && docker-php-ext-install pdo pdo_mysql pdo_pgsql mbstring zip \
        bcmath intl gd exif fileinfo curl

RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
    && php composer-setup.php --install-dir=/usr/bin --filename=composer \
    && php -r "unlink('composer-setup.php');"

WORKDIR /var/www/html

COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-interaction \
    --prefer-dist --optimize-autoloader

COPY . .
COPY --from=assets /app/public/build ./public/build
RUN composer dump-autoload --optimize --no-dev \
    && chown -R www-data:www-data storage bootstrap/cache

COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

EXPOSE 10000
CMD ["entrypoint.sh"]

لماذا ثلاث مراحل (Multi-stage build)؟

الصورة النهائية يجب أن تحتوي PHP وكودك فقط، بدون Node ولا ملفات node_modules (مئات الميغابايت)، ولا أدوات البناء. نبني الأصول في مرحلة مؤقتة ثم ننسخ ناتجها فقط (public/build). النتيجة: صورة أصغر، نشر أسرع، وسطح هجوم أقل.

المرحلة 1: composer:2 AS vendor

  • لماذا مرحلة منفصلة لـ Composer؟ لأن مرحلة بناء الأصول (Vite) في تطبيقات Filament تحتاج ملفات من vendor/ (ملفات CSS التابعة لـ Filament تُستورد من داخل vendor). فنثبّت الحزم أولاً ثم نمرّرها لمرحلة الأصول.
  • COPY composer.json composer.lock وحدهما أولاً: هذه خدعة تخزين الطبقات (layer caching). Docker يعيد استخدام الطبقة المخزنة ما دام الملفان لم يتغيرا، فلا يعيد تنزيل كل الحزم مع كل تعديل بسيط في الكود.
  • --no-dev: لا نحتاج حزم الاختبار والتطوير في الإنتاج.
  • --no-scripts --no-autoloader: السكربتات (مثل package:discover) تحتاج الكود الكامل، وهو غير منسوخ بعد. نولّد الـ autoloader لاحقاً.
  • --ignore-platform-reqs: هذه المرحلة تحتاج الملفات فقط ولا تشغّل PHP، فلا داعي لأن تعترض على امتدادات غير مثبتة في صورة composer. انتبه: هذا الخيار يخفي عدم التوافق في إصدار PHP، لذلك نتحقق منه في المرحلة النهائية (التي لا تستخدمه).

المرحلة 2: node:22-alpine AS assets

  • npm ci بدل npm install: يثبّت الإصدارات المقفلة في package-lock.json بالضبط ويفشل إن لم تتطابق، فالبناء قابل للتكرار.
  • ننسخ ما يحتاجه Vite فقط (resources وpublic وapp وملفات الإعداد) بدل COPY . .، حتى لا تُبطل أي تعديلات غير ذات صلة كاش هذه المرحلة. (app ضرورية لأن Tailwind يمسح ملفات PHP بحثاً عن أسماء الـ classes.)
  • public/build/manifest.json يتولّد هنا. هذا الملف مُدرج في .gitignore عادةً، فلا يوجد في المستودع. وبدونه تحصل على الخطأ الشهير ViteManifestNotFoundException: Vite manifest not found، وهو أول خطأ واجهناه فعلاً. الحل هو بناء الأصول داخل الـ Dockerfile.

المرحلة 3: php:8.3-cli-alpine

  • لماذا cli وليس fpm؟ سنشغّل الخادم المدمج عبر php artisan serve. هو عملية واحدة بسيطة تكفي لحركة مرور خفيفة وللخطة المجانية. للإنتاج الثقيل الأفضل nginx + php-fpm أو FrankenPHP/Octane، لكنهما يضيفان تعقيداً (عدة عمليات وإعدادات) لا يحتاجه مشروع صغير.
  • alpine: أصغر حجماً بكثير من Debian.
  • أي إصدار PHP؟ يجب أن يتوافق مع composer.lock. هذه مشكلة واقعية واجهتنا: ملف القفل كان مولداً على جهاز تطوير يعمل بـ PHP 8.4 وفيه حزم Symfony تتطلب PHP 8.4.1 أو أحدث، فرفض composer install داخل صورة 8.3. الحلان: إما رفع الصورة إلى php:8.4-cli-alpine، أو تثبيت composer config platform.php 8.3 محلياً وإعادة توليد القفل. القاعدة: إصدار صورة Docker = إصدار PHP الذي ولّدت عليه composer.lock.
  • apk add ... -dev: الحزم ذات اللاحقة -dev هي ملفات الـ headers اللازمة لتجميع إضافات PHP. مثلاً postgresql-dev لـ pdo_pgsql وlibzip-dev لـ zip وicu-dev لـ intl.
  • docker-php-ext-install: يجمّع الإضافات التي يحتاجها Laravel وحزمك.
    • pdo_pgsql لـ Neon، وpdo_mysql إن أردت دعم MySQL أيضاً.
    • exif وfileinfo مطلوبتان لحزمة رفع الملفات (Spatie Media Library وSpatie Image). بدونهما يفشل فحص المنصة في Composer.
    • gd للصور، intl للتدويل (Filament)، bcmath وmbstring وzip شائعة.
    • أما tokenizer وctype وopenssl وdom فهي مدمجة في الصورة الرسمية أصلاً.
  • تثبيت Composer بـ php composer-setup.php: سكربت واحد صغير بدل سحب صورة Composer كاملة في المرحلة النهائية.
  • WORKDIR /var/www/html: المجلد الذي يعمل منه التطبيق. كل الأوامر اللاحقة نسبية له.
  • نسخ composer ثم COPY . .: نفس حيلة الكاش. تثبيت الحزم (الثقيل) في طبقة منفصلة لا تتأثر بتغيير ملفات التطبيق.
  • --optimize-autoloader: يبني خريطة classes جاهزة فيُسرّع الإقلاع.
  • COPY --from=assets ...public/build: نأخذ الأصول المبنية فقط من المرحلة 2.
  • composer dump-autoload --optimize --no-dev بعد نسخ الكود: لأننا ثبتنا الحزم بـ --no-scripts قبل وجود الكود، نحتاج توليد الـ autoloader النهائي الذي يعرف classes تطبيقك.
  • chown ... storage bootstrap/cache: هذان المجلدان يجب أن يكونا قابلين للكتابة (كاش الـ views والجلسات والـ logs). بدونهما تحصل على أخطاء صلاحيات.
  • EXPOSE 10000: توثيق فقط. Render يمرر المنفذ في متغير PORT (القيمة الافتراضية 10000 حسب الوثائق)، ويجب أن يستمع التطبيق على 0.0.0.0.

ملف docker/entrypoint.sh

أنشئ المجلد docker/ في جذر المشروع وضع فيه الملف entrypoint.sh بهذا المحتوى:

#!/bin/sh
set -e

# نُنشئ الكاش هنا لا أثناء البناء، لأن متغيرات البيئة
# (DB_URL, APP_KEY...) لا تتوفر إلا وقت التشغيل.
php artisan config:cache
php artisan route:cache
php artisan view:cache

# ترحيل قاعدة البيانات عند كل إقلاع (آمن للتكرار).
# مع Neon نفضّل الرابط المباشر (غير الـ pooler) للترحيل.
if [ -n "$DB_DIRECT_URL" ]; then
    php artisan migrate --force --database=pgsql_direct
else
    php artisan migrate --force
fi

# exec: يستبدل الـ shell بعملية PHP فتصلها إشارات الإيقاف (SIGTERM) مباشرة.
exec php artisan serve --host=0.0.0.0 --port="${PORT:-10000}"

لماذا نستخدم ملف entrypoint أصلاً؟

الحاوية تحتاج أوامر تُنفَّذ وقت التشغيل لا وقت البناء، ومكانها الطبيعي هو أمر البدء (CMD/ENTRYPOINT). الملف يجمعها في مكان واحد منظم بدل سطر طويل داخل Dockerfile:

  • وقت البناء مقابل وقت التشغيل: أثناء docker build لا تتوفر أسرارك (APP_KEY وDB_URL)، لأن Render يمررها كمتغيرات بيئة للحاوية عند تشغيلها فقط. لذلك لا يصح تشغيل config:cache ولا migrate داخل RUN، بل عند الإقلاع.
  • ترتيب مضمون: نبني الكاش، ثم نرحّل القاعدة، ثم نشغّل الخادم، وإن فشلت خطوة يتوقف كل شيء بفضل set -e فلا يعمل التطبيق على قاعدة غير جاهزة.
  • منطق شرطي: فحص وجود DB_DIRECT_URL (لـ Neon) أسهل وأوضح داخل سكربت منه داخل سطر CMD.
  • سهولة القراءة والتعديل: إضافة أمر جديد (مثل storage:link أو db:seed) سطر واحد في الملف.

شرح القرارات:

  • config:cache عند الإقلاع لا أثناء docker build: لو شغّلناه أثناء البناء لخبز (bake) قيماً فارغة أو خاطئة في الكاش.
  • route:cache: يفشل إن كان لديك routes بـ Closure. حوّلها لـ controllers أو احذف هذا السطر.
  • migrate --force: في بيئة production يطلب Laravel تأكيداً تفاعلياً، ولا يوجد TTY في الحاوية، فيُلغى الأمر بصمت دون --force.
  • لماذا الترحيل داخل الإقلاع؟ لأن الخطة المجانية لا تتيح (حسب ما وجدنا وقت الكتابة) خطوة Pre-Deploy منفصلة، فهذا أبسط مكان. يعيبه أنه يعمل مع كل إيقاظ من السكون (ولا يفعل شيئاً إن لم توجد ترحيلات جديدة)، وأن ترحيلاً فاشلاً يعني حاوية لا تقلع. تحقق من صفحة الخطط في Render إن تغيّر ذلك. وإن كنت تفضّل تشغيل الترحيل يدوياً فاحذف هذه الخطوة.
  • exec: بدونها يبقى الـ shell هو العملية الرئيسية ولا تصل إشارة الإيقاف إلى PHP.

إن واجهتك مشكلة: ضع الأوامر مباشرة في الـ Dockerfile

ملف الـ entrypoint خارجي عن الـ Dockerfile، فله مشاكل لا توجد في السطر المباشر. إن ظهر عندك أحد الأعراض التالية:

العرض في Logsالسبب
exec format error أو /bin/sh^M: bad interpreter أو not foundالملف محفوظ بنهايات أسطر Windows (CRLF) فلا يفهمه Linux
permission deniedنسيت chmod +x (أو لم يُنسخ الملف)
entrypoint.sh: not foundالمسار خطأ أو استُبعد docker/ في .dockerignore

فالحل الأبسط حذف الملف وكتابة الأوامر مباشرة في آخر الـ Dockerfile. احذف هذه الأسطر الثلاثة:

COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
CMD ["entrypoint.sh"]

واستبدلها بسطر CMD واحد (وهو ما نستخدمه فعلاً في مشروعنا):

CMD sh -c "php artisan config:cache && php artisan route:cache && php artisan view:cache && php artisan migrate --force ${DB_DIRECT_URL:+--database=pgsql_direct} && exec php artisan serve --host=0.0.0.0 --port=${PORT:-10000}"

شرح هذا السطر:

  • sh -c "...": مطلوب لأن صيغة CMD المباشرة (بدون أقواس مربعة) لا تفهم && ولا ${PORT} إلا عبر shell. بدونه لا يتوسّع متغير PORT.
  • && بين الأوامر: ينفّذ كل أمر بعد نجاح سابقه فقط، وهو يؤدي دور set -e في السكربت: إن فشل migrate لا يعمل الخادم.
  • ${DB_DIRECT_URL:+--database=pgsql_direct}: يتحول إلى --database=pgsql_direct فقط إذا كان المتغير DB_DIRECT_URL معرّفاً، وإلا يختفي. هذا بديل الـ if/else في السكربت. إن كنت لا تستخدم رابطاً مباشراً (Postgres على Render مثلاً) فاحذفه.
  • exec قبل php artisan serve: لنفس سبب السكربت: تصل إشارات الإيقاف إلى PHP مباشرة.

قاعدة عامة: ابدأ بالسطر المباشر في CMD لأنه أقل أجزاءً قد تتعطل. وانتقل لملف entrypoint عندما تكبر الخطوات أو تحتاج منطقاً شرطياً معقداً. وإن استخدمت ملفاً فاحفظه بنهايات أسطر LF (في VS Code: أسفل النافذة، غيّر CRLF إلى LF) أو ضع * text=auto eol=lf في .gitattributes.

ملف .dockerignore

مهم لتسريع البناء ولأمنك:

.git
.env
node_modules
vendor
storage/logs/*
storage/framework/cache/*
storage/framework/views/*
public/build
tests

استبعاد .env يمنع تسرب أسرارك داخل الصورة، واستبعاد vendor وnode_modules المحليين يمنع تعارضها مع ما يُبنى داخل الحاوية.

خامساً: إنشاء قاعدة البيانات على Neon

  1. سجّل في Neon وأنشئ Project.
  2. اختر الـ region الأقرب لخدمة Render (مثلاً Frankfurt مع Frankfurt). كل طلب SQL يقطع الشبكة بين الخدمتين، وعدم تطابق المنطقة (مثلاً أمريكا مع أوروبا) يضيف عشرات الميلي ثانية لكل استعلام، وتتضاعف مع الصفحات الكثيرة الاستعلامات.
  3. من Connection details ستجد نوعين من الروابط:
    • Pooled connection: المضيف فيه اللاحقة -pooler. يمر عبر PgBouncer ويتحمل عدداً كبيراً من الاتصالات. للتطبيق أثناء التشغيل.
    • Direct connection: نفس المضيف بلا -pooler. للترحيلات (migrations) حسب توصية Neon.
  4. انسخ الرابطين. شكلهما:
postgresql://USER:PASSWORD@ep-xxxx-pooler.eu-central-1.aws.neon.tech/neondb?sslmode=require
postgresql://USER:PASSWORD@ep-xxxx.eu-central-1.aws.neon.tech/neondb?sslmode=require

الرابط من لوحة Neon قد يتضمن أيضاً channel_binding=require. إن واجهت خطأ اتصال غريباً مع مكتبة libpq القديمة فجرّب حذفه والإبقاء على sslmode=require.

إعداد Laravel

في .env (أو متغيرات Render):

DB_CONNECTION=pgsql
DB_URL=postgresql://USER:PASSWORD@ep-xxxx-pooler....neon.tech/neondb?sslmode=require
DB_DIRECT_URL=postgresql://USER:PASSWORD@ep-xxxx....neon.tech/neondb?sslmode=require

Laravel يفكّ DB_URL تلقائياً إلى host وport وdatabase وusername وpassword (المخطط postgresql:// يُعامل كـ pgsql). لا تضع معه DB_HOST وDB_PORT حتى لا تلتبس عليك الأمور.

ولاتصال الترحيلات المباشر أضف في config/database.php داخل connections:

'pgsql_direct' => [
    'driver' => 'pgsql',
    'url' => env('DB_DIRECT_URL'),
    'charset' => 'utf8',
    'prefix' => '',
    'prefix_indexes' => true,
    'search_path' => 'public',
    'sslmode' => 'require',
],

وبما أن Neon يوقف الـ compute بعد 5 دقائق خمول، فأول استعلام بعدها أبطأ قليلاً. لا تعدّه عطلاً.

سادساً: البديل — Postgres على Render نفسه

  1. New ← PostgreSQL، اختر نفس الـ region لخدمة الويب، والخطة Free.
  2. بعد الإنشاء ستجد Internal Database URL (للخدمات داخل نفس الـ region، عبر شبكة خاصة وأسرع) وExternal Database URL (للاتصال من جهازك، ويتطلب SSL).
  3. ضع الـ Internal URL في DB_URL.
  4. تذكّر انتهاء صلاحية القاعدة المجانية بعد 30 يوماً، وأنها بلا نسخ احتياطية. خذ نسخة دورية يدوياً بـ pg_dump إن كانت البيانات تهمك.

سابعاً: إنشاء الخدمة على Render

  1. ارفع المشروع إلى GitHub (تأكد أن .env غير مرفوع).
  2. في Render: New ← Web Service واربط المستودع.
  3. Runtime = Docker، Instance Type = Free، واختر الـ region.
  4. تحت Advanced ضع Health Check Path = /up.
  5. أضف Environment Variables:
المتغيرالقيمةملاحظة
APP_NAMEاسم تطبيقك
APP_ENVproduction
APP_DEBUGfalseلا تتركه true أبداً في الإنتاج
APP_KEYناتج php artisan key:generate --showيبدأ بـ base64: ، سر
APP_URLhttps://اسم-الخدمة.onrender.comثم نطاقك المخصص لاحقاً
DB_CONNECTIONpgsql
DB_URLرابط Neon المجمّع (pooled)سر
DB_DIRECT_URLرابط Neon المباشرسر، للترحيلات
LOG_CHANNELstderr
SESSION_DRIVERdatabase أو cookie
CACHE_STOREfile
QUEUE_CONNECTIONsync
FILESYSTEM_DISKs3مع متغيرات AWS_*
MAIL_MAILERresend مثلاًمع مفتاح المزود
  1. اضغط Create Web Service. أول بناء يستغرق عدة دقائق (تثبيت Composer وNode وتجميع الإضافات). تابع Logs.

للتحقق: افتح https://اسم-الخدمة.onrender.com/up فيجب أن ترى صفحة نجاح. ثم افتح الصفحة الرئيسية.

ثامناً: Blueprint — إعداد كل شيء بملف

يمكنك وصف الخدمة في ملف بدل النقر في اللوحة. اسم الملف الافتراضي حسب وثائق Render هو render.yaml في جذر المستودع (الوثائق لا تذكر render.yml، فاستخدم .yaml ما لم تعيد تسميته أثناء الإعداد):

services:
  - type: web
    name: my-laravel
    runtime: docker
    plan: free
    healthCheckPath: /up
    envVars:
      - key: APP_ENV
        value: production
      - key: APP_DEBUG
        value: "false"
      - key: LOG_CHANNEL
        value: stderr
      - key: CACHE_STORE
        value: file
      - key: QUEUE_CONNECTION
        value: sync
      - key: DB_CONNECTION
        value: pgsql
      # الأسرار: يُسأل عنها مرة واحدة عند إنشاء الـ Blueprint
      - key: APP_KEY
        sync: false
      - key: DB_URL
        sync: false
      - key: DB_DIRECT_URL
        sync: false

sync: false تعني: لا تضع القيمة في المستودع، سيطلبها Render منك عند الإنشاء. هكذا تبقى الأسرار خارج Git. وإن استخدمت Postgres على Render فاربطه بـ fromDatabase بدل sync: false:

      - key: DB_URL
        fromDatabase:
          name: my-db
          property: connectionString

databases:
  - name: my-db
    plan: free

تاسعاً: أخطاء واجهناها في مشروع حقيقي وحلولها

الخطأالسببالحل
ViteManifestNotFoundExceptionpublic/build/manifest.json غير موجود (مُستبعد من Git)بناء الأصول داخل الـ Dockerfile (المرحلة 2)
Mixed Content (صفحة https تطلب أصولاً http)Laravel لا يعرف أنه خلف proxytrustProxies(at: '*') وURL::forceScheme('https')
composer.lock يطلب PHP أعلى من الصورةقفل مولّد على PHP 8.4 والصورة 8.3طابق إصدار الصورة مع إصدار PHP الذي ولّد القفل
column "title" cannot be cast automatically to type jsonترحيل يغيّر عموداً نصياً إلى json وهذا يقبله MySQL ولا يقبله Postgresاستخدم USING column::json (DB::statement('ALTER TABLE ... USING ...')) أو أنشئ عموداً جديداً وانسخ
SQLSTATE[25P02] current transaction is abortedكاش قاعدة البيانات (CACHE_STORE=database) على Neon المجمّع، فعملية increment داخل transaction تفشلCACHE_STORE=file
could not find driverالإضافة pdo_pgsql أو pdo_mysql غير مثبتة في الصورةأضفها في docker-php-ext-install (وفي apk add الـ -dev المقابل)
foreign_key_checks غير معروف على Postgresأمر خاص بـ MySQL داخل ترحيل أو seederاستبدله بـ Schema::disableForeignKeyConstraints() المتوافق مع كل المحركات
صور مرفوعة تختفي بعد النشرالقرص مؤقتFILESYSTEM_DISK=s3
أول زيارة بطيئة جداًالخدمة نائمة (15 دقيقة خمول)طبيعي في الخطة المجانية. للتجنب: خطة مدفوعة

درس عام من الأخطاء السابقة: اكتب كودك متوافقاً مع أكثر من محرك قاعدة بيانات. أمثلة التعارض بين MySQL وPostgres: دالة LIKE (حساسية الأحرف)، وتحويل أنواع الأعمدة، وإيقاف فحص المفاتيح الخارجية، وفرز القيم الفارغة.

عاشراً: ملاحظات تشغيلية

  • لا تحاول إبقاء الخدمة مستيقظة بـ ping دوري. يعتبره Render مخالفاً لروح الخطة المجانية (نقاش في منتدى Render)، كما أنه يستهلك ساعاتك الـ 750. إن احتجت جاهزية دائمة فانتقل لخطة مدفوعة.
  • نطاق مخصص: من إعدادات الخدمة ← Custom Domains، وأنشئ سجل CNAME عند مزود النطاق، ثم حدّث APP_URL.
  • الأمان: APP_DEBUG=false، وأسرارك في متغيرات البيئة لا في الكود، ولا تفتح لوحة الإدارة لأي حساب مسجّل (اشترط صلاحية مدير صريحة)، وعطّل التسجيل العام إن لم تكن بحاجة له.
  • أدوات التشخيص: Laravel Telescope وLog Viewer مفيدان، لكن قيّدهما بمدير النظام فقط وبقاعدة بيانات تتحمل الكتابة. تذكر أن قرص Render مؤقت فسجلات الملفات لا تبقى.
  • المهام المجدولة (scheduler): لا توجد عملية خلفية في الخطة المجانية. إن احتجت schedule:run فاستخدم خدمة Cron خارجية تستدعي مساراً محمياً، أو انتقل لخطة تدعم Cron Jobs.

الخلاصة

  • Render + Docker يعطيك نشراً قابلاً للتكرار، وNeon يعطيك Postgres مجانياً بلا تاريخ انتهاء.
  • الصورة تُبنى على 3 مراحل: Composer ثم Vite ثم PHP، فتبقى صغيرة وسريعة.
  • كل ما هو دائم (بيانات، ملفات، جلسات) يخرج من الحاوية، لأن الخدمة المجانية تنام وتُعاد.
  • أخطاء النشر الشائعة قابلة للتوقع: manifest وproxy وإصدار PHP وفروق Postgres عن MySQL.

في المقال التالي من هذه السلسلة نشرح البديل الأبسط والأقل قدرة: استضافة Laravel على InfinityFree مع MySQL.

المصادر