انتقال للمقال
وقت القراءة: ≈ 10 دقائق

إنشاء 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 مع أمثلة مختلفة ومفاهيم جديدة