رفتن به محتوا
LoopX

دستورات Dockerfile

توی این درس یاد می‌گیری دستورهای اصلی Dockerfile هر کدام دقیقاً چه می‌کنند و کی زمان اجرایشان است: FROM و RUN و COPY و ADD و WORKDIR و ENV و ARG و EXPOSE و USER. فرق COPY و ADD و فرق ENV و ARG (مهم‌ترین سوءتفاهم‌ها) را با دستور واقعی می‌بینی، با docker build --build-arg هنگام build مقدار می‌دهی، و در آخر یک Dockerfile با همه‌ی دستورات اصلی می‌نویسی.

ساختن image مثل چیدن یک آشپزخانه‌ی تازه است: FROM یعنی از کدام آشپزخانه‌ی خالی شروع کنیم، RUN یعنی «همین الان یک کار انجام بده» (مثلاً قفسه نصب کن)، COPY یعنی «این وسیله را از انبار من بیاور»، WORKDIR یعنی «پشت کدام میز بایستیم»، ENV یعنی «برچسب‌هایی که همیشه روی دیوار می‌مانند»، و ARG یعنی «یادداشتی که فقط موقع چیدن لازم بود». نکته‌ی ظریف: RUN موقع ساختن (build) اجرا می‌شود؛ نه موقع اجرای کانتینر.

بیشتر دستورها موقع build اجرا می‌شوند. ENV و EXPOSE و USER و CMD فقط در متادیتای image ثبت می‌شوند و روی رفتار کانتینر هنگام اجرا اثر دارند.
ex1/Dockerfile
FROM alpine:3.20
RUN echo "ساخته شده موقع build" > /build-info.txt
WORKDIR /app
RUN pwd > /app/where.txt
CMD ["sh", "-c", "cat /build-info.txt; cat /app/where.txt; pwd"]
Terminal window
docker build -t lx-di-run ./ex1 2>&1 | grep -E '^#[0-9]+ \[[0-9]+/[0-9]+\]' | sed -E 's/@sha256:[0-9a-f]+//' | awk '!s[$0]++'
docker run --rm lx-di-run
خروجی
#4 [1/4] FROM docker.io/library/alpine:3.20
#5 [2/4] RUN echo "ساخته شده موقع build" > /build-info.txt
#6 [3/4] WORKDIR /app
#7 [4/4] RUN pwd > /app/where.txt
ساخته شده موقع build
/app
/app
  • FROM alpine:3.20 یک tag مشخص را انتخاب می‌کند (به‌جای latest) تا build تکرارپذیر باشد.
  • RUN دستور را موقع build اجرا و نتیجه‌اش را داخل image ذخیره می‌کند (فایل build-info.txt را همان‌جا ساختیم).
  • WORKDIR /app پوشه را می‌سازد (اگر نباشد) و دستورهای بعدی و پروسه‌ی نهایی را در آن اجرا می‌کند. به‌جای RUN cd /app که فقط برای همان یک دستور اثر دارد.
ex2/Dockerfile
FROM alpine
COPY data.tar.gz /copied/data.tar.gz
ADD data.tar.gz /added/
CMD ["sh", "-c", "echo COPY:; ls /copied; echo ADD:; ls /added"]
Terminal window
mkdir -p ex2/payload && echo "hello" > ex2/payload/note.txt
tar czf ex2/data.tar.gz -C ex2/payload .
docker build -t lx-di-copy ./ex2 >/dev/null 2>&1
docker run --rm lx-di-copy
خروجی
COPY:
data.tar.gz
ADD:
note.txt

COPY فایل را همان‌طور که هست کپی می‌کند (tar همچنان یک فایل است)، ولی ADD یک آرشیو tar را خودکار باز می‌کند (و می‌تواند URL هم دانلود کند). این جادوی اضافه‌ی ADD اغلب باعث غافلگیری می‌شود. قاعده: برای کپی ساده همیشه COPY؛ ADD فقط وقتی واقعاً باز کردن آرشیو لازم است.

⚡ بررسی سریع

کدام یک آرشیو tar را خودکار باز می‌کند؟

ex3/Dockerfile
FROM alpine
ARG APP_VERSION=1.0
ENV APP_ENV=production
RUN echo "نسخه‌ی build: $APP_VERSION | محیط: $APP_ENV" > /info.txt
CMD ["sh", "-c", "cat /info.txt; echo \"موقع اجرا: APP_ENV=$APP_ENV APP_VERSION=[$APP_VERSION]\""]
Terminal window
docker build -t lx-di-env --build-arg APP_VERSION=2.5 ./ex3 >/dev/null 2>&1
docker run --rm lx-di-env
خروجی
نسخه‌ی build: 2.5 | محیط: production
موقع اجرا: APP_ENV=production APP_VERSION=[]
  • ARG APP_VERSION=1.0 یک متغیر فقط‌برای build با مقدار پیش‌فرض است. با docker build --build-arg APP_VERSION=2.5 عوضش کردیم و RUN آن را خواند.
  • ENV APP_ENV=production متغیر را هم در build و هم در اجرا نگه می‌دارد.
  • موقع اجرا APP_ENV هست ولی APP_VERSION خالی است؛ ARG داخل کانتینر در دسترس نیست.

حالا ببین ARG در تاریخچه‌ی image دیده می‌شود (پس برای رازها مناسب نیست):

Terminal window
docker history --no-trunc lx-di-env --format '{{.CreatedBy}}' | grep -E "APP_VERSION" | head -2 | cut -c1-110
خروجی
CMD ["sh" "-c" "cat /info.txt; echo \"موقع اجرا: APP_ENV=$APP_ENV APP_VERSION=[$APP_VERSION]\""]
RUN |1 APP_VERSION=2.5 /bin/sh -c echo "نسخه‌ی build: $APP_VERSION | محیط: $APP_ENV" > /info.txt #
ex4/Dockerfile
FROM alpine
RUN adduser -D -u 1001 appuser
USER appuser
EXPOSE 8080 9090
CMD ["sh", "-c", "whoami; id -u"]
Terminal window
docker build -t lx-di-user ./ex4 >/dev/null 2>&1
docker run --rm lx-di-user
docker image inspect lx-di-user --format 'EXPOSE: {{.Config.ExposedPorts}} | USER: {{.Config.User}}'
خروجی
appuser
1001
EXPOSE: map[8080/tcp:{} 9090/tcp:{}] | USER: appuser

USER appuser باعث می‌شود دستورهای بعدی و پروسه‌ی نهایی با کاربر غیر root اجرا شوند (امنیت بهتر؛ درس «امنیت پایه»). EXPOSE فقط مستندات است: می‌گوید برنامه روی کدام پورت‌ها گوش می‌دهد، ولی پورتی روی میزبان باز نمی‌کند (آن با -p است).

مثال ۵: همه‌ی دستورها در یک Dockerfile

Section titled “مثال ۵: همه‌ی دستورها در یک Dockerfile”

تمرین اصلی: یک Dockerfile که همه‌ی دستورات اصلی را دارد و یک صفحه‌ی وب با کاربر غیر root نمی‌شود روی پورت ۸۰ اجرا کرد؛ پس از nginx-unprivileged نمی‌گیریم و ساده‌تر یک سرور کوچک با پایتون می‌سازیم:

ex5/Dockerfile
FROM python:3.12-slim
ARG GREETING="سلام"
ENV MESSAGE="$GREETING از داخل image"
WORKDIR /srv
COPY index.html .
RUN useradd -m -u 1001 web && chown -R web /srv
USER web
EXPOSE 8000
CMD ["python", "-m", "http.server", "8000", "--bind", "0.0.0.0"]
Terminal window
echo "<h1>همه‌ی دستورات</h1>" > ex5/index.html
docker build -t lx-di-all --build-arg GREETING="درود" ./ex5 2>&1 | grep -E '^#[0-9]+ \[[0-9]+/[0-9]+\]' | sed -E 's/@sha256:[0-9a-f]+//' | awk '!s[$0]++'
docker run -d --name lx-di1 -p 8353:8000 lx-di-all >/dev/null; sleep 2
curl -s localhost:8353
docker exec lx-di1 sh -c 'echo "کاربر: $(whoami) | MESSAGE=$MESSAGE"'
خروجی
#4 [1/4] FROM docker.io/library/python:3.12-slim
#6 [2/4] WORKDIR /srv
#7 [3/4] COPY index.html .
#8 [4/4] RUN useradd -m -u 1001 web && chown -R web /srv
<h1>همه‌ی دستورات</h1>
کاربر: web | MESSAGE=درود از داخل image

پنج‌شش دستور کنار هم: پایه (FROM)، آرگومان build (ARG)، متغیر ماندگار (ENV که از ARG ساخته شده)، پوشه‌ی کار (WORKDIR)، کپی (COPY)، ساخت کاربر (RUN)، کاربر نهایی (USER)، مستند پورت (EXPOSE) و دستور اجرا (CMD). مقدار درود با --build-arg وارد شد و در MESSAGE ماند.

هر دستورِ «مؤثر» (RUN، COPY، ADD) یک لایه‌ی جدید می‌سازد؛ دستورهای متادیتا (ENV، EXPOSE، USER، WORKDIR، CMD) لایه‌ی فایلی نمی‌سازند و فقط تنظیمات image (config) را عوض می‌کنند. ببین:

Terminal window
docker image inspect lx-di-all --format 'تعداد لایه‌های فایل: {{len .RootFS.Layers}}'
docker image inspect lx-di-all --format 'ENV: {{.Config.Env}}{{"\n"}}WorkingDir: {{.Config.WorkingDir}}{{"\n"}}User: {{.Config.User}}' | head -3 | cut -c1-120
خروجی
تعداد لایه‌های فایل: 7
ENV: [PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LANG=C.UTF-8 GPG_KEY=7169605F62C7
WorkingDir: /srv
User: web

نتیجه‌ی مهم برای کش (درس بعدی‌ها): دستورهایی که لایه می‌سازند اگر تغییر نکنند کش می‌شوند؛ و ترتیب مهم است. همچنین ARG قبل از FROM فقط برای خود FROM در دسترس است و بعد از FROM باید دوباره اعلام شود.

دستور کار لایه‌ی جدید؟
FROM img:tag image پایه بنیاد
RUN cmd اجرای دستور هنگام build بله
COPY src dst کپی از context بله
ADD src dst مثل COPY + باز کردن tar + URL بله
WORKDIR dir پوشه‌ی کار خیر (متادیتا؛ پوشه را می‌سازد)
ENV K=V متغیر برای build و اجرا خیر
ARG K=V متغیر فقط build خیر
EXPOSE port مستندسازی پورت خیر
USER name کاربر پروسه خیر
CMD [...] دستور پیش‌فرض اجرا خیر
فرق ENV ARG
در build بله بله
در اجرای کانتینر بله خیر
مقدار از خط فرمان docker run -e docker build --build-arg
در docker history دیده می‌شود دیده می‌شود (برای راز مناسب نیست)

۱) RUN cd اثر ماندگار ندارد

Section titled “۱) RUN cd اثر ماندگار ندارد”
Terminal window
mkdir -p m1
printf 'FROM alpine\nRUN mkdir /data && cd /data\nRUN pwd\nWORKDIR /data\nRUN pwd\n' > m1/Dockerfile
docker build --no-cache --progress=plain ./m1 2>&1 | grep -A1 -E 'RUN pwd' | grep -vE '^--' | grep -E '^#[0-9]+ [0-9.]+ /|RUN pwd'
خروجی
#6 [3/5] RUN pwd
#6 0.553 /
#8 [5/5] RUN pwd
#8 0.402 /data

هر RUN یک پوشه‌ی کار تازه (همان /) دارد؛ cd در یک RUN روی RUN بعدی اثر ندارد (اولی / چاپ می‌کند). راه‌حل: WORKDIR.

مثال ۳: APP_VERSION در اجرا خالی بود. راه‌حل: اگر در اجرا لازم است، ENV APP_VERSION=$APP_VERSION بعد از ARG بگذار.

Terminal window
mkdir -p m3
printf 'FROM alpine\nUSER ghost\n' > m3/Dockerfile
docker build -t lx-di-user ./m3 >/dev/null 2>&1
docker run --rm lx-di-user echo hi 2>&1 | head -1
خروجی
docker: Error response from daemon: unable to find user ghost: no matching entries in passwd file

USER در build خطا نمی‌دهد، ولی هنگام اجرا کاربر پیدا نمی‌شود. راه‌حل: اول کاربر را با RUN adduser (یا useradd) بساز، بعد USER.

۴) COPY با مسیر نسبی اشتباه

Section titled “۴) COPY با مسیر نسبی اشتباه”
Terminal window
mkdir -p m4
printf 'FROM alpine\nCOPY missing.txt /x\n' > m4/Dockerfile
docker build -t lx-di-copy ./m4 2>&1 | grep -E 'not found' | head -1 | sed -E 's/ref [^ ]+/ref .../' | cut -c1-140
خروجی
#6 ERROR: failed to calculate checksum of ref ... "/missing.txt": not found

مسیر مبدأ نسبت به context است و فایل نیست (not found). راه‌حل: مسیر و محتوای context را بررسی کن.

ADD ممکن است تار را باز کند یا از URL بخواند و رفتار غیرمنتظره بدهد. راه‌حل: COPY.

✎ تمرینآسان

یک Dockerfile با FROM alpine، WORKDIR /w و CMD ["pwd"] بساز و اجرا کن؛ چه چیزی چاپ می‌شود؟

دیدن جواب
Terminal window
mkdir -p e1
printf 'FROM alpine\nWORKDIR /w\nCMD ["pwd"]\n' > e1/Dockerfile
docker build -t lx-di-run ./e1 >/dev/null 2>&1
docker run --rm lx-di-run
خروجی
/w
✎ تمرینمتوسط

با ARG NAME=world و RUN echo "hello $NAME" > /msg یک image بساز؛ یک بار با پیش‌فرض و یک بار با --build-arg NAME=ali build کن و ثابت کن پیام فرق می‌کند.

دیدن جواب
Terminal window
mkdir -p e2
printf 'FROM alpine\nARG NAME=world\nRUN echo "hello $NAME" > /msg\nCMD ["cat","/msg"]\n' > e2/Dockerfile
docker build -t lx-di-env ./e2 >/dev/null 2>&1; docker run --rm lx-di-env
docker build -t lx-di-env --build-arg NAME=ali ./e2 >/dev/null 2>&1; docker run --rm lx-di-env
خروجی
hello world
hello ali
✎ تمرینسخت

تمرین اصلی: یک Dockerfile با همه‌ی دستورات اصلی (FROM، ARG، ENV، WORKDIR، COPY، RUN، USER، EXPOSE، CMD) بنویس که یک فایل متنی را چاپ کند و با کاربر غیر root اجرا شود. ثابت کن کاربر root نیست.

دیدن جواب
Terminal window
mkdir -p e3 && echo "سلام از فایل کپی‌شده" > e3/hello.txt
cat > e3/Dockerfile <<'LXEOF'
FROM alpine
ARG USERNAME=worker
ENV APP_HOME=/opt/app
WORKDIR $APP_HOME
COPY hello.txt .
RUN adduser -D $USERNAME && chown $USERNAME hello.txt
USER $USERNAME
EXPOSE 8080
CMD ["sh","-c","whoami; cat hello.txt"]
LXEOF
docker build -t lx-di-all ./e3 >/dev/null 2>&1
docker run --rm lx-di-all
خروجی
worker
سلام از فایل کپی‌شده
؟ آزمونک
  1. RUN چه موقع اجرا می‌شود؟

  2. تفاوت اصلی COPY و ADD؟

  3. ARG در کانتینر در حال اجرا…

  4. EXPOSE چه می‌کند؟

  5. چرا WORKDIR بهتر از RUN cd است؟

  • FROM پایه، RUN اجرا موقع build، COPY/ADD کپی (ترجیحاً COPY)، WORKDIR پوشه‌ی کار، USER کاربر غیر root.
  • ENV برای build و اجرا؛ ARG فقط build (و در docker history دیده می‌شود، پس برای رازها نه).
  • EXPOSE فقط مستندات است.
  • دستورهای RUN/COPY/ADD لایه می‌سازند؛ بقیه فقط متادیتا.
  • docker build --build-arg K=V مقدار ARG را می‌دهد.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
FROM alpine:3.20پایه‌ی مشخص (به‌جای latest)
RUN apk add --no-cache curlاجرای دستور هنگام build
COPY src dstکپی ساده از context
ADD archive.tar.gz /dir/کپی + باز کردن tar
WORKDIR /appپوشه‌ی کار
ENV APP_ENV=productionمتغیر build و اجرا
ARG VERSION=1.0متغیر فقط build
EXPOSE 8080مستندسازی پورت
USER appuserکاربر غیر root
docker build --build-arg K=V -t img .دادن مقدار به ARG
docker history --no-trunc imgدستورهای ساخت هر لایه