کدنامهمرجع‌های مهندسی نرم‌افزار، به فارسی
Docker · فصل ۴Docker · Chapter 4

Dockerfile، لایه‌ها و cache

Dockerfile, layers, and build cache

تا اینجا imageهای آماده را اجرا می‌کردی. حالا نوبت برنامهٔ خودت است. در این فصل قدم‌به‌قدم یک Dockerfile می‌سازیم، می‌بینیم نقطهٔ آخر docker build چرا مهم است، cache چطور زمان build را کم می‌کند و وقتی چیزی خراب شد از روی خروجی می‌فهمیم مشکل در ساخت image بوده یا موقع اجرای container.

So far you have been running other people’s images. This chapter is where you become the builder: you write the recipe for your own image, understand what the builder can see, learn why cache sometimes saves minutes and sometimes surprises you, and diagnose whether a failure belongs to build time or runtime.

۹۰دقیقهٔ مطالعه و اجراminutes
۱۸تمرین با پاسخsolved exercises
۴نمودار مفهومیdiagrams
۱آزمایشگاه عملیhands-on lab

اگر برنامه مال خودت باشد، Docker از کجا بداند چه بسازد؟From “ready-made image” to “my own application”

فصل قبل با nginx:alpine راحت بود: image آماده بود و ما فقط اجراش می‌کردیم. حالا فرض کن یک برنامهٔ ساده نوشته‌ای و می‌خواهی آن را برای همکارت بفرستی. گفتن «Node نصب کن، بعد این دستورها را بزن» دوباره ما را به همان درد فصل اول برمی‌گرداند. اینجاست که Dockerfile وارد داستان می‌شود.

In the previous chapter we worked with nginx:alpine. The image already existed; you only ran it, inspected logs, entered the container, and published a port. The next question is more important: if the application is yours, what do you give Docker so it can build the same environment every time?

فرض کن server.js روی لپ‌تاپت با npm start اجرا می‌شود. برای ساختن همان محیط روی سیستم دیگر، فقط خود فایل کافی نیست. باید نسخهٔ Node، وابستگی‌ها، پوشهٔ کاری و دستور شروع برنامه هم معلوم باشد. Dockerfile همین تصمیم‌ها را کنار هم و قابل‌تکرار ثبت می‌کند.

Suppose you have a server.js that runs on your laptop with npm start. That sentence still leaves several questions unanswered for a teammate: which Node version, how dependencies are installed, which directory the app runs from, which files belong in the environment, and which process should become the container’s main process.

Dockerfile جواب قابل‌نسخه‌بندیِ همین سؤال‌هاست. خود Dockerfile image نیست؛ یک دستور ساخت است. docker build این دستور ساخت را با ورودی‌هایش اجرا می‌کند و یک image می‌سازد. بعداً docker run از آن image یک container تازه می‌سازد. اگر این سه واژه را قاطی نکنی، نصف خطاهای این فصل از همان اول قابل‌فهم می‌شوند.

A Dockerfile is the version-controlled answer to those questions. The Dockerfile is not the image; it is a recipe. docker build executes that recipe with its inputs and produces an image. Later, docker run creates a fresh container from that image. Keeping those three objects separate makes most failures in this chapter much easier to reason about.

Source files to Dockerfile build to image to container source filesapp + manifests Dockerfilebuild recipe imagedemo-web:1.0 containerruntime instance

فلش‌ها مسیر تولید را نشان می‌دهند: کد برنامه و دستور ساخت ورودی build هستند؛ خروجی build یک image است؛ container بعداً از image ساخته می‌شود.

The arrows show the production path: source and recipe are build inputs; the build produces an image; a container is created from that image later.

یک پیش‌بینی قبل از ادامهMake one prediction before continuing

اگر server.js را تغییر بدهی اما دوباره docker build نزنی، اجرای بعدی image قدیمی کد جدید را نمی‌بیند. چرا؟ چون docker run از فایل‌های پروژه‌ات container نمی‌سازد؛ از image ساخته‌شده استفاده می‌کند. کمی جلوتر این را عمداً آزمایش می‌کنیم.

If you edit server.js but do not run docker build again, the next container from the old image will not contain your new code. Why? Because docker run does not build from your project files; it runs the image that already exists. We will deliberately test this later.

یک برنامهٔ خیلی کوچک می‌سازیم تا تمرکز روی Docker بماندA tiny app that keeps the focus on Docker

برای این فصل به پروژهٔ شلوغ نیاز نداریم. یک سرور HTTP کوچک کافی است: وقتی بالا آمد یک پیام می‌دهد، روی پورت مشخصی گوش می‌کند و هر درخواست را در لاگ می‌نویسد. همین سه رفتار برای تمام آزمایش‌های فصل کافی‌اند.

We do not need a large framework for this lesson. A tiny HTTP server is enough to make three things observable: the app starts, listens on a port, and writes one log line per request.

demo-web/server.js
const http = require('node:http');

  const port = process.env.PORT || 3000;

  http.createServer((req, res) => {
    console.log(new Date().toISOString(), req.method, req.url);
    res.writeHead(200, { 'content-type': 'text/plain' });
    res.end('hello from CodeNames\n');
  }).listen(port, () => {
    console.log('listening on', port);
  });
demo-web/package.json
{
    "name": "codenames-docker-demo",
    "version": "1.0.0",
    "private": true,
    "scripts": { "start": "node server.js" }
  }

برای اینکه npm ci رفتار تکرارپذیر داشته باشد، یک lockfile هم می‌خواهیم. یک بار روی سیستم میزبان اجرا کن:

To make npm ci reproducible, we also want a lockfile. Run this once on the host:

create lockfile
cd demo-web
  npm install --package-lock-only
هدف این فصل Node نیستThis is not a Node.js lesson

Node را فقط چون مثال کوچکی می‌دهد انتخاب کرده‌ایم. مفاهیم FROM، COPY، RUN، cache و build context برای Python، .NET، Java و بقیهٔ stackها هم همان منطق را دارند.

We chose Node only because it gives us a tiny example. The ideas behind FROM, COPY, RUN, cache, and build context apply just as well to Python, .NET, Java, and other stacks.

این نقطهٔ آخر docker build دقیقاً چه کار می‌کند؟Build context: the final dot is not decoration

تقریباً همه اول کار فقط روی Dockerfile تمرکز می‌کنند و آن نقطهٔ آخر دستور را نادیده می‌گیرند. درحالی‌که همان نقطه تعیین می‌کند Docker برای این build اجازه دارد کدام فایل‌ها را ببیند.

The first thing beginners often overlook is the final dot:

build command
docker build -t demo-web:1.0 .
                                  ^
                                  build context

وقتی آخر دستور . می‌گذاری، پوشهٔ فعلی می‌شود build context. یعنی COPY فقط می‌تواند از فایل‌هایی بردارد که داخل همین محدوده‌اند. اگر Dockerfile جای دیگری باشد یا از پوشهٔ اشتباه build کنی، اولین خطای عجیب معمولاً همین‌جا خودش را نشان می‌دهد.

The . says that the current directory is the build context: the set of files the builder is allowed to use for this build. When the Dockerfile says COPY server.js ./, the source is resolved relative to the context root, not from arbitrary locations on your disk.

project tree
demo-web/
  ├── Dockerfile
  ├── .dockerignore
  ├── package.json
  ├── package-lock.json
  ├── server.js
  └── node_modules/      # local files; we do not want these in the context
Project folder filtered by dockerignore into build contextproject directorysource · .git · .envnode_modules · lockfile.dockerignoreremove unwanted inputbuild contextfiles builder can use

.dockerignore قبل از COPY روی ورودی build اثر می‌گذارد؛ فایلی که از context کنار گذاشته شده، برای COPY در دسترس نیست.

.dockerignore filters build input before COPY; a file excluded from the context is unavailable to COPY.

عمداً از context اشتباه build کن

Deliberately build from the wrong context

correct context
cd demo-web
  docker build -t demo-web:1.0 .

حالا فرض کن از parent پوشه build می‌کنی ولی Dockerfile داخل demo-web است:

Now suppose you build from the parent directory while the Dockerfile lives in demo-web:

wrong relationship between Dockerfile and context
cd ..
  docker build -f demo-web/Dockerfile -t demo-web:1.0 .

  # representative failure
  ERROR [3/5] COPY package*.json ./
  failed to compute cache key: "/package.json": not found

نکته اینجاست: -f فقط محل Dockerfile را مشخص کرد؛ context هنوز همان . است. COPY package*.json ./ دنبال package.json در root context می‌گردد، نه خودکار داخل directoryای که Dockerfile در آن قرار دارد. پس قبل از دست‌کاری COPY، سؤال درست این است: «context من کجاست؟»

The key point is that -f only selected the Dockerfile; the context is still the final .. COPY package*.json ./ looks from the context root, not automatically from the Dockerfile’s directory. Before changing COPY blindly, ask the correct question: “what is my context?”

context را برای رسیدن به اطلاعات محرمانه بزرگ نکنDo not enlarge context just to reach a secret

مسیر کد برنامه در COPY از build context می‌آید. نوشتن ../ راهی برای دسترسی آزاد به parent سیستم‌فایل نیست؛ سازنده کد برنامه مسیر را به context محدود می‌کند. اگر یک اطلاعات محرمانه بیرون پروژه داری، راه‌حل درست «بزرگ‌تر کردن context و COPY کردن اطلاعات محرمانه داخل image» نیست.

The source of COPY comes from the build context. Writing ../ is not a free escape into the parent filesystem; the builder constrains source paths to the context. If you have a secret outside the project, the right answer is not “make the context larger and COPY the secret into the image.”

.dockerignore؛ چیزهایی که نباید اصلاً وارد build شوند.dockerignore: not everything on disk belongs in the build

پروژهٔ واقعی پر از چیزهایی است که Docker لازم ندارد: node_modules، پوشهٔ .git، فایل‌های موقت و شاید فایل‌های حساس. اگر همه را وارد build context کنی هم کار بیشتر می‌شود، هم احتمال اشتباه بالاتر می‌رود. .dockerignore کمک می‌کند از همان اول ورودی build را تمیز نگه داری.

Think of the context as the input bundle for a build. .dockerignore is the list of things that should not enter that bundle. It keeps the context smaller and prevents irrelevant local files from becoming build inputs.

.dockerignore
node_modules
  .git
  .env
  npm-debug.log
  *.local

node_modules را از سیستم میزبان نمی‌خواهیم چون وابستگی باید داخل image و بر اساس همان image پایه نصب شود. .git برای اجرای برنامه لازم نیست. .env هم ممکن است اطلاعات محرمانه داشته باشد. اما یک نکتهٔ مهم: .dockerignore سیستم مدیریت اطلاعات محرمانه نیست. فقط کمک می‌کند چیزی ناخواسته وارد context نشود.

We do not want host node_modules because dependencies should be installed inside the image against that base environment. .git is not needed to run the app. .env may contain secrets. But one distinction matters: .dockerignore is not a secret-management system. It only reduces the chance of sending unwanted files into the context.

یک خطا مفید

A useful failure

break .dockerignore on purpose
# temporarily add this line:
  server.js

  docker build -t demo-web:broken .

  # representative output
  ERROR [5/5] COPY server.js ./
  failed to compute cache key: "/server.js": not found

اینجا Docker «فایل را گم نکرده». تو به سازنده گفته‌ای سرور.js جزو ورودی build نباشد و چند خط بعد از او خواسته‌ای همان فایل را COPY کند. خطا دقیقاً تضاد بین دو تصمیم خودت را نشان می‌دهد.

Docker did not mysteriously “lose” the file. You told the builder that server.js was not part of the build input, then later asked it to COPY that file. The error exposes a contradiction between two choices in your own build definition.

Dockerfile را حفظ نکن؛ دلیل هر خط را بفهمDo not memorize a Dockerfile; each line is a decision

می‌توانستم Dockerfile نهایی را همین اول نشان بدهم و بگویم این هفت خط را کپی کن. ولی آن‌وقت دفعهٔ بعد که پروژه فرق کند، دوباره گیر می‌کنی. بهتر است فایل را خط‌به‌خط بسازیم و بعد از هر خط بپرسیم: این یکی چه چیزی را عوض کرد و چرا الآن به آن نیاز داریم؟

Instead of staring at a finished file and memorizing keywords, we will build it one instruction at a time. After each line you should be able to answer: “what did this change during the build, and does it affect runtime too?”

۱) FROM: از چه سیستم‌فایل و runtimeای شروع می‌کنیم؟

1) FROM: which filesystem and runtime do we start from?

Dockerfile
FROM node:22-alpine

FROM image پایه را انتخاب می‌کند. اینجا یعنی build را روی سیستم‌فایل کوچک Alpine شروع می‌کنیم که Node هم از قبل داخلش هست. هنوز کد خودمان داخل image نیست. هنوز سرور هم اجرا نشده. فقط نقطهٔ شروع image را انتخاب کرده‌ایم.

FROM selects the base image. Here the build starts from a small Alpine filesystem that already contains Node. Our source code is not in the image yet, and no server is running; we have only chosen the starting point.

۲) WORKDIR: از اینجا به بعد «اینجا» کجاست؟

2) WORKDIR: what does “here” mean from now on?

Dockerfile
FROM node:22-alpine
  WORKDIR /app

WORKDIR /app می‌گوید instructionهای بعدی relative pathهایشان را از /app حساب کنند؛ اگر پوشه نباشد، ساخته می‌شود. این با RUN cd /app یکی نیست. cd فقط همان شل invocation را جابه‌جا می‌کند، اما WORKDIR بخشی از تنظیمات image می‌شود و دستور زمان اجرا هم از همان پوشه شروع می‌شود.

WORKDIR /app makes later instructions resolve relative paths from /app, creating the directory if needed. This is not the same as RUN cd /app. A shell cd only affects that RUN invocation, while WORKDIR becomes image configuration and also sets the starting directory for the runtime command.

۳) اول فایل‌های وابستگی را COPY کن

3) Copy dependency manifests first

Dockerfile
COPY package*.json ./

فعلاً فقط package.json و package-lock.json را می‌آوریم. چرا کد برنامه را همین الان کامل COPY نمی‌کنیم؟ چون dependencyها معمولاً کمتر از کد برنامه تغییر می‌کنند. این تصمیم چند بخش بعد، وقتی cache را می‌بینیم، اثرش را نشان می‌دهد.

For now we copy only package.json and package-lock.json. Why not copy all source code immediately? Because dependency manifests usually change less often than application source. The payoff becomes obvious when we inspect cache behavior later.

۴) RUN: کاری که هنگام build انجام می‌شود

4) RUN: work that happens during the build

Dockerfile
RUN npm ci --omit=dev

این دستور موقع ساخت image اجرا می‌شود، نه هر بار که container بالا می‌آید. اگر وابستگی نصب ناموفق شود، build باید همان‌جا ناموفق شود. نتیجهٔ نصب، مثل node_modules، وارد سیستم‌فایل image می‌شود و build بعدی می‌تواند همین مرحله را از cache استفادهٔ دوباره کند.

This command runs while building the image, not every time a container starts. If dependency installation fails, the build should fail at this step. The result, such as node_modules, becomes part of the image filesystem, and later builds may reuse this step from cache.

۵) حالا کد برنامه را اضافه کن

5) Now add application source

Dockerfile
COPY server.js ./

تغییرات روزمره معمولاً همین‌جاست؛ متن پاسخ، هدایت جدید یا منطق برنامه. چون این COPY بعد از npm ci آمده، تغییر کردن کد برنامه لازم نیست وابستگی نصب را دوباره اجرا کند—به شرطی که manifestها عوض نشده باشند.

Most day-to-day changes happen here: a response string, a new route, application logic. Because this COPY comes after npm ci, editing source does not need to rerun dependency installation—as long as the dependency manifests remain unchanged.

۶) ENV و EXPOSE: تنظیمات با شبکه publishing یکی نیست

6) ENV and EXPOSE: configuration is not port publishing

Dockerfile
ENV PORT=3000
  EXPOSE 3000

ENV PORT=3000 یک مقدار پیش‌فرض داخل image تنظیمات ثبت می‌کند که پردازش زمان اجرا می‌تواند بخواند. EXPOSE 3000 هم اعلام می‌کند برنامه قرار است روی پورت 3000 گوش بدهد؛ اما هیچ سیستم میزبان portای باز نمی‌کند. برای مرورگر هنوز همان چیزی را که در فصل ۳ یاد گرفتی لازم داری: مثلاً -p 8080:3000.

ENV PORT=3000 records a default value in image configuration for the runtime process to read. EXPOSE 3000 documents that the application is expected to listen on port 3000, but it does not publish a host port. Browser access still needs what you learned in Chapter 3, for example -p 8080:3000.

۷) CMD: اگر کاربر دستور دیگری نداد، چه processی اجرا شود؟

7) CMD: what process runs when the user supplies no command?

Dockerfile
CMD ["npm", "start"]

CMD هنگام build اجرا نمی‌شود. فقط پیش‌فرض زمان اجرا دستور را داخل image تنظیمات ثبت می‌کند. وقتی بعداً docker run demo-web:1.0 بزنی، این پیش‌فرض اجرا می‌شود. اگر خودت بعد از image name دستور دیگری بدهی، آن دستور جای CMD را برای همان container می‌گیرد.

CMD does not execute during the build. It records the default runtime command in image configuration. Later, docker run demo-web:1.0 uses that default. If you provide another command after the image name, that command replaces CMD for that container.

حالا فایل نهایی را ببینNow read the finished Dockerfile
Dockerfile
FROM node:22-alpine
  WORKDIR /app

  COPY package*.json ./
  RUN npm ci --omit=dev

  COPY server.js ./

  ENV PORT=3000
  EXPOSE 3000

  CMD ["npm", "start"]

اگر الآن بتوانی برای هر خط بگویی «build-time است یا زمان اجرا تنظیمات، چه ورودی‌ای دارد و تغییرش چه چیزی را invalidate می‌کند»، دیگر Dockerfile را حفظ نکرده‌ای؛ فهمیده‌ای.

If you can now explain for every line whether it is build-time work or runtime configuration, what input it depends on, and what changing it may invalidate, you are no longer memorizing a Dockerfile—you understand it.

RUN داخل Dockerfile با docker run چه فرقی دارد؟RUN and docker run only look similar

اسم این دو آن‌قدر شبیه است که اوایل راحت قاطی می‌شوند. RUN هنگام ساخت image اجرا می‌شود؛ اما docker run بعداً از image یک container می‌سازد و آن را اجرا می‌کند. یکی مربوط به «ساختن» است، دیگری مربوط به «اجرا کردن».

This is one of the most common beginner confusions. RUN npm ci in a Dockerfile is part of building the image. docker run ... in the terminal happens after the build and creates a container. The two commands change different state.

QuestionRUN in Dockerfiledocker run in terminal
کِی؟When?هنگام docker buildduring docker buildبعد از build، هنگام ساخت/راه‌اندازی containerafter build, while creating/starting a container
روی چه چیزی اثر می‌گذارد؟What does it affect?سیستم‌فایل/وضعیت مرحلهٔ build و image خروجیbuild-stage filesystem/state and resulting imagecontainer تازه و وضعیت زمان اجراa new container and runtime state
مثال این فصلExample hereRUN npm ci --omit=devdocker run -p 8080:3000 demo-web:1.0
اگر ناموفق شود کجا را نگاه می‌کنیم؟Where do we look on failure?BuildKit خروجی و همان build مرحلهBuildKit output and that build stepps -a، کد خروج، لاگ‌ها، پورت نگاشتps -a, exit code, logs, port mapping
first build
docker build -t demo-web:1.0 .

  # representative BuildKit output
  [+] Building 8.9s (10/10) FINISHED
  => [internal] load build definition from Dockerfile
  => [1/5] FROM docker.io/library/node:22-alpine
  => [2/5] WORKDIR /app
  => [3/5] COPY package*.json ./
  => [4/5] RUN npm ci --omit=dev
  => [5/5] COPY server.js ./
  => exporting to image
  => naming to docker.io/library/demo-web:1.0

از این خروجی چه می‌فهمیم؟ فقط اینکه build کامل شده و image با برچسب موردنظر ساخته شده. هنوز نمی‌دانیم HTTP سرور بالا می‌آید یا نه. برای آن باید زمان اجرا را جداگانه امتحان کنیم.

What does this output prove? Only that the build completed and an image was produced under the requested tag. It does not prove the HTTP server can stay alive. Runtime is a separate question.

runtime verification
docker run -d --name demo-web -p 8080:3000 demo-web:1.0
  docker ps --filter name=demo-web
  docker logs demo-web
  curl -i http://localhost:8080

این چهار خط چهار سؤال جدا را جواب می‌دهند: container ساخته شده؟ هنوز Running است؟ پردازش لاگ داده؟ و مسیر پورت سیستم میزبان تا خود برنامه واقعاً پاسخ می‌دهد؟ «build شد» و «برنامه سالم اجرا می‌شود» یک جمله نیستند.

These four commands answer four separate questions: does the container exist, is it still Running, did the process emit logs, and does the host-to-application path actually respond? “The image built” and “the application runs correctly” are not the same statement.

image چطور مرحله‌به‌مرحله ساخته می‌شود؟Layers: an image is not one magical monolithic file

Dockerfile از بالا به پایین اجرا می‌شود و هر دستور نتیجهٔ مرحلهٔ قبل را تحویل می‌گیرد. بعضی دستورها فایل‌های image را تغییر می‌دهند و بعضی فقط تنظیماتش را. فعلاً لازم نیست وارد جزئیات storage driver شویم؛ همین کافی است که بدانیم هر مرحله ورودی خودش را دارد و روی نتیجهٔ قبلی بنا می‌شود.

You read a Dockerfile from top to bottom, and the builder also evaluates each instruction as part of the build history. Some instructions change the filesystem, while others mainly record configuration or metadata. You do not need storage-driver internals yet; one model is enough: each step has defined inputs and builds on the result of earlier steps.

InstructionMain effectWhy it matters later
FROMimage پایه/stagebase image/stageنقطهٔ شروع همهٔ stepهای بعدیstarting state for everything after it
COPYفایل‌های context را به سیستم‌فایل می‌آوردadds context files to the filesystemتغییر ورودی فایل می‌تواند cache این مرحله و stepهای بعدی را بشکندchanged file input can invalidate this step and later steps
RUNدستور build-time اجرا می‌کندexecutes build-time workخروجی دستور بخشی از وضعیت image بعدی می‌شودits result becomes part of later image state
ENVتنظیمات ثبت می‌کندrecords configurationپردازش زمان اجرا مقدار پیش‌فرض را می‌بیندruntime processes see the default
CMDپیش‌فرض زمان اجرا دستور ثبت می‌کندrecords a default runtime commandروی build اجرا نمی‌شود؛ روی رفتار container اثر می‌گذاردdoes not run at build time; affects container behavior
یک ساده‌سازی رایج که بهتر است تکرارش نکنیA common simplification worth avoiding

نگو «هر خط Dockerfile حتماً یک سیستم‌فایل لایه مستقل می‌سازد». برای فهم cache دقیق‌تر است بگویی سازنده هر دستور Dockerfile را به‌عنوان یک build مرحله با ورودی و نتیجه بررسی می‌کند؛ بعضی دستورهای Dockerfile سیستم‌فایل را تغییر می‌دهند و بعضی بیشتر image تنظیمات را.

Avoid saying “every Dockerfile line always creates its own filesystem layer.” For cache reasoning, it is more accurate to think of each instruction as a build step with inputs and a result; some change the filesystem while others mainly update image configuration.

cache را با چشم ببین؛ فقط تعریفش را نخوانSee cache with your own eyes: four builds, four stories

اگر فقط بگوییم «Docker cache دارد»، احتمالاً فردا یادت می‌رود. پس چهار بار build می‌کنیم: بار اول از صفر، بار دوم بدون هیچ تغییر، بار سوم فقط با تغییر server.js و بار چهارم با تغییر وابستگی‌ها. از روی خروجی دقیقاً می‌بینیم کدام مرحله دوباره اجرا می‌شود و کدام نه.

Cache becomes intuitive when you compare several builds side by side. The goal is not to memorize the word CACHED; the useful question is “which input changed, and from which step did reuse stop being valid?”

Cache-friendly Dockerfile orderCOPY package*.json ./RUN npm ci --omit=devCOPY server.js ./source edit rebuilds here and below

وابستگی ورودی‌ها را زودتر و کد برنامه پر‌تغییر را دیرتر می‌آوریم تا تغییر روزمرهٔ کد برنامه، نصب وابستگی را بی‌دلیل تکرار نکند.

Put stable dependency inputs earlier and frequently changing source later, so everyday source edits do not unnecessarily rerun dependency installation.

Build #1 — هیچ cacheای برای پروژه نداریم

Build #1 — no project cache yet

fresh build
docker build -t demo-web:1.0 .

  # relevant output
  => [3/5] COPY package*.json ./
  => [4/5] RUN npm ci --omit=dev
  => [5/5] COPY server.js ./

در build اول طبیعی است که کار واقعاً انجام شود. حالا بدون تغییر هیچ فایلی همان دستور را دوباره بزن.

On the first build, the work genuinely has to happen. Now run the exact same build again without changing any file.

Build #2 — بدون تغییر

Build #2 — unchanged

unchanged build
docker build -t demo-web:1.0 .

  # representative output
  => CACHED [2/5] WORKDIR /app
  => CACHED [3/5] COPY package*.json ./
  => CACHED [4/5] RUN npm ci --omit=dev
  => CACHED [5/5] COPY server.js ./

این یعنی سازنده برای همین ورودی‌ها نتیجهٔ قابل‌استفادهٔ دوباره پیدا کرده است. نه اینکه Docker «اجرا را حذف کرده»؛ بلکه تشخیص داده ساختن دوبارهٔ همان نتیجه لازم نیست.

This means the builder found reusable results for those inputs. Docker did not “delete RUN”; it determined that rebuilding the same result was unnecessary.

Build #3 — فقط کد برنامه را تغییر بده

Build #3 — change only application source

edit only server.js
# change only the response text in server.js, then:
  docker build -t demo-web:1.1 .

  # expected shape
  => CACHED [3/5] COPY package*.json ./
  => CACHED [4/5] RUN npm ci --omit=dev
  =>        [5/5] COPY server.js ./

این همان لحظه‌ای است که ترتیب Dockerfile معنی پیدا می‌کند. وابستگی ورودی‌ها تغییر نکرده‌اند، پس نصب قابل‌استفادهٔ دوباره است. فقط مرحله مربوط به کد برنامه باید دوباره اجرا شود.

This is where Dockerfile ordering starts to matter. Dependency inputs did not change, so installation remains reusable. Only the step that depends on application source needs to run again.

Build #4 — lockfile را تغییر بده

Build #4 — change the lockfile

dependency change
# change dependencies so package-lock.json changes, then:
  docker build -t demo-web:1.2 .

  # expected shape
  =>        [3/5] COPY package*.json ./
  =>        [4/5] RUN npm ci --omit=dev
  =>        [5/5] COPY server.js ./

وقتی ورودی مرحلهٔ COPY manifest عوض شد، cache آن مرحله دیگر نمایندهٔ وضعیت جدید نیست. از آن نقطه به بعد stepهای بعدی هم باید با وضعیت جدید بررسی و ساخته شوند. این همان cache invalidation است.

When the manifest COPY input changes, that cached result no longer represents the new state. From that point onward, later steps must be evaluated against the new state. That is cache invalidation.

قانون کاربردیPractical rule

instructionهایی که ورودی‌شان کمتر تغییر می‌کند را تا جای ممکن قبل از چیزهای پر‌تغییر قرار بده. این قانون جادویی نیست؛ نتیجهٔ مستقیم همان سؤال است: «این مرحله به چه inputی وابسته است؟»

Place instructions whose inputs change less often before frequently changing inputs when practical. This is not magic; it follows directly from asking, “what input does this step depend on?”

چرا ترتیب خط‌های Dockerfile واقعاً مهم است؟Two Dockerfiles can build the same app but have very different rebuild cost

PatternDockerfile shapeWhat happens on a source-only edit?
ضعیف برای cacheCache-unfriendlyCOPY . .
RUN npm ci --omit=dev
هر تغییر کد برنامه، ورودی COPY را عوض می‌کند؛ نصب بعدی هم دوباره اجرا می‌شود.Any source change alters COPY input, so the following install runs again.
بهترBetterCOPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
کد برنامه تغییر فقط stepهای بعد از COPY کد برنامه را invalidate می‌کند؛ نصب cached می‌ماند.A source edit invalidates only the later source step; dependency install stays cached.

دو Dockerfile می‌توانند در نهایت یک برنامهٔ یکسان بسازند، ولی یکی با هر تغییر کوچک مجبور شود وابستگی‌ها را از نو نصب کند و دیگری نه. فرقشان فقط در این است که کدام فایل‌ها را زودتر وارد build کرده‌ایم.

Do not turn this into a blind rule. The deeper principle is to keep expensive work behind stable inputs. Real projects, monorepos, generated files, and build tools may require a different ordering.

اگر خواستی ببینی image چطور ساخته شده، سراغ history بروUse image history to inspect what you built—but do not overinterpret it

image history
docker image history demo-web:1.1

  # representative output
  IMAGE        CREATED BY                                      SIZE
  <id>         CMD ["npm" "start"]                              0B
  <missing>    EXPOSE map[3000/tcp:{}]                          0B
  <missing>    ENV PORT=3000                                    0B
  <missing>    COPY server.js ./                                1.2kB
  <missing>    RUN /bin/sh -c npm ci --omit=dev                 ...

گاهی لازم است از خود image بپرسی چه مراحلی پشت سرش بوده‌اند. docker image history برای همین خوب است: یک تصویر کلی از دستورها و سهم تقریبی آن‌ها می‌دهد. فقط حواست باشد history سلامت برنامه را ثابت نمی‌کند؛ هنوز باید container را اجرا کنی.

docker image history is useful for connecting Dockerfile instructions to the resulting image: you can inspect order and approximate contribution. But history does not prove the application is healthy, the port is published correctly, or PID 1 will remain alive. Those are runtime questions.

اطلاعات محرمانه را به لایه نسپارDo not bake secrets into image layers

اطلاعات محرمانه را در ENV یا دستور مستقیم داخل Dockerfile نگذار. پاک‌کردن آن در یک مرحله بعدی لزوماً حضور قبلی‌اش را از تاریخچه یا layerها بی‌اثر نمی‌کند. این فصل وارد اطلاعات محرمانه mount پیشرفته نمی‌شود؛ همین مرز را نگه دار: اطلاعات محرمانه جزو دستور ساخت یا سیستم‌فایل نهایی image نیست.

Do not place secrets in Dockerfile ENV values or command literals. Removing them in a later step does not necessarily erase their earlier presence from image history or layers. This chapter does not teach advanced secret mounts yet; keep the boundary: secrets do not belong in the recipe or final image filesystem.

CMD و ENTRYPOINT؛ کدام بخش قرار است قابل‌تغییر باشد؟CMD and ENTRYPOINT: a default is different from a fixed executable

برای برنامهٔ سادهٔ ما CMD کاملاً کافی است: می‌گوید اگر کاربر دستور دیگری نداد، این برنامه را اجرا کن. ENTRYPOINT وقتی به درد می‌خورد که image قرار است همیشه یک فایل اجرایی مشخص داشته باشد و فقط آرگومان‌هایش عوض شوند. با یک مثال کوچک فرقشان خیلی روشن‌تر می‌شود.

So far our image only needs CMD, which is the clearest choice for a simple app. But you should understand why ENTRYPOINT exists, because real images often combine the two.

Image runtime command resolutionimage configurationENTRYPOINT + CMDdocker runoptional argumentscontainer main processPID 1 chain

image تنظیمات یک پیش‌فرض می‌دهد؛ docker run می‌تواند CMD را جایگزین کند یا برای ENTRYPOINT آرگومان بدهد؛ نتیجه پردازش اصلی container است.

Image configuration provides defaults; docker run can override CMD or provide arguments to ENTRYPOINT; the result becomes the container’s main process.

CMD: پیش‌فرض قابل‌جایگزینی

CMD: a replaceable default

observe CMD
docker image inspect demo-web:1.1 \
    --format 'cmd={{json .Config.Cmd}} entrypoint={{json .Config.Entrypoint}}'

  # representative output
  cmd=["npm","start"] entrypoint=null

  docker run --rm demo-web:1.1 node --version
  # v22.x.x

در دستور آخر، node --version جای CMD را گرفته است؛ image تغییر نکرده، فقط این container با دستور دیگری ساخته شده. اگر دوباره image را بدون دستور اجرا کنی، پیش‌فرض همان npm start است.

In the final command, node --version replaces CMD. The image itself is unchanged; only this container was created with a different command. Run the image again without a command and the default is still npm start.

ENTRYPOINT: وقتی image قرار است مثل یک فایل اجرایی مشخص رفتار کند

ENTRYPOINT: when the image is meant to behave like one executable

برای فهم تفاوت، یک image آزمایشی کوچک بساز:

To make the distinction concrete, build a tiny experimental image:

Dockerfile.entrypoint
FROM alpine:3.23
  ENTRYPOINT ["printf"]
  CMD ["hello\n"]
ENTRYPOINT + CMD behavior
docker build -f Dockerfile.entrypoint -t printf-demo .
  docker run --rm printf-demo
  # hello

  docker run --rm printf-demo 'CodeNames\n'
  # CodeNames

اینجا ENTRYPOINT می‌گوید فایل اجرایی اصلی printf است و CMD آرگومان پیش‌فرض آن را می‌دهد. argumentی که در docker run می‌دهی CMD را عوض می‌کند، اما ENTRYPOINT سر جای خودش می‌ماند. این همان دلیلی است که CMD و ENTRYPOINT دو مفهوم جدا هستند.

Here ENTRYPOINT fixes printf as the executable, while CMD provides its default argument. Arguments supplied to docker run replace CMD, while ENTRYPOINT remains. That is the practical reason the two concepts are separate.

QuestionCMDENTRYPOINT
نقش اصلیPrimary roleپیش‌فرض دستور یا پیش‌فرض آرگومان‌هاdefault command or default argumentsفایل اجرایی اصلی imageprimary executable
argumentهای docker rundocker run argumentsمعمولاً CMD را replace می‌کنندnormally replace CMDدر exec form معمولاً به ENTRYPOINT اضافه می‌شوندnormally append to exec-form ENTRYPOINT
برای برنامه سادهٔ این فصلFor our simple appکافی و خواناsufficient and clearضروری نیستnot required

EXPOSE پورت را روی سیستم تو باز نمی‌کندDo not confuse EXPOSE with publishing a port

این یکی از آن اشتباه‌هایی است که فقط با یک آزمایش کوتاه برای همیشه جا می‌افتد. داخل Dockerfile نوشته‌ایم EXPOSE 3000. حالا container را بدون -p اجرا می‌کنیم و می‌بینیم با اینکه برنامه داخل container روی 3000 گوش می‌دهد، هنوز از localhost راهی به آن نداریم.

This confusion is common enough to deserve a separate experiment. Our image contains EXPOSE 3000. Run it without -p:

EXPOSE is not publish
docker run -d --name no-publish demo-web:1.1
  docker ps --filter name=no-publish
  curl http://localhost:8080

  # curl should fail because no host port was published

داخل container برنامه می‌تواند روی 3000 گوش بدهد و image هم 3000 را expose کرده باشد، ولی هنوز هیچ نگاشت از سیستم میزبان به container ساخته نشده. حالا container را recreate کن:

The application may be listening on 3000 inside the container and the image may document that port with EXPOSE, but no host-to-container mapping exists yet. Recreate it with publishing:

publish explicitly
docker rm -f no-publish
  docker run -d --name demo-web -p 8080:3000 demo-web:1.1
  docker ps --filter name=demo-web
  curl http://localhost:8080

اگر این بار جواب گرفتی، فرق دو مفهوم را عملاً دیده‌ای: EXPOSE دربارهٔ intent/تنظیمات image است؛ -p دربارهٔ شبکه نگاشت در زمان ساخت container.

If the request now succeeds, you have observed the distinction directly: EXPOSE describes image intent/configuration, while -p creates a network mapping when the container is created.

وقتی build یا اجرا خراب شد، اول ببین مشکل در کدام مرحله استFailure workshop: locate the broken boundary before fixing it

از اینجا به بعد عمداً چند چیز را خراب می‌کنیم. قرار نیست با هر خطا یک دستور تصادفی بزنیم. اول می‌پرسیم خطا قبل از ساخته‌شدن image رخ داده یا بعد از آن؟ فایل پیدا نشده؟ یکی از دستورهای build شکست خورده؟ container بالا آمده ولی برنامه جواب نمی‌دهد؟ همین سؤال اول، مسیر بررسی را خیلی کوتاه‌تر می‌کند.

From here on, treat every error as a clue. Before trying random commands, ask which boundary is broken: context, build step, image artifact, startup command, or runtime/port?

COPY فایل را پیدا نمی‌کندCOPY cannot find a file

اولین سؤال: فایل نسبت به build context کجاست و آیا .dockerignore حذفش کرده؟ اگر build در همان COPY ناموفق می‌شود، هنوز هیچ runtimeای برای debug کردن وجود ندارد.

First question: where is the file relative to the build context, and did .dockerignore remove it? If the build fails at COPY, there is no runtime to debug yet.

RUN npm ci ناموفق می‌شودRUN npm ci fails

همان BuildKit مرحله را بخوان. lockfile وجود دارد؟ package manifest معتبر است؟ شبکه/package registry مشکل دارد؟ پورت نگاشت و docker logs اینجا هنوز بی‌ربط‌اند چون container ساخته نشده است.

Read the failing BuildKit step. Does the lockfile exist? Is the package manifest valid? Is the registry/network reachable? Port mapping and docker logs are irrelevant here because no container has been created yet.

build موفق است، container فوراً Exited می‌شودBuild succeeds, container immediately exits

حالا وارد زمان اجرا شده‌ای. docker ps -a، کد خروج و docker logs NAME را ببین. بعد CMD/ENTRYPOINT را inspect کن. «build موفق» فقط خروجی ساخته‌شده را ثابت می‌کند؛ سالم بودن PID 1 را نه.

Now you are in runtime territory. Inspect docker ps -a, exit code, and docker logs NAME, then inspect CMD/ENTRYPOINT. A successful build proves the artifact exists, not that PID 1 stays healthy.

container Up است، مرورگر جواب نمی‌دهدContainer is Up, browser does not respond

اول لاگ‌ها را ببین که برنامه روی چه portی گوش می‌دهد؛ بعد ستون PORTS را با docker ps بررسی کن. EXPOSE 3000 به‌تنهایی نگاشت نیست. اگر برنامه روی 3000 است، برای مثال باید چیزی مثل 0.0.0.0:8080->3000/tcp ببینی.

First read logs to see which port the app listens on, then inspect the PORTS column in docker ps. EXPOSE 3000 alone is not a mapping. If the app listens on 3000, you should see something like 0.0.0.0:8080->3000/tcp.

build خیلی سریع است ولی برنامه هنوز کد قدیمی را نشان می‌دهدThe build is fast but the app still shows old code

قبل از متهم‌کردن cache، بررسی کن واقعاً کدام برچسب را build و کدام برچسب را اجرا کرده‌ای. container قبلی هم ممکن است هنوز از image قبلی ساخته شده باشد. docker image ls، image reference در docker inspect و recreate کردن container شواهد بهتری از حدس هستند.

Before blaming cache, verify which tag you built and which tag you ran. An existing container may still have been created from an older image. docker image ls, the image reference in docker inspect, and recreating the container are better evidence than guessing.

۱۸ تمرین؛ اول حدس بزن، بعد با Docker امتحانش کن18 exercises: predict first, then run

این بخش قرار نیست تبدیل شود به هجده سؤال خشک و هجده جواب کوتاه. قبل از بازکردن هر پاسخ، یک پیش‌بینی بنویس؛ اگر ممکن است دستورش را هم اجرا کن. بعد پاسخ را بخوان و ببین دلیل تو با چیزی که Docker نشان داده یکی بوده یا نه.

If you open every solution immediately, this becomes just another page to read. Before revealing an answer, write at least one prediction or command.

حل‌شدهSolved۰ / ۱۸
تمرین ۴٫1 · Exercise 4.1مقدماتی · Beginner۴ min

Dockerfile، image و container را در یک جمله برای هرکدام تعریف کن؛ طوری که هیچ‌کدام را به‌جای دیگری به کار نبری.

Define Dockerfile, image, and container in one sentence each without using one as a synonym for another.

پاسخ و دلیل · Solution and reasoning

پاسخ: Dockerfile دستور ساخت متنی build است؛ image خروجی ساخته‌شده ساخته‌شده برای اجراست؛ container یک زمان اجرا instance با شناسه و وضعیت خودش است.

Answer: a Dockerfile is a text build recipe; an image is the built artifact used as a runtime template; a container is a runtime instance with its own identity and state.

چرا مهم است؟ تغییر کردن Dockerfile به‌تنهایی image را عوض نمی‌کند، و حذف container هم image را حذف نمی‌کند. این سه چرخهٔ عمر جدا دارند.

Why it matters: editing a Dockerfile does not change an existing image, and removing a container does not remove its image. The three have separate lifecycles.

تمرین ۴٫2 · Exercise 4.2مقدماتی · Beginner۴ min

در docker build -t demo-web:1.0 . نقطه دقیقاً چه نقشی دارد؟

What exactly does the final dot mean in docker build -t demo-web:1.0 .?

پاسخ و دلیل · Solution and reasoning

پاسخ: . build context را مشخص می‌کند؛ یعنی مجموعهٔ فایل‌هایی که سازنده می‌تواند برای instructionهایی مثل COPY استفاده کند.

Answer: . selects the build context, the set of files the builder can use for instructions such as COPY.

آزمایش: دستور را از پوشه دیگری اجرا کن و ببین COPY چگونه نسبت به context جدید رفتار می‌کند. محل Dockerfile و root context لزوماً یکی نیستند.

Experiment: run the build from a different directory and observe how COPY resolves from the new context. Dockerfile location and context root are not necessarily the same.

تمرین ۴٫3 · Exercise 4.3مقدماتی · Beginner۴ min

چرا EXPOSE 3000 باعث نمی‌شود localhost:3000 روی سیستم میزبان خودکار کار کند؟

Why does EXPOSE 3000 not automatically make localhost:3000 work on the host?

پاسخ و دلیل · Solution and reasoning

پاسخ: EXPOSE فراداده/intent image است؛ نگاشت سیستم میزبان نمی‌سازد. انتشار در زمان ساخت container با -p HOST:CONTAINER انجام می‌شود.

Answer: EXPOSE records image metadata/intent; it does not create host mapping. Publishing happens when the container is created with -p HOST:CONTAINER.

شاهد: یک بار container را بدون -p و یک بار با -p 8080:3000 اجرا کن و ستون PORTS را مقایسه کن.

Evidence: run once without -p and once with -p 8080:3000, then compare the PORTS column.

تمرین ۴٫4 · Exercise 4.4مقدماتی · Beginner۴ min

RUN npm ci و docker run demo-web:1.0 چه تفاوت بنیادی دارند؟

What is the fundamental difference between RUN npm ci and docker run demo-web:1.0?

پاسخ و دلیل · Solution and reasoning

پاسخ: اجرا در build اجرا می‌شود و نتیجه‌اش وارد image می‌شود؛ docker run بعداً از image یک container تازه می‌سازد و زمان اجرا دستور را اجرا می‌کند.

Answer: RUN executes during the build and its result becomes part of the image; docker run later creates a container from that image and starts its runtime command.

دام رایج: اگر اجرا ناموفق شود، دنبال container لاگ نگرد؛ container هنوز ساخته نشده است.

Common trap: if RUN fails, do not look for container logs; no container has been created yet.

تمرین ۴٫5 · Exercise 4.5تمرین · Practice۵ min

فقط server.js را تغییر داده‌ای. انتظار داری RUN npm ci در build بعدی اجرا شود یا cached بماند؟ چرا؟

Only server.js changed. Should RUN npm ci execute again or stay cached, and why?

پاسخ و دلیل · Solution and reasoning

پاسخ: با ترتیب Dockerfile این فصل، باید cached بماند چون ورودی وابستگی stage تغییر نکرده است.

Answer: with this chapter’s ordering, it should stay cached because the dependency-stage input did not change.

شاهد: BuildKit باید برای COPY manifest و اجرا نصب عبارت CACHED نشان دهد، در حالی که COPY کد برنامه دوباره اجرا می‌شود.

Evidence: BuildKit should show CACHED for manifest COPY and dependency install while the source COPY reruns.

تمرین ۴٫6 · Exercise 4.6تمرین · Practice۵ min

اگر package-lock.json عوض شود، چرا نصب باید دوباره اجرا شود؟

Why should dependency installation rerun when package-lock.json changes?

پاسخ و دلیل · Solution and reasoning

پاسخ: lockfile بخشی از ورودی وابستگی مرحله است. وقتی ورودی عوض شود، نتیجهٔ cache قدیمی دیگر نمایندهٔ وابستگی graph جدید نیست.

Answer: the lockfile is part of the dependency step’s input. Once that input changes, the old cached result no longer represents the new dependency graph.

نکته: cache باید درست باشد، نه فقط سریع. استفادهٔ دوباره اشتباه از ساخت دوباره کندتر خطرناک‌تر است.

Point: cache must be correct, not merely fast. Incorrect reuse would be worse than a slower rebuild.

تمرین ۴٫7 · Exercise 4.7تمرین · Practice۵ min

چرا copy کردن node_modules از سیستم میزبان معمولاً تصمیم بدی است؟

Why is copying host node_modules usually a bad idea?

پاسخ و دلیل · Solution and reasoning

پاسخ: هم context را بزرگ می‌کند، هم dependencyهای نصب‌شده روی سیستم میزبان ممکن است با OS/architecture داخل image سازگار نباشند. وابستگی را در محیط build image نصب می‌کنیم.

Answer: it inflates the context, and host-installed dependencies may not match the image OS/architecture. Install dependencies inside the image build environment instead.

شاهد: اندازهٔ context و presence/absence node_modules را با و بدون ignore مقایسه کن.

Evidence: compare context transfer and file presence with and without ignoring node_modules.

تمرین ۴٫8 · Exercise 4.8تمرین · Practice۵ min

COPY server.js ./ ناموفق شده. قبل از تغییر Dockerfile، دو بررسی اولت چیست؟

COPY server.js ./ failed. Before editing the Dockerfile, what are your first two checks?

پاسخ و دلیل · Solution and reasoning

پاسخ: اول root build context و محل واقعی فایل؛ بعد .dockerignore. اگر کد برنامه اصلاً ورودی سازنده نیست، تغییر مقصد یا راه‌اندازی دوباره Docker مسئله را حل نمی‌کند.

Answer: first check the build-context root and actual file location, then .dockerignore. If the source is not part of builder input, changing the destination or restarting Docker does not solve the problem.

شاهد و دام: دستور را از پوشه پروژه اجرا کن و با pwd و ls -la server.js ثابت کن فایل زیر همان نقطهٔ آخر docker build ... . است. دام رایج تغییر مقصد در COPY یا نوشتن ../server.js است؛ مسیر دوم بیرون از context است و Docker عمداً آن را نمی‌پذیرد.

Evidence and trap: run the command from the project directory and use pwd plus ls -la server.js to prove that the file sits below the final-dot root of docker build ... .. A common trap is changing the COPY destination or writing ../server.js; that path is outside the context and Docker intentionally rejects it.

تمرین ۴٫9 · Exercise 4.9تمرین · Practice۵ min

چرا WORKDIR /app با RUN cd /app جایگزین خوبی ندارد؟

Why is RUN cd /app not a good replacement for WORKDIR /app?

پاسخ و دلیل · Solution and reasoning

پاسخ: cd فقط همان اجرا شل را جابه‌جا می‌کند؛ WORKDIR برای instructionهای بعدی و زمان اجرا تنظیمات باقی می‌ماند.

Answer: cd only affects that RUN shell; WORKDIR persists for later instructions and runtime configuration.

آزمایش: بعد از build با docker run --rm demo-web:1.1 pwd working پوشه پیش‌فرض را ببین.

Experiment: after building, inspect the default working directory with docker run --rm demo-web:1.1 pwd.

تمرین ۴٫10 · Exercise 4.10تمرین · Practice۴ min

بعد از تغییر کردن کد برنامه، چرا فقط docker run demo-web:1.0 کافی نیست؟

After editing source, why is simply running docker run demo-web:1.0 not enough?

پاسخ و دلیل · Solution and reasoning

پاسخ: برچسب هنوز به image ساخته‌شدهٔ قبلی اشاره می‌کند. کد برنامه روی disk تا وقتی ساخت دوباره نکنی وارد خروجی ساخته‌شده جدید نمی‌شود.

Answer: the tag still references the previously built image. Source on disk does not enter a new artifact until you rebuild.

شاهد: با docker build -t demo-web:1.1 . خروجی ساخته‌شده جدید بساز و همان برچسب را اجرا کن.

Evidence: build a new artifact with docker build -t demo-web:1.1 . and run that exact tag.

تمرین ۴٫11 · Exercise 4.11تمرین · Practice۵ min

build موفق است ولی container فوراً Exited می‌شود. از کجا شروع می‌کنی؟

The build succeeds but the container exits immediately. Where do you start?

پاسخ و دلیل · Solution and reasoning

ترتیب: docker ps -a → کد خروج → docker logs → inspect CMD/ENTRYPOINT. چون build تمام شده، مسئله حالا زمان اجرا است.

Order: docker ps -a → exit code → docker logs → inspect CMD/ENTRYPOINT. Since the build finished, the problem is now runtime.

دام رایج: قبل از اینکه ثابت کنی پردازش زنده است سراغ پورت نرو.

Common trap: do not debug ports before proving the process stays alive.

تمرین ۴٫12 · Exercise 4.12تمرین · Practice۵ min

با imageای که فقط CMD دارد، docker run --rm demo-web:1.1 node --version چه چیزی را عوض می‌کند؟

For an image that only defines CMD, what does docker run --rm demo-web:1.1 node --version change?

پاسخ و دلیل · Solution and reasoning

پاسخ: دستور زمان اجرا همان container را جایگزین می‌کند؛ Dockerfile و image تنظیمات را mutate نمی‌کند. اجرای بعدی بدون جایگزین دوباره CMD پیش‌فرض را می‌گیرد.

Answer: it overrides the runtime command for that container; it does not mutate the Dockerfile or image configuration. A later run without an override uses the default CMD again.

شاهد و دام: با docker image inspect demo-web:1.1 --format '{{json .Config.Cmd}}' پیش‌فرض ذخیره‌شده را بخوان، سپس خروجی نسخه را ببین و یک‌بار بدون جایگزین اجرا کن. دام این است که جایگزین را تغییر دائمی image بدانی؛ این فقط تنظیمات همان container تازه را عوض می‌کند.

Evidence and trap: read the stored default with docker image inspect demo-web:1.1 --format '{{json .Config.Cmd}}', observe the version output, then run once without an override. The trap is treating an override as a permanent image edit; it changes configuration only for that new container.

تمرین ۴٫13 · Exercise 4.13سناریو · Scenario۶ min

در imageای با ENTRYPOINT ["printf"] و CMD ["hello\n"]، اجرای docker run IMAGE 'bye\n' چه پردازش/argumentی می‌سازد؟

For an image with ENTRYPOINT ["printf"] and CMD ["hello\n"], what process/argument results from docker run IMAGE 'bye\n'?

پاسخ و دلیل · Solution and reasoning

پاسخ: ENTRYPOINT همان printf می‌ماند و آرگومان زمان اجرا جای CMD را می‌گیرد؛ نتیجهٔ مفهومی printf 'bye\n' است.

Answer: ENTRYPOINT remains printf, while the runtime argument replaces CMD; conceptually the result is printf 'bye\n'.

دلیل: با exec-form ENTRYPOINT، argumentهای اجرا به فایل اجرایی ثابت می‌رسند؛ این رفتار با image‌ای که فقط CMD دارد فرق دارد.

Reason: with exec-form ENTRYPOINT, run arguments are passed to the fixed executable; that differs from an image that only defines CMD.

تمرین ۴٫14 · Exercise 4.14سناریو · Scenario۶ min

دو Dockerfile داری: یکی COPY . . را قبل از RUN npm ci می‌گذارد و دیگری manifestها را جدا copy می‌کند. برای teamی که روزی چند بار کد برنامه تغییر می‌کند، کدام بهتر است و دقیقاً چرا؟

Two Dockerfiles differ only in ordering: one does COPY . . before RUN npm ci, while the other copies manifests separately first. Which is better for a team that edits source many times per day, and exactly why?

پاسخ و دلیل · Solution and reasoning

پاسخ: نسخهٔ دوم، چون کد برنامه تغییر ورودی وابستگی مرحله را عوض نمی‌کند. در نسخهٔ اول، COPY بزرگ قبل از اجرا با هر تغییر تغییر می‌کند و نصب بعدی را هم invalidate می‌کند.

Answer: the second. A source edit does not change dependency-step inputs. In the first Dockerfile, the broad COPY changes on every edit and invalidates the later install step.

شاهد: خروجی BuildKit هر دو را بعد از یک تغییر فقط در سرور.js مقایسه کن.

Evidence: compare BuildKit output from both Dockerfiles after changing only server.js.

تمرین ۴٫15 · Exercise 4.15سناریو · Scenario۶ min

docker image history چه چیزی را می‌تواند ثابت کند و چه چیزی را نمی‌تواند؟

What can docker image history prove, and what can it not prove?

پاسخ و دلیل · Solution and reasoning

می‌تواند: ترتیب و شکل کلی instructionهای image و سهم تقریبی آن‌ها را نشان دهد. نمی‌تواند: ثابت کند HTTP برنامه سالم اجرا می‌شود، پورت انتشار شده یا پردازش بعداً crash نمی‌کند.

It can: show the rough instruction/history sequence and approximate contribution. It cannot: prove the HTTP app is healthy, a host port is published, or the process will not crash later.

شاهد و دام: docker image history demo-web:1.1 را کنار docker ps -a و یک curl بخوان: اولی سرنخ build می‌دهد، دومی وضعیت زمان اجرا و سومی رفتار HTTP را. دام این است که تاریخچه را audit کامل اطلاعات محرمانه یا اثبات سلامت زمان اجرا بدانی؛ برای هیچ‌کدام کافی نیست.

Evidence and trap: read docker image history demo-web:1.1 beside docker ps -a and a curl: the first supplies build clues, the second runtime state, and the third HTTP behavior. The trap is treating history as a complete secret audit or proof of runtime health; it is sufficient for neither.

تمرین ۴٫16 · Exercise 4.16سناریو · Scenario۶ min

لاگ می‌گوید برنامه روی 3000 گوش می‌دهد، container هم Up است، ولی curl localhost:8080 ناموفق می‌شود. چه چیزی را بررسی می‌کنی؟

Logs say the app listens on 3000 and the container is Up, but curl localhost:8080 fails. What do you inspect?

پاسخ و دلیل · Solution and reasoning

اول: ستون PORTS یا inspect پورت bindings. باید mappingی مثل 0.0.0.0:8080->3000/tcp وجود داشته باشد. اگر نیست، EXPOSE کمکی نمی‌کند؛ container را با -p 8080:3000 recreate کن.

First: inspect the PORTS column or port bindings. You need a mapping such as 0.0.0.0:8080->3000/tcp. If it is absent, EXPOSE does not help; recreate the container with -p 8080:3000.

شاهد و دام: docker inspect NAME --format '{{json .NetworkSettings.Ports}}' باید binding مورد انتظار را نشان دهد. اگر binding درست است اما درخواست هنوز ناموفق می‌شود، لاگ و تنظیمات برنامه را برای listen روی 0.0.0.0:3000 بررسی کن؛ processی که فقط روی 127.0.0.1 داخل container گوش می‌دهد، از انتشار بیرون قابل‌دسترسی نیست.

Evidence and trap: docker inspect NAME --format '{{json .NetworkSettings.Ports}}' should show the expected binding. If it is correct but the request still fails, inspect logs and app configuration for listening on 0.0.0.0:3000; a process bound only to 127.0.0.1 inside the container is not reachable through publishing.

تمرین ۴٫17 · Exercise 4.17سناریو · Scenario۶ min

چطور .dockerignore هم performance را بهتر می‌کند و هم احتمال اشتباه را کم می‌کند؟ چرا با این حال security مرز کامل نیست؟

How does .dockerignore improve performance and reduce mistakes, yet still fail to be a complete security boundary?

پاسخ و دلیل · Solution and reasoning

پاسخ: فایل‌های بی‌استفاده را از context کنار می‌گذارد، پس ورودی کوچک‌تر و قابل‌پیش‌بینی‌تر می‌شود و چیزهایی مثل .env کمتر تصادفی وارد build می‌شوند. اما اگر اطلاعات محرمانه را مستقیم در Dockerfile، ENV یا اجرا مستقیم قرار بدهی، .dockerignore آن را نجات نمی‌دهد.

Answer: it removes unnecessary files from the context, making input smaller and more predictable and reducing accidental inclusion of files such as .env. But it cannot protect a secret that you explicitly place in Dockerfile ENV or RUN literals.

شاهد و دام: در build خروجی به اندازهٔ context نگاه کن و با docker image history بررسی کن فایل ناخواسته به لایه نرفته باشد. دام رایج یکی دانستن .gitignore و .dockerignore است: اولی رفتار Git را کنترل می‌کند و دومی ورودی Docker سازنده را.

Evidence and trap: inspect the context size in build output and use docker image history to check that an unwanted file did not enter a layer. A common trap is equating .gitignore with .dockerignore: the first controls Git, while the second controls Docker builder input.

تمرین ۴٫18 · Exercise 4.18سناریو · Scenario۷ min

یک گزارش کوتاه برای سناریوی «build موفق است ولی مرورگر چیزی نشان نمی‌دهد» بنویس. ترتیب بررسی‌ات باید از خروجی ساخته‌شده تا زمان اجرا قابل‌دفاع باشد.

Write a short report for “the build succeeded but the browser shows nothing.” Your diagnostic order should be defensible from artifact to runtime.

پاسخ و دلیل · Solution and reasoning

ترتیب پیشنهادی: ۱) برچسب/image درست را با docker image ls تأیید کن؛ ۲) container را با docker ps -a پیدا کن؛ ۳) اگر Exited است کد خروج و لاگ‌ها را بخوان؛ ۴) اگر Up است لاگِ listening و پردازش را بررسی کن؛ ۵) PORTS را با نگاشت مورد انتظار تطبیق بده؛ ۶) در آخر curl را اجرا کن.

Suggested order: 1) confirm the expected image/tag with docker image ls; 2) locate the container with docker ps -a; 3) if Exited, inspect exit code and logs; 4) if Up, confirm listening logs/process; 5) compare PORTS with the expected mapping; 6) finally run curl.

چرا این ترتیب؟ چون هر مرحله پیش‌نیاز مرحلهٔ بعدی است؛ تغییر تصادفی پورت وقتی پردازش اصلاً مرده، فقط مشکل را پنهان می‌کند.

Why this order? Each step establishes a prerequisite for the next. Randomly changing ports while the process is dead only hides the real problem.

آزمایشگاه: برنامهٔ خودت را از فایل تا container دنبال کنChapter lab: source to image, then a deliberately invalidated cache

آزمایشگاه · حدود ۳۵ دقیقهLab · about 35 minutes

سناریو: «یک برنامه کوچک را طوری image کن که نفر بعدی بتواند دقیقاً همان build را تکرار کند»

Scenario: “image a tiny app so another person can reproduce the same build”

همان برنامهٔ کوچک این فصل را از صفر می‌سازی، image می‌گیری، اجراش می‌کنی، cache را امتحان می‌کنی و بعد عمداً خرابش می‌کنی. آخر کار باید بتوانی فقط با نگاه به خروجی‌ها توضیح بدهی چه چیزی تغییر کرد و چرا build بعدی رفتار متفاوتی داشت.

Create the chapter files in a fresh directory. Name the image chapter4-web:1.0 and the container chapter4-web. Do not record merely “done”; capture the command and the key output for each step.

  1. Context را تعریف کنDefine the context

    tree پروژه و .dockerignore را ثبت کن. برای هر COPY بنویس کد برنامه از کجای context می‌آید.

    Record the project tree and .dockerignore. For every COPY, state where its source comes from in the context.

  2. Dockerfile را build کنBuild the Dockerfile

    با docker build -t chapter4-web:1.0 . image بساز و خط naming/export نهایی را نگه دار.

    Build with docker build -t chapter4-web:1.0 . and retain the final naming/export evidence.

  3. زمان اجرا را جداگانه ثابت کنProve runtime separately

    با -d -p 8080:3000 اجرا کن؛ وضعیت، لاگ listening و پاسخ curl را ثبت کن.

    Run with -d -p 8080:3000; record status, the listening log, and a curl response.

  4. Cache hit را ببینObserve a cache hit

    بدون تغییر ساخت دوباره کن و stepهایی که CACHED هستند نام ببر.

    Rebuild without changes and list the steps reported as CACHED.

  5. فقط کد برنامه invalidationSource-only invalidation

    فقط متن پاسخ در server.js را تغییر بده. ساخت دوباره کن و ثابت کن نصب وابستگی هنوز cached است.

    Change only the response text in server.js. Rebuild and show that dependency installation remains cached.

  6. وابستگی invalidationDependency invalidation

    وابستگی/lockfile را تغییر بده و دوباره build کن. توضیح بده چرا این بار npm ci rerun شد.

    Change a dependency/lockfile and rebuild. Explain why npm ci reruns this time.

  7. خرابی عمدیDeliberate failure

    یا server.js را موقتاً ignore کن، یا CMD را خراب کن. قبل از راه‌حل، مشخص کن خطا build-time است یا زمان اجرا و با خروجی مربوطه این ادعا را ثابت کن.

    Temporarily ignore server.js or break CMD. Before fixing it, classify the failure as build-time or runtime and prove that classification with the relevant output.

  8. پاک‌سازی آگاهانهDeliberate cleanup

    بعد از ذخیرهٔ خروجی‌ها، container را حذف کن. image را فقط اگر برای تمرین بعد لازم نداری پاک کن.

    After retaining the outputs, remove the container. Remove the image only if you no longer need it for the next exercise.

معیار موفقیت · Success criteria

موفقیت یعنی بتوانی بدون جملهٔ مبهم «Docker cache کرد» توضیح بدهی دقیقاً کدام ورودی‌ها ثابت ماندند، کدام مرحله دوباره ساخته شد، build success را از زمان اجرا success جدا کنی و حداقل یک خطا را از روی خروجی در مرز درست تشخیص بدهی.

Success means you can explain exactly which inputs stayed stable, which step rebuilt, distinguish build success from runtime success, and diagnose at least one deliberate failure at the correct boundary using its output—not merely say “Docker cached it.”

اگر پروژه را به همکارت بدهی، چه چیزهایی باید همراهش باشد؟What evidence is enough for a teammate to reproduce your result?

CheckpointWhat to captureWhat it proves
contextcontextpwd, tree/listing, .dockerignoreCOPY sourceها واقعاً جزو build ورودی هستندCOPY sources are actually part of build input
خروجی ساخته‌شدهartifactdocker build -t chapter4-web:1.0 .
docker image ls chapter4-web
image با برچسب موردنظر ساخته شدهthe expected image/tag exists
زمان اجراruntimedocker ps -a
docker logs chapter4-web
main پردازش بالا آمده یا علت خروج چیستwhether the main process stayed alive or why it exited
شبکه مسیرnetwork pathPORTS + curl -i http://localhost:8080نگاشت سیستم میزبان و خود برنامه واقعاً پاسخ می‌دهندhost mapping and the app actually respond
cachecacheunchanged build + source-only build + lockfile buildکجا cache hit شد و چرا invalidate شدwhere cache hit and why it invalidated

مرجع سریعQuick reference

docker build -t NAME:TAG .
  docker image ls NAME
  docker image history NAME:TAG
  docker image inspect NAME:TAG
  docker run -d --name NAME -p 8080:3000 IMAGE
  docker logs NAME
  docker ps -a
  docker rm -f NAME
FROM
  WORKDIR
  COPY
  RUN
  ENV
  EXPOSE
  CMD
  ENTRYPOINT

  .dockerignore
مدل نهایی فصلThe chapter’s durable model

اگر بخواهی کل فصل را در یک تصویر ذهنی نگه داری: Dockerfile دستور ساخت است، build context فایل‌هایی است که Docker می‌تواند ببیند، build یک image می‌سازد، cache کارهای تکراری را دوباره انجام نمی‌دهد و docker run بعداً از همان image یک container می‌سازد.

A Dockerfile is the recipe; build context is builder input; a build produces an image; the image carries built filesystem state and configuration; cache reuses compatible step results; and a container is the later runtime instance of that image.