پروژهٔ ۱ — یک سرویس، یک ایمیج
Project 1 — One service, one image
این بار Dockerfile آماده تحویل نمیگیری. یک سرویس کوچک را میشناسی، خودت بستهبندیاش میکنی و با مدرک نشان میدهی که همکار دیگری میتواند image را از registry بگیرد و اجرا کند.
This time you will not be handed a finished Dockerfile. You will inspect a small service, package it yourself, and prove that another developer can pull and run its image from a registry.
مأموریت اول: یک برنامهٔ محلی را تبدیل کن به imageای که واقعاً قابلتحویل باشدFrom “it works on my laptop” to a portable image
به تیمی اضافه شدهای که یک برنامهٔ وب کوچک دارد. روی لپتاپ با npm start بالا میآید، اما هنوز کسی آن را containerize نکرده. همتیمیات فقط یک خواسته دارد: «image را بده تا روی ماشین خودم pull کنم، روی هر میزبان port آزادی بالا بیاورم و همان پاسخ را بگیرم.»
You have joined a team with a small web application. It runs with npm start, but nobody has containerized it yet. A teammate says: “I want to pull the image, run it on any free host port, and get the same response.”
این پروژه قرار نیست فصل تازهای دربارهٔ Dockerfile باشد. همهٔ قطعات را قبلاً دیدهای. اینجا باید آنها را کنار هم بگذاری و ثابت کنی خروجی کارت واقعاً قابلحمل است: build تکرارپذیر، runtime تمیز، کاربر non-root، healthcheck، تنظیمات زمان اجرا، لاگ، limit و در پایان registry.
Each arrow is a verifiable hand-off: source is built, the image gets a version tag, the registry stores it, and a pulled copy serves a response.
محدوده عمداً کوچک است: یک Node HTTP API، یک image برنامه و یک container. Compose، پایگاهداده و reverse proxy وارد این پروژه نمیشوند. این محدودیت کمک میکند بفهمیم خود image چه چیزی را تضمین میکند و چه چیزی را باید هنگام اجرا تنظیم کنیم.
The scope is deliberately small: one Node HTTP API, one application image, and one container. Compose, a database, and a reverse proxy are out of scope. That boundary helps us see what belongs in the image and what must be configured at run time.
تحویل نهایی باید برای نفر بعد قابلاستفاده باشد، نه فقط برای خودتWhat should you have at the end?
در پایان یک پوشهٔ پروژه داری با source، lockfile، .dockerignore و Dockerfile. image با tag نسخهدار ساخته میشود، روی میزبان port دلخواه اجرا میشود، health و user و limitهایش قابلبررسیاند و لاگها از بیرون دیده میشوند. بعد همان image را push میکنی، نسخهٔ محلی را حذف میکنی و دوباره از registry برمیگردانی.
Deliver a project directory with source, package-lock.json, .dockerignore, and a Dockerfile. Build a version-tagged image; run it with a chosen name and port; inspect health, user, configuration, logs, and limits; then push it, remove the local copy, and pull and run it again from the registry.
معیار موفقیت این نیست که «روی سیستم من کار کرد». نفر دیگری باید بدون source بتواند image را pull و run کند. اگر برای اجرا مجبور شدی چیزی را دستی داخل container اصلاح کنی، هنوز خروجی ساختهشده قابلتحویل نداری.
The starter has a real build step: TypeScript becomes JavaScript in dist/. A multi-stage build is therefore useful here, not ceremonial: build tools stay in the first stage and runtime receives only what it needs. For an app with no build step, do not add a second stage just to have two FROM lines.
اول برنامه را بفهم؛ بعد Dockerfile بنویسTake the starter; do not write the Dockerfile yet
قبل از اینکه حتی یک خط Dockerfile اضافه کنی، برنامهٔ starter را مستقیم اجرا کن. ببین از کدام port میخواند، چه فایلی build میشود، کجا log میدهد و آیا در زمان اجرا چیزی روی دیسک مینویسد. اگر رفتار وضعیت پایه را ندانی، بعداً نمیفهمی خطا از برنامه است یا containerization.
These are the app’s three small source files. First build a mental model: which file is source, which command produces JavaScript for runtime, and which path receives run-time writes?
{
"name": "one-service-one-image",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js"
},
"devDependencies": {
"@types/node": "^22.15.0",
"typescript": "^5.8.3"
}
}{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}import { appendFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { createServer } from 'node:http';
const port = Number(process.env.PORT || 3000);
const mode = process.env.APP_MODE || 'development';
const dataDir = process.env.DATA_DIR || './runtime';
mkdirSync(dataDir, { recursive: true });
const server = createServer((request, response) => {
const path = (request.url || '/').split('?')[0];
console.log(`${new Date().toISOString()} ${request.method} ${path}`);
if (path === '/health') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ status: 'ok' }));
return;
}
if (path === '/write') {
try {
appendFileSync(join(dataDir, 'events.log'), 'write probe\n');
response.writeHead(201);
response.end('write succeeded\n');
} catch (error) {
console.error('runtime write failed:', error);
response.writeHead(500);
response.end('runtime write failed\n');
}
return;
}
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ app: 'one-service', mode, version: '1.0.0' }));
});
server.listen(port, '0.0.0.0', () => {
console.log(`listening on ${port}; mode=${mode}; dataDir=${dataDir}`);
});این مرحله کوتاه است ولی ارزش دارد. قرار نیست Docker مشکل برنامهای را که از اول اجرا نمیشود پنهان کند. خروجی مستقیم برنامه را نگه دار تا بعداً با رفتار داخل container مقایسهاش کنی.
In the code, /health reports app readiness; /write exists for a failure drill; ordinary requests produce a stdout log. Write errors go to stderr. PORT and APP_MODE are read at run time, so changing them should not require a rebuild.
فایلها را در پوشهای تازه با این چیدمان بساز. پس از npm install، فایل lock را نگه دار و commit کن؛ Dockerfile باید بتواند dependency دقیقشده را با npm ci نصب کند.
Create the files in a fresh directory with this layout. After npm install, keep and commit the lockfile; the Docker build should be able to install the pinned dependency tree with npm ci.
one-service/
├── package.json
├── tsconfig.json
└── src/
└── server.tsnpm install npm run build npm start
در یک terminal دیگر، curl http://localhost:3000/ و curl http://localhost:3000/health را بزن. یک بار هم curl -i http://localhost:3000/write را امتحان کن. اگر برنامه محلی کار نمیکند، هنوز وقت سرزنش Docker نیست؛ اول build یا اجرای Node را درست کن.
In another terminal, try curl http://localhost:3000/ and curl http://localhost:3000/health. Also try curl -i http://localhost:3000/write. If the local app does not work, Docker is not yet a suspect; fix the build or Node process first.
چهارده گام؛ هر گام فقط وقتی تمام است که شاهدش را داشته باشیFourteen milestones; one piece of evidence at each
پروژه را یکباره با Dockerfile نهایی شروع نکن. اول image سادهای بساز که کار میکند، بعد build context را تمیز کن، multi-stage را اضافه کن، user را محدود کن، healthcheck و تنظیمات runtime را بسنج و در آخر سراغ registry برو. این ترتیب عمداً از «کار میکند» به «قابلتحویل است» حرکت میکند.
Move on when you can show evidence for the previous milestone. The goal is not a Dockerfile packed with instructions; it is an artifact another person can pull and run independently.
۱. starter را بخوان1. Read the starter
بعد از هر گام یک خروجی کوتاه نگه دار: image ساخته شد؟ container با چه userی اجرا شد؟ health چه گفت؟ limit واقعاً اعمال شد؟ اگر چیزی شکست، همان مرحله را اصلاح کن و بعد جلو برو. جمعکردن چند تغییر با هم فقط تشخیص را سخت میکند.
Trace the request path, health endpoint, run-time variables, and write to DATA_DIR. Identify which files belong in the image and which are needed only to build.
راهنماییHint
TypeScript source در src/ است و script build در package.json آن را به dist/ میبرد. Node در مرحلهٔ اجرا dist/server.js را میخواند.
TypeScript source is in src/; the build script in package.json writes JavaScript to dist/. Node reads dist/server.js at run time.
۲. بدون Docker اجرا کن2. Run it without Docker
با npm install، npm run build و npm start اجرا کن؛ سپس root، health و write probe را آزمایش کن. این وضعیت پایه بعداً مرجع مقایسه است.
Run npm install, npm run build, and npm start; then test root, health, and the write probe. This baseline is your comparison point later.
راهنماییHint
در پاسخ root باید mode و version را ببینی. برای اجرای /write یکبار DATA_DIR=./runtime تنظیم کن یا مقدار پیشفرض را نگه دار؛ برنامه پوشه را میسازد.
The root response should show mode and version. For /write, set DATA_DIR=./runtime once or keep the default; the app creates the directory.
۳. build context و .dockerignore را طراحی کن3. Design the build context and .dockerignore
ریشهٔ پروژه باید context باشد؛ نه پوشهٔ والد و نه کل repository. فایلهای میزبان، خروجی محلی، اسرار و دادهٔ runtime را وارد context نکن؛ اما source و lockfile را ناخواسته حذف نکن.
Use the project root as the context—not its parent and not the whole repository. Keep host files, local output, secrets, and run-time data out, but do not accidentally exclude source or the lockfile.
راهنماییHint
حداقل دربارهٔ node_modules، dist، runtime، logهای npm، پوشهٔ Git و فایلهای محیطی فکر کن. بعداً بررسی کن Docker واقعاً source و package-lock.json را میبیند.
Consider node_modules, dist, runtime, npm logs, Git metadata, and environment files. Later, verify that Docker can still see source and package-lock.json.
۴. نخستین Dockerfile کارا را بساز4. Build your first working Dockerfile
فعلاً هدف کمینهسازی نیست: image بساز و یک container بالا بیاور. انتخاب context، کپیکردن lockfile و اجرای scriptهای build و start را خودت به هم وصل کن.
Do not optimize yet: build an image and start a container. Work out the context, lockfile copy, and how the build and start scripts fit together.
راهنماییHint
با build stage واحد شروع کن: پایهٔ Node، WORKDIR، کپی manifestها، npm ci، کپی source، npm run build و فرمان foreground برای server. پورت را در این مرحله هم در app و هم هنگام docker run -p درست در نظر بگیر.
Start with one build stage: Node base, WORKDIR, copy manifests, npm ci, copy source, npm run build, and a foreground server command. Keep the app port and docker run -p mapping straight.
۵. image را مشاهده کن5. Observe the image
با docker image ls اندازه را ببین؛ با docker image inspect تنظیمات را؛ و با docker history بفهم چه مرحلههایی در image تکمرحلهای ماندهاند.
Check size with docker image ls, configuration with docker image inspect, and retained build steps with docker history.
راهنماییHint
اگر ابزار TypeScript، source یا node_modules توسعه در image نهایی مانده، بپرس آیا برای اجرای dist/server.js واقعاً لازماند.
If TypeScript, source, or development node_modules remain in the final image, ask whether they are actually needed to run dist/server.js.
۶. build را از runtime جدا کن6. Separate build from runtime
TypeScript مرحلهٔ build است؛ runtime به Node و خروجی JavaScript نیاز دارد، نه compiler. Dockerfile را multi-stage کن و فقط خروجی ساختهشده لازم و فایل آغاز برنامه را به stage نهایی ببر. اندازه را دوباره اندازه بگیر.
TypeScript is a build-time tool; runtime needs Node and compiled JavaScript, not the compiler. Make the Dockerfile multi-stage and copy only the required artifact and start metadata into the final stage. Measure again.
راهنماییHint
stageها را با نقششان نامگذاری کن، مثلاً build و runtime. COPY --from=build باید از مسیر واقعی خروجی compiler کپی کند؛ وجود آن را با build log یا اجرای target build ثابت کن.
Name stages by role, such as build and runtime. COPY --from=build must use the compiler’s real output path; prove it from build output or by building that target.
۷. با کاربر non-root اجرا کن7. Run as a non-root user
در image نهایی یک کاربر غیرریشه داشته باش و مالکیت فایلهای موردنیازش را درست تنظیم کن. docker top و docker inspect را مدرک قرار بده؛ فقط به نیت Dockerfile اکتفا نکن.
Create a non-root user in the final image and assign ownership deliberately. Use docker top and docker inspect as evidence; do not rely on Dockerfile intent alone.
راهنماییHint
کاربر app به خواندن /app/dist و نوشتن در /app/runtime نیاز دارد. کل image را writable نکن؛ فقط مسیر لازم را مالک همان user کن.
The app user needs to read /app/dist and write to /app/runtime. Do not make the whole image writable; assign only the required path to that user.
۸. healthcheck واقعی اضافه کن8. Add a meaningful healthcheck
healthcheck باید مسیر سرویس سلامت را بسنجد، نه اینکه صرفاً وجود فرایند را ثابت کند. مسیر درست را انتخاب کن و بررسی کن probe از binary موجود در image استفاده میکند.
The healthcheck should test the health endpoint, not merely prove that a process exists. Choose the real path and verify that the probe uses a binary present in the image.
راهنماییHint
چون Node در runtime هست و curl تضمینشده نیست، یک probe کوتاه با Node و HTTP مناسب است. کد خروج صفر یعنی موفق؛ برای مشاهده، چند ثانیه صبر کن و Health را با docker inspect بخوان.
Node is present at run time while curl is not guaranteed, so a short Node HTTP probe is a good fit. Exit code zero means success; wait briefly and inspect Health.
۹. تنظیمات را بدون rebuild عوض کن9. Change run-time configuration without rebuilding
PORT و APP_MODE را در زمان اجرای container تنظیم کن. پاسخ مسیر سرویس باید تغییر mode را نشان دهد. میزبان port در سمت چپ -p است و port داخل container در سمت راست.
Set PORT and APP_MODE when starting the container. The endpoint should reflect the changed mode. In -p, the host port is on the left and the container port is on the right.
راهنماییHint
اگر برنامه در container روی 3000 است، نگاشت مثلاً 8087:3000 است. عوضکردن فقط میزبان port، برنامه را جابهجا نمیکند؛ عوضکردن PORT داخل container نیازمند هماهنگکردن سمت راست mapping و healthcheck است.
If the app listens on 3000 in the container, a mapping could be 8087:3000. Changing only the host port does not move the app; changing its in-container PORT requires updating the right side of the mapping and the healthcheck.
۱۰. لاگها را از مسیر درست ببین10. Read logs through the right channel
چند درخواست بفرست و docker logs را بخوان. رویداد معمول باید در stdout و خطای نوشتن در stderr ظاهر شود؛ container را برای دیدن log به shell تعاملی وابسته نکن.
Send a few requests and read docker logs. Normal events should appear on stdout and write errors on stderr; do not depend on an interactive shell to see logs.
راهنماییHint
گزینههای -f و --tail 20 برای دنبالکردن و محدودکردن خروجیاند. Docker لاگها رفتار یکسانی برای همهٔ logging driverها تضمین نمیکند؛ برای این آزمایش از تنظیم معمول محلی Docker استفاده کن.
Use -f to follow and --tail 20 to limit output. Docker logs is not identical across every logging driver; use the normal local Docker logging setup for this experiment.
۱۱. هنگام اجرا حد بگذار11. Set run-time limits
این workload کوچک است؛ حدهای محافظهکارانه انتخاب کن و تنظیمات ساختهشده را inspect کن. محدودیت CPU، memory و PID در docker run است، نه چیزی که با تغییر کد نیاز به rebuild داشته باشد.
This is a small workload; choose conservative limits and inspect the resulting configuration. CPU, memory, and PID limits belong to docker run, not to a code change that requires a rebuild.
راهنماییHint
برای نمونهٔ آموزشی، 128 MiB، نیم CPU و 64 PID سقفهای کوچک و محدودند؛ اگر محیط تو برای Node کافی نیست، حد را با دلیل کمی بالا ببر. limit مدرک مصرف واقعی یا تضمین نبودن فشار منابع نیست.
For this exercise, 128 MiB, half a CPU, and 64 PIDs are small bounded ceilings; if your environment needs more for Node, raise a limit with a reason. A limit is not evidence of actual use or a guarantee against resource pressure.
۱۲. tag نسخهدار را به registry بفرست12. Push a versioned tag to a registry
یک registry در دسترس انتخاب کن، image را با نام namespace خودت و tagی مثل 1.0.0 برچسب بزن، وارد registry شو و push کن. secret ورود را داخل Dockerfile یا دستور ذخیرهشده ننویس.
Choose an available registry, tag the image under your own namespace with a version such as 1.0.0, authenticate, and push. Do not put registry credentials in a Dockerfile or saved command.
راهنماییHint
نام کامل image معمولاً شکل registry/namespace/name:version دارد. Docker Hub میتواند USERNAME/one-service:1.0.0 باشد؛ برای GHCR از namespace حساب خودت استفاده کن. مطمئن شو tag محلی دقیقاً همان است که push میکنی.
A full image name usually looks like registry/namespace/name:version. Docker Hub could use USERNAME/one-service:1.0.0; for GHCR, use your account namespace. Confirm the local tag is exactly the one you push.
۱۳. نسخهٔ محلی را بردار13. Remove the local copy
پس از ثبت شواهد و اطمینان از push، containerهای آزمایشی خودت را حذف کن و سپس tagهای همین image را از cache محلی پاک کن. از prune کلی استفاده نکن؛ هدف پاککردن خروجی ساختهشده پروژه است، نه فایلهای دیگر کاربر.
After capturing evidence and confirming the push, remove your own test container and then the local tags for this image. Do not use a broad prune; the target is this project’s artifact, not other user data.
راهنماییHint
Docker تا وقتی container از image استفاده میکند ممکن است اجازهٔ حذف ندهد. نام دقیق container و image خودت را با docker ps -a و docker image ls پیدا کن؛ پیش از حذف، شواهد لازم را ذخیره کن.
Docker may refuse to remove an image that a container still uses. Find your exact container and image names with docker ps -a and docker image ls; save your evidence before removal.
۱۴. pull کن و دوباره ثابت کن14. Pull and prove it again
image را از registry pull کن و با نام container تازه، میزبان port انتخابی و تنظیمات زمان اجرا اجرا کن. پاسخ، health، user، log و limit را دوباره بررسی کن. این مرحله مهمترین تفاوت «روی لپتاپ build شد» با «قابلتوزیع است» را ثابت میکند.
Pull the image from the registry and run it under a new container name with a chosen host port and run-time configuration. Recheck response, health, user, logs, and limits. This is the key proof that “built on my laptop” has become “distributable.”
راهنماییHint
قبل از pull با docker image ls و پس از حذف محلی مطمئن شو tag واقعاً رفته است. اگر registry عمومی نیست، همین جا معلوم میشود احراز هویت pull روی ماشین تازه هم لازم است.
Use docker image ls to confirm the tag is gone before pulling. If the registry is private, this is where you discover that a fresh machine also needs pull credentials.
سه خرابی عمدی؛ پروژه وقتی جدی میشود که بتوانی خودت جمعش کنیThree deliberate failures; evidence before repair
سه خطا را جداگانه ایجاد میکنیم: میزبان port اشتباه، healthcheck با مسیر غلط و permission مشکلدار برای مسیر runtime. در هر مورد قبل از اصلاح، یک شاهد ثبت کن. قرار نیست با تغییر تصادفی چند گزینه به نتیجه برسی؛ باید بدانی کدام مرز خراب است.
Run each drill separately and restore a known-good image between them. For each, record the symptom, which layer it implicates, and what evidence you need before changing anything.
خرابی ۱ — میزبان port اشتباهFailure 1 — wrong host port
بعد از repair همان درخواست یا healthcheck قبلی را دوباره اجرا کن. «دیگر خطایی ندیدم» معیار کافی نیست. پروژه باید به همان وضعیت قابلانتظار قبل از خرابی برگردد.
Run the container with an intentionally wrong mapping such as -p 8087:3001 while the app listens on 3000. docker ps may show it Up, but a host request fails. Compare docker port, the PORTS column, and the “listening on 3000” log. Which side of the mapping is wrong?
خرابی ۲ — healthcheck مسیر اشتباه میزندFailure 2 — healthcheck probes the wrong path
مسیر probe را از /health به /ready تغییر بده و image تازه بساز. فرایند و root ممکن است پاسخ بدهند، اما health پس از چند probe ناموفق unhealthy میشود. .State.Health.Log را بخوان؛ آن را با status واقعی curl /health مقایسه کن و بعد مسیر را تعمیر و container را از image تازه بساز.
Change the probe path from /health to /ready and build a new image. The process and root endpoint may still respond, while health becomes unhealthy after failed probes. Read .State.Health.Log, compare it with curl /health, then repair the path and recreate from the new image.
خرابی ۳ — runtime پوشه قابلنوشتن نیستFailure 3 — runtime directory is not writable
در یک نسخهٔ موقت، دایرکتوری /app/runtime را root-owned با مجوز 755 بساز و container را با user غیرریشه اجرا کن. / و /health هنوز پاسخ میدهند؛ curl -i /write باید 500 بدهد و docker logs خطای permission را نشان دهد. user و مالکیت مسیر را بررسی کن؛ با chmod 777 دور نزن.
In a temporary image, make /app/runtime root-owned with mode 755, then run as the non-root user. / and /health should still respond; curl -i /write should return 500 and docker logs should show a permission error. Inspect the user and directory ownership; do not escape with chmod 777.
راهنمایی برای تعمیرRepair hint
در زمان build فقط مسیر نوشتنی را به user برنامه واگذار کن، یا با COPY --chown مالکیت خروجی ساختهشده را تنظیم کن. بعد image جدید بساز و هر سه شاهد HTTP، log و مالکیت را دوباره بگیر.
During the build, assign only the writable directory to the app user, or use COPY --chown for artifact ownership. Rebuild, then recheck HTTP, logs, and ownership.
قبولی پروژه با چند سؤال روشن سنجیده میشودAcceptance: prove it with commands and output
در این بخش چیزی برای حدسزدن نداریم. reviewer باید بتواند با چند دستور ساده ثابت کند سرویس پاسخ میدهد، health سبز میشود، فرایند root نیست، تنظیمات بدون rebuild عوض میشوند، لاگ بیرون میآید و limitها واقعاً وجود دارند.
In these commands, one-service:1.0.0 is the example local image and 8087 is only an example free host port; choose your own. Before running, stop and remove an older container with the same name only if it belongs to this project.
docker build -t one-service:1.0.0 .
docker image inspect one-service:1.0.0 --format 'user={{.Config.User}} exposed={{json .Config.ExposedPorts}} size={{.Size}}'
docker image history one-service:1.0.0پورت میزبان یا نام image در نمونهها پیشنهادیاند؛ اصل رفتار مهم است. اگر بهخاطر پورت اشغالشده عدد دیگری انتخاب کردی، فقط آن را مستند کن تا نفر بعد بداند کجا باید درخواست بفرستد.
In inspect, user should not be empty or 0. History helps show what the multi-stage build kept out of the final image; size alone is not a security or performance test.
docker run -d --name one-service-check -p 8087:3000 -e PORT=3000 -e APP_MODE=acceptance --memory=128m --cpus=0.50 --pids-limit=64 one-service:1.0.0 curl -i http://localhost:8087/ curl -i http://localhost:8087/health docker ps --filter name=one-service-check
در JSON پاسخ، mode باید acceptance باشد. برای health ممکن است چند ثانیه زمان لازم باشد؛ فقط بعد از دیدن healthy موفقیت را ثبت کن. container Up بودن بهتنهایی ثابت نمیکند healthcheck موفق است.
The JSON response should show mode as acceptance. Health may take a few seconds; record success only after it says healthy. Up alone does not prove that the healthcheck passed.
docker inspect one-service-check --format 'state={{.State.Status}} health={{.State.Health.Status}} user={{.Config.User}} memory={{.HostConfig.Memory}} nanoCPUs={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}}'
docker inspect one-service-check --format '{{.Config.User}}'
docker top one-service-check
curl -i http://localhost:8087/write
docker logs --tail 20 one-service-checkسطر inspect باید نشان بدهد که فرایند زنده است، health سالم شده، user غیرریشه است و مقادیر memory/CPU/PID صفر یا نامحدود نیستند. docker top فرایند را نشان میدهد؛ docker logs درخواستها و نتیجهٔ write probe را. اگر write موفق بود، پاسخ 201 و اگر عمدی permission را خراب کردهای، 500 و خطای stderr را ثبت کن.
The inspect line should show a live process, healthy status, a non-root user, and non-zero memory/CPU/PID limits. docker top shows the process; docker logs shows requests and the write probe. A successful write returns 201; an intentionally broken permission returns 500 with a stderr error.
حالا همان image را با APP_MODE=staging اجرا کن؛ پاسخ باید عوض شود بیآنکه build تازهای انجام شود. این شاهد جدایی تنظیمات از خروجی ساختهشده است. ID را قبل و بعد مقایسه کن:
Now run the same image with APP_MODE=staging; the response should change without a new build. That proves configuration is separate from the artifact. Compare its ID before and after:
docker image inspect one-service:1.0.0 --format '{{.Id}}'
docker rm -f one-service-check
docker run -d --name one-service-staging -p 8087:3000 -e PORT=3000 -e APP_MODE=staging --memory=128m --cpus=0.50 --pids-limit=64 one-service:1.0.0
curl -i http://localhost:8087/
docker inspect one-service-staging --format 'image={{.Image}} mode={{.Config.Env}}'شناسهٔ image در دو inspect باید یکی باشد؛ پاسخ HTTP باید mode جدید را داشته باشد. به این ترتیب تغییر از run تنظیمات آمده، نه build.
The image ID from both inspections should match, while the HTTP response shows the new mode. That proves the change came from run configuration, not a rebuild.
آخرین آزمون: source را کنار بگذار و فقط خروجی ساختهشده را تحویل بدهDistribution test: push, remove locally, pull
تا اینجا هنوز image روی همان ماشین build وجود دارد. برای اثبات توزیع، آن را با tag نسخهدار push کن، container را جمع کن و نسخهٔ محلی را حذف کن. بعد همان image را از registry pull و اجرا کن. این لحظه فرق «containerization محلی» با «خروجی ساختهشده قابلتحویل» را نشان میدهد.
Choose an explicit version instead of the mutable latest tag. Replace REGISTRY_IMAGE below with your full image name; on Docker Hub, for example, use USERNAME/one-service:1.0.0.
docker tag one-service:1.0.0 REGISTRY_IMAGE docker login REGISTRY_HOST docker push REGISTRY_IMAGE
برای این پروژه از latest بهعنوان تنها مرجع استفاده نکن. یک نسخهٔ روشن مثل 1.0.0 انتخاب کن تا نفر بعد بداند دقیقاً چه چیزی را pull کرده است.
If you chose Docker Hub, use the complete image name and usually run docker login without a host. For a private registry, use a scoped credential or token; do not put it in shell history or source.
docker rm -f one-service-check
docker image rm one-service:1.0.0 REGISTRY_IMAGE
docker image ls
docker pull REGISTRY_IMAGE
docker run -d --name one-service-pulled -p 8087:3000 -e PORT=3000 -e APP_MODE=pulled --memory=128m --cpus=0.50 --pids-limit=64 REGISTRY_IMAGE
curl -i http://localhost:8087/
docker ps --filter name=one-service-pulled
docker inspect one-service-pulled --format 'health={{.State.Health.Status}} user={{.Config.User}}'دستور حذف را فقط برای دو tagی اجرا کن که خودت به این پروژه اختصاص دادهای؛ اگر container دیگری از آنها استفاده میکند، اول مالکیتش را بررسی کن. پس از pull، پاسخ باید mode=pulled باشد و health به healthy برسد. حالا image را بدون فایلهای build محلی اجرا کردهای.
Remove only the two tags you assigned to this project; if another container uses them, check ownership first. After the pull, the response should show mode=pulled and health should become healthy. You have now run the image without relying on local build files.
راهحل مرجع را بعد از ساخت خودت باز کنReference solution; now compare it with your decisions
این بخش پاسخ تقلبی پروژه نیست؛ یک پیادهسازی مرجع برای مقایسه است. قبل از دیدنش نسخهٔ خودت را تا حد ممکن تمام کن. بعد خطبهخط بررسی کن هر تصمیم مرجع به کدام معیار پذیرش جواب میدهد و آیا در راهحل تو همان نیاز با روش دیگری تأمین شده یا نه.
If you built your own version, pause before reading. This is not the only valid Dockerfile, but each line answers a project requirement: reproducible build, final artifact separated from development tools, non-root user, explicit writable path, and a working probe.
node_modules dist runtime .git .env .env.* npm-debug.log*
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM node:22-alpine AS runtime
ENV NODE_ENV=production
ENV PORT=3000
ENV APP_MODE=production
ENV DATA_DIR=/app/runtime
WORKDIR /app
RUN addgroup -S app && adduser -S -G app app \
&& mkdir -p /app/runtime \
&& chown app:app /app/runtime
COPY --from=build --chown=app:app /app/dist ./dist
USER app
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=2s --start-period=5s --retries=3 \
CMD ["node", "-e", "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
CMD ["node", "dist/server.js"]ممکن است Dockerfile تو دقیقاً شبیه مرجع نباشد و همچنان درست باشد. چیزی که باید یکسان بماند رفتار قابلسنجش است: build تمیز، runtime کوچکتر، user غیرریشه، مسیر نوشتنی روشن، health سالم و image قابلpull.
The runtime stage copies only compiled JavaScript; the manifest, compiler, and source are not needed. We run Node directly so the app stays in the foreground without an extra wrapper. The app writes only under /app/runtime, which belongs to the app user. The healthcheck uses Node rather than relying on curl being installed. EXPOSE documents a port; the run command performs publishing.
one-service:1.0.0 نسخهٔ خروجی ساختهشده توست. node:22-alpine فقط پایهٔ نمونه است و tag آن میتواند در طول زمان جابهجا شود. برای release تکرارپذیرتر، نسخهٔ پایه را طبق سیاست تیم pin کن؛ برای verify آموزشی، اصل مهم این است که همان image tag خودت را push و pull کنی.
one-service:1.0.0 versions your artifact. node:22-alpine is only the example base and its tag can move over time. For more reproducible releases, pin the base according to team policy; for this exercise, the essential proof is that you push and pull your own versioned image.
چکلیست مرور نهاییFinal review checklist
- آیا build context همان پوشهٔ برنامه است و
.dockerignoreفایلهای حساس و محلی را کنار میگذارد؟ - آیا lockfile در image build استفاده میشود و TypeScript در runtime نهایی باقی نمانده؟
- آیا برنامه با user غیرریشه اجرا میشود و فقط مسیر لازم writable است؟
- آیا
PORTوAPP_MODEبدون rebuild تغییر میکنند؟ - آیا healthcheck مسیر سرویس واقعی را میسنجد و health به healthy میرسد؟
- آیا درخواست و خطا در stdout/stderr دیده میشوند؟
- آیا inspect مقدار limitهای memory، CPU و PID را نشان میدهد؟
- آیا image نسخهدار را push کردی، tag محلی را هدفمند برداشتی، و pull-and-run را ثابت کردی؟
- آیا failureهای port، health مسیر و permission را با شواهد تشخیص دادی؟
- Is the build context the app directory, with local and sensitive files excluded by
.dockerignore? - Does the build use the lockfile, and is TypeScript absent from the final runtime stage?
- Does the app run as non-root, with only the required path writable?
- Can
PORTandAPP_MODEchange without rebuilding? - Does the healthcheck probe the real endpoint and eventually become healthy?
- Are requests and errors visible on stdout/stderr?
- Does inspect show memory, CPU, and PID limits?
- Did you push a versioned image, remove its local tag deliberately, and prove pull-and-run?
- Did you diagnose the port, health-path, and permission failures from evidence?
افزودهٔ اختیاری: حالا که پروژه قبول شده، یک لایه محدودترش کنOptional extension: read-only and digest
بعد از قبولی اصلی میتوانی root filesystem را read-only کنی و فقط مسیرهای لازم را بهصورت volume یا tmpfs نوشتنی بدهی. این بخش bonus است چون هدف پروژه اول، تحویل یک image سالم و قابلانتقال است؛ نه اینکه همهٔ hardeningهای ممکن را یکجا تحمیل کنیم.
The core project is complete; now try one extra layer. Use --read-only for the root filesystem and grant write access only to explicit temporary or data paths. This app’s /app/runtime write needs an explicit volume or mount, and /tmp can use tmpfs. Test it—do not assume read-only also locks every mount.
پس از push، digest ثبتشده را از registry پیدا کن و بهجای tag، image را با name@sha256:… اجرا کن. tag خواناست و digest محتوای دقیق را pin میکند؛ پیش از تحویل، پاسخ و health را دوباره بسنج.
After pushing, retrieve the registry digest and run name@sha256:… instead of a tag. A tag is readable; a digest pins exact content. Recheck the response and health before calling the extension complete.
تحویل خوب یعنی همتیمی بدون تو هم بتواند آن را اجرا کندProject hand-off
در README کوتاهت فقط چیزهایی را بگذار که نفر بعد لازم دارد: نام کامل image و نسخه، فرمان pull/run، port مورد انتظار، health مسیر سرویس و فرمان دیدن logها. secret یا dump بلند خروجی لازم نیست.
Hand your teammate only the full image name and version, a pull/run command with the chosen host port, the expected health result, and the command for viewing logs. No secret or large output is needed. If they can pull and run the image on a machine without your source, the project has moved beyond “it works on my laptop.”