نشر 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_DRIVER | database أو cookie | جلسات file تضيع عند كل إعادة تشغيل فيُسجَّل خروج المستخدمين |
CACHE_STORE | file | الكاش يمكن فقدانه، والأهم أن كاش قاعدة البيانات سبّب لنا خطأ (انظر قسم الأخطاء) |
QUEUE_CONNECTION | sync | لا يوجد worker منفصل في الخطة المجانية، فتُنفَّذ المهام داخل الطلب |
FILESYSTEM_DISK | s3 | الملفات المرفوعة تحتاج تخزيناً دائماً (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
- سجّل في Neon وأنشئ Project.
- اختر الـ region الأقرب لخدمة Render (مثلاً Frankfurt مع Frankfurt). كل طلب SQL يقطع الشبكة بين الخدمتين، وعدم تطابق المنطقة (مثلاً أمريكا مع أوروبا) يضيف عشرات الميلي ثانية لكل استعلام، وتتضاعف مع الصفحات الكثيرة الاستعلامات.
- من Connection details ستجد نوعين من الروابط:
- Pooled connection: المضيف فيه اللاحقة
-pooler. يمر عبر PgBouncer ويتحمل عدداً كبيراً من الاتصالات. للتطبيق أثناء التشغيل. - Direct connection: نفس المضيف بلا
-pooler. للترحيلات (migrations) حسب توصية Neon.
- Pooled connection: المضيف فيه اللاحقة
- انسخ الرابطين. شكلهما:
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 نفسه
- New ← PostgreSQL، اختر نفس الـ region لخدمة الويب، والخطة Free.
- بعد الإنشاء ستجد Internal Database URL (للخدمات داخل نفس الـ region، عبر شبكة خاصة وأسرع) وExternal Database URL (للاتصال من جهازك، ويتطلب SSL).
- ضع الـ Internal URL في
DB_URL. - تذكّر انتهاء صلاحية القاعدة المجانية بعد 30 يوماً، وأنها بلا نسخ احتياطية. خذ نسخة دورية يدوياً بـ
pg_dumpإن كانت البيانات تهمك.
سابعاً: إنشاء الخدمة على Render
- ارفع المشروع إلى GitHub (تأكد أن
.envغير مرفوع). - في Render: New ← Web Service واربط المستودع.
- Runtime = Docker، Instance Type = Free، واختر الـ region.
- تحت Advanced ضع Health Check Path =
/up. - أضف Environment Variables:
| المتغير | القيمة | ملاحظة |
|---|---|---|
APP_NAME | اسم تطبيقك | |
APP_ENV | production | |
APP_DEBUG | false | لا تتركه true أبداً في الإنتاج |
APP_KEY | ناتج php artisan key:generate --show | يبدأ بـ base64: ، سر |
APP_URL | https://اسم-الخدمة.onrender.com | ثم نطاقك المخصص لاحقاً |
DB_CONNECTION | pgsql | |
DB_URL | رابط Neon المجمّع (pooled) | سر |
DB_DIRECT_URL | رابط Neon المباشر | سر، للترحيلات |
LOG_CHANNEL | stderr | |
SESSION_DRIVER | database أو cookie | |
CACHE_STORE | file | |
QUEUE_CONNECTION | sync | |
FILESYSTEM_DISK | s3 | مع متغيرات AWS_* |
MAIL_MAILER | resend مثلاً | مع مفتاح المزود |
- اضغط 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
تاسعاً: أخطاء واجهناها في مشروع حقيقي وحلولها
| الخطأ | السبب | الحل |
|---|---|---|
ViteManifestNotFoundException | public/build/manifest.json غير موجود (مُستبعد من Git) | بناء الأصول داخل الـ Dockerfile (المرحلة 2) |
| Mixed Content (صفحة https تطلب أصولاً http) | Laravel لا يعرف أنه خلف proxy | trustProxies(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.
المصادر
- Render: Deploy for Free (حدود الخطة المجانية، السكون، القرص المؤقت، انتهاء Postgres)
- Render: Web Services (المنفذ الافتراضي 10000 والاستماع على 0.0.0.0)
- Render: Blueprint spec (ملف
render.yamlوحقولsyncوfromDatabase) - Neon: حدود الخطة المجانية
- Neon: اختيار نوع الاتصال (pooled / direct)
- Neon: Laravel migrations