توی این درس یاد میگیری دستورهای اصلی Dockerfile هر کدام دقیقاً چه میکنند و کی زمان اجرایشان است: FROM و RUN و COPY و ADD و WORKDIR و ENV و ARG و EXPOSE و USER. فرق COPY و ADD و فرق ENV و ARG (مهمترین سوءتفاهمها) را با دستور واقعی میبینی، با docker build --build-arg هنگام build مقدار میدهی، و در آخر یک Dockerfile با همهی دستورات اصلی مینویسی.
تشبیه: چیدن آشپزخانه
Section titled “تشبیه: چیدن آشپزخانه”ساختن image مثل چیدن یک آشپزخانهی تازه است: FROM یعنی از کدام آشپزخانهی خالی شروع کنیم، RUN یعنی «همین الان یک کار انجام بده» (مثلاً قفسه نصب کن)، COPY یعنی «این وسیله را از انبار من بیاور»، WORKDIR یعنی «پشت کدام میز بایستیم»، ENV یعنی «برچسبهایی که همیشه روی دیوار میمانند»، و ARG یعنی «یادداشتی که فقط موقع چیدن لازم بود». نکتهی ظریف: RUN موقع ساختن (build) اجرا میشود؛ نه موقع اجرای کانتینر.
زمان اجرای هر دستور
Section titled “زمان اجرای هر دستور”مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: FROM، RUN و WORKDIR
Section titled “مثال ۱: FROM، RUN و WORKDIR”FROM alpine:3.20RUN echo "ساخته شده موقع build" > /build-info.txtWORKDIR /appRUN pwd > /app/where.txtCMD ["sh", "-c", "cat /build-info.txt; cat /app/where.txt; pwd"]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/appFROM alpine:3.20یک tag مشخص را انتخاب میکند (بهجایlatest) تا build تکرارپذیر باشد.RUNدستور را موقع build اجرا و نتیجهاش را داخل image ذخیره میکند (فایلbuild-info.txtرا همانجا ساختیم).WORKDIR /appپوشه را میسازد (اگر نباشد) و دستورهای بعدی و پروسهی نهایی را در آن اجرا میکند. بهجایRUN cd /appکه فقط برای همان یک دستور اثر دارد.
مثال ۲: COPY در برابر ADD
Section titled “مثال ۲: COPY در برابر ADD”FROM alpineCOPY data.tar.gz /copied/data.tar.gzADD data.tar.gz /added/CMD ["sh", "-c", "echo COPY:; ls /copied; echo ADD:; ls /added"]mkdir -p ex2/payload && echo "hello" > ex2/payload/note.txttar czf ex2/data.tar.gz -C ex2/payload .docker build -t lx-di-copy ./ex2 >/dev/null 2>&1docker run --rm lx-di-copyCOPY:data.tar.gzADD:note.txtCOPY فایل را همانطور که هست کپی میکند (tar همچنان یک فایل است)، ولی ADD یک آرشیو tar را خودکار باز میکند (و میتواند URL هم دانلود کند). این جادوی اضافهی ADD اغلب باعث غافلگیری میشود. قاعده: برای کپی ساده همیشه COPY؛ ADD فقط وقتی واقعاً باز کردن آرشیو لازم است.
کدام یک آرشیو tar را خودکار باز میکند؟
ADD ویژگیهای اضافه دارد (باز کردن tar و دانلود URL)؛ COPY ساده و قابلپیشبینی است.
مثال ۳: ENV در برابر ARG
Section titled “مثال ۳: ENV در برابر ARG”FROM alpineARG APP_VERSION=1.0ENV APP_ENV=productionRUN echo "نسخهی build: $APP_VERSION | محیط: $APP_ENV" > /info.txtCMD ["sh", "-c", "cat /info.txt; echo \"موقع اجرا: APP_ENV=$APP_ENV APP_VERSION=[$APP_VERSION]\""]docker build -t lx-di-env --build-arg APP_VERSION=2.5 ./ex3 >/dev/null 2>&1docker 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 دیده میشود (پس برای رازها مناسب نیست):
docker history --no-trunc lx-di-env --format '{{.CreatedBy}}' | grep -E "APP_VERSION" | head -2 | cut -c1-110CMD ["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 #مثال ۴: USER و EXPOSE
Section titled “مثال ۴: USER و EXPOSE”FROM alpineRUN adduser -D -u 1001 appuserUSER appuserEXPOSE 8080 9090CMD ["sh", "-c", "whoami; id -u"]docker build -t lx-di-user ./ex4 >/dev/null 2>&1docker run --rm lx-di-userdocker image inspect lx-di-user --format 'EXPOSE: {{.Config.ExposedPorts}} | USER: {{.Config.User}}'appuser1001EXPOSE: map[8080/tcp:{} 9090/tcp:{}] | USER: appuserUSER appuser باعث میشود دستورهای بعدی و پروسهی نهایی با کاربر غیر root اجرا شوند (امنیت بهتر؛ درس «امنیت پایه»). EXPOSE فقط مستندات است: میگوید برنامه روی کدام پورتها گوش میدهد، ولی پورتی روی میزبان باز نمیکند (آن با -p است).
مثال ۵: همهی دستورها در یک Dockerfile
Section titled “مثال ۵: همهی دستورها در یک Dockerfile”تمرین اصلی: یک Dockerfile که همهی دستورات اصلی را دارد و یک صفحهی وب با کاربر غیر root نمیشود روی پورت ۸۰ اجرا کرد؛ پس از nginx-unprivileged نمیگیریم و سادهتر یک سرور کوچک با پایتون میسازیم:
FROM python:3.12-slimARG GREETING="سلام"ENV MESSAGE="$GREETING از داخل image"WORKDIR /srvCOPY index.html .RUN useradd -m -u 1001 web && chown -R web /srvUSER webEXPOSE 8000CMD ["python", "-m", "http.server", "8000", "--bind", "0.0.0.0"]echo "<h1>همهی دستورات</h1>" > ex5/index.htmldocker 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 2curl -s localhost:8353docker 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 ماند.
پشت پرده
Section titled “پشت پرده”هر دستورِ «مؤثر» (RUN، COPY، ADD) یک لایهی جدید میسازد؛ دستورهای متادیتا (ENV، EXPOSE، USER، WORKDIR، CMD) لایهی فایلی نمیسازند و فقط تنظیمات image (config) را عوض میکنند. ببین:
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تعداد لایههای فایل: 7ENV: [PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LANG=C.UTF-8 GPG_KEY=7169605F62C7WorkingDir: /srvUser: webنتیجهی مهم برای کش (درس بعدیها): دستورهایی که لایه میسازند اگر تغییر نکنند کش میشوند؛ و ترتیب مهم است. همچنین ARG قبل از FROM فقط برای خود FROM در دسترس است و بعد از FROM باید دوباره اعلام شود.
جدولهای مرجع
Section titled “جدولهای مرجع”| دستور | کار | لایهی جدید؟ |
|---|---|---|
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 |
دیده میشود | دیده میشود (برای راز مناسب نیست) |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) RUN cd اثر ماندگار ندارد
Section titled “۱) RUN cd اثر ماندگار ندارد”mkdir -p m1printf 'FROM alpine\nRUN mkdir /data && cd /data\nRUN pwd\nWORKDIR /data\nRUN pwd\n' > m1/Dockerfiledocker 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.
۲) انتظار ARG موقع اجرا
Section titled “۲) انتظار ARG موقع اجرا”مثال ۳: APP_VERSION در اجرا خالی بود. راهحل: اگر در اجرا لازم است، ENV APP_VERSION=$APP_VERSION بعد از ARG بگذار.
۳) کاربری که وجود ندارد
Section titled “۳) کاربری که وجود ندارد”mkdir -p m3printf 'FROM alpine\nUSER ghost\n' > m3/Dockerfiledocker build -t lx-di-user ./m3 >/dev/null 2>&1docker run --rm lx-di-user echo hi 2>&1 | head -1docker: Error response from daemon: unable to find user ghost: no matching entries in passwd fileUSER در build خطا نمیدهد، ولی هنگام اجرا کاربر پیدا نمیشود. راهحل: اول کاربر را با RUN adduser (یا useradd) بساز، بعد USER.
۴) COPY با مسیر نسبی اشتباه
Section titled “۴) COPY با مسیر نسبی اشتباه”mkdir -p m4printf 'FROM alpine\nCOPY missing.txt /x\n' > m4/Dockerfiledocker 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 برای کپی ساده
Section titled “۵) ADD برای کپی ساده”ADD ممکن است تار را باز کند یا از URL بخواند و رفتار غیرمنتظره بدهد. راهحل: COPY.
یک Dockerfile با FROM alpine، WORKDIR /w و CMD ["pwd"] بساز و اجرا کن؛ چه چیزی چاپ میشود؟
دیدن جواب
mkdir -p e1printf 'FROM alpine\nWORKDIR /w\nCMD ["pwd"]\n' > e1/Dockerfiledocker build -t lx-di-run ./e1 >/dev/null 2>&1docker run --rm lx-di-run/wبا ARG NAME=world و RUN echo "hello $NAME" > /msg یک image بساز؛ یک بار با پیشفرض و یک بار با --build-arg NAME=ali build کن و ثابت کن پیام فرق میکند.
دیدن جواب
mkdir -p e2printf 'FROM alpine\nARG NAME=world\nRUN echo "hello $NAME" > /msg\nCMD ["cat","/msg"]\n' > e2/Dockerfiledocker build -t lx-di-env ./e2 >/dev/null 2>&1; docker run --rm lx-di-envdocker build -t lx-di-env --build-arg NAME=ali ./e2 >/dev/null 2>&1; docker run --rm lx-di-envhello worldhello aliتمرین اصلی: یک Dockerfile با همهی دستورات اصلی (FROM، ARG، ENV، WORKDIR، COPY، RUN، USER، EXPOSE، CMD) بنویس که یک فایل متنی را چاپ کند و با کاربر غیر root اجرا شود. ثابت کن کاربر root نیست.
دیدن جواب
mkdir -p e3 && echo "سلام از فایل کپیشده" > e3/hello.txtcat > e3/Dockerfile <<'LXEOF'FROM alpineARG USERNAME=workerENV APP_HOME=/opt/appWORKDIR $APP_HOMECOPY hello.txt .RUN adduser -D $USERNAME && chown $USERNAME hello.txtUSER $USERNAMEEXPOSE 8080CMD ["sh","-c","whoami; cat hello.txt"]LXEOFdocker build -t lx-di-all ./e3 >/dev/null 2>&1docker run --rm lx-di-allworkerسلام از فایل کپیشدهآزمونک
Section titled “آزمونک”RUN چه موقع اجرا میشود؟
نتیجهی RUN در یک لایهی image ذخیره میشود.
تفاوت اصلی COPY و ADD؟
برای کپی ساده COPY را بنویس.
ARG در کانتینر در حال اجرا…
اگر در اجرا لازم است، آن را به ENV تبدیل کن.
EXPOSE چه میکند؟
باز کردن واقعی با -p یا -P است.
چرا WORKDIR بهتر از RUN cd است؟
هر RUN با پوشهی کار تازه شروع میشود.
جمعبندی
Section titled “جمعبندی”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 | دستورهای ساخت هر لایه |