multi-stage build و کوچککردن image
Multi-stage builds and reducing image size
در فصل قبل image خودت را build کردی. حالا سؤال تازه این است: چرا imageای که درست کار میکند، هنوز ابزارهایی را با خودش حمل میکند که برنامه در زمان اجرا به آنها نیاز ندارد؟
In the previous chapter you built your own image. Now comes the next question: why can a working image still carry tools that the application never needs at runtime?
image کار میکند؛ پس چرا هنوز سنگین است؟The image works—so why is it still heavy?
فصل قبل بالاخره image خودت را ساختی و با curl هم دیدی که برنامه جواب میدهد. خیلی طبیعی است همینجا بگویی: «خب، تمام شد دیگر!» اما بیایید قبل از تمامکردن کار، درِ image را باز کنیم. برنامهٔ ما برای اجرا فقط یک فایل JavaScript و خود Node را لازم دارد؛ پس TypeScript، فایلهای منبع و وابستگیهای توسعه آن تو چه کار میکنند؟
In Chapter 4 you put a small application into an image and ran a container. If curl returns the expected response, it is tempting to call the job finished. Ask one more question: does the program really need TypeScript, the compiler, source files, and development dependencies while it is running?
قرار نیست مستقیم سراغ راهحل برویم. اول یک image تکمرحلهای میسازیم که کاملاً کار میکند، ولی عمداً وسایل کارگاه را هم با محصول نهایی داخل یک جعبه میگذارد. بعد اندازهاش میگیریم و میبینیم دقیقاً چه چیزهایی بیدلیل همراه برنامه ماندهاند. وقتی مشکل را با چشم دیدیم، multi-stage build دیگر یک دستور حفظی نیست؛ جواب یک سؤال واقعی است.
To answer, we will first build a single-stage image. It intentionally keeps everything: source files, node_modules, TypeScript, and the build output. That is not wrong as a starting point. The problem begins when we assume the build environment and the runtime environment must be identical.
پیکانها فقط مسیر کار را نشان میدهند: کد منبع با ابزارهای build تبدیل به فایل قابلاجرا میشود و بخش زمان اجرا همان فایل را اجرا میکند. سؤال سادهٔ فصل این است: اگر اره و پیچگوشتی برای ساخت محصول لازم بودهاند، چرا باید داخل جعبهٔ محصولی باشند که به مشتری میدهیم؟
Arrows show transformations: build tools turn source into an artifact, and the runtime executes that artifact. This chapter asks why the build tools should remain in the final image.
پس فعلاً چیزی را «بهینه» نکن. اول image را اندازه بگیر و docker image history را نگاه کن. تا وقتی نفهمیدهای چه چیزی جا گرفته، کوچککردن image بیشتر شبیه حدسزدن است تا مهندسی.
مرحلهٔ builder را مثل کارگاه در نظر بگیر: کامپایلر، فایلهای منبع و ابزارهای ساخت آنجاست. image نهایی مثل جعبهای است که محصول آماده را تحویل میدهی. قرار نیست دستگاه برش و میز کار را هم داخل جعبه بگذاری؛ فقط چیزی را میبری که برای استفاده لازم است.
Think of the builder stage as the workshop: compiler, source, and build tools live there. The final image is the box you ship. You do not ship the workbench and cutting tools with the finished product; you copy only what is needed to use it.
Before introducing the solution, measure the image and read its history. If you do not know what made it large, shrinking it is only guesswork.
آزمایش اول: image سالم را اندازه بگیرExperiment one: measure the working image
برای اینکه بحث گم نشود، یک پروژهٔ خیلی کوچک به نام demo-ts داریم. برنامه فقط یک پاسخ HTTP ساده میدهد. TypeScript را هم نه برای آموزش TypeScript، بلکه فقط برای این آوردهایم که یک مرحلهٔ واقعیِ «ساختن» داشته باشیم: چیزی وارد build شود و در پایان فایل دیگری از آن بیرون بیاید.
Consider a small project named demo-ts. The program uses only Node’s built-in HTTP API; TypeScript is here only to make the difference between source and artifact visible, not to turn this into a TypeScript course.
demo-ts/ ├── src/server.ts ├── package.json ├── package-lock.json ├── tsconfig.json └── .dockerignore
در package.json، scriptِ build، TypeScript را اجرا میکند و خروجی را در dist/server.js میگذارد. package-lock.json هم باید کنار manifest باشد تا npm ci همان وابستگی tree قفلشده را نصب کند.
In package.json, the build script runs TypeScript and writes dist/server.js. package-lock.json must sit beside the manifest so npm ci installs the locked dependency tree.
$ docker image ls demo-ts REPOSITORY TAG IMAGE ID CREATED SIZE demo-ts single 91ab... seconds ago 182MB $ docker image history demo-ts:single IMAGE CREATED CREATED BY SIZE 91ab... seconds ago CMD ["node","dist/server.js"] 0B <missing> seconds ago RUN npm run build 4.8MB <missing> seconds ago COPY . . 12.4MB <missing> seconds ago RUN npm ci 66MB <missing> seconds ago FROM node:22-bookworm-slim 99MB
به عددها نچسب. این اندازهها نمونهاند و روی سیستم تو با توجه به tag، معماری و image پایه فرق میکنند. چیزی که باید ببینی خودِ الگوست: image نهایی فقط dist/server.js را اجرا میکند، ولی هنوز فایلهای منبع، TypeScript و وابستگیهای توسعه را هم با خودش حمل میکند.
This is not a measurement from one particular machine. The numbers are representative; tags, architecture, the base image, and build time change the size. The important clue is that source files and development dependencies live in the same image that is supposed to run only dist/server.js.
| Single-stage content | Needed to build? | Needed to run this app? |
|---|---|---|
| TypeScript compiler | بلهYes | خیرNo |
src/server.ts | بلهYes | خیر؛ runtime کد JS را میخواندNo; runtime reads JS |
dev node_modules | بلهYes | خیرNo |
dist/server.js | خروجی buildBuild output | بلهYes |
| Node runtime | برای اجرای script build هم میتواند لازم باشدMay also run the build script | بلهYes |
چند FROM یعنی چند محیط جداMultiple FROM instructions mean separate environments
اولین فکری که ممکن است به ذهن برسد این است: «آخر build فایلهای اضافه را پاک میکنم.» میشود، اما راه تمیزتری داریم. در Dockerfile چند FROM مینویسیم و برای هر مرحله یک محیط جدا میسازیم. محیط اول کار ساخت را انجام میدهد؛ محیط آخر از یک image پایهٔ تازه شروع میشود و فقط چیزهایی را میگیرد که صریحاً به آن منتقل کنیم.
The solution is not manually deleting files from the same stage. We use multiple FROM instructions. Each FROM starts a new stage with its own base and filesystem; only what you explicitly bring to the final stage gets there.
در مسیر تکمرحلهای، یک فایلسیستم همهچیز را نگه میدارد. در مسیر چندمرحلهای، پیکانها build و سپس انتقال صریح خروجی build را نشان میدهند؛ کامپایلر خودبهخود به image نهایی نمیرود.
In the single-stage path, one filesystem keeps everything. In the multi-stage path, arrows show the build and the explicit artifact transfer; the compiler does not appear in the final image automatically.
مرحلهٔ اول را builder صدا میزنیم؛ چون کارش ساختن است. مرحلهٔ آخر جایی است که برنامه واقعاً اجرا میشود. اسمگذاری کمک میکند وقتی مینویسی COPY --from=builder دقیقاً معلوم باشد از کدام محیط چیزی برمیداری؛ لازم نیست شمارهٔ مرحلهها را حفظ کنی.
In our example, we name the first stage builder. The last stage is the runtime. Naming matters because COPY --from=builder refers to a stage’s role rather than a fragile number. If you later add a test stage, the name remains readable.
Docker همهٔ stageها را برای cache و build مقصد میشناسد، اما image معمولی که بدون --target میسازی از آخرین stage میآید. وجود builder به معنی وجود فایلهای builder در نهایی نیست.
Docker knows every stage for cache and build targets, but a normal build without --target produces the last stage. The existence of a builder does not put builder files in the final image.
مثال کوچک: TypeScript فقط نقش build tool داردA small example: TypeScript is only the build tool
برنامهٔ ما عمداً ساده است: یک HTTP server که روی پورت ۳۰۰۰ جواب میدهد. اگر این برنامه را با Node معمولی بنویسی، runtime فقط فایل JavaScript خروجی را میخواهد. پس میتوانیم وابستگیهای build را در جایی نگه داریم که خروجی build را میسازد.
Our application is intentionally small: an HTTP server that answers on port 3000. If you wrote it directly in Node, the runtime would need only the generated JavaScript file. That lets us keep build dependencies where they produce the artifact.
// src/server.ts
import { createServer } from "node:http";
const port = Number(process.env.PORT ?? 3000);
const server = createServer((_request, response) => {
response.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
response.end("hello from a multi-stage image\n");
});
server.listen(port, "0.0.0.0", () => {
console.log("listening on " + port);
});
در یک پروژهٔ واقعی، قبل از build باید npm install --package-lock-only یا نصب معمولی، lockfile را ایجاد کند. اینجا نسخهٔ lockfile را کنار پروژه فرض میکنیم؛ چون بدون آن npm ci عمداً متوقف میشود.
In a real project, npm install --package-lock-only or a normal install must create the lockfile before the build. We assume the lockfile is committed beside the project; without it, npm ci intentionally stops.
{
"private": true,
"scripts": {
"build": "tsc",
"start": "node dist/server.js"
},
"devDependencies": {
"@types/node": "22.10.0",
"typescript": "5.7.2"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
این فایل فقط قرارداد خروجی را روشن میکند: هر فایل TypeScript از src به JavaScript در dist میرود. اگر این قرارداد عوض شود، مسیر COPY --from هم باید دوباره بررسی شود.
This file makes the output contract explicit: TypeScript files under src become JavaScript under dist. If that contract changes, re-check the COPY --from path too.
این وابستگیها برای کامپایل لازماند، نه برای اجرای dist/server.js. چون برنامه فقط از API داخلی Node استفاده میکند، runtime به بستهٔ محیط واقعی جداگانهای نیاز ندارد. این انتخاب، تفاوت دو stage را واضح و قابلاندازهگیری میکند.
These dependencies are needed to compile, not to execute dist/server.js. Because the app uses only Node’s built-in APIs, the runtime needs no separate production package. That makes the difference between the two stages clear and measurable.
نسخهٔ A: اول image کارا بسازVersion A: first build a working image
نسخهٔ A را عمداً ساده و تکمرحلهای نگه میداریم. هنوز دنبال image کوچک نیستیم؛ اول یک نسخهٔ سالم میخواهیم که بتوانیم بعداً منصفانه با نسخهٔ چندمرحلهای مقایسهاش کنیم. اگر نقطهٔ شروع سالم نباشد، هر مقایسهای بعدش بیمعنی میشود.
Version A is intentionally single-stage. We are not optimizing yet; we first need a runnable baseline so the later comparison means something.
FROM node:22-bookworm-slim WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build EXPOSE 3000 CMD ["node", "dist/server.js"]
دستورها را یک بار آرام از بالا به پایین بخوان. فایلهای وابستگی زودتر میآیند تا cache با هر تغییر کوچک کد از بین نرود؛ بعد کد منبع وارد میشود و TypeScript آن را میسازد. مشکل اینجاست که همان محیطی که ابزار ساخت را لازم داشته، قرار است image نهایی هم باشد؛ بنابراین همهٔ وسایل کارگاه همانجا میمانند.
Read the instructions slowly: the manifest and lockfile come first so dependency caching is separated from source changes; then source is copied and compiled. The result still keeps node_modules, source, and compiler tools beside the artifact.
$ docker build -f Dockerfile.single -t demo-ts:single . [+] Building 8.6s (10/10) FINISHED => [internal] load build definition from Dockerfile.single => [5/6] RUN npm run build => exporting to image => naming to docker.io/library/demo-ts:single $ docker run --rm -d --name demo-single -p 8080:3000 demo-ts:single 7d91... $ curl -sS localhost:8080 hello from a multi-stage image
این خروجی دو چیز را ثابت میکند: build خروجی build ساخته شده و runtime پاسخ میدهد. هنوز دربارهٔ اندازه یا امنیت نتیجهای نگرفتهایم؛ برای آن باید image را جداگانه بررسی کنیم.
This output proves two things: the build produced an artifact and the runtime answers. It says nothing yet about size or security; those require separate image inspection.
نسخهٔ B: builder و runtime را جدا کنVersion B: separate builder and runtime
حالا همان پروژه را دو اتاق میکنیم. اتاق اول، builder، همهٔ ابزارهای ساخت را دارد و فایل نهایی را تولید میکند. اتاق دوم از یک image پایهٔ تمیز شروع میشود. از اتاق اول فقط dist را برمیداریم و میآوریم اینجا. همین جدایی، تمام ایدهٔ multi-stage build است.
Now we have two FROM instructions. The first stage compiles the source. The second starts from a fresh base and takes only the output file from the builder. That separation is the heart of a multi-stage build.
FROM node:22-bookworm-slim AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY tsconfig.json ./ COPY src ./src RUN npm run build FROM node:22-bookworm-slim AS runtime WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/dist ./dist EXPOSE 3000 CMD ["node", "dist/server.js"]
FROM ... AS builder stage را نامگذاری میکند. COPY --from=builder /app/dist ./dist میگوید کد منبع این COPY فایلسیستم stage builder است، نه build context میزبان. مقصد ./dist هم نسبت به WORKDIR /app در runtime محاسبه میشود.
FROM ... AS builder names the stage. COPY --from=builder /app/dist ./dist says that the source filesystem is the builder stage, not the host build context. The destination ./dist is resolved relative to WORKDIR /app in the runtime stage.
| Concern | Build stage | Runtime stage |
|---|---|---|
| Base purpose | محیط ساختBuild environment | محیط اجراExecution environment |
| Source TypeScript | کپی میشودCopied | کپی نمیشودNot copied |
| TypeScript and dev dependencies | نصب و مصرف میشوندInstalled and used | وارد نمیشوندNever added |
dist/server.js | تولید میشودProduced | با COPY --from وارد میشودCopied with COPY --from |
| Final command | لازم نیستNot required | اینجا تعریف میشودDefined here |
این نکته خیلی مهم است: نصب کامپایلر در builder به این معنی نیست که در مرحلهٔ نهایی هم وجود دارد. دو مرحله فایلسیستم جدا دارند. اگر چیزی را لازم داری، باید آگاهانه منتقلش کنی؛ Docker قرار نیست حدس بزند کدام فایل برای اجرای برنامه مهم است.
Installing a compiler in the builder does not make it present in the runtime. Stages do not share files by magic; transfer must be explicit through COPY --from or by choosing a previous stage as a base with FROM.
COPY --from را مثل یک آدرس دقیق بخوانRead COPY --from as a path contract
این دستور را مثل یک آدرس سهقسمتی بخوان: builder میگوید از کدام مرحله بخوان؛ /app/dist میگوید فایل در آن مرحله کجاست؛ و ./dist میگوید در مرحلهٔ فعلی کجا قرارش بده. اگر یکی از این سه را اشتباه بگیری، build همانجا میایستد و هنوز اصلاً به اجرای container نرسیدهای.
The three names in this instruction have different roles: builder is a stage name; /app/dist is an absolute path inside the builder filesystem; ./dist is a destination in the runtime filesystem. If you confuse any of them with a laptop path, the build fails before runtime begins.
پیکان اول خواندن خروجی build از stage builder و پیکان دوم نوشتن آن در runtime را نشان میدهد؛ این مسیر درخواست شبکه یا copy از host نیست.
The first arrow reads the artifact from the builder and the second writes it into runtime; this is not a network request or a copy from the host.
اگر شک کردی فایل واقعاً در builder ساخته شده یا نه، همان مرحله را جدا بساز: docker build --target builder .... این مثل این است که قبل از بستهبندی محصول، وارد خود کارگاه شوی و قفسهٔ خروجی را نگاه کنی. فقط حواست باشد image حاصل از --target builder همان image نهایی برنامه نیست.
For practice, build docker build --target builder -t demo-ts:builder . and inspect the stage separately. Targeting a stage helps debugging; do not confuse that tag with the final image.
آزمایش دوم: قبل و بعد را با همان سؤال مقایسه کنExperiment two: compare before and after with the same questions
حالا وقت مقایسه است، ولی فقط یک چیز را تغییر بده: ساختار تکمرحلهای در برابر چندمرحلهای. برنامه، خانوادهٔ image پایه و دستور اجرا را ثابت نگه دار. اگر همزمان همهچیز را عوض کنی، وقتی اندازه تغییر کرد دیگر نمیفهمی علتش multi-stage بوده یا تغییر دیگری.
Run both images with the same base family, application, and command. If you also change the base, architecture, and application, you will not know whether the difference came from multi-stage or from those other changes.
$ docker build -f Dockerfile.multi -t demo-ts:multi .
[+] Building 5.1s (11/11) FINISHED
=> [builder 5/5] RUN npm run build
=> [runtime 1/2] COPY --from=builder /app/dist ./dist
=> naming to docker.io/library/demo-ts:multi
$ docker image ls demo-ts --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY TAG SIZE
demo-ts single 182MB
demo-ts multi 118MB
$ docker image history demo-ts:multi
IMAGE CREATED CREATED BY SIZE
a82c... seconds ago CMD ["node","dist/server.js"] 0B
<missing> seconds ago COPY /app/dist 4.8MB
<missing> seconds ago FROM node:22-bookworm-slim 99MB
در این نمونه image کوچکتر شده چون فایلهای منبع، TypeScript و وابستگیهای توسعه اصلاً وارد مرحلهٔ نهایی نشدهاند. خبری از فشردهسازی جادویی نیست؛ فقط چیزهای غیرلازم را از همان ابتدا داخل جعبهٔ نهایی نگذاشتهایم.
In this representative sample, the image is smaller because dev dependencies, the compiler, and source files never entered the final image—not because of magical compression. The runtime base still contributes substantially, and shared layers may not be stored twice locally.
| Command | Question answered | Limit |
|---|---|---|
docker image ls | اندازهٔ image/tag چقدر است؟How large is the image/tag? | رفتار runtime را ثابت نمیکندDoes not prove runtime behavior |
docker image history | کدام instructionها layer ساختهاند؟Which instructions made layers? | جایگزین بررسی و test نیستNot a substitute for inspect or tests |
docker inspect | تنظیمات و دستور نهایی چیست؟What are the final config and command? | محتوای کامل هر layer را توضیح نمیدهدDoes not explain every layer’s content |
docker run + curl | برنامه واقعاً اجرا و پاسخگوست؟Does the app run and answer? | بهتنهایی image اندازه را نمیسنجدDoes not measure image size alone |
cache بین stageها: سریعتر شدن با حذف فایلها یکی نیستCache across stages: speed is not the same as final contents
اینجا دو موضوع خیلی راحت با هم قاطی میشوند. یکی این است که «چه چیزی داخل image نهایی میرود؟» و دیگری اینکه «در build بعدی کدام مرحله دوباره اجرا میشود؟» اولی دربارهٔ محتوای image نهایی است؛ دومی دربارهٔ cache و سرعت build. ممکن است مرحلهٔ builder کاملاً از cache استفاده شود، ولی هیچکدام از فایلهای اضافی آن وارد image نهایی نشوند.
Multi-stage has two separate questions: what enters the final image, and which steps rerun on the next build? The cache may speed up the builder even though builder files never enter the final image.
پیکانها وابستگی را نشان میدهند: تغییر بسته files، npm ci و stepهای بعدی را invalid میکند؛ تغییر کد منبع فقط از layer وابسته به کد منبع به بعد اثر میگذارد.
Arrows show dependencies: changing package files invalidates npm ci and later steps; changing source affects only the source-dependent layer and what follows it.
# 1 · fresh builder docker build -f Dockerfile.multi -t demo-ts:multi . # 2 · no file changed docker build -f Dockerfile.multi -t demo-ts:multi . # 3 · edit src/server.ts only docker build -f Dockerfile.multi -t demo-ts:multi . # 4 · edit package-lock.json docker build -f Dockerfile.multi -t demo-ts:multi .
| Change | Expected builder output | Why |
|---|---|---|
| Nothing | CACHED for dependency and source steps | ورودیها هماناندInputs are unchanged |
src/server.ts only | npm ci cached; compile reruns | وابستگی input عوض نشدهDependency input did not change |
package-lock.json | npm ci and downstream steps rerun | درخت وابستگی عوض شدهDependency tree changed |
| Builder cache removed | All relevant steps rerun | cache و image نهایی یکی نیستندCache and final image are different things |
نکتهٔ مهم: cache میتواند لایههای builder را روی ماشین نگه دارد، اما این به معنی کپیشدن آنها در image نهایی نیست. برای دیدن رفتار دقیق، --progress=plain بزن و به اولین stepی نگاه کن که دیگر CACHED نیست.
Important: the cache may keep builder layers on the machine, but that does not copy them into the final image. Use --progress=plain and find the first step that is no longer CACHED.
کوچکتر همیشه یعنی Alpine نیستSmaller does not automatically mean Alpine
عمداً سراغ imageهای خیلی عجیب و کوچک نرفتیم. اگر همزمان multi-stage را یاد بگیری و image پایه را هم عوض کنی، دوباره معلوم نیست نتیجه از کجا آمده. انتخاب image پایه باید با نیاز برنامه، کتابخانههای native، ابزارهای عیبیابی و قواعد تیم جور باشد؛ نه اینکه فقط کوچکترین عدد جدول را انتخاب کنیم.
We used node:22-bookworm-slim so the main question stays isolated. You can choose another runtime, but the choice should follow application needs, native-library compatibility, debugging tools, and team policy—not only the smallest number on the screen.
| Choice | Useful when | Question to verify |
|---|---|---|
| Debian slim | سازگاری و عیبیابی مهم استCompatibility and debugging matter | library native و معماری درست است؟Do native libraries and architecture match? |
| Alpine | با musl و محدودیتهایش آگاهانه انتخاب شدهYou knowingly choose musl and its trade-offs | برنامه و وابستگیها روی musl تست شدهاند؟Have the app and dependencies been tested on musl? |
| Distroless / minimal runtime | خروجی build و روش عیبیابی آماده استThe artifact and debugging method are ready | بدون shell چطور diagnose میکنیم؟How will we diagnose without a shell? |
کمشدن ابزارها و packageهای اضافی میتواند سطح حمله را کمتر کند، اما این یک میانبُر برای امنیت نیست. image کوچک هنوز ممکن است وابستگی آسیبپذیر، تنظیمات بد یا کاربر root داشته باشد. پس «کوچکتر» را با «امن» مساوی نگذار.
Multi-stage often makes an image cleaner and can reduce attack surface because fewer tools and packages remain at runtime. But “smaller equals secure” is not a valid claim: you still need separate checks for non-root execution, patches, dependencies, configuration, and application behavior.
وقتی COPY --from خراب میشود، هنوز به runtime نرسیدهایWhen COPY --from fails, runtime has not started yet
حالا عمداً یک اشتباه ساده بساز: نام مرحله را در COPY --from غلط بنویس. ممکن است متن خطا کمی عجیب باشد و حتی شبیه تلاش برای پیدا کردن image دیگری دیده شود. قبل از دستکاری ده خط Dockerfile، فقط یک سؤال بپرس: «اسمی که اینجا نوشتهام دقیقاً با FROM ... AS ... یکی است؟»
Create this failure deliberately: write COPY --from=build /app/dist ./dist while the stage is actually named builder. The error may mention pulling or not finding an image named build; wording varies by builder.
[runtime 1/2] COPY --from=build /app/dist ./dist ------ > [runtime 1/2] COPY --from=build /app/dist ./dist: ------ failed to solve: pull access denied for build, repository does not exist or may require authorization
این خطا هیچ ربطی به خراببودن container نهایی ندارد؛ container هنوز ساخته نشده. اگر نام مرحله درست بود، قدم بعدی مسیر فایل است. COPY --from فایل را از داخل همان مرحله میخواند، نه از پوشهای روی سیستم تو. همین تفکیک جلوی خیلی از تغییرهای تصادفی را میگیرد.
Docker is not saying that the final container is broken; no container has been created yet. First compare the name with FROM ... AS .... If the name is correct, check the path next: COPY --from reads from the root of the builder filesystem, not from the host’s current directory.
| Failure symptom | Likely boundary | Evidence-first next step |
|---|---|---|
| Unknown stage / pull denied | نام stageStage name | FROM ... AS builder و --from=builder را مقایسه کن |
| Source path not found | خروجی build مسیرArtifact path | docker build --target builder --progress=plain . و listing artifact |
| Build succeeds, app exits | runtime دستور / خروجی buildRuntime command / artifact | docker run، docker logs و docker inspect |
| Build succeeds, browser empty | port or app bindPort or app bind | PORTS، listen address و curl را به ترتیب بخوان |
دو خطا دیگر هم آموزشیاند: اگر RUN npm run build را حذف کنی، /app/dist تولید نمیشود؛ اگر بهجای /app/dist مسیر خیالی مثل /app/build بدهی، builder خروجی build را پیدا نمیکند. هر دو قبل از runtime رخ میدهند.
Two more failures are useful: if you remove RUN npm run build, /app/dist is never produced; if you use a fictional path such as /app/build instead of /app/dist, the builder cannot find the artifact. Both happen before runtime.
با docker build --target builder -t demo-ts:builder --progress=plain . مرحلهٔ ساخت را جدا ببین. میتوانی موقتاً بعد از build یک RUN ls -la /app/dist اضافه کنی و بعد از فهمیدن مسیر آن را حذف کنی.
Use docker build --target builder -t demo-ts:builder --progress=plain . to see the build stage separately. You can temporarily add RUN ls -la /app/dist after the build and remove it once you understand the path.
۱۸ تمرین: اول پیشبینی، بعد اجرا18 exercises: predict first, then run
این ۱۸ تمرین قرار نیست هجده بار یک قالب تکراری را پر کنی. بعضیجا فقط پیشبینی میکنی، بعضیجا خروجی build را میخوانی و بعضیجا باید قبل از تغییر Dockerfile بگویی مشکل احتمالاً کجاست. جواب را زود باز نکن؛ اول یک دلیل برای حدست بنویس.
The exercises are intentionally varied. Some ask you to measure size and history, some test stage and cache reasoning, and some ask you to name the broken boundary before touching the Dockerfile.
قبل از دیدن Dockerfile multi-stage، حدس بزن single-stage چه چیزهایی را کنار dist/server.js نگه میدارد.
Before seeing the multi-stage Dockerfile, predict what a single-stage image keeps beside dist/server.js.
پاسخ و دلیل · Solution and reasoning
فکر اصلی: single-stage معمولاً کد منبع، tsconfig، بسته-lock، node_modules و خود TypeScript را هم نگه میدارد، چون همان stage هم build را انجام داده و هم image نهایی شده است.
Conclusion: a single-stage image usually keeps source, tsconfig, package-lock, node_modules, and TypeScript because the same stage both performed the build and became the final image.
چطور چکش کنیم؟ با docker image history demo-ts:single و اندازهٔ image بررسی کن؛ دام این است که «کار میکند» را با «فقط چیز لازم را دارد» یکی بدانی.
Evidence and trap: check with docker image history demo-ts:single and the image size; the trap is equating “it works” with “it contains only what is needed.”
از جدول پروژه، دو مورد را نام ببر که برای build لازماند اما برای اجرای server.js لازم نیستند.
From the project table, name two things needed to build but not to run server.js.
پاسخ و دلیل · Solution and reasoning
جواب: TypeScript و وابستگیهای توسعهی node_modules نمونههای روشناند. کد منبع TypeScript هم ورودی build است، اما Node در runtime فایل JavaScript خروجی را میخواند.
Answer: TypeScript and the development dependencies in node_modules are clear examples. TypeScript source is also a build input, while Node reads the generated JavaScript at runtime.
شاهد و دام: docker run را با image multi اجرا کن؛ اگر فقط dist را کپی کردهای، نبودن کد منبع نشانهٔ نقص نیست. دام، تلاش برای اجرای tsc در runtime است.
Evidence and trap: run the multi-stage image; if only dist was copied, the absence of source is not a defect. The trap is trying to run tsc in the runtime.
در یک Dockerfile دو FROM داریم. هرکدام چه مرزی ایجاد میکنند؟
A Dockerfile has two FROM instructions. What boundary does each one create?
پاسخ و دلیل · Solution and reasoning
پیشبینی درست: هر FROM یک stage تازه با پایه و فایلسیستم مستقل شروع میکند. stage دوم از اولی فایل نمیبیند مگر اینکه آن را با COPY --from یا الگوی صریح دیگری منتقل کنی.
Conclusion: each FROM starts a new stage with its own base and filesystem. The second stage cannot see the first stage’s files unless you transfer them explicitly with COPY --from or another explicit pattern.
حواست به این دام باشد: در خروجی build دنبال نامهای [builder] و [runtime] بگرد. دام این است که فکر کنی دو stage فقط دو نام برای یک container هستند.
Evidence and trap: look for [builder] and [runtime] in build output. The trap is treating two stages as two names for one container.
چرا AS builder بهتر از اشاره به stage شمارهٔ صفر است؟
Why is AS builder better than referring to stage zero?
پاسخ و دلیل · Solution and reasoning
راه فکرکردن: نام builder نقش را توضیح میدهد و با جابهجایی یا اضافهشدن stageها کمتر شکننده است. عدد صفر فقط جایگاه فعلی را میگوید، نه دلیل وجود stage را.
Answer: the name builder explains the role and is less fragile when stages are reordered or added. Zero tells you only the current position, not the stage’s purpose.
شاهد و دام: COPY --from=builder را با COPY --from=0 مقایسه کن. هر دو ممکن است کار کنند، اما دام دوم وقتی stage جدیدی جلوتر اضافه شود، مسیر اشتباه را انتخاب میکند.
Evidence and trap: compare COPY --from=builder with COPY --from=0. Both may work, but the numeric form can select the wrong stage when a new stage is inserted earlier.
سه بخش COPY --from=builder /app/dist ./dist را توضیح بده.
Explain the three parts of COPY --from=builder /app/dist ./dist.
پاسخ و دلیل · Solution and reasoning
اینجا جواب روشن است: builder منبع stage است؛ /app/dist مسیر کد منبع در ریشهٔ فایلسیستم همان stage است؛ ./dist مقصد در stage فعلی و نسبتدادهشده به WORKDIR است.
Answer: builder selects the source stage; /app/dist is the source path from that stage’s filesystem root; ./dist is the destination in the current stage, relative to its WORKDIR.
دام: این دستور از host فایل نمیخواند. اگر میخواهی از context میزبان copy کنی، --from را حذف میکنی؛ اگر خروجی build stage را میخواهی، آن را نگه میداری.
Trap: this instruction does not read from the host. Remove --from to copy from build context; keep it when the artifact comes from a stage.
اگر builder در /app/dist/server.js خروجی بسازد اما runtime از /app/build کپی کند، قبل از تغییر پایه چه چیزی را بررسی میکنی؟
If the builder produces /app/dist/server.js but runtime copies from /app/build, what do you check before changing the base image?
پاسخ و دلیل · Solution and reasoning
اینجا چه میبینیم؟ اول قرارداد خروجی build را بررسی میکنم: script build، tsconfig.json و log مرحلهٔ کامپایل باید با مسیر /app/dist سازگار باشند. مشکل مسیر است، نه اندازهٔ پایه.
Conclusion: I first check the artifact contract: the build script, tsconfig.json, and compile log must agree with /app/dist. The problem is the path, not the base-image size.
مدرک: builder را با docker build --target builder --progress=plain . بساز و listing مسیر را موقتاً اضافه کن. دام، حلکردن COPY اشتباه با --no-cache است.
Evidence: build the builder with docker build --target builder --progress=plain . and temporarily list the path. The trap is trying to fix a wrong COPY with --no-cache.
دو Dockerfile هر دو image سالم میسازند؛ یکی single-stage و دیگری multi-stage است. در کدام خروجی دنبال توضیح اختلاف اندازه میگردی؟
Two Dockerfiles both produce working images; one is single-stage and one multi-stage. Which output explains the size difference?
پاسخ و دلیل · Solution and reasoning
اول این را ببین: docker image ls اختلاف اندازه را نشان میدهد و docker image history کمک میکند آن را به layerهای copy، نصب و پایه نسبت بدهی. بعد باید با run ثابت کنی هر دو واقعاً رفتار یکسان دارند.
Answer: docker image ls shows the size difference, while docker image history helps relate it to base, install, and copy layers. Then use run to prove both actually behave equivalently.
دام: تاریخچه اندازهگیری سلامت برنامه نیست و اندازه هم proof امنیت نیست. سه پرسش اندازه، تاریخچه و runtime را جدا نگه دار.
Trap: history is not a runtime-health measurement, and size is not a security proof. Keep the size, history, and runtime questions separate.
در docker image history demo-ts:multi فقط پایه و COPY خروجی build را میبینی. از این خروجی چه میفهمی و چه نمیفهمی؟
In docker image history demo-ts:multi you see only the base and artifact COPY. What does that tell you, and what does it not tell you?
پاسخ و دلیل · Solution and reasoning
جواب کوتاه: این نشانه با مدل ما سازگار است: نهایی stage از پایه خودش شروع شده و فقط خروجی build را گرفته است. اما تاریخچه فهرست کامل همهٔ فایلها یا proof پاسخگویی HTTP نیست.
Conclusion: that is consistent with the model: the final stage starts from its own base and takes only the artifact. History is not a complete file listing or proof of HTTP behavior.
مدرک تکمیلی: برای تنظیمات از docker inspect، برای فایل قابلاجرا از اجرای container و برای پاسخ از curl استفاده کن. دام، نتیجهگیری بیشازحد از یک دستور است.
Supporting evidence: use docker inspect for config, run the container for executable behavior, and curl for the response. The trap is overclaiming from one command.
فقط src/server.ts را تغییر دادهای. در build بعدی انتظار داری npm ci دوباره اجرا شود؟
You changed only src/server.ts. Should you expect npm ci to run again?
پاسخ و دلیل · Solution and reasoning
جواب کوتاه: اگر manifest و lockfile همان باشند و ترتیب Dockerfile درست باشد، نه. layer مربوط به COPY package*.json و RUN npm ci cache میماند؛ کامپایل و layerهای بعدی دوباره اجرا میشوند.
Answer: not if the manifest and lockfile are unchanged and the Dockerfile order is sound. The package COPY and RUN npm ci layers stay cached; compile and later layers rerun.
شاهد: با --progress=plain اولین خط غیرCACHED را پیدا کن. دام، جابهجاکردن COPY . . قبل از نصب است که cache وابستگی را بیدلیل invalid میکند.
Evidence: use --progress=plain and find the first line that is not CACHED. The trap is putting COPY . . before install and invalidating dependency cache unnecessarily.
اگر package-lock.json تغییر کند، کدام stepها باید دوباره اجرا شوند و چرا؟
If package-lock.json changes, which steps should rerun and why?
پاسخ و دلیل · Solution and reasoning
نکتهٔ اصلی: از layerای که lockfile را مصرف میکند، npm ci باید دوباره اجرا شود و stepهای وابسته، از جمله کامپایل و COPY خروجی build، هم ممکن است rebuild شوند. تغییر وابستگی ورودی نصب را عوض کرده است.
Conclusion: the layer that consumes the lockfile must rerun npm ci, and dependent steps such as compile and artifact COPY may rebuild. The dependency input changed.
دام: cache فقط چون دستور متن یکسان است حفظ نمیشود؛ ورودیهای instruction هم مهماند. خروجی نمونهای را با cache واقعی ماشین خودت اشتباه نکن.
Trap: cache is not preserved merely because the command text is unchanged; instruction inputs matter too. Do not confuse representative output with your machine’s exact result.
چرا docker build --target builder برای عیبیابی مفید است، اما نباید tag آن را image نهایی بدانی؟
Why is docker build --target builder useful for debugging, but not the final image?
پاسخ و دلیل · Solution and reasoning
چیزی که باید بماند: مقصد به تو اجازه میدهد stageای را که خروجی build میسازد جدا build و بررسی کنی. اما builder هنوز کامپایلر، کد منبع و وابستگی توسعه دارد و هدفش runtime تمیز نیست.
Answer: target lets you build and inspect the stage that produces the artifact. But the builder still contains compiler, source, and development dependencies; it is not intended to be the clean runtime.
شاهد و دام: با docker run --rm demo-ts:builder sh -lc 'find /app/dist -maxdepth 2 -type f -print' بگیر و بعد نهایی tag را جدا اجرا کن. دام، اندازهگیری builder و گزارشکردن آن بهعنوان محیط واقعی image است.
Evidence and trap: run docker run --rm demo-ts:builder sh -lc 'find /app/dist -maxdepth 2 -type f -print', then run the final tag separately. The trap is measuring the builder and reporting it as the production image.
خطا میگوید stageای به نام build پیدا یا pull نشده، اما Dockerfile نوشته AS builder. مسیر تشخیص را بنویس.
The error says a stage named build could not be found or pulled, but the Dockerfile says AS builder. Write the diagnosis path.
پاسخ و دلیل · Solution and reasoning
اگر درست فکر کرده باشی: نامهای دو طرف قرارداد یکی نیستند. اول FROM ... AS builder را پیدا میکنم، سپس همهٔ COPY --fromها را به همان نام اصلاح میکنم و دوباره build میگیرم.
Conclusion: the two sides of the contract use different names. I find FROM ... AS builder, correct each COPY --from to that name, and rebuild.
دام: این خطا هنوز runtime نیست؛ container و port نقشی ندارند. تغییر docker run یا پاککردن container مسئلهٔ build را حل نمیکند.
Trap: this is not a runtime failure; containers and ports are irrelevant yet. Changing docker run or removing a container cannot fix a build-stage name.
نام stage درست است اما /app/dist پیدا نمیشود. سه فرضیهٔ قابلآزمایش بنویس.
The stage name is correct but /app/dist is missing. Write three testable hypotheses.
پاسخ و دلیل · Solution and reasoning
فرضیهها: build اجرا نشده؛ خروجی در مسیر دیگری مثل /app/build است؛ یا WORKDIR و مسیر کد منبع آن چیزی نیست که فکر میکنیم. هر سه با log، تنظیمات و listing در builder قابلآزمایشاند.
Hypotheses: the build never ran; output is elsewhere such as /app/build; or WORKDIR and source paths differ from our assumption. All three can be tested with logs, configuration, and a builder listing.
مدرک: docker build --target builder --progress=plain . بزن و موقتاً RUN find /app -maxdepth 3 -type f اضافه کن. دام، تغییر همزمان چند مسیر است که علت را پنهان میکند.
Evidence: run docker build --target builder --progress=plain . and temporarily add RUN find /app -maxdepth 3 -type f. The trap is changing several paths at once and hiding the cause.
برای این برنامه چرا node:22-bookworm-slim را بهعنوان انتخاب آموزشی نگه داشتیم و فوراً به Alpine نرفتیم؟
Why did we keep node:22-bookworm-slim for this lesson instead of immediately switching to Alpine?
پاسخ و دلیل · Solution and reasoning
نتیجهٔ بررسی: چون میخواهیم اثر multi-stage را جدا از تفاوت libc، native module و ابزار عیبیابی بسنجیم. Debian slim مسیر قابلپیشبینیتری برای این مثال میدهد.
Answer: because we want to measure the effect of multi-stage separately from libc, native-module, and debugging-tool differences. Debian slim gives this example a more predictable baseline.
دام: Alpine ممکن است انتخاب خوبی باشد، اما کوچکترین پایه بهتنهایی بهترین انتخاب نیست. باید سازگاری برنامه، وابستگی و روش تشخیص را تست کنی.
Trap: Alpine may be a good choice, but the smallest base is not automatically the best. Test application compatibility, dependencies, and your diagnostic method.
همتیمیات میگوید «image multi-stage کوچکتر است، پس امن است». ادعا را اصلاح کن.
A teammate says, “The multi-stage image is smaller, so it is secure.” Correct the claim.
پاسخ و دلیل · Solution and reasoning
اصلاح: multi-stage معمولاً ابزار و بسته کمتری در runtime میگذارد و این میتواند سطح حمله و هزینهٔ نگهداری را کم کند؛ اما امنیت نتیجهٔ اندازهٔ تنها نیست.
Correction: multi-stage often leaves fewer tools and packages at runtime, which can reduce attack surface and maintenance cost; security is not a result of size alone.
بررسی لازم: non-root بودن، patch level، وابستگیها، تنظیمات و رفتار برنامه را جدا بررسی کن. دام، تبدیل یک نشانهٔ مثبت به تضمین کامل است.
Required checks: separately inspect non-root execution, patch level, dependencies, configuration, and application behavior. The trap is turning one positive signal into a complete guarantee.
برای اثبات اینکه image نهایی واقعاً خروجی build را اجرا میکند، چه زنجیرهای از دستورها میزنی؟
What command chain proves that the final image actually runs the artifact?
پاسخ و دلیل · Solution and reasoning
زنجیره: با docker image inspect دستور را بخوان؛ با docker run -d --name demo-multi -p 8080:3000 demo-ts:multi اجرا کن؛ سپس docker ps، docker logs و curl را بخوان.
Chain: read the command with docker image inspect; run docker run -d --name demo-multi -p 8080:3000 demo-ts:multi; then read docker ps, docker logs, and curl.
برای مطمئنشدن: اگر build موفق باشد اما container فوراً exit کند، به runtime دستور یا خروجی build برگرد. دام، نتیجهگیری سلامت runtime فقط از موفقیت build است.
Evidence and trap: if the build succeeds but the container exits immediately, return to the runtime command or artifact. The trap is inferring runtime health from build success alone.
دو Dockerfile را مقایسه کن: یکی COPY . . را قبل از npm ci دارد و دیگری manifest را جدا copy میکند. کدام برای تغییر روزانهٔ کد منبع بهتر است؟
Compare two Dockerfiles: one copies COPY . . before npm ci, the other copies the manifest separately. Which is better for daily source changes?
پاسخ و دلیل · Solution and reasoning
جمعبندی این تمرین: نسخهٔ دوم بهتر است: تا وقتی manifest و lockfile تغییر نکردهاند، layer نصب وابستگی میتواند cache بماند. نسخهٔ اول با هر تغییر کد منبع ورودی نصب را هم تغییر میدهد.
Conclusion: the second is better: as long as the manifest and lockfile stay unchanged, the dependency-install layer can remain cached. The first changes the install input on every source edit.
شاهد: یک کد منبع edit انجام بده و خروجی --progress=plain دو build را کنار هم بگذار. دام، انتظار cache بدون توجه به ترتیب instructionهاست.
Evidence: make one source edit and compare the two --progress=plain outputs. The trap is expecting cache without considering instruction order.
سناریو: build موفق است، image multi کوچکتر است، اما مرورگر چیزی نشان نمیدهد. از خروجی build تا runtime چه گزارشی مینویسی؟
Scenario: the build succeeds, the multi-stage image is smaller, but the browser shows nothing. What report do you write from artifact to runtime?
پاسخ و دلیل · Solution and reasoning
ترتیب پیشنهادی: tag و تاریخچه image را ثبت کن؛ با مقصد builder وجود /app/dist/server.js را ثابت کن؛ دستور را با بررسی بخوان؛ با docker ps -a state و exit code را ببین؛ اگر Up است PORTS و listen address را با curl بررسی کن.
Suggested order: record the image tag and history; use the builder target to prove /app/dist/server.js exists; read the command with inspect; check state and exit code with docker ps -a; if it is Up, inspect PORTS and the listen address with curl.
چرا این ترتیب؟ چون هر مرحله پیشنیاز بعدی است و خطا در build را با خطا در runtime قاطی نمیکند. دام، تغییر تصادفی پایه یا port پیش از خواندن اولین مدرک است.
Why this order? Each step establishes a prerequisite for the next and keeps build failures separate from runtime failures. The trap is randomly changing the base or port before reading the first piece of evidence.
آزمایشگاه کامل: از image سنگین تا runtime تمیزFull lab: from a heavy image to a clean runtime
یک همتیمی از تو image سبکتر میخواهد
A teammate asks for a leaner image
این آزمایشگاه را مثل یک مأموریت کوچک انجام بده: یک image داری که کار میکند ولی زیادی بار همراهش است. اول وزنش را ثبت میکنی، بعد کارگاه و محصول را جدا میکنی، دوباره اندازه میگیری، cache را بههم میزنی و در آخر یک COPY --from خراب را از روی خروجی build پیدا میکنی. هدف فقط رسیدن به Dockerfile نهایی نیست؛ باید بتوانی توضیح بدهی چرا هر خط آنجاست.
Start from the same demo-ts project. Build the single-stage version and collect evidence; then write the multi-stage version, compare size and history, observe cache with source and lockfile changes, deliberately break COPY --from, diagnose from build output, and clean everything up at the end.
- کد منبعSource
فایلهای
server.ts، manifest، lockfile و tsconfig را ثبت کن.Record
server.ts, the manifest, lockfile, and tsconfig. - baselineBaseline
Dockerfile.singleرا build کن؛ خروجیdocker image lsوdocker image historyرا ذخیره کن.Build
Dockerfile.single; savedocker image lsanddocker image historyoutput. - runtimeRuntime
هر دو tag را با
-p 8080:3000اجرا کن و باcurlثابت کن که پاسخ یکی است.Run both tags with
-p 8080:3000and prove withcurlthat the response is equivalent. - multi-stageMulti-stage
Dockerfile.multiرا مرحلهبهمرحله بنویس؛ قبل از build پیشبینی کن کدام فایلها نهایی را ترک میکنند.Write
Dockerfile.multistep by step; predict which files leave the final image before building. - مقایسهCompare
اندازه، تاریخچه، دستور و مسیر خروجی build را در یک جدول کوتاه مقایسه کن. اعداد را نمونهای یا واقعی برچسب بزن.
Compare size, history, command, and artifact path in a short table. Label values as representative or measured.
- cacheCache
فقط کد منبع را تغییر بده و دوباره build کن؛ سپس lockfile را تغییر بده و تفاوت stepهای cached را یادداشت کن.
Change only source and rebuild; then change the lockfile and record which cached steps differ.
- شکستBreak
نام stage یا مسیر
COPY --fromرا عمداً غلط کن. متن خطا، مرز خراب و اصلاح را بنویس.Deliberately break the stage name or
COPY --frompath. Record the error, broken boundary, and fix. - تحویلHandoff
tagهای آزمایشی و containerها را حذف کن و یک گزارش کوتاه با شواهد، محدودیتها و نتیجه بنویس.
Remove test tags and containers and write a short report with evidence, limitations, and the result.
گزارش مرجع · Reference report
گزارش خوب نمیگوید «multi-stage بهتر است» و تمام. میگوید baseline چه اندازه و چه layerهایی داشت؛ نهایی چه چیزهایی را نگه داشت؛ هر دو پاسخ یکسان دادند؛ cache با کدام تغییر شکست؛ خطاِ COPY در build رخ داد نه runtime؛ و پاکسازی با docker image ls و docker ps -a بررسی شد.
A good report does not stop at “multi-stage is better.” It states the baseline size and layers, what the final image kept, whether both images answered equivalently, which change invalidated cache, that the COPY failure happened at build time rather than runtime, and that cleanup was checked with docker image ls and docker ps -a.
مدل نهایی را با خودت ببرCarry the final model with you
اگر بخواهیم کل فصل را در یک تصویر نگه داریم: builder کارگاه است، فایل خروجی محصول است و مرحلهٔ آخر جعبهٔ تحویل. multi-stage چیزی را جادویی پنهان نمیکند؛ فقط مجبورمان میکند آگاهانه بگوییم چه چیزی از کارگاه وارد جعبه شود. همین انتخاب صریح است که image نهایی را تمیزتر و قابلفهمتر میکند.
Multi-stage is not a magic box that hides files. You create a build environment, produce an artifact with a path contract, start a fresh runtime, and explicitly transfer only what is needed. Then you measure size, history, command, and real behavior separately.
| Claim | Evidence | Do not overclaim |
|---|---|---|
| Final is smaller | docker image ls | کوچکتر بودن بهتنهایی امنیت نیستSize alone is not security |
| Artifact exists | docker build --target builder + listing | تاریخچه جای listing نیستHistory is not a file listing |
| Runtime works | docker run + curl | build موفق، runtime سالم را تضمین نمیکندA successful build does not guarantee runtime health |
| Cache helps | --progress=plain and CACHED | cache محتوای نهایی نیستCache is not final-image content |
فصل بعد موضوع عوض میشود: اینبار نه خود image، بلکه دادهای مهم است که نباید با حذف container از بین برود. همینطور قدمبهقدم جلو میرویم؛ هر فصل یک مسئلهٔ واقعی را حل میکند و ابزار بعدی وقتی وارد میشود که واقعاً به آن نیاز پیدا کردهایم.
Later chapters cover data, networking, Compose, secrets, and deployment. This chapter solved one question deeply: how to separate build from runtime and prove what was transferred.
مرجع سریعQuick reference
FROM base AS builderstage ساخت را نامگذاری میکندnames a build stageCOPY --from=builder /src /destخروجی build را از stage میخواندcopies an artifact from a stagedocker build --target builder .stage میانی را برای عیبیابی میسازدbuilds an intermediate stage for debuggingdocker image ls NAMEاندازه و tag را نشان میدهدshows size and tagsdocker image history NAMEتاریخچه instructionها را نشان میدهدshows instruction historydocker build --progress=plain .cache و اولین step خراب را خواناتر میکندmakes cache and the first failing step visibleراهنمای multi-stage builds · مرجع Dockerfile و COPY --from · بهینهسازی cache · مرجع image تاریخچه
Multi-stage builds guide · Dockerfile and COPY --from reference · Cache optimization · Image history reference