توی این درس یاد میگیری تا حالا image ها را از Docker Hub میگرفتی؛ حالا خودت image میسازی. میفهمی Dockerfile چیست (یک فایل متنی که دستور ساخت image را مینویسد)، build context یعنی چه و چرا نقطهی آخر docker build -t myapp . مهم است، چطور به image اسم و tag میدهی (-t) و آن را با docker run اجرا میکنی. در انتها یک image میسازی که یک صفحهی HTML سرو میکند.
تشبیه: دستور پخت
Section titled “تشبیه: دستور پخت”Dockerfile مثل دستور پخت است: فهرست مواد (پایهی کار) و مراحل کار به ترتیب. وقتی آن را به آشپز میدهی (docker build)، یک غذای آمادهی بستهبندیشده (image) تحویل میگیری که هر وقت خواستی گرمش کنی (docker run). فرق مهم: Dockerfile متن است و میتوانی آن را در git نگه داری و با همکارت به اشتراک بگذاری؛ هر کس از رویش build کند، همان image را میگیرد.
از Dockerfile تا کانتینر
Section titled “از Dockerfile تا کانتینر”مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: کوچکترین Dockerfile
Section titled “مثال ۱: کوچکترین Dockerfile”یک پوشهی خالی با یک فایل به اسم دقیق Dockerfile (بدون پسوند):
FROM alpineCMD ["echo", "سلام از داخل image من"]حالا ساختش (-t اسم میدهد؛ نقطهی آخر context است، یعنی «پوشهی فعلی»):
docker build -t lx-hello . 2>&1 | grep -E '^#[0-9]+ (\[|DONE)|naming to'docker run --rm lx-hello#1 [internal] load build definition from Dockerfile#1 DONE 0.0s#2 [internal] load metadata for docker.io/library/alpine:latest#2 DONE 0.0s#3 [internal] load .dockerignore#3 DONE 0.0s#4 [1/1] FROM docker.io/library/alpine:latest@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6#5 naming to docker.io/library/lx-hello:latest done#5 DONE 0.0sسلام از داخل image منFROM alpine: شروع از image پایهیalpine. هر Dockerfile باFROMشروع میشود.CMD [...]: دستوری که وقتی کانتینر شروع شد اجرا شود.
خروجی docker build مرحلهبهمرحله است: هر #N یک گام است (load build definition خواندن Dockerfile، load metadata گرفتن اطلاعات image پایه، …). در پایان naming to ... اسم image را میگوید.
مثال ۲: image که یک صفحهی HTML سرو میکند
Section titled “مثال ۲: image که یک صفحهی HTML سرو میکند”تمرین اصلی درس. به جای alpine از nginx:alpine شروع میکنیم و صفحهی خودمان را داخلش میگذاریم:
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><title>سایت من</title></head><body><h1>سلام از image خودم!</h1></body></html>FROM nginx:alpineCOPY index.html /usr/share/nginx/html/index.htmldocker build -t lx-site:1.0 ./site 2>&1 | grep -E '^#[0-9]+ \[|naming to'docker run -d --name lx-fd1 -p 8350:80 lx-site:1.0 >/dev/nullsleep 2curl -s localhost:8350 | grep h1#1 [internal] load build definition from Dockerfile#2 [internal] load metadata for docker.io/library/nginx:alpine#3 [internal] load .dockerignore#4 [internal] load build context#5 [1/2] FROM docker.io/library/nginx:alpine@sha256:df221db836e1754089190208cee7eeda94f233197056426eda74a43ab1abeac2#6 [2/2] COPY index.html /usr/share/nginx/html/index.html#7 naming to docker.io/library/lx-site:1.0 done<body><h1>سلام از image خودم!</h1></body>اینجا بهجای . مسیر ./site را دادهایم: context میتواند هر پوشهای باشد، و Dockerfile داخل آن پوشه خودکار پیدا میشود. دستور COPY مبدأ مقصد فایل را از context (پوشهی پروژه) به داخل image کپی میکند. image نهایی همهچیز لازم برای اجرا را دارد: nginx، تنظیماتش و صفحهی ما. حالا این image را میتوانی به هر جایی ببری (درس tag و push).
در docker build -t myapp . نقطهی آخر یعنی چه؟
نقطه یعنی پوشهی فعلی؛ داکر محتوای آن را (و Dockerfile را) برای build میفرستد.
مثال ۳: context و حجمش
Section titled “مثال ۳: context و حجمش”وقتی docker build میزنی، داکر کل محتوای context را برای builder میفرستد. اگر پوشهات پر از فایلهای بیربط باشد، build کند میشود:
mkdir -p ctx-bigprintf 'FROM alpine\nCOPY . /app\nCMD ["ls","/app"]\n' > ctx-big/Dockerfiledd if=/dev/zero of=ctx-big/big.bin bs=1m count=40 2>/dev/nullecho "با فایل ۴۰ مگابایتی:"docker build --no-cache -t lx-ctx ./ctx-big 2>&1 | grep -E 'transferring context: [0-9.]+(MB|kB|B) [0-9.]+s done'rm ctx-big/big.binecho "بعد از پاک کردنش:"docker build --no-cache -t lx-ctx ./ctx-big 2>&1 | grep -E 'transferring context: [0-9.]+(MB|kB|B) [0-9.]+s done'docker rmi lx-ctx >/dev/nullبا فایل ۴۰ مگابایتی:#5 transferring context: 41.95MB 0.6s doneبعد از پاک کردنش:#4 transferring context: 31B 0.0s doneاین Dockerfile با COPY . /app کل context را لازم دارد. دفعهی اول یک فایل ۴۰ مگابایتی در context بود و داکر حدود ۴۰ مگابایت را فرستاد؛ بعد از پاک کردنش فقط چند بایت. (BuildKit هوشمند است و فقط فایلهایی را میفرستد که واقعاً لازماند؛ ولی با COPY . . یعنی همه.) (درس .dockerignore نشان میدهد چطور فایلهای اضافه را از context بیرون نگه داری.) اگر Dockerfile در پوشهی دیگری است، با -f مسیر معرفیاش میکنی: docker build -f deploy/Dockerfile ..
مثال ۴: اسم و tag
Section titled “مثال ۴: اسم و tag”-t اسم:tag به image برچسب میدهد؛ میتوانی چند -t بدهی:
docker build -t lx-site:1.0 -t lx-site:latest ./site 2>&1 | grep -c "naming to"docker images lx-site --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}'2REPOSITORY TAG IMAGE IDlx-site 1.0 5f080625ebb7lx-site latest 5f080625ebb7هر دو tag به یک image (یک ID) اشاره میکنند. اگر -t ندهی، image فقط با شناسهی هش ساخته میشود و اسم ندارد (dangling)؛ پس همیشه -t بگذار.
مثال ۵: بازسازی بعد از تغییر
Section titled “مثال ۵: بازسازی بعد از تغییر”cat > site/index.html <<'LXEOF'<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><title>سایت من</title></head><body><h1>نسخهی دوم</h1></body></html>LXEOFdocker build -t lx-site:2.0 ./site 2>&1 | grep -E '^#[0-9]+ \[|CACHED'docker run -d --name lx-fd2 -p 8351:80 lx-site:2.0 >/dev/null; sleep 2curl -s localhost:8351 | grep h1curl -s localhost:8350 | grep h1#1 [internal] load build definition from Dockerfile#2 [internal] load metadata for docker.io/library/nginx:alpine#3 [internal] load .dockerignore#4 [internal] load build context#5 [1/2] FROM docker.io/library/nginx:alpine@sha256:df221db836e1754089190208cee7eeda94f233197056426eda74a43ab1abeac2#6 [2/2] COPY index.html /usr/share/nginx/html/index.html#6 CACHED<body><h1>نسخهی دوم</h1></body><body><h1>سلام از image خودم!</h1></body>بعد از عوض کردن فایل، image جدید ساختیم و کانتینر جدید تغییر را نشان داد؛ ولی کانتینر قبلی (روی ۸۳۵۰) هنوز نسخهی قدیمی را دارد. image ثابت است: عوض کردن فایل روی دیسک، image موجود را عوض نمیکند؛ باید دوباره build کنی و کانتینر را از image تازه بسازی.
مثال ۶: ببین داخل image چیست
Section titled “مثال ۶: ببین داخل image چیست”docker history lx-site:2.0 --format 'table {{.CreatedBy}}\t{{.Size}}' | head -4docker image inspect lx-site:2.0 --format 'تعداد لایهها: {{len .RootFS.Layers}} | دستور پیشفرض: {{.Config.Cmd}}'CREATED BY SIZECOPY index.html /usr/share/nginx/html/index.… 24.6kBRUN /bin/sh -c set -x && apkArch="$(cat … 51.4MBENV ACME_VERSION=0.4.1 0Bتعداد لایهها: 9 | دستور پیشفرض: [nginx -g daemon off;]بالاترین ردیفها لایهی COPY ما (چند بایت) روی لایههای nginx:alpine است. image ما = image پایه + یک لایهی کوچک.
پشت پرده: وقتی docker build اجرا میشود
Section titled “پشت پرده: وقتی docker build اجرا میشود”۱) CLI محتوای context (و Dockerfile) را برای builder میفرستد (BuildKit، که داخل daemon است). ۲) builder دستورهای Dockerfile را یکییکی اجرا میکند؛ هر دستور مؤثر (مثل RUN و COPY) یک لایهی جدید میسازد. ۳) نتیجه یک image (فهرست لایهها + تنظیمات) است که با هش مشخص میشود و نام و tag به آن چسبانده میشود. چون هر مرحله را میشود کش کرد، build دوم معمولاً خیلی سریعتر است (درس لایهها و کش).
دو نکتهی مهم: Dockerfile فقط یک دستور ساخت است، نه محیط اجرا: دستورهایش هنگام build اجرا میشوند، نه هنگام run (بهجز CMD که فقط ثبت میشود). و context محدودهی دید build است: COPY فقط به فایلهای داخل context دسترسی دارد (اشتباه ۲ را ببین).
جدولهای مرجع
Section titled “جدولهای مرجع”| دستور Dockerfile | کار |
|---|---|
FROM image |
image پایه (اولین دستور) |
COPY src dst |
کپی از context به image |
CMD [...] |
دستور پیشفرض اجرای کانتینر |
| دستور / گزینه | کار |
|---|---|
docker build -t N:T . |
ساخت با اسم و tag از context فعلی |
docker build -f FILE . |
Dockerfile با اسم یا مسیر دیگر |
docker build -t a:1 -t a:latest . |
چند tag برای یک image |
docker build --no-cache . |
بدون استفاده از کش |
docker run IMAGE |
اجرای کانتینر از image ساختهشده |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) فایل Dockerfile پیدا نشد
Section titled “۱) فایل Dockerfile پیدا نشد”mkdir -p empty-dirdocker build -t lx-none ./empty-dir 2>&1 | grep -E 'ERROR'ERROR: failed to build: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directoryدر پوشهی فعلی فایلی به اسم دقیق Dockerfile (با D بزرگ، بدون پسوند) نیست. راهحل: پوشهی درست را بررسی کن یا با -f مسیر را بده.
۲) COPY فایلی که خارج از context است
Section titled “۲) COPY فایلی که خارج از context است”mkdir -p ctx-badprintf 'FROM alpine\nCOPY ../outside.txt /x\n' > ctx-bad/Dockerfileecho hi > outside.txtdocker build -t lx-bad ./ctx-bad 2>&1 | grep -E 'ERROR'#5 ERROR: failed to calculate checksum of ref 3uko94hpx8qfnep9m1ut84163::rrlhl6xio3ek92jg27hvkfh3f: "/outside.txt": not foundERROR: failed to build: failed to solve: failed to compute cache key: failed to calculate checksum of ref 3uko94hpx8qfnep9m1ut84163::rrlhl6xio3ek92jg27hvkfh3f: "/outside.txt": not foundCOPY فقط به داخل context دسترسی دارد؛ .. از آن بیرون میزند. راهحل: فایل را داخل پوشهی پروژه بگذار، یا context را عوض کن (docker build -f proj/Dockerfile .).
۳) فراموش کردن context (نقطهی آخر)
Section titled “۳) فراموش کردن context (نقطهی آخر)”docker build -t lx-x 2>&1 | head -2ERROR: docker: 'docker buildx build' requires 1 argumentdocker build یک مسیر context میخواهد. راهحل: . را بنویس.
۴) tag ندادن
Section titled “۴) tag ندادن”بدون -t، image بیاسم میشود و پیدا کردنش سخت. راهحل: همیشه -t اسم:نسخه.
۵) تغییر فایل و انتظار تغییر کانتینر قدیمی
Section titled “۵) تغییر فایل و انتظار تغییر کانتینر قدیمی”مثال ۵: image و کانتینر موجود عوض نمیشوند. راهحل: دوباره build و کانتینر را از image جدید بساز.
یک Dockerfile با FROM alpine و CMD ["echo","hello"] بساز، با اسم lx-t1 build کن و اجرایش کن.
دیدن جواب
mkdir -p ex1printf 'FROM alpine\nCMD ["echo","hello"]\n' > ex1/Dockerfiledocker build -t lx-t1 ./ex1 2>&1 | grep -c "naming to"docker run --rm lx-t1docker rmi lx-t1 >/dev/null2helloتمرین اصلی: یک image بساز که صفحهی HTML تو (با متن خوش آمدی) را سرو کند، روی پورت 8352 اجرا کن و با curl ثابت کن متن را میبینی.
دیدن جواب
mkdir -p ex2echo '<h1>خوش آمدی</h1>' > ex2/index.htmlprintf 'FROM nginx:alpine\nCOPY index.html /usr/share/nginx/html/index.html\n' > ex2/Dockerfiledocker build -t lx-t2 ./ex2 2>&1 | grep -c "naming to"docker run -d --name lx-ex2 -p 8352:80 lx-t2 >/dev/null; sleep 2curl -s localhost:8352docker rm -f lx-ex2 >/dev/null; docker rmi lx-t2 >/dev/null2<h1>خوش آمدی</h1>دو tag (1.0 و latest) برای یک image بساز، ثابت کن ID یکی است، بعد فایل را عوض کن و فقط 1.1 را build کن؛ ثابت کن حالا 1.0 و 1.1 ID متفاوت دارند و latest هنوز به نسخهی قدیمی اشاره میکند (tag قابل جابهجایی است).
دیدن جواب
mkdir -p ex3echo '<p>v1</p>' > ex3/index.htmlprintf 'FROM nginx:alpine\nCOPY index.html /usr/share/nginx/html/\n' > ex3/Dockerfiledocker build -t lx-t3:1.0 -t lx-t3:latest ./ex3 >/dev/null 2>&1echo '<p>v2</p>' > ex3/index.htmldocker build -t lx-t3:1.1 ./ex3 >/dev/null 2>&1docker images lx-t3 --format '{{.Tag}} {{.ID}}' | sortdocker rmi lx-t3:1.0 lx-t3:latest lx-t3:1.1 >/dev/null1.0 82b51a1b686d1.1 0015d43a8489latest 82b51a1b686d1.0 و latest همID هستند؛ 1.1 ID جدید دارد. latest خودکار به آخرین build نمیپرد؛ باید خودت tag بزنی.
آزمونک
Section titled “آزمونک”Dockerfile چیست؟
docker build آن را میخواند و image میسازد.
اولین دستور هر Dockerfile معمولاً چیست؟
FROM image پایه را مشخص میکند.
build context چیست؟
نقطه در docker build . یعنی context همین پوشه است.
اگر index.html را عوض کنی، کانتینر قبلاً در حال اجرا چه میشود؟
image ثابت است.
با -t lx-site:1.0 چه میکنی؟
بدون -t image بیاسم (dangling) میماند.
جمعبندی
Section titled “جمعبندی”- Dockerfile دستور ساخت image است؛
docker build -t اسم:tag .از رویش image میسازد وdocker runاجرایش میکند. FROM(پایه)،COPY(کپی از context)،CMD(دستور پیشفرض) سه دستور اولیهاند.- context پوشهایست که برای build فرستاده میشود؛
COPYفقط به آن دسترسی دارد و حجمش روی سرعت اثر دارد. - image ثابت است؛ تغییر فایل یعنی build مجدد و کانتینر جدید.
- همیشه
-tبگذار؛ یک image میتواند چند tag داشته باشد.
| دستور | کاری که میکند |
|---|---|
docker build -t myapp . | ساخت image با اسم از context فعلی |
docker build -t myapp:1.0 -t myapp:latest . | چند tag |
docker build -f path/Dockerfile . | Dockerfile دیگر |
docker run --rm myapp | اجرای image ساختهشده |
docker images myapp | فهرست image های یک اسم |
docker history myapp | لایههای image |
FROM image | image پایه |
COPY src dst | کپی فایل از context |
CMD ["cmd","arg"] | دستور پیشفرض |