إنشاء Docker Image باستخدام Dockerfile
السلام عليكم ورحمة الله وبركاته
يمكنك متابعة السلسلة بالترتيب أو الانتقال مباشرة إلى أي مقال:
المقدمة
في المقالات السابقة تعرفنا على الـ Docker وكيفية التعامل مع الـ Images والـ Containers
وتعلمنا كيف نحمل Images جاهزة من الـ Docker Hub ونشغلها على جهازنا
مثل Alpine Linux و Nginx وغيرها من الـ Images الجاهزة
لكن السؤال الآن هو ماذا لو أردنا إنشاء Image خاصة بنا تناسب احتياجات تطبيقنا ؟
هنا يأتي دور الـ Dockerfile الذي يتيح لنا بناء Images مخصصة تحتوي على كل ما يحتاجه تطبيقنا
وقد تستخدم هذه الـ Images على المستوى الشخصي أو في الشركات أو يمكنك نشرها على الـ Docker Hub ليستخدمها الآخرون
معظم الحالات تكون بالطبع إنشاء Dockerfile خاص بنا لبناء Image تناسب تطبيقنا على مستوى الشخصي أو في المشاريع التي نعمل عليها
في هذه المقالة سنتعرف على الـ Dockerfile بشكل عملي
وسنتعلم كيف نكتبه خطوة بخطوة مع أمثلة عملية لإنشاء Image يقوم باستضافة موقع باستخدام Nginx
ما هو الـ Dockerfile ؟
الـ Dockerfile هو ملف نصي بسيط يحتوي على مجموعة من التعليمات
هذه التعليمات تخبر الـ Docker كيف يقوم ببناء Image خاصة بتطبيقنا
كل سطر في الـ Dockerfile يمثل خطوة من خطوات بناء الـ Image
وكل خطوة تضيف طبقة جديدة على الـ Image
هذه الطبقات تسمى بـ Image Layers وهى طبقات تتراكم فوق بعضها لتكوين الـ Image النهائية
كل طبقة كما قلنا تمثل خطوة أوأمر يعين لبناء الـ Image
ومفهوم الـ Image Layers مهم جدًا وسنذكره كثيرًا لأنه يؤثر على سرعة بناء الـ Image وحجمها النهائي
بحيث أن اختلاف ترتيب الأوامر في الـ Dockerfile وتعديلها حتى لو تعدلت خطوة واحدة فقط
قد يؤثر بشكل كبير على سرعة البناء وحجم الـ Image النهائية
تخيل أنك قمت ببناء موقع يتكون من HTML و CSS و JavaScript
وتريد تشغيله ورؤيته داخل بيئة تحاكي موقع الاستضافة الحقيقية على الإنترنت
عادةً ستستخدم Nginx أو Apache لاستضافة الموقع على جهازك
لذا قد تفكر بإنشاء Container يحتوي على Nginx ويقوم باستضافة موقعك
ثم ستحاول الدخول إلى هذا الـ Container ونسخ ملفات موقعك يدويًا من جهازك إلى داخل الـ Container بطريقة ما في مجلد /usr/share/nginx/html/ الذي يستضيف فيه Nginx الملفات الثابتة
وكل مرة تقوم بإنشاء Container جديد لنفس الموقع ستقوم بنفس الخطوات يدويًا
هناك حلول كثيرة لهذه المشكلة بالطبع عن طريق استخدام الـ Volumes أو الـ Bind Mounts
لكنني سأحاول استغلال المثال لأشرح الـ Dockerfile بطريقة عملية لك
بدلًا من تكرار هذه الخطوات يدويًا في كل مرة
نكتبها مرة واحدة في ملف الـ Dockerfile
ثم نستخدم هذا الملف لبناء Image تحتوي على موقعنا جاهز للعمل داخل Nginx
إنشاء أول Dockerfile
دعنا نبدأ بمثال بسيط جدًا
سننشئ Dockerfile لموقع بسيط يعرض صفحة Welcome to eltabarani.com
أولًا لننشئ مجلد جديد لمشروعنا:
> mkdir static-site-img
> cd static-site-img
الآن لننشئ ملف index.html بسيط داخل المجلد static-site-img:
<!DOCTYPE html>
<html>
<head>
<title>Tabarani</title>
</head>
<body>
<h1>Welcome to eltabarani.com</h1>
</body>
</html>
لنتخيل جميعًا أن ملف index.html هذا هو موقعنا الرائع الذي نريد استضافته باستخدام Nginx داخل Docker
الآن لننشئ ملف Dockerfile في نفس المجلد
لاحظ أن اسم الملف يجب أن يكون Dockerfile بالضبط بدون أي امتداد
ونكتب التالي داخل ملف الـ Dockerfile:
FROM nginx:alpine
COPY ./index.html /usr/share/nginx/html/
هذا هو أبسط Dockerfile يمكننا كتابته
دعنا نشرح كل سطر فيه بالتفصيل
أولًا لدينا أمر FROM وهو يحدد الـ Base Image التي سنبني عليها
بحيث أن كل Image في الـ Dockerfile تبدأ من Base Image معينة
نحن هنا نستخدم nginx:alpine كـ Base Image للـ Image التي سننشئها
FROM nginx:alpine
الـ FROM تعد أو أمر نكتبه في Dockerfile
هكذا نحن نبدأ من Image جاهزة تحتوي على Nginx الخاصة بتوزيعة Alpine Linux الخفيفة
يمكننا استخدام أي Image أخرى كـ Base Image حسب احتياجاتنا
مثل node:20-alpine أو python:3.12 أو php:8.3 أو ubuntu:24.04
ملحوظة: عندما نحددnginx:alpineفإن الـDockerسيحاول تحميل هذه الـImageمن الـDocker Hub
إذا لم تكن موجودة على جهازك، سيقوم بتحميلها تلقائيًا أثناء عملية البناء كما اعتدنا
نسخة nignx:alpine موجودة على جهازنا بالفعل لذا لن يقوم الـ Docker بتحميلها مرة أخرى
لكن إذا لم تكن موجودة على جهازك، سيقوم بتحميلها من الإنترنت أثناء عملية البناء
أو يمكنك تحميلها يدويًا مسبقًا باستخدام الأمر docker pull nginx:alpine
لدينا الآن الأمر COPY الذي يقوم بنسخ الملفات من جهازنا إلى داخل الـ Image
COPY ./index.html /usr/share/nginx/html/
هنا نحن ننسخ ملف index.html من مجلد المشروع على جهازنا إلى مجلد /usr/share/nginx/html/ داخل الـ Image
وكما تعلمنا في المقالة السابقة، أن الـ /usr/share/nginx/html/ هو المجلد الافتراضي الذي يستخدمه Nginx لاستضافة الملفات الثابتة
وذكرنا أنه يحاول العثور على ملف index.html داخل هذا المجلد ليكون هو الصفحة الرئيسية للموقع
بناء الـ Docker Image
الآن بعد أن كتبنا الـ Dockerfile
دعنا نرى الملفات التي أنشأناها:
> ls
Dockerfile index.html
لدينا الآن ملف Dockerfile وملف index.html داخل مجلد static-site-img
وكتبنا في الـ Dockerfile أننا نريد بناء Image تعتمد على nginx:alpine وتنسخ ملف index.html إلى مجلد استضافة Nginx
الآن سنقوم ببناء الـ Image من ملف الـ Dockerfile الذي أنشأناه
سنستخدمه الأمر docker image build أو docker build لبناء الـ Image
> docker image build .
[+] Building 1.7s (8/8) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 96B 0.0s
=> [internal] load metadata for docker.io/library/nginx:alpine 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 174B 0.0s
=> [1/2] FROM docker.io/library/nginx:alpine@sha256:b0f7830b6bfaa1258f45d94c240ab668ced1b3651c8a222aefe6683447c7bf55 1.1s
=> => resolve docker.io/library/nginx:alpine@sha256:b0f7830b6bfaa1258f45d94c240ab668ced1b3651c8a222aefe6683447c7bf55 1.0s
=> [auth] library/nginx:pull token for registry-1.docker.io 0.0s
=> [2/2] COPY ./index.html /usr/share/nginx/html/ 0.0s
=> exporting to image 0.3s
=> => exporting layers 0.1s
=> => exporting manifest sha256:6f90d05cfc34322f913cfbb1272289c551c76f5b6768ec3cca8ac341ba17678f 0.0s
=> => exporting config sha256:8e1e07ef22765af32463de58b2097ad12ff39c61d9422a4efc1cd1cb8ddce67b 0.0s
=> => exporting attestation manifest sha256:caa13666be7421464d1bc142057474337e3cda60e7aa622c1182a2397a99907c 0.0s
=> => exporting manifest list sha256:e38e620c298f703151c845ab3ce3765196fbcb652940a31aaa8a95d9f8b3174a 0.0s
=> => naming to moby-dangling@sha256:e38e620c298f703151c845ab3ce3765196fbcb652940a31aaa8a95d9f8b3174a 0.0s
=> => unpacking to moby-dangling@sha256:e38e620c298f703151c845ab3ce3765196fbcb652940a31aaa8a95d9f8b3174a 0.1s
هنا استخدمنا الأمر docker image build .
النقطة . في نهاية الأمر تعني أن الـ Dockerfile موجود في المجلد الحالي
لأن أمر الـ build يحتاج إلى معرفة مسار المجلد الذي يحتوي على الـ Dockerfile
والمسار هنا يكون relative path أو absolute path حسب ما تريد
وفي حالتنا هو المجلد الحالي لذا سنخدم النقطة . التي تعبر الـ relative path للمجلد الحالي
ملحوظة: النقطة.لا تشير فقط لموقع الـDockerfile، بل تحدد أيضًا الـBuild Context
وهو المجلد الذي سيتم إرسال محتوياته للـDocker Engineأثناء البناء
لذلك الأمرCOPY ./index.htmlينسخ من هذا الـContextوليس من أي مكان آخر على جهاز
لاحظ أننا بعد تنفيذ الأمر، الـ Docker بدأ في قراءة الـ Dockerfile وتنفيذ التعليمات خطوة بخطوة
ويمكنك رؤية كل خطوة في عملية البناء وكم من الوقت استغرقتها كل خطوة
وكل خطوة تظهر برقم مثل [1/2], [2/2] وهكذا
وكل هطوة تعبر عن Image Layer جديدة يتم إنشاؤها
في النهاية بعد انتهاء البناء، الـ Docker يخبرنا أن الـ Image تم إنشاؤها بنجاح
بعد انتهاء البناء يمكننا التحقق من أن الـ Image تم إنشاؤها:
> docker image list
REPOSITORY TAG IMAGE ID CREATED SIZE
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> e38e620c298f 5 minutes ago 92.2MB
nginx alpine b0f7830b6bfa 11 days ago 92.2MB
ubuntu latest cd1dba651b30 12 days ago 117MB
nginx latest c881927c4077 12 days ago 237MB
alpine latest 865b95f46d98 5 weeks ago 13MB
حسنًا، أين الـ Image التي أنشأناها ؟
الـ Image التي أنشأناها هى التي تدعى <none> وهذا معناه أننا لم نعطها اسمًا أو tag أثناء البناء
لذا لنحذف هذه الـ Image ونقم بإعادة بنائها مع إعطائها اسمًا
> docker image rm e38e620c298f
الآن لنقم بإعادة بناء الـ Image مع إعطائها اسمًا static-site-img:
> docker image build -t static-site-img .
[+] Building 2.0s (8/8) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 96B 0.0s
=> [internal] load metadata for docker.io/library/nginx:alpine 0.1s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/2] FROM docker.io/library/nginx:alpine@sha256:b0f7830b6bfaa1258f45d94c240ab668ced1b3651c8a222aefe6683447c7bf55 1.6s
=> => resolve docker.io/library/nginx:alpine@sha256:b0f7830b6bfaa1258f45d94c240ab668ced1b3651c8a222aefe6683447c7bf55 1.6s
=> [internal] load build context 0.0s
=> => transferring context: 32B 0.0s
=> [auth] library/nginx:pull token for registry-1.docker.io 0.0s
=> CACHED [2/2] COPY ./index.html /usr/share/nginx/html/ 0.0s
=> exporting to image 0.2s
=> => exporting layers 0.0s
=> => exporting manifest sha256:6f90d05cfc34322f913cfbb1272289c551c76f5b6768ec3cca8ac341ba17678f 0.0s
=> => exporting config sha256:8e1e07ef22765af32463de58b2097ad12ff39c61d9422a4efc1cd1cb8ddce67b 0.0s
=> => exporting attestation manifest sha256:0bfc20edc032f5e93ae21a6402e2a9ff347d058fa22c140116595f5737e80dd0 0.0s
=> => exporting manifest list sha256:3f60ea63cc059c0ec98cbc988387654192bfe48cdebf4e176633f8e599ea13b7 0.0s
=> => naming to docker.io/library/static-site-img:latest 0.0s
=> => unpacking to docker.io/library/static-site-img:latest 0.1s
هكذا تم بناء الـ Image بنجاح مع إعطائها اسمًا static-site-img ستلا
ستلاحظ في الخطوة الثانية => CACHED [2/2] COPY ./index.html /usr/share/nginx/html/ أن الـ Docker استخدم الـ Cache لهذه الخطوة
هذا لأننا لم نقم بتعديل ملف index.html منذ آخر مرة بنينا فيها الـ Image
لذا استخدم الـ Docker الـ Cache لتسريع عملية البناء بدلًا من إعادة تنفيذ هذه الخطوة مرة أخرى
وستلاحظ هذا السلوك كثيرًا أثناء بناء الـ Images في المستقبل
لذالك نهتم جدًا بترتيب الأوامر في الـ Dockerfile لأن ترتيب الأوامر ونوعيتها وطريقة كتابتها يؤثر بشكل كبير على استفادتنا من الـ Cache أثناء البناء بالتالي تسريع عملية البناء وتقليل حجم الـ Image النهائية
الآن لنتحقق من الـ Images الموجودة على جهازنا:
> docker image list
static-site-img latest 3f60ea63cc05 13 minutes ago 92.2MB
nginx alpine b0f7830b6bfa 11 days ago 92.2MB
ubuntu latest cd1dba651b30 12 days ago 117MB
nginx latest c881927c4077 12 days ago 237MB
alpine latest 865b95f46d98 5 weeks ago 13MB
ستلاحظ أن الـ Image التي أنشأناها الآن تدعى static-site-img
ولاحظ أن Docker أعطى الـ Image الجديدة tag افتراضيًا باسم latest
هذا هو السلوك الافتراضي عند عدم تحديد tag أثناء البناء فهو يستخدم latest كمسمى افتراضي
وlatest لا يعني بالضرورة أنها أحدث نسخة من الـ Image كما يعتقد البعض
إنما هو مجرد اسم tag افتراضي
ويمكننا إعطاء الـ Image أي اسم أو tag نريده أثناء البناء باستخدام الخيار -t بهذا الشكل -t name:tag
على أي حال، أصبح الآن لدينا Docker Image مخصصة تحتوي على موقعنا الثابت داخل Nginx
تشغيل الـ Container من الـ Image
الآن لنقم بتشغيل Container من الـ Image التي أنشأناها:
> docker container run -d -p 8080:80 --rm --name my-site-container static-site-img
549cc9b871e968b0a180268ea4fbd7fafd2ef039fcf920f3f508547472cb98a4
هنا استخدمنا الأمر docker container run لإنشاء Container بإسم my-site-container من الـ Image التي أنشأناها static-site-img
استخدمنا الخيار -d لتشغيل الـ Container في الخلفية
واستخدمنا -p 8080:80 لعمل Port Mapping بين الـ port رقم 80 داخل الـ Container والـ port رقم 8080 على جهازنا
واستخدمنا الخيار --rm ليتم حذف الـ Container تلقائيًا عند إيقافه
الآن إذا فتحنا المتصفح وذهبنا إلى http://localhost:8080 لترى موقعنا تم استضافته بنجاح داخل الـ Docker Container بوساطة Nginx
مع رسالة الترحيب التي كتبناها في ملف index.html التي تقول Welcome to eltabarani.com
الآن لنرى الـ Containers التي لدينا:
> docker container list --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
549cc9b871e9 static-site-img "/docker-entrypoint.…" 9 seconds ago Up 8 seconds 0.0.0.0:8080->80/tcp my-site-container
لاحظ أن الـ Container الذي أنشأناه يعمل بنجاح من الـ Image التي أنشأناها باستخدام الـ Dockerfile
حتى أنك ترى اسم الـ Image التي أنشأناها static-site-img في عمود الـ IMAGE
لنقم الآن بإيقاف الـ Container:
> docker container stop my-site-container
my-site-container
بما أننا استخدمنا الخيار --rm أثناء تشغيل الـ Container
فإن الـ Container سيتم حذفه تلقائيًا بعد إيقافه
لذا إذا تحققنا من الـ Containers مرة أخرى:
> docker container list --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
لن نجد أي Containers على الإطلاق
خاتمة
في هذه المقالة تعلمنا كيف ننشئ Dockerfile بسيط لبناء Docker Image مخصصة لتطبيقنا
وتعلمنا كيفية كتابة أوامر الـ Dockerfile خطوة بخطوة
وشرحنا كيفية بناء الـ Image من ملف الـ Dockerfile باستخدام الأمر docker build
وأخيرًا تعلمنا كيفية تشغيل Container من الـ Image التي أنشأناها باستخدام الـ Dockerfile
الـ Dockerfile هو أداة قوية جدًا في عالم الـ Docker وبها مميزات كثيرة
لكنني أحببت أن تكون هذه المقالة قصيرة ومباشرة وبسيطة لتقديم المفهوم الأساسي للـ Dockerfile
في المقالات القادمة سنتعرف على أوامر أخرى متقدمة في الـ Dockerfile مع أمثلة مختلفة ومفاهيم جديدة