مشروع مصغر: بناء وتغليف تطبيق تفاعلي مع قاعدة بيانات داخل حاوية معزولة
مشروع مصغر: لماذا نغلف التطبيق وقاعدة البيانات داخل بيئة معزولة؟
في المشاريع الحديثة، لم يعد تشغيل التطبيق مباشرة على نظام التشغيل خياراً عملياً عند البحث عن الثبات وسهولة النقل. الفكرة الأساسية في Containerization هي عزل التطبيق مع اعتمادياته داخل بيئة متوقعة يمكن تشغيلها على أي خادم تقريباً بنفس السلوك.
هذا النموذج يعالج مشكلة اختلاف البيئات التي ناقشناها سابقاً في مقال مشكلة “الكود يعمل على جهازي فقط” وكيف يحلها Docker نهائياً؟. وعند إضافة قاعدة بيانات في حاوية مستقلة، نحصل على فصل معماري نظيف بين طبقة التطبيق وطبقة البيانات مع قابلية أعلى للصيانة والتوسع.
في هذا المشروع المصغر سنبني تطبيقاً تفاعلياً بسيطاً يعتمد على Node.js لحفظ الرسائل داخل PostgreSQL، ثم نغلف المكونين عبر Docker مع ملف تشغيل موحد يختصر الإعدادات ويجعل بيئة التطوير أقرب إلى بيئة الإنتاج.
المعمارية المقترحة للمشروع
المعمارية هنا صغيرة لكنها تمثل نمطاً حقيقياً يُستخدم في أنظمة أكبر. لدينا خدمة تطبيق تستقبل الطلبات من المتصفح، وخدمة قاعدة بيانات معزولة، وبينهما شبكة داخلية خاصة تنقل الاتصال دون كشف قاعدة البيانات مباشرة للعالم الخارجي.
- حاوية للتطبيق
app. - حاوية لقاعدة البيانات
db. - وحدة تخزين دائمة
Volumeلحفظ البيانات. - شبكة داخلية افتراضية تربط الخدمات معاً.
إذا كنت تحتاج أساساً متيناً قبل هذا التطبيق، فراجع مقال تثبيت Docker وإعداد بيئة العمل على Linux و Windows، ثم مقال أوامر Docker الأساسية للتحكم: تشغيل، إيقاف، فحص، وحذف الحاويات لفهم دورة حياة الحاويات عملياً.
هيكل الملفات وبناء التطبيق
سنفترض هيكلاً بسيطاً يحتوي على ملف تطبيق، تعريف الاعتماديات، وملف بناء الصورة. التطبيق يعرض نموذجاً لإرسال رسالة قصيرة ثم يحفظها في قاعدة البيانات ويعيد عرض الرسائل المخزنة. هذه الفكرة تكفي لفهم الاتصال بين الخدمات دون تعقيد واجهة المستخدم.
ملف التطبيق الأساسي
const express = require('express');
const { Pool } = require('pg');
const app = express();
app.use(express.urlencoded({ extended: true }));
const pool = new Pool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: 5432
});
app.get('/', async (req, res) => {
await pool.query(`
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL
)
`);
const result = await pool.query('SELECT * FROM messages ORDER BY id DESC');
const items = result.rows.map(row => `<li>${row.content}</li>`).join('');
res.send(`
<h1>Interactive Messages App</h1>
<form method="POST" action="/add">
<input name="content" placeholder="Type a message" required />
<button type="submit">Save</button>
</form>
<ul>${items}</ul>
`);
});
app.post('/add', async (req, res) => {
await pool.query('INSERT INTO messages(content) VALUES($1)', [req.body.content]);
res.redirect('/');
});
app.listen(3000, () => console.log('App running on port 3000'));
رغم أن الكود أعلاه ليس معقداً، إلا أنه يوضح نقطة هندسية مهمة: التطبيق لا يعرف عنوان IP ثابتاً لقاعدة البيانات، بل يعتمد على اسم الخدمة داخل الشبكة الداخلية، وهي ممارسة ممتازة عند الانتقال لاحقاً إلى Kubernetes أو بيئات السحابة.
ملف بناء الصورة
إذا كنت قد قرأت سابقاً كتابة أول Dockerfile: تحويل سكربت Python إلى صورة (Image) معزولة، فستلاحظ أن الفكرة نفسها تتكرر هنا ولكن مع خدمة ويب كاملة.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
يعتمد هذا الملف على صورة خفيفة من Alpine لتقليل الحجم، ويستفيد من ترتيب التعليمات لتحسين التخزين المؤقت أثناء إعادة البناء. عملياً، هذا يسرّع أنابيب CI/CD عندما تتكرر عمليات البناء عدة مرات يومياً.
تشغيل التطبيق وقاعدة البيانات عبر ملف موحد
بدلاً من تشغيل كل حاوية يدوياً، سنستخدم ملف تنسيق يربط الخدمات مع البيئة والشبكة والتخزين. هذا الأسلوب يقترب من مفهوم Declarative Infrastructure حيث تصبح البيئة قابلة للتكرار من خلال ملف واحد فقط.
version: "3.9"
services:
app:
build: .
container_name: interactive-app
ports:
- "3000:3000"
environment:
DB_HOST: db
DB_USER: appuser
DB_PASSWORD: securepass
DB_NAME: appdb
depends_on:
- db
db:
image: postgres:15-alpine
container_name: interactive-db
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: securepass
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
الميزة المهمة هنا أن خدمة db لا تحتاج إلى نشر منفذ خارجي إذا كان الوصول إليها مطلوباً فقط من التطبيق. هذا يخفف السطح المعرض للهجوم ويحسن العزل. كما أن استخدام التخزين الدائم (Docker Volumes): كيف نمنع ضياع قواعد البيانات عند توقف الحاوية؟ ضروري لأن حذف الحاوية لا يجب أن يعني حذف البيانات.
أوامر التشغيل والاختبار
docker compose up --build -d
docker compose ps
docker compose logs -f app
docker compose logs -f db
بعد التشغيل، افتح المتصفح على http://localhost:3000 وأضف عدة رسائل. ثم أوقف الحاويات وأعد تشغيلها للتحقق من بقاء البيانات. هذه خطوة اختبارية صغيرة لكنها تكشف مبكراً أي خلل في ربط التخزين أو المتغيرات البيئية أو صلاحيات القاعدة.
أفضل الممارسات الأمنية والتشغيلية
لا تضع كلمات المرور مباشرة داخل ملفات المستودع عند بناء مشروع فعلي. استخدم
Secretsأو متغيرات بيئية مُدارة من منصة النشر، وامنع رفع أي ملف يحتوي بيانات حساسة إلىGit.
وجود
depends_onلا يعني أن قاعدة البيانات أصبحت جاهزة للاستقبال فوراً. في البيئات الجادة أضف آليةhealthcheckأو سكربت انتظار لتفادي الأعطال عند الإقلاع الأول.
كذلك يُفضّل تشغيل التطبيق بمستخدم غير root، وتقليص الحزم المثبتة داخل الصورة، وفحصها بأدوات تحليل الثغرات قبل النشر. هذه تفاصيل قد تبدو صغيرة، لكنها فارقة جداً عند تحويل المشروع من تجربة تعليمية إلى خدمة تتلقى زيارات حقيقية.
كيف نربط المشروع بأنابيب CI/CD؟
بعد نجاح التشغيل محلياً، تأتي الخطوة الأهم في منهج DevOps، وهي أتمتة البناء والاختبار والنشر. إذا كنت جديداً على الفلسفة نفسها، فمقال ما هو DevOps؟ ولماذا تدفع الشركات ثروات لمهندسي الأتمتة السحابية؟ يقدم الإطار المفاهيمي الأشمل.
في خط أنابيب نموذجي عبر GitHub Actions أو Jenkins يمكن تنفيذ المراحل التالية:
- فحص الكود وتشغيل الاختبارات.
- بناء صورة التطبيق من ملف
Dockerfile. - وسم الصورة برقم إصدار واضح.
- رفع الصورة إلى
Registry. - نشرها إلى الخادم أو إلى عنقود
Kubernetes.
وعندما يكبر المشروع، يمكن إدارة البنية عبر Terraform لإنشاء الخوادم والشبكات، ثم استخدام Ansible لتهيئة الأنظمة. بهذه الطريقة لا يصبح المشروع مجرد تطبيق يعمل، بل جزءاً من منصة قابلة للقياس والتكرار والمراجعة.
الخلاصة
هذا المشروع المصغر يوضح مبدأً جوهرياً في الحوسبة السحابية: افصل المكونات، غلفها داخل حاويات، عرّف البيئة ككود، ثم اجعل تشغيلها قابلاً للتكرار. من خلال حاوية تطبيق وحاوية قاعدة بيانات مع تخزين دائم، تكون قد طبقت نسخة مصغرة من بنية إنتاج حقيقية.
وإذا أردت التوسع أكثر، انتقل بعد ذلك إلى تحسين الصورة، إضافة اختبارات، تفعيل reverse proxy، ثم نقل التشغيل إلى بيئة أوركسترا مثل Kubernetes. هنا فقط تبدأ القيمة الحقيقية للحاويات بالظهور على مستوى الاعتمادية والسرعة والانضباط التشغيلي.