ساخت Image با Dockerfile و بهینه‌سازی لایه‌ها

ماهیت یک Docker Image و مفهوم لایه‌ها (Layers)

ایمیج یک قالب فقط‌خواندنی (Read-Only) شامل سیستم‌عامل پایه، وابستگی‌ها، کد برنامه و تنظیمات پیش‌فرض است. کانتینر در حقیقت یک نمونه‌ی در حال اجرا (Instance) از یک ایمیج است که داکر روی آن یک لایه‌ی نازک و موقت برای خواندن و نوشتن (Read/Write Container Layer) قرار می‌دهد.

 1┌───────────────────────────────────────────────┐
 2│ [R/W] Container Layer (تغییرات موقت کانتینر)  │ ◄── پاک با حذف کانتینر
 3├───────────────────────────────────────────────┤
 4│ [R/O] Layer 4: CMD ["python", "app.py"]       │
 5├───────────────────────────────────────────────┤
 6│ [R/O] Layer 3: COPY . /app                    │ ◄── ایمیج (فقط‌خواندنی)
 7├───────────────────────────────────────────────┤
 8│ [R/O] Layer 2: RUN pip install -r reqs.txt    │
 9├───────────────────────────────────────────────┤
10│ [R/O] Layer 1: FROM python:3.11-slim          │
11└───────────────────────────────────────────────┘

هر دستوری در فایل Dockerfile (مانند RUN، COPY، ADD) یک لایه‌ی مجزا به ایمیج اضافه می‌کند. به لطف مکانیزم Copy-on-Write (CoW)، اگر دو یا چند کانتینر از یک ایمیج ساخته شوند، همه‌ی آن‌ها لایه‌های ایمیج را به‌صورت مشترک می‌خوانند و هیچ فضای اضافه‌ای مصرف نمی‌شود.


دستورهای اصلی در Dockerfile

یک Dockerfile فایلی متنی بدون پسوند است که گام‌های ساخت یک ایمیج را به صورت خودکار تعریف می‌کند:

  • FROM: مشخص کردن ایمیج پایه (مثلاً python:3.11-slim یا node:20-alpine). همیشه اولین دستور غیراز کامنت یا ARG است.
  • WORKDIR: تعیین دایرکتوری کاری درون کانتینر برای اجرای دستورات بعدی (به‌جای استفاده‌ی نامنظم از دایرکتوری روت /).
  • COPY: کپی کردن فایل‌ها یا دایرکتوری‌ها از سیستم میزبان به داخل ایمیج.
  • ADD: شبیه COPY، اما توانایی اکسترکت خودکار آرشیوهای tar و دانلود از URL را دارد. در عمل برای جلوگیری از رفتارهای غیرمنتظره، معمولا استفاده از COPY توصیه می‌شود.
  • RUN: اجرای دستور در زمان ساخت ایمیج (مانند نصب پکیج‌ها با apt-get یا npm install).
  • ENV: تعریف متغیرهای محیطی که هم در زمان ساخت و هم در زمان اجرای کانتینر در دسترس خواهند بود.
  • ARG: متغیرهای موقتی که فقط در زمان ساخت ایمیج (docker build --build-arg) معتبرند و در ایمیج نهایی ذخیره نمی‌شوند.
  • EXPOSE: صرفاً یک برچسب مستندسازی (Documentation) برای اعلام پورتی که برنامه روی آن گوش می‌دهد.
  • CMD و ENTRYPOINT: دستوری که هنگام استارت کانتینر اجرا می‌شود:
    • فرم استاندارد (Exec Form): CMD ["python", "app.py"] (توصیه‌شده، بدون ایجاد شل میانی).
    • فرم شل (Shell Form): CMD python app.py (تحت پردازش /bin/sh -c اجرا می‌شود).

فایل .dockerignore و Build Context

هنگام اجرای دستور docker build .، نقطه‌ی پایانی (.) نشان‌دهنده‌ی Build Context است. داکر کلاینت ابتدا تمام فایل‌های این مسیر را بسته‌بندی کرده و برای دیمن داکر می‌فرستد.

اگر دایرکتوری شما شامل پوشه‌های حجیمی مانند .git/، node_modules/ یا فایل‌های حساسی مانند .env باشد، حجم و زمان بیلد بسیار بالا می‌رود و احتمال نشت اطلاعات محرمانه وجود دارد.

همیشه در کنار Dockerfile یک فایل به نام .dockerignore بسازید:

1.git
2.gitignore
3.env
4node_modules
5*.log
6__pycache__

بهینه‌سازی کش لایه‌ها (Layer Caching)

داکر هنگام بیلد، اگر دستوری و فایل‌های مرتبط با آن تغییر نکرده باشند، از کش بیلد قبلی استفاده می‌کند. ترتیب دستورات در Dockerfile بسیار تعیین‌کننده است:

روش غیراصولی:

1FROM python:3.11-slim
2WORKDIR /app
3COPY . .
4RUN pip install -r requirements.txt
5CMD ["python", "app.py"]

در این حالت، با هر تغییر کوچک در کدهای برنامه، خط COPY . . بیلد کش را باطل می‌کند و دستور سنگین pip install باید هر بار از اول اجرا و دانلود شود!

روش اصولی و بهینه:

 1FROM python:3.11-slim
 2WORKDIR /app
 3
 4# ابتدا فقط فایل تعریف وابستگی‌ها را کپی و نصب می‌کنیم
 5COPY requirements.txt .
 6RUN pip install --no-cache-dir -r requirements.txt
 7
 8# در مرحله آخر کدهای پروژه را کپی می‌کنیم
 9COPY . .
10
11CMD ["python", "app.py"]

در این حالت، تا زمانی که فایل requirements.txt تغییر نکند، داکر مرحله‌ی نصب وابستگی‌ها را از کش فوری می‌خواند و زمان بیلد به زیر چند ثانیه کاهش می‌یابد.


چندمرحله‌ای ساختن ایمیج‌ها (Multi-stage Builds)

در زبان‌های کامپایلری (مانند Go، Rust یا Java) یا برنامه‌های فرانت‌اند (React، Vue)، ابزارهای بیلد، کامپایلرها و کدهای سورس حجم بسیار بالایی (چند صد مگابایت یا گیگابایت) دارند، در حالی که در محیط Production فقط به فایل باینری کامپایل‌شده یا فایل‌های خروجی HTML/JS نیاز داریم.

با Multi-stage Build می‌توانید چند ایمیج مجزا در یک Dockerfile تعریف کنید و فقط خروجی تمیز را به ایمیج نهایی منتقل کنید:

 1# مرحله اول: بیلد و کامپایل برنامه (Build Stage)
 2FROM golang:1.22-alpine AS builder
 3WORKDIR /src
 4COPY go.mod go.sum ./
 5RUN go mod download
 6COPY . .
 7RUN CGO_ENABLED=0 go build -o /bin/myapp .
 8
 9# مرحله دوم: محیط اجرایی نهایی و سبک (Production Stage)
10FROM alpine:3.19
11WORKDIR /app
12COPY --from=builder /bin/myapp /app/myapp
13EXPOSE 8080
14CMD ["/app/myapp"]

نتیجه؟ حجم ایمیج نهایی به‌جای ۶۰۰ مگابایت به کمتر از ۲۰ مگابایت کاهش می‌یابد و هیچ ابزار اضافی یا سورس کدی در محیط پروداکشن وجود ندارد که سطح حملات امنیتی را بالا ببرد.


ℹ️ نکته و راهنمای امنیتی: اجرای کانتینر با کاربر غیر روت (USER)

به‌طور پیش‌فرض، کانتینرها با کاربر root اجرا می‌شوند. در محیط‌های واقعی و استانداردهای امنیتی Production، اجرای کانتینر با دسترسی root به‌دلیل خطرات امنیتی توصیه نمی‌شود؛ زیرا اگر مهاجم بتواند از برنامه‌ی داخل کانتینر سوءاستفاده کند، ممکن است شانس بیشتری برای فرار از کانتینر (Container Escape) و نفوذ به سیستم‌عامل میزبان داشته باشد.

برای اجرای امن با کاربر غیر روت در Dockerfile:

 1FROM python:3.11-slim
 2
 3# ایجاد گروه و کاربر سیستمی بدون دسترسی روت
 4RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
 5
 6WORKDIR /app
 7COPY requirements.txt .
 8RUN pip install --no-cache-dir -r requirements.txt
 9
10COPY . .
11
12# تغییر مالکیت فایل‌ها به کاربر جدید
13RUN chown -R appuser:appuser /app
14
15# تغییر کاربر فعال کانتینر
16USER appuser
17
18CMD ["python", "app.py"]

همچنین در توزیع‌های مبتنی بر Alpine می‌توانید از adduser استفاده کنید:

1RUN addgroup -S appgroup && adduser -S appuser -G appgroup
2USER appuser

توجه: به دلیل محدودیت زمان دوره‌ی هفتگی و برای جلوگیری از درگیر شدن با خطاهای پیچیده‌ی دسترسی فایل‌ها (Permission Denied) در گام‌های اولیه، استفاده از این تنظیم در تمرین‌های این هفته اختیاری است، اما شناخت آن برای محیط‌های واقعی سازمانی ضروری است.


آزمایش عملی: ساخت و اجرای ایمیج با docker build

بیایید یک پروژه‌ی ساده‌ی پایتونی ایجاد کنیم، برای آن Dockerfile بنویسیم، ایمیج را بسازیم و کانتینر آن را اجرا کنیم:

گام ۱: ایجاد دایرکتوری و فایل برنامه‌ی پایتون

یک دایرکتوری به نام python-demo بسازید و وارد آن شوید:

1mkdir -p ~/python-demo
2cd ~/python-demo

یک اسکریپت پایتون ساده به نام app.py ایجاد کنید:

1# app.py
2import os
3import sys
4
5course_name = os.getenv("COURSE_NAME", "DevOps Course")
6print(f"Hello from inside Docker Container!")
7print(f"Welcome to {course_name} (Week 5: Docker & Images)")
8print(f"Python Version: {sys.version.split()[0]}")

گام ۲: نوشتن فایل Dockerfile

در همان دایرکتوری، فایلی به نام Dockerfile بسازید:

 1# استفاده از ایمیج سبک پایتون
 2FROM python:3.11-slim
 3
 4# تعیین مسیر کاری درون کانتینر
 5WORKDIR /app
 6
 7# کپی کردن اسکریپت برنامه به داخل کانتینر
 8COPY app.py .
 9
10# تعریف متغیر محیطی پیش‌فرض
11ENV COURSE_NAME="TechStack DevOps"
12
13# دستوری که با استارت کانتینر اجرا می‌شود
14CMD ["python", "app.py"]

گام ۳: ساخت ایمیج با دستور docker build

دستور زیر را در همان مسیر اجرا کنید (نقطه‌ی انتهایی نشان‌دهنده‌ی مسیر فعلی است):

1docker build -t myapp:1.0.0 .

برای مشاهده‌ی ایمیج ساخته‌شده در لیست ایمیج‌های سیستم:

1docker images

گام ۴: اجرای کانتینر از روی ایمیج ساخته‌شده

اکنون کانتینر را از روی ایمیج محلی خود اجرا کنید:

1docker run --rm myapp:1.0.0

خروجی مشابه زیر در ترمینال چاپ می‌شود:

1Hello from inside Docker Container!
2Welcome to TechStack DevOps (Week 5: Docker & Images)
3Python Version: 3.11.x

همچنین می‌توانید با فلگ -e مقدار متغیر محیطی را تغییر دهید:

1docker run --rm -e COURSE_NAME="CESSA's TechStack" myapp:1.0.0

گام ۵: حذف ایمیج (در صورت عدم نیاز)

1docker rmi myapp:1.0.0

منابع یادگیری

ویدیو

مستندات و مقالات