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

متغیرهای محیطی و secret

Environment variables and secrets

همان image باید روی لپ‌تاپ توسعه‌دهنده، محیط آزمایش و محیط شبیه production اجرا شود. اگر برای هر تغییر پیکربندی image تازه بسازیم، چه چیزی را حل کرده‌ایم؟ و اگر رمز را داخل Dockerfile بگذاریم، چه چیزی را بی‌دلیل در image پخش کرده‌ایم؟

The same image should run on a developer laptop, in test, and in a production-like environment. If every configuration change requires a new image, what have we gained? And if we put a password in the Dockerfile, where have we copied it unnecessarily?

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

یک image، چند محیط؛ قرار نیست برای هر تغییر تنظیمات دوباره بسازیمشOne image, three environments: should we rebuild it every time?

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

Imagine the Chapter 8 application working on your laptop. A teammate now runs the same application in test, and later it will run on a server. The code and dependencies are the same, but the database address, application mode, port, and connection password vary by environment.

اولین راه‌حل وسوسه‌کننده این است که همه‌چیز را داخل Dockerfile بگذاریم؛ برنامه که اجرا شد همان مقادیر را می‌بیند. برای PORT=3000 شاید مشکلی نباشد، اما وقتی پای رمز وسط بیاید دیگر فقط «تنظیم پیش‌فرض» نداریم؛ یک مقدار حساس را وارد چیزی کرده‌ایم که ساخته، کپی و شاید منتشر می‌شود.

If the source hardcodes DB_HOST=localhost, a password, the application mode, and a port, every environment change pushes us toward editing code and rebuilding the image. So a natural question is: “Should we rebuild the image every time configuration changes?”

راه ساده‌ای به نظر می‌رسد: چند مقدار را در Dockerfile بگذاریم تا برنامه موقع اجرا پیدایشان کند. اما حالا تصور کن کسی این را اضافه کند:

One tempting shortcut is to place values in the Dockerfile so the application can find them at runtime. But now imagine someone adding this:

a deliberately unsafe Dockerfile fragment · fake value only
ENV DB_HOST=localhost \
    DB_PASSWORD=LAB_ONLY_NOT_A_CREDENTIAL \
    APP_MODE=development \
    PORT=3000

پس دو سؤال را از هم جدا می‌کنیم: تنظیمات هر محیط را چطور هنگام اجرا بدهیم؟ و مقدار حساسی مثل رمز را چطور به برنامه برسانیم بدون اینکه مثل یک تنظیم معمولی همه‌جا پخش شود؟

This Dockerfile does more than choose application defaults: it records the password in image configuration. Anyone with access to the image, build history, or container configuration may be able to see it. And localhost inside the web container points back to that container, not to its neighboring PostgreSQL service.

پس دو مسئلهٔ جدا داریم: مقدارهای مخصوص هر محیط را کجا بگذاریم تا image قابل استفادهٔ دوباره بماند؟ و مقدار حساسی مثل رمز را چطور به برنامه برسانیم، بی‌آنکه آن را مثل یک تنظیم معمولی در image یا متن Compose پخش کنیم؟

We have two different problems: where should environment-specific values live so the image stays reusable, and how can we deliver a sensitive value such as a password without spreading it through the image or ordinary Compose text?

One application image is paired with distinct runtime configuration in development, test, and production-like environments one application imagesame code + dependenciesno environment password runtime inputsconfig + granted secrets developer · local values test · test values production-like · managed values reuse artifactsupply per run

فلش اول یعنی همان image ساخته‌شده را در اختیار هر اجرا می‌گذاریم. سه فلش بعدی نشان می‌دهند هر محیط هنگام اجرا تنظیمات خودش را به همان image می‌دهد؛ «یکسان بودن image» به معنی یکسان بودن پیکربندی نیست. secret هم جداگانه و فقط به serviceای می‌رسد که لازم دارد.

The first arrow means we reuse one built image. The three outgoing arrows mean each environment supplies its own settings at runtime; reusing an image does not mean reusing identical configuration. A secret is granted separately and only to the service that needs it.

قبل از انتخاب فایل، بفهم این مقدار اصلاً چه جور چیزی استCode, image, configuration, and secrets play different roles

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

Before deciding where a value belongs, separate four things. Source code defines how the application works; the image carries the runnable artifact and non-sensitive defaults; runtime configuration selects behavior for this run; a secret is a credential whose access should be limited.

چیزپرسش تشخیصینمونهٔ این برنامهمقصد معمول
کدبرنامه چه کاری انجام می‌دهد؟query زدن به PostgreSQLsource و image
imageچه artifactای را در همه‌جا اجرا می‌کنیم؟Node، packageها و serverregistry یا ماشین میزبان
تنظیم غیرحساساین اجرا باید با چه رفتاری کار کند؟APP_MODE، DB_HOST، portCompose، .env یا env_file
secretچه مقدار دسترسی‌ای باید محدود بماند؟رمز PostgreSQLفایل secret مجازشده برای service

این تقسیم‌بندی قانون «همه‌چیز را environment variable کن» نیست. سؤال ساده‌تر این است: «این مقدار چیست، چه کسی واقعاً به آن نیاز دارد و چه زمانی باید در اختیارش باشد؟» همین سؤال جلوی خیلی از تصمیم‌های عجولانه را می‌گیرد.

This is not a rule that everything must become an environment variable. An address or mode may fit another configuration mechanism, and sensitive data does not become safe merely because it is a variable. Ask instead: “What is this value, who needs to know it, and when?”

مدل ذهنی پایهThe basic model

image را تا جای ممکن مستقل از محیط نگه دار. هنگام اجرای service، تنظیمات غیرحساس را صریح به آن بده. secret را از فایل یا سازوکاری جداگانه وارد کن و دسترسی را به service لازم محدود کن. Docker این تصمیم معماری را به‌جای تو نمی‌گیرد.

Keep the image as independent of its environment as practical. Supply non-sensitive settings explicitly when the service runs. Deliver secrets separately and grant them only to the service that needs them. Docker does not make this design decision for you.

ENV در Dockerfile یک پیش‌فرض image استENV in a Dockerfile is an image default

فصل ۰۸ برنامه را با image خودمان build کرد. می‌توانیم برای برنامه پیش‌فرض غیرحساسی مثل حالت اجرا و port بگذاریم. مقدار ENV در build برای دستورهای بعدی هم در دسترس است و در image نهایی باقی می‌ماند؛ بنابراین container تازه آن را به‌عنوان مقدار پیش‌فرض runtime می‌بیند.

Chapter 8 built our own application image. We can give the application non-sensitive defaults such as its mode and listening port. A Dockerfile ENV is available to later build instructions and persists in the final image, so a new container receives it as a runtime default.

web/Dockerfile · safe defaults only
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY server.js ./
ENV APP_MODE=production PORT=3000
EXPOSE 3000
CMD ["npm", "start"]

اینجا APP_MODE و PORT پیش‌فرض‌اند، نه حدس قطعی دربارهٔ محیط. EXPOSE هم فقط metadata مربوط به port برنامه است؛ فرایند را به آن port مجبور نمی‌کند و میزبان port publish نمی‌کند. اگر runtime مقدار دیگری بفرستد، برنامه باید واقعاً از آن استفاده کند.

Here APP_MODE and PORT are defaults, not guesses about every environment. EXPOSE is port metadata; it does not force the process to listen there or publish a host port. If runtime configuration supplies another value, the application must actually use it.

An image provides defaults and runtime configuration overrides them when Docker creates the container same imageENV defaultsno environment secret container creationDocker combines inputs -e / Compose environmentruntime override effective process environmentvalue the app reads image defaultsoverrideresolved result

فلش از image به container یعنی Docker از تنظیمات پیش‌فرض image شروع می‌کند. فلش سبز نشان می‌دهد -e یا Compose می‌تواند در زمان ساخت container مقدار همان نام را override کند؛ فلش بعدی نشان‌دهندهٔ محیط مؤثری است که فرایند برنامه می‌خواند. این تغییر به‌تنهایی image را rebuild نمی‌کند.

The image-to-container arrow means Docker starts with image defaults. The green arrow shows that -e or Compose can override a value while creating the container; the final arrow represents the effective environment the process reads. This change alone does not rebuild the image.

روشچه زمانی مقدار می‌رسد؟کجا می‌ماند؟نمونهٔ مناسب
Dockerfile ENVهنگام build به دستورهای بعدی؛ پیش‌فرض برای containerدر پیکربندی image و containerPORT=3000 یا حالت پیش‌فرض
docker run -eهنگام ساخت همان containerپیکربندی runtime آن containerآزمایش یک مقدار متفاوت
Compose environmentهنگام اجرای serviceپیکربندی runtime containerآدرس غیرحساس database

بیایید image را یک بار بسازیم و فقط مقدار runtime را عوض کنیم. دستور --env مقدار پیش‌فرض image را برای همین container می‌پوشاند؛ اگر دوباره docker image inspect کنی، image همان image قبلی است.

Build the image once and vary only the runtime value. --env overrides the image default for this container; inspecting the image again shows that the image itself is unchanged.

one image, two runtime modes
$ docker image inspect --format '{{.Id}}' compose-lab-web
sha256:7ab...  # representative image ID

$ docker run --rm --env APP_MODE=development --entrypoint node compose-lab-web \
  -e 'console.log(process.env.APP_MODE, process.env.PORT)'
development 3000

$ docker run --rm --env APP_MODE=test --entrypoint node compose-lab-web \
  -e 'console.log(process.env.APP_MODE, process.env.PORT)'
test 3000

دو container کوتاه‌عمر از همان image ساخته شدند؛ mode فرق داشت اما image ID نه. این همان جداسازی‌ای است که می‌خواهیم. در ادامه همین تصمیم را برای دو service فصل ۰۸ داخل Compose می‌گیریم.

Two short-lived containers came from the same image; the mode changed, but the image ID did not. That is the separation we want. Next we will make the same choice for both Chapter 8 services in Compose.

رمز داخل Dockerfile از محیط build بیرون می‌رودA Dockerfile password can escape the build environment

حالا همان shortcut بد را با یک مقدار ساختگی قابل مشاهده می‌کنیم. این متن رمز واقعی نیست؛ فقط نشانگر آزمایش است. Dockerfile زیر را به‌عنوان نمونهٔ جداگانه در نظر بگیر و با image اصلی برنامه اشتباه نگیر.

Now make the unsafe shortcut observable with a fake marker. It is not a real password; it exists only for this demonstration. Treat this as a separate example, not the real application Dockerfile.

Dockerfile.leaky · demonstration only
FROM alpine:3.22
ENV DB_PASSWORD=LAB_ONLY_NOT_A_CREDENTIAL
CMD ["sh", "-c", "echo demo"]

$ docker build -f Dockerfile.leaky -t compose-lab:web-leaky .
$ docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' compose-lab:web-leaky
DB_PASSWORD=LAB_ONLY_NOT_A_CREDENTIAL

$ docker history --no-trunc compose-lab:web-leaky
IMAGE      CREATED BY
...        ENV DB_PASSWORD=LAB_ONLY_NOT_A_CREDENTIAL

خروجی اول نشان می‌دهد مقدار در image تنظیمات مانده؛ history هم دستور build را نشان می‌دهد. ممکن است لایه‌های image و registry به افراد بیشتری از محیط runtime برسند. حذف‌کردن ENV در یک لایهٔ بعدی secret را از سابقهٔ قبلی پاک نمی‌کند. به همین دلیل رمز را در ENV یا ARG قرار نده؛ برای secret لازم هنگام build ابزار جداگانهٔ BuildKit دارد، اما آن موضوع این فصل نیست.

The first output shows the value retained in image configuration; history also records the build instruction. Image layers and registries may be accessible to more people than the runtime. Removing the variable in a later layer does not erase the earlier record. Do not put a password in ENV or ARG; BuildKit has a separate mechanism for secrets needed during a build, which is outside this chapter.

اگر secret واقعاً لو رفتIf a real secret was exposed

اول اعتبارنامه را باطل یا عوض کن و مصرف‌کننده‌هایش را به مقدار تازه منتقل کن. حذف فایل از ثبت در Git بعدی، پاک‌کردن tag یا بازنویسی history تضمین نمی‌کند کسی نسخهٔ قبلی را نگرفته باشد. .gitignore جلوی ثبت معمول فایلِ ردیابی‌نشده را می‌گیرد؛ رمز را رمزنگاری یا پس‌گرفته نمی‌کند.

First revoke or replace the credential and move its consumers to the new value. Removing a file in the next commit, deleting a tag, or rewriting history cannot guarantee that nobody copied the earlier version. .gitignore can prevent normal tracking of an untracked file; it does not encrypt or retract a secret.

دو اسم شبیه، دو کار متفاوت: .env با env_file یکی نیستSimilar names, different jobs: .env and env_file

اینجا اسم‌ها آدم را گول می‌زنند. فایل .env کنار Compose معمولاً به خود Compose کمک می‌کند جای‌نگهدارهایی مثل ${APP_PORT} را پر کند. اما env_file یعنی مقادیر آن فایل را به محیط همان سرویس بده. پس وجود یک مقدار در .env به‌تنهایی ثابت نمی‌کند برنامه داخل container آن را می‌بیند.

Separate three moments in Compose: the shell and a project .env help Compose construct its model; interpolation such as ${DB_HOST} inserts a value into that model; only a service's environment or env_file setting places a variable in the container process environment.

Host shell and project .env values are interpolated into the Compose model, and only explicitly forwarded values reach the container host shellexported values project .envCompose lookup file Compose interpolation${DB_HOST} → postgresresolves the model service configurationenvironment / env_fileexplicit handoff to container container process environment source candidatesresolved modelonly mapped values

فلش‌های سمت چپ کاندیداهای interpolation را به Compose می‌رسانند؛ در این پروژه shell از فایل .env اولویت بالاتری دارد. فلش میانی نتیجه را وارد مدل نهایی می‌کند. فلش آخر فقط برای مقدارهایی وجود دارد که service صریحاً با environment یا env_file به container می‌دهد؛ خود .env به‌تنهایی چنین فلشی ندارد.

The left arrows bring interpolation candidates to Compose; for this project the shell has higher precedence than the project .env. The middle arrow inserts the result into the resolved model. The final arrow exists only for values a service explicitly passes with environment or env_file; .env alone has no such arrow.

نام / محلچه کسی می‌خواندش؟خودش وارد container می‌شود؟نقش این فصل
پروژه .envDocker Compose CLI برای interpolationنه؛ فقط اگر در service ارجاعش بدهیمقدارهای محلی برای ${...}
service env_file:Compose برای پیکربندی همان serviceبله، key-valueهایش به process می‌رسندچند تنظیم غیرحساس و تکرارشونده
service environment:Composeبله، همان نام‌های تعریف‌شدهمقدار صریح یا interpolationشده
docker compose --env-fileCompose CLIنه؛ فایل interpolation پیش‌فرض را عوض می‌کندانتخاب مجموعه‌مقدارهایی مثل test

پس .env و env_file: یکی نیستند. اولی کنار Compose file معمولاً منبع interpolation است؛ دومی زیر یک service می‌گوید چه متغیرهایی وارد container شوند. Compose هم فایل .env را خودش داخل image یا container کپی نمی‌کند و برنامهٔ Node ما هم خودکار آن را نمی‌خواند.

So .env and env_file: are not interchangeable. The first is commonly a source for Compose interpolation; the second, under a service, says which variables enter the container. Compose does not copy the project .env into the image or container, and our Node program does not read it automatically.

two files, two stages
# .env · read by Compose for substitution; safe, non-secret lab values
APP_PORT=8080
PORT=3000
DB_HOST=postgres
DB_USER=lesson
DB_NAME=lesson
APP_MODE=development

# web.runtime.env · supplied to the web container by service env_file
APP_MODE=development

# compose.yaml
services:
  web:
    ports:
      - "127.0.0.1:${APP_PORT:-8080}:${PORT:-3000}"
    environment:
      DB_HOST: ${DB_HOST:-postgres}
      PORT: ${PORT:-3000}
    env_file:
      - ./web.runtime.env

Compose در سطر ports مقدار میزبان port را از .env جاگذاری می‌کند و DB_HOST و PORT را چون زیر environment آمده‌اند به web می‌دهد. APP_MODE از web.runtime.env می‌آید. اگر فقط APP_MODE را در .env بنویسی و هر دو ویژگی service را برداری، آن مقدار به container نمی‌رسد؛ Dockerfile پیش‌فرض یا مقدار دیگری باقی می‌ماند.

Compose substitutes the host port from .env, then passes DB_HOST and PORT to web because they appear under environment. APP_MODE comes from web.runtime.env. If you put only APP_MODE in .env and remove both service attributes, it never reaches the container; the Dockerfile default or another value remains.

جای‌گذاری در فایل Compose با رسیدن مقدار به برنامه یکی نیستInterpolation puts a value in the model, not automatically in the process

فرض کن Compose مقدار ${APP_PORT} را از .env پیدا کرده و مدل نهایی را درست ساخته. این فقط ثابت می‌کند Compose آن مقدار را هنگام خواندن فایل داشته است. اگر همان نام زیر environment یا از راه env_file به سرویس داده نشده باشد، فرایند داخل container لزوماً چیزی از آن نمی‌داند.

In a Compose file, place ${NAME} inside a field and the CLI substitutes it before creating containers. The value can come from the shell or an interpolation file. In Compose, a shell value outranks the project .env; with simple syntax, a missing variable may produce a warning and an empty value.

Syntaxاگر مقدار وجود نداشتاگر خالی بودمثال مناسب
${VAR}warning و جای‌گذاری خالیمقدار خالی می‌ماندفقط وقتی خالی‌بودن عمدی است
${VAR:-default}از default استفاده می‌کنداز default استفاده می‌کندport محلی با fallback روشن
${VAR-default}از default استفاده می‌کندخالی را حفظ می‌کندوقتی خالی با unset فرق دارد
${VAR:?message}config با خطا متوقف می‌شودconfig با خطا متوقف می‌شودhostname یا مقدار ضروری

برای مقدار اختیاری مثل port داخلی می‌توانی پیش‌فرض بگذاری: ${PORT:-3000}. برای میزبان پایگاه‌داده که بدون آن برنامه نباید شروع شود، ${DB_HOST:?Set DB_HOST in .env} خطای زودهنگام و روشن‌تری از نام میزبان خالی می‌دهد. این دو syntax مسئلهٔ یکسانی را حل نمی‌کنند.

For an optional value such as the application port, use a default: ${PORT:-3000}. For a database host without which the application should not start, ${DB_HOST:?Set DB_HOST in .env} fails early with a clearer message than an empty hostname. These forms solve different problems.

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

Now inspect the resolved model. The source file shows placeholders; config output shows the values Compose resolved them to. That is useful for comparing source and result, but never put a real secret into output that may be recorded, stored, or shared.

inspect a deliberately non-sensitive resolved model
$ docker compose config
name: compose-lab-env
services:
  web:
    environment:
      APP_MODE: development
      DB_HOST: postgres
      PORT: "3000"
    env_file:
      - path: ./web.runtime.env
    ports:
      - target: 3000
        published: "8080"
...

دو نکته از خروجی می‌خوانیم: DB_HOST نهایی شده، و مقدارهای web.runtime.env ممکن است در بخش محیط مؤثر دیده شوند. خود فایل کد منبع با این خروجی یکی نیست. اگر رمز را به environment یا env_file بدهی، تنظیمات نهایی می‌تواند آن را نشان دهد؛ در نسخهٔ secret-file چنین مقداری در بخش environment نداریم.

Read two things from the output: DB_HOST was resolved, and values from web.runtime.env may appear in the effective service environment. The source file is not the same thing as resolved output. If a password is passed through environment or env_file, resolved config can show it; the secret-file version has no such password environment value.

تنظیمات ابزار محرمانه‌سازی نیستConfig output does not redact secrets

docker compose config مدل را برای بررسی چاپ می‌کند، نه اینکه secretها را از نمایش پنهان کند. مثال‌های این بخش فقط مقدارهای نمایشی‌اند. در پروژهٔ واقعی اگر خروجی را جایی ثبت می‌کنی، اول منبع مقدارها را بررسی کن و secret را وارد interpolation یا log نکن.

docker compose config prints the model for inspection; it is not a secret-redaction tool. This section uses only demonstration values. In a real project, check the input sources before capturing output, and do not interpolate secrets into configuration or logs.

وقتی یک نام از چند جا مقدار دارد، حدس نزن؛ ببین کدام منبع برنده شدهWhen the same variable has several values, which one reaches the container?

در پروژهٔ واقعی یک متغیر ممکن است پیش‌فرض image داشته باشد، در فایل محیطی مقدار دیگری بگیرد و در Compose هم دوباره نوشته شود. به‌جای حفظ‌کردن کورکورانه، منبع‌ها را پیدا کن و با docker compose config و یک مقدار غیرحساس ببین Compose در نهایت چه مدلی ساخته است.

So far, each name had one source. In a real project, an image default, a service env_file, and environment can all define the same name. In the simple setup used here—with explicit, non-sensitive values—environment overrides env_file, and both override an image default. A one-off docker compose run -e is higher still.

اولویت برای environment containerمنبعنکتهٔ اجرایی
بالاترdocker compose run -eفقط برای همان اجرای یک‌باره؛ تنظیم پایدار service نیست
بعدservice environmentبرای کلید مشترک، مقدار تعریف‌شده در env_file را می‌پوشاند
بعدservice env_fileمقدارهای صریحش از default داخل image بالاترند
پایین‌ترDockerfile ENVdefault نهایی وقتی service مقدار دیگری نفرستاده است

برای interpolation یک تقدم جدا هم به خاطر بسپار: shellای که فرمان Compose را اجرا می‌کند از فایل داده‌شده با --env-file و آن فایل از .env پیش‌فرض اولویت بالاتری دارد. هر دو ترتیب را در یک آزمایش ساده نگه می‌داریم: منبع interpolation مقدار جای‌نگهدار را تعیین می‌کند؛ ویژگی service تعیین می‌کند مقدار وارد container بشود یا نه. اگر مقدار داخل env_file خودش از shell یا فایل interpolation resolve شود، تقدم کامل Compose ظریف‌تر است؛ در آن حالت جدول کامل مستندات را ببین.

There is a separate precedence for interpolation: the shell running Compose outranks a file passed with --env-file, which outranks the default project .env. Keep the two orders separate: an interpolation source fills a placeholder; a service attribute determines whether that value enters the container. If a value inside service env_file is itself interpolated from shell or a CLI file, Compose's full precedence is more nuanced; consult the complete docs for that case.

یک استثنای کاربردی برای اجرای یک‌باره هم هست: docker compose run -e APP_MODE=test web ... می‌تواند برای همان اجرای run مقدار موقت بدهد. لازم نیست این مورد را با تقدم فایل‌های up قاطی کنی؛ برای آزمایش عادی service، اول خروجی نهایی را با یک نام غیرحساس بررسی کن.

There is also a useful one-off override: docker compose run -e APP_MODE=test web ... can set a temporary value for that run. Keep this separate from the files used by up; for a normal service, inspect the final value using a non-sensitive variable.

a safe precedence experiment with APP_MODE
# Dockerfile default: APP_MODE=production
# web.runtime.env: APP_MODE=development
# compose.yaml:     environment: APP_MODE: test

$ docker compose exec web node -e 'console.log(process.env.APP_MODE)'
test

خروجی test شاهد است که service-level environment از env_file و پیش‌فرض image جلو زده است. برای تصمیم‌گیری دربارهٔ secret همین آزمایش را تکرار نکن؛ رمز را به ترمینال نده تا صرفاً تقدم را ثابت کنی.

The test output proves that service-level environment outranks both env_file and the image default. Do not repeat this experiment with a secret; a password does not need to be printed to prove precedence.

رمز را مثل تنظیم معمولی پخش نکن؛ فقط به سرویس لازم بدهGive the password as a granted file, not a broad environment value

environment برای تنظیمات خیلی مناسب است، اما «داخل environment بودن» خودش محافظت امنیتی ایجاد نمی‌کند. برای رمز آزمایشگاه، آن را به‌صورت فایل جدا به سرویس‌هایی می‌دهیم که واقعاً لازم دارند. این کار دست‌کم باعث می‌شود رمز را بی‌دلیل داخل Dockerfile یا فهرست متغیرهای همهٔ سرویس‌ها پخش نکنیم.

In Chapter 8, both services received a demo password in their environment. That made the connection easy to see, but environment values can appear in inspect output, resolved config, debugging tools, or accidental logs. Environment variables are also inherited by many child processes, so do not treat them as a vault.

مقایسهتنظیم environmentCompose secret از فایل
مصرف در برنامهprocess.env.DB_PASSWORDخواندن فایل مجازشده، مثلاً /run/secrets/db_password
دامنهٔ دسترسیهمهٔ processهایی که آن environment را می‌بینندفقط serviceهایی که secret را صریح درخواست کرده‌اند
چه چیزی در config می‌آید؟ممکن است مقدار password در resolved config دیده شودتعریف file source و مسیر mount؛ نه متن فایل به‌عنوان env value
حد امنیتوابسته به دسترسی به metadata و processهادسترسی محدودتر در container؛ منبع فایل همچنان روی میزبان است

در Compose secret را یک بار در ریشه از منبع فایل تعریف می‌کنی و بعد فقط به service لازم می‌دهی. برای اتصال به secret، PostgreSQL رسمی متغیر تصویری POSTGRES_PASSWORD_FILE را می‌شناسد؛ در برنامهٔ خودمان قرارداد صریح DB_PASSWORD_FILE را پیاده می‌کنیم. پسوند _FILE قابلیت جادویی Docker نیست؛ فقط برنامه یا imageای که آن قرارداد را پیاده کرده می‌فهمدش.

In Compose, declare a file-backed secret once at the root, then grant it only to the service that needs it. The official PostgreSQL image recognizes its image-specific POSTGRES_PASSWORD_FILE setting; our own app will implement the explicit DB_PASSWORD_FILE convention. The _FILE suffix is not a Docker magic feature—only an image or application that implements it will understand it.

A local secret source file is granted by Compose to selected services and mounted as a file inside each container host source fileignored, local lab valuenot in Git Compose secretper-service grantfile-backed, local Compose web container/run/secrets/db_passwordapp reads file contents postgres reads same granted file source bytesgrant + mountsecond service grant

فلش اول یعنی Compose از فایل میزبان منبع secret می‌گیرد. فلش میانی نشان‌دهندهٔ اعطای دسترسی و mount کردن یک فایل read-only برای web است؛ شاخهٔ دوم همان secret را جداگانه به PostgreSQL می‌دهد. هر دو service باید آن را در Compose درخواست کنند و برنامه باید مسیر فایل را بخواند.

The first arrow means Compose takes secret input from a host file. The middle arrow represents granting access and mounting a read-only file for web; the second branch grants the same secret separately to PostgreSQL. Both services must request it, and the application must read the file path.

file-backed در Compose محلی، معادل secret رمز‌شدهٔ Swarm نیستA local file-backed Compose secret is not an encrypted Swarm secret

برای اجرای محلی با Docker Compose روی Linux containerها، secretای که file: دارد از فایل میزبان به‌شکل bind mount تک‌فایلی در container در دسترس قرار می‌گیرد. این کار مقدار را از environment و Compose literal بیرون می‌آورد و دسترسی service را محدود می‌کند، اما فایل میزبان را رمز نمی‌کند و از مدیر میزبان یا Docker daemon پنهانش نمی‌سازد. ضمانت‌های ذخیره‌سازی رمز‌شده و mount حافظه‌ای مربوط به سازوکار Swarm است؛ آن‌ها را به اجرای Compose محلی نسبت نده. مسیر Windows containerها محدودیت جداگانه دارد و از دامنهٔ این lab بیرون است.

For local Docker Compose with Linux containers, a secret using file: is made available from the host as a single-file bind mount. This moves the value out of the ordinary environment and Compose literal and narrows service access, but it does not encrypt the host file or hide it from the host administrator or Docker daemon. Encrypted storage and in-memory mounts are Swarm mechanisms; do not attribute those guarantees to local Compose. Windows containers have separate limitations outside this lab's scope.

پس سه مرز را می‌بینیم: فایل باید روی میزبان محافظت شود؛ فقط serviceهای لازم باید در فهرست secret باشند؛ و برنامه باید همان مسیر mountشده را بخواند. این بهبود نسبت به رمز در image یا environment است، نه وعدهٔ «کاملاً امن» بودن.

That leaves three boundaries to manage: protect the host file, grant the secret only to necessary services, and make the application read the mounted path. This is an improvement over putting a password in an image or broad environment—not a promise of being “completely secure.”

برنامه باید خودش فایل را بخواندThe application must read the file itself

Docker secret را به‌صورت environment variable جایگزین‌شده تحویل نمی‌دهد؛ فایل در یک مسیر mount می‌شود. برنامهٔ فصل ۰۸ مقدار اتصال را از process.env.DB_PASSWORD می‌خواند. پس باید آن قرارداد را تغییر دهیم: مسیر DB_PASSWORD_FILE را از environment بگیریم، فایل را بخوانیم و فقط متنش را به کتابخانهٔ PostgreSQL بدهیم.

Docker does not replace a secret file with an environment variable; it mounts a file at a path. The Chapter 8 application reads process.env.DB_PASSWORD. We must change that contract: read the path from DB_PASSWORD_FILE, read the file, and pass only its contents to the PostgreSQL library.

web/server.js · file-consuming version
const fs = require("node:fs");
const http = require("node:http");
const { Pool } = require("pg");

const passwordPath = process.env.DB_PASSWORD_FILE;
if (!passwordPath) {
  throw new Error("DB_PASSWORD_FILE is required");
}
const password = fs.readFileSync(passwordPath, "utf8").replace(/\r?\n$/, "");
if (!password) throw new Error("database password file is empty");

const db = new Pool({
  host: process.env.DB_HOST,
  port: Number(process.env.DB_PORT || 5432),
  user: process.env.DB_USER,
  password,
  database: process.env.DB_NAME
});

const mode = process.env.APP_MODE || "not-set";
const port = Number(process.env.PORT || 3000);
http.createServer(async (_req, res) => {
  try {
    const result = await db.query(
      "SELECT current_database() AS name, current_timestamp AS time"
    );
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify({ ...result.rows[0], mode }));
  } catch (error) {
    console.error("database request failed:", error.message);
    res.writeHead(503, { "content-type": "text/plain" });
    res.end("database request failed");
  }
}).listen(port, "0.0.0.0", () => {
  console.log(`web listening on 0.0.0.0:${port}; mode=${mode}`);
});

این برنامه هنگام شروع فایل را می‌خواند؛ اگر مسیر اشتباه یا فایل خالی باشد، با خطایی روشن از شروع بازمی‌ایستد. عمداً مقدار رمز را در log یا پاسخ HTTP چاپ نمی‌کند. مقدار APP_MODE را در پاسخ می‌بینیم تا بتوانیم پیکربندی runtime را بدون نمایش secret بررسی کنیم.

The app reads the file at startup; a wrong path or empty file stops startup with a clear error. It deliberately never prints the password in a log or HTTP response. It returns APP_MODE so we can verify runtime configuration without displaying the secret.

توجه کن که فایل در حافظهٔ فرایند خوانده می‌شود و Pool همان مقدار را نگه می‌دارد. جایگزین‌کردن فایل روی میزبان لزوماً مقدار حافظه را عوض نمی‌کند؛ برای اعمال مقدار تازه باید فرایند و ارتباط پایگاه‌داده را آگاهانه به‌روز کنی.

The file is read into process memory and retained by the connection pool. Replacing the host file does not necessarily change the in-memory value; applying a new credential requires deliberately updating the process and database connection.

پیکربندی را در همان برنامه، مرحله‌به‌مرحله جابه‌جا کنیمMove configuration in stages in the same application

به جای آنکه فایل «امن نهایی» را یک‌باره ظاهر کنیم، تغییر هر مقدار را با دلیلش دنبال می‌کنیم. برنامه همان web و PostgreSQL فصل قبل است؛ سؤال هر مرحله یکی است: این مقدار چیست و کدام جزء واقعاً باید آن را بداند؟

Instead of dropping in a supposedly “secure final” file, follow each move and its reason. This is still the Chapter 8 web and PostgreSQL application; at every step ask what the value is and which component actually needs it.

گام اول: مقدارهای ثابت در Compose قابل دیدن‌اند، اما جابه‌جایی‌شان سخت استStep one: literals are visible in Compose, but awkward to move

فصل قبل برای lab از مقدارهای آشکارا آزمایشی lesson و dev-only استفاده کرد. آن‌ها راز واقعی نیستند؛ اما حالا حتی DB_HOST، حالت و port را هم برای هر اجرا می‌خواهیم تنظیم کنیم. image نباید برای هر تغییر این مقادیر دوباره ساخته شود.

The previous lab used the obvious demo values lesson and dev-only. They are not real credentials, but now we want to vary DB_HOST, mode, and port per run as well. The image should not need rebuilding for each of these changes.

starting point · no real password
services:
  web:
    environment:
      APP_MODE: development
      DB_HOST: postgres
      DB_PASSWORD: dev-only
      PORT: "3000"
  postgres:
    environment:
      POSTGRES_PASSWORD: dev-only

چون این مقدارها در service قرار دارند، Compose می‌تواند آن‌ها را بدهد؛ اما image و environment به هم چسبیده نیستند. با این حال رمز در فایل Compose و هر خروجی‌ای که آن را resolve کند دیده می‌شود. ابتدا تنظیم‌های غیرحساس را جابه‌جا می‌کنیم، بعد رمز را از مسیرشان خارج می‌کنیم.

Because these values are under services, Compose can pass them through; they are not baked into the image. Still, the password is visible in the Compose file and in output that resolves it. First move non-sensitive settings, then move the password out of that path.

گام دوم: .env جای placeholderها را پر کندStep two: let .env fill the placeholders

مقدارهای محلی را در فایل .env کنار compose.yaml می‌گذاریم و فقط با ${...} به فیلدهای مربوط ارجاع می‌دهیم. این فایل برای interpolation است؛ پس اگر APP_MODE را در آن نوشته باشی اما در Compose هیچ‌جا به آن ارجاع ندهی، برنامه آن را دریافت نمی‌کند.

Put local values in a project .env beside compose.yaml, then reference them in the relevant fields with ${...}. This file supplies interpolation; if it contains APP_MODE but the Compose model never references it, the application does not receive it.

.env · demonstration values only, not real credentials
APP_PORT=8080
PORT=3000
DB_HOST=postgres
DB_USER=lesson
DB_NAME=lesson
APP_MODE=development
DB_PASSWORD=LAB_ONLY_NOT_A_CREDENTIAL

حالا سطرهای Compose را از literal به ارجاع تغییر بده. برای چند دقیقه مقدار ساختگی را از .env هم عبور می‌دهیم تا مسیر را ببینیم؛ این نسخهٔ میانی برای رمز واقعی مناسب نیست.

Now change the Compose literals to references. For a moment, pass the fake marker through .env too so we can observe the path; this intermediate version is not suitable for a real password.

Compose interpolation · intentionally temporary password path
services:
  web:
    ports:
      - "127.0.0.1:${APP_PORT:-8080}:${PORT:-3000}"
    environment:
      APP_MODE: ${APP_MODE:-development}
      PORT: ${PORT:-3000}
      DB_HOST: ${DB_HOST:?Set DB_HOST in .env}
      DB_USER: ${DB_USER:-lesson}
      DB_NAME: ${DB_NAME:-lesson}
      DB_PASSWORD: ${DB_PASSWORD:?Set the lab-only placeholder}
  postgres:
    environment:
      POSTGRES_USER: ${DB_USER:-lesson}
      POSTGRES_DB: ${DB_NAME:-lesson}
      POSTGRES_PASSWORD: ${DB_PASSWORD:?Set the lab-only placeholder}

اگر DB_HOST حذف یا خالی شود، config به‌جای اینکه web را با نام میزبان خالی بسازد، به‌خاطر :? متوقف می‌شود. مقدارهای port از :- استفاده می‌کنند، پس نبودن یا خالی‌بودنشان به پیش‌فرض می‌رسد. برای اثبات تفاوت، روی این دادهٔ ساختگی docker compose config بزن: خروجی نهایی خود مقدار آزمایشی را نشان می‌دهد؛ این شاهد خوبی است که رمز نباید از این مسیر بیاید.

If DB_HOST is missing or empty, config fails at :? instead of creating web with an empty hostname. Port values use :-, so unset or empty values fall back to a default. To see the distinction, run docker compose config with this fake data: resolved output displays the marker, proving that a password should not travel through this path.

گام سوم: env_file برای تنظیم runtime خود webStep three: use env_file for web runtime settings

حالت برنامه قرار است فقط تنظیم فرایند داخل web باشد؛ برای چنین مقدار غیرحساسی یک فایل جدا تعریف می‌کنیم. مقدارهایش با .env یکی نیستند: service به env_file اشاره می‌کند تا Compose آن‌ها را به container بدهد. می‌توانی فایل غیرحساس را نگه داری یا نسخهٔ نمونه‌اش را به Git بسپاری.

Application mode is a setting for the web process. We can put this non-sensitive value in a separate file. Its role differs from project .env: the service references it through env_file so Compose passes its entries into the container. This non-sensitive file can be kept or versioned as a sample.

web.runtime.env · non-secret service configuration
APP_MODE=development

اکنون APP_MODE را از بخش environment بردار و زیر همان service بنویس:

Now remove APP_MODE from environment and place this under the same service instead:

service-level env_file · values enter only web
services:
  web:
    env_file:
      - ./web.runtime.env
    environment:
      DB_HOST: ${DB_HOST:?Set DB_HOST in .env}
      PORT: ${PORT:-3000}
      DB_USER: ${DB_USER:-lesson}
      DB_NAME: ${DB_NAME:-lesson}
      DB_PASSWORD: ${DB_PASSWORD:?temporary fake value}

مقدار APP_MODE این بار از env_file می‌آید؛ PostgreSQL اصلاً به آن نیازی ندارد. اگر همان کلید را در environment هم بنویسی، مقدار صریح آن بر فایل برتری دارد. پس حذف سطر قدیمی هم بخشی از جابه‌جایی است، نه تمیزکاری تزئینی.

This time APP_MODE comes from env_file; PostgreSQL does not need it. If you also set the same key in environment, that explicit value wins. Removing the old line is part of the move, not cosmetic cleanup.

گام چهارم: رمز از environment بیرون می‌رود و به فایل secret تبدیل می‌شودStep four: move the password out of the environment into a secret file

فایل .env را از DB_PASSWORD پاک کن. یک فایل محلی به نام .env.db-password بساز که فقط مقدار واضحاً آزمایشی env-lab-only را در خود دارد. این نام را انتخاب کرده‌ایم چون قانون فعلی .gitignore پروژه فایل‌های .env.* را نادیده می‌گیرد؛ قانون ignore دسترسی به فایل را محدود نمی‌کند.

Remove DB_PASSWORD from project .env. Create a local file named .env.db-password containing only the unmistakably disposable value env-lab-only. This name matches the repository's existing .env.* ignore rule; ignore does not restrict access to the file.

در Compose، رمز را از environment هر دو service بردار. منبع را یک بار در کلید سطح بالا تعریف و فقط برای web و PostgreSQL مجاز کن. در این فایل، نام secret برای Compose است؛ مقدار رمز همچنان داخل فایل میزبان می‌ماند.

Remove the password from both services' environments. Define its source once at the top level and grant it only to web and PostgreSQL. In this file, the secret's name is for Compose; the password remains in the host file.

final Compose model · the password is no longer an environment entry
name: compose-lab-env

services:
  web:
    build: ./web
    ports:
      - "127.0.0.1:${APP_PORT:-8080}:${PORT:-3000}"
    env_file:
      - ./web.runtime.env
    environment:
      PORT: ${PORT:-3000}
      DB_HOST: ${DB_HOST:?Set DB_HOST in .env}
      DB_PORT: ${DB_PORT:-5432}
      DB_USER: ${DB_USER:-lesson}
      DB_NAME: ${DB_NAME:-lesson}
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    depends_on:
      - postgres

  postgres:
    image: postgres:17
    environment:
      POSTGRES_USER: ${DB_USER:-lesson}
      POSTGRES_DB: ${DB_NAME:-lesson}
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - pg-data:/var/lib/postgresql/data

volumes:
  pg-data:

secrets:
  db_password:
    file: ./.env.db-password

اکنون فقط web و PostgreSQL اجازهٔ دسترسی دارند. Compose فایل را در مسیر /run/secrets/db_password در هر دو container در اختیارشان می‌گذارد. برنامهٔ ما DB_PASSWORD_FILE را می‌خواند؛ image رسمی PostgreSQL قرارداد خودش یعنی POSTGRES_PASSWORD_FILE را می‌فهمد. نام این دو متغیر یکی نیست و هیچ‌کدام قابلیت عمومی خود Docker نیست.

Only web and PostgreSQL are now granted access. Compose makes the file available at /run/secrets/db_password in both containers. Our app reads DB_PASSWORD_FILE; the official PostgreSQL image understands its own POSTGRES_PASSWORD_FILE setting. The names differ, and neither variable is a universal Docker feature.

A password written in Dockerfile ENV flows into image metadata and history and can then be copied with the image Dockerfile ENVpassword literalunsafe source image metadatainspect + historypersists after build image distributionregistry / cache / exportbroader audience rotate exposed credential build recordimage copiedresponse after leak

فلش نخست می‌گوید literal در دستور ساخت ثبت می‌شود؛ فلش دوم یعنی image قابل توزیع حاوی metadata و history آن است. شاخهٔ سبز یادآوری می‌کند اگر مقدار واقعی منتشر شد، پاسخ اول چرخاندن اعتبارنامه است؛ پاک‌کردن نسخه‌ای که شاید کپی شده آن را پس نمی‌گیرد.

The first arrow means the literal is recorded in a build instruction; the second means the distributable image carries metadata and history. The green branch reminds us that if a real value escaped, the first response is to rotate the credential; deleting a copy cannot retrieve one that may already have been copied.

برای بررسی، مقدار بی‌خطر را بخوان؛ نه رمز راInspect harmless values, not the password

در Compose نهایی با config هنوز می‌توانی ببینی میزبان port و DB_HOST چگونه resolve شده‌اند و کدام service secret را درخواست می‌کند. برای دیدن محیط مؤثر web، از داخل همان فرایند فقط کلیدهای غیرحساس را چاپ کن. رمز را به خروجی inspect یا دستور copy/جای‌گذاری تبدیل نکن.

In the final Compose model, config can still show how the host port and DB_HOST resolve, and which service requests the secret. To inspect web's effective environment, print only an allowlist of non-sensitive keys from inside that service. Do not turn the password into inspect output or copy/paste material.

an allowlist: show selected values, never all of process.env
$ docker compose exec web node -e 'for (const k of ["APP_MODE","PORT","DB_HOST","DB_NAME"]) console.log(k + "=" + (process.env[k] ?? "<unset>"))'
APP_MODE=development
PORT=3000
DB_HOST=postgres
DB_NAME=lesson

$ docker compose exec web node -e 'const fs=require("node:fs"); const p=process.env.DB_PASSWORD_FILE; let readable=false; try { readable=!!p && fs.readFileSync(p).length > 0 } catch {} console.log("password file configured:",!!p,"readable:",readable)'
password file configured: true readable: true

$ docker compose exec postgres sh -c 'test -s /run/secrets/db_password && echo "database secret file is readable"'
database secret file is readable

این بررسی می‌گوید فرایند چه تنظیم‌های allowlistشده‌ای می‌بیند و فایل قابل خواندن هست یا نه؛ متن رمز را چاپ نمی‌کند. بعد با درخواست واقعی می‌توانی ثابت کنی ارتباط web با پایگاه‌داده برقرار است. اگر پرس‌وجو خطا داد، log را بخوان؛ رمز را برای پیدا کردن علت در log echo نکن.

These checks show an allowlist of process settings and whether the file can be read; they never print its contents. Then a real request can verify that web reaches the database. If a query fails, inspect logs—do not echo the password to diagnose it.

verify application behavior through its public response
$ curl.exe --fail http://127.0.0.1:8080
{"name":"lesson","time":"2026-09-24T...","mode":"development"}

برای مرور تقدم متغیرها هم می‌توانی به‌جای چاپ همهٔ environment، فقط APP_MODE را بخوانی. docker inspect ابزار مفیدی است، اما خروجی .Config.Env ممکن است مقدارهای حساس را نشان دهد؛ اگر از آن استفاده می‌کنی، فیلد و کلید را محدود کن و خروجی را منتشر نکن.

To check precedence, inspect only APP_MODE rather than printing every environment value. docker inspect is useful, but .Config.Env can reveal sensitive values; if you use it, limit the fields and keys, and do not publish the output.

وقتی مقدار اشتباه است، مسیرش را از منبع تا مصرف‌کننده دنبال کنWhen the app sees the wrong value, find the boundary first

بیشتر خطاهای این فصل از این نیستند که Docker «خراب» شده باشد؛ یک مقدار در جای اشتباه تعریف شده، نامش غلط است یا برنامه دنبال فایل دیگری می‌گردد. از منبع شروع کن و قدم‌به‌قدم جلو برو: Compose چه چیزی خوانده؟ سرویس چه چیزی گرفته؟ برنامه از کدام نام یا مسیر استفاده کرده؟

Configuration crosses several boundaries: shell and interpolation file, Compose model, container environment or secret file, and finally the application. A value can exist on the host yet never cross the next boundary. Instead of printing everything, identify the path the value was supposed to take.

failure one · required variable is missing or empty
# .env contains a typo: DB_HSOT=postgres
# compose.yaml expects: ${DB_HOST:?Set DB_HOST in .env}

$ docker compose config
error while interpolating services.web.environment.DB_HOST:
Set DB_HOST in .env

این خطا قبل از ساخت container رخ داده؛ پس هنوز شبکه یا PostgreSQL مقصر نیستند. Compose نام مورد انتظار DB_HOST را پیدا نکرده. کلید دو طرف را حرف‌به‌حرف مقایسه کن. اگر مقدار را عمداً خالی بگذاری، :? هم خطا می‌دهد؛ برای مقدار اختیاری از :- استفاده کن.

This fails before any container is created, so the network and PostgreSQL are not the cause yet. Compose did not find the expected DB_HOST. Compare the keys character by character. An intentionally empty value also fails with :?; use :- for optional settings.

failure two · .env did not appear inside web
# .env has APP_MODE=test, but Compose has no APP_MODE reference.
# Dockerfile has ENV APP_MODE=production.

$ docker compose exec web node -e 'console.log(process.env.APP_MODE)'
production

این خروجی عجیب نیست: Compose از .env برای interpolation استفاده می‌کند، اما service هرگز APP_MODE را درخواست نکرده است. راه‌حل این نیست که secretهای بیشتری به فایل بیفزایی؛ یا APP_MODE: ${APP_MODE} زیر environment بگذار، یا آن را به env_file مخصوص web منتقل کن.

This output is expected: Compose used .env for interpolation, but the service never requested APP_MODE. The fix is not to add more secrets to that file; either map APP_MODE: ${APP_MODE} under environment or move it to web's env_file.

failure three · secret granted at a path the app does not read
$ docker compose ps -a
NAME                     SERVICE    STATUS
compose-lab-env-web-1    web        Exited (1)

$ docker compose logs --tail=8 web
web-1 | Error: ENOENT: no such file or directory, open '/run/secrets/db_passwrod'

$ docker compose config
services:
  web:
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_passwrod
    secrets:
      - db_password

Compose فایل secret را با نام db_password mount کرده، اما web دنبال db_passwrod می‌گردد؛ دو قرارداد با هم جور نیستند. config مسیر را نشان می‌دهد و log می‌گوید شکست موقع خواندن فایل بوده. نام مسیر را به /run/secrets/db_password اصلاح کن و فقط web را recreate کن.

Compose mounted a secret named db_password, but web looked for db_passwrod; the two contracts disagree. Config shows the path, and the log locates failure at file reading. Correct it to /run/secrets/db_password and recreate web.

خطا چهارم: فایل .env وارد ثبت در Git شده استFailure four: a project .env was committed

یک نفر فایل محلی را حذف کرده و حالا می‌گوید secret دیگر در Git نیست. اما فایل در ثبت در Git قدیمی track شده بود؛ ignore کردنش از امروز به بعد جلوی commitهای تازه را می‌گیرد، نه نسخه‌هایی را که قبلاً ثبت یا clone شده‌اند.

Someone deleted the local file and says the secret is no longer in Git. But an earlier commit tracked it; ignoring it now can prevent future commits, not erase copies already recorded or cloned.

read history only with a fake marker · never expose a real value
$ git log --oneline -- .env
83a1f2d add local compose settings

$ git show 83a1f2d:.env
DB_PASSWORD=LAB_ONLY_NOT_A_CREDENTIAL

$ git check-ignore -v .env
.gitignore:37:.env  .env

در این نمونه مقدار فقط نشانگر ساختگی است. اگر این خط رمز واقعی بود، اقدام اول revoke یا تعویض کردن اعتبارنامه بود؛ بعد باید مصرف‌کننده‌ها را منتقل، دسترسی نسخهٔ تازه را محدود و با تیم دربارهٔ history و cloneها هماهنگ کرد. git rm --cached .env می‌تواند tracking آینده را متوقف کند، اما گذشته را پاک نمی‌کند؛ بازنویسی history هم تضمین نمی‌کند clone قبلی ناپدید شود.

The value here is only a fake marker. If it were a real password, first revoke or rotate the credential; then migrate consumers, restrict access to its replacement, and coordinate review of history and clones. git rm --cached .env can stop future tracking, but it does not erase the past; rewriting history cannot guarantee old clones disappear.

نشانهمرز محتملبررسی کم‌خطر بعدی
config از متغیر لازم خطا می‌دهدtypo، نبودن یا خالی‌بودن مقدار interpolationنام Compose را با کلید source مقایسه کن؛ فقط کلیدهای غیرحساس را چاپ کن
مقدار .env در برنامه دیده نمی‌شودهیچ ارجاعی زیر environment یا env_file نیستخروجی APP_MODE و service model را بررسی کن
PostgreSQL می‌گوید password لازم استPOSTGRES_PASSWORD_FILE یا secret mount غایب استconfig، log postgres و وجود path را بسنج؛ مقدار را چاپ نکن
web پاسخ 503 می‌دهدhostname، DB credential یا database readinesslogs web postgres و تنظیم‌های غیرحساس
خطای ENOENT در شروع webمتغیر DB_PASSWORD_FILE با target فایل یکی نیستمسیر تعریف secret و مسیر مصرف برنامه را مقایسه کن
رمز در config یا inspect ظاهر شدsecret از environment یا image ENV می‌آیدcredential واقعی را rotate کن؛ بعد منبع و history را اصلاح کن
بعد از عوض‌کردن فایل اتصال هنوز شکست می‌خوردPostgreSQL موجود با رمز قبلی مقداردهی اولیه شدهتفاوت فایل secret و credential ذخیره‌شده در database را در نظر بگیر

مورد آخر نکته‌ای دربارهٔ volume فصل ۰۶ است. image رسمی PostgreSQL متغیر POSTGRES_PASSWORD_FILE را هنگام مقداردهی اولیهٔ مسیر دادهٔ خالی به‌کار می‌برد؛ تغییر فایل یا environment به‌تنهایی رمز کاربر ذخیره‌شده در volume قدیمی را عوض نمی‌کند. چرخاندن رمز یعنی تغییر اعتبارنامه در پایگاه‌داده و جابه‌جایی هماهنگ مصرف‌کننده‌ها، نه فقط ویرایش یک فایل.

The last case reconnects to Chapter 6 volumes. The official PostgreSQL image uses POSTGRES_PASSWORD_FILE while initializing an empty data directory; changing that file or environment alone does not change the role password in an existing volume. Rotation means changing the database credential and coordinating its consumers, not merely editing one file.

تمرین‌ها: مسیر هر مقدار را تا جایی که مصرف می‌شود دنبال کنExercises: trace each value to its consumer

هر تمرین را مثل ردگیری یک بسته ببین. مقدار از کجا آمده، Compose با آن چه کرده، کدام سرویس آن را گرفته و برنامه در نهایت از کجا خوانده؟ قبل از بازکردن پاسخ همین مسیر را با یک یا دو جمله برای خودت بنویس. این خیلی ماندگارتر از حفظ‌کردن اسم فایل‌هاست.

For each scenario, identify where the value is defined, which component reads it, and what remains unproven. Write a prediction before opening the solution; this chapter is about tracing boundaries, not memorizing filenames.

۱. برای هر مقدار یک خانه پیدا کن1. Give each value a home

کد به پایگاه‌داده پرس‌وجو می‌زند، نشانی DB_HOST بین لپ‌تاپ و محیط آزمایش فرق می‌کند، APP_MODE حالت اجرا را مشخص می‌کند و برای ورود به پایگاه‌داده رمز لازم است. هرکدام را کجا می‌گذاری و چرا؟

The code issues a query, DB_HOST differs between laptop and test, APP_MODE selects behavior, and a password is needed to log in. Where would you put each, and why?

یک طراحی معقولOne reasonable design

منطق پرس‌وجو در کد منبع و image می‌ماند؛ نشانی میزبان و حالت، تنظیم‌های غیرحساس زمان اجرا هستند؛ رمز یک secret جداست که فقط web و پایگاه‌داده لازم دارند. اگر هر سه محیط یک image از کد منبع یکسان اجرا می‌کنند، برای نشانی میزبان و حالت نباید Dockerfile را هر بار عوض کرد.

Query logic belongs in source/the image; host and mode are non-sensitive runtime settings; the password is a separate secret granted only to web and the database. If all three environments run one image from the same source, neither host nor mode should require editing the Dockerfile each time.

۲. کدام مورد می‌تواند ENV باشد؟2. Which value could be an image ENV default?

از میان PORT=3000، DB_HOST=localhost و DB_PASSWORD=... کدام‌یک را می‌توان به‌عنوان پیش‌فرض غیرحساس در Dockerfile نگه داشت؟ موردهای نامناسب چه مرزی را می‌شکنند؟

Among PORT=3000, DB_HOST=localhost, and DB_PASSWORD=..., which could remain as a non-sensitive Dockerfile default? What boundary do the unsuitable choices violate?

پاسخ با دو احتیاطAnswer, with two caveats

PORT=3000 می‌تواند پیش‌فرض برنامه باشد، اگر کد آن را بخواند. DB_HOST=localhost در container معمولاً مقصد اشتباهی است و میان محیط‌ها هم فرق می‌کند؛ رمز اصلاً نباید در image باشد. حتی port پیش‌فرض را می‌توان هنگام اجرا عوض کرد.

PORT=3000 can be an app default if the code uses it. DB_HOST=localhost is usually the wrong destination inside a container and differs by environment; a password should not be in the image at all. Even the port default can be overridden at runtime.

۳. چه چیزی rebuild نمی‌خواهد؟3. Which change does not require a rebuild?

image با APP_MODE=production ساخته شده. می‌خواهی همان image را یک بار با mode آزمایشی و یک بار با mode توسعه اجرا کنی. اولین گزینهٔ مناسب چیست؟

An image was built with APP_MODE=production. You want to run that same image once in test mode and once in development mode. What is the first suitable option?

تغییر در زمان اجراChange it at runtime

از docker run --env APP_MODE=test IMAGE یا مقدار Compose استفاده کن. ENV در image یک پیش‌فرض است و تنظیم زمان اجرا می‌تواند آن را override کند؛ به تغییر کد یا ساخت لایهٔ تازه نیاز نیست.

Use docker run --env APP_MODE=test IMAGE or a Compose value. Image ENV is a default that runtime configuration can override; no source change or new image layer is needed.

۴. چرا رمز در history پیدا شد؟4. Why did the password show up in history?

کسی می‌گوید رمز را بعداً با ENV DB_PASSWORD= خالی کرده، اما docker history --no-trunc مقدار قبلی را نشان می‌دهد. توضیح بده چه اتفاقی افتاده و قدم اول برای رمز واقعی چیست.

Someone says they later cleared the password with ENV DB_PASSWORD=, but docker history --no-trunc still shows its earlier value. Explain why, and name the first response for a real credential.

history را نمی‌شود با لایهٔ بعدی فراموش کردA later layer cannot make history forget

دستور قبلی و metadata لایهٔ قبلی ممکن است همچنان در image history بماند؛ image از ابتدا دیگر برای secret پاک محسوب نمی‌شود. اول اعتبارنامه واقعی را revoke/تعویض کن، بعد Dockerfile، imageهای توزیع‌شده و فرایند انتشار را اصلاح کن.

The earlier instruction and layer metadata can remain in image history; the image is not clean of the secret merely because a later layer clears it. First revoke or rotate the real credential, then fix the Dockerfile, distributed images, and release process.

۵. .env را کدام برنامه می‌خواند؟5. Which program reads project .env?

فایل .env کنار Compose است و APP_MODE=test دارد، ولی docker compose exec web node -e 'console.log(process.env.APP_MODE)' مقدار production چاپ می‌کند. اولین توضیح چیست؟

Project .env beside Compose contains APP_MODE=test, but docker compose exec web node -e 'console.log(process.env.APP_MODE)' prints production. What is the first explanation?

جای‌نگهدار الزاماً به فرایند نمی‌رسدA placeholder source is not automatically process input

Compose می‌تواند .env را برای interpolation بخواند، اما تا وقتی فیلد service آن را به environment یا env_file ندهد، وارد container نشده است. خروجی فعلی احتمالاً پیش‌فرض داخل image است.

Compose can read project .env for interpolation, but until a service passes it through environment or env_file, it is not in the container. The current output is probably the image default.

۶. به Compose بگو کدام نام را جای‌گذاری کند6. Tell Compose which name to substitute

در .env نوشته‌ای DB_HSOT=postgres؛ Compose file از ${DB_HOST} استفاده می‌کند و warning می‌دهد. چرا؟ تغییر حداقلی چیست؟

Project .env says DB_HSOT=postgres; the Compose file references ${DB_HOST} and warns. Why, and what is the smallest fix?

کلیدها باید دقیقاً هم‌نام باشندThe keys must match exactly

Compose دنبال DB_HOST می‌گردد و کلید DB_HSOT نام دیگری است. typo را اصلاح کن و برای میزبان لازم از ${DB_HOST:?Set DB_HOST} استفاده کن تا نبودن یا خالی‌بودن پیش از start معلوم شود.

Compose looks for DB_HOST; DB_HSOT is a different key. Fix the typo and use ${DB_HOST:?Set DB_HOST} for a required host so unset or empty input fails before startup.

۷. خالی با تعریف‌نشده یکی است؟7. Is empty the same as unset?

در `.env`، APP_PORT= است و Compose از ${APP_PORT:-8080} استفاده می‌کند. چه میزبان portای resolve می‌شود؟

Project .env has APP_PORT=, while Compose uses ${APP_PORT:-8080}. Which host port is resolved?

پیش‌فرض برای مقدار خالی هم فعال می‌شودThe default also applies to an empty value

عدد ۸۰۸۰. عملگر :- برای تعریف‌نشده و خالی به پیش‌فرض می‌رود. اگر عمداً بخواهی خالی را نگه داری و فقط تعریف‌نشده را جایگزین کنی، syntax بدون colon یعنی ${APP_PORT-8080} رفتار متفاوتی دارد.

8080. The :- form uses its default for both unset and empty values. If you want to preserve an intentionally empty value and substitute only when unset, the form without the colon—${APP_PORT-8080}—differs.

۸. کدام فایل مقدار را به web می‌دهد؟8. Which file passes a value to web?

تفاوت کار این دو را با یک جمله توضیح بده: .env کنار Compose و env_file: ./web.runtime.env زیر service web.

Explain the difference in one sentence: project .env beside Compose, versus env_file: ./web.runtime.env under the web service.

زمان و مصرف‌کننده فرق می‌کنندDifferent stages, different consumers

.env را Compose CLI معمولاً برای placeholderهای فایل می‌خواند؛ env_file سرویس key-valueها را به environment container می‌دهد. اولی به‌تنهایی برنامه را configure نمی‌کند.

Compose CLI commonly reads project .env for file placeholders; service env_file passes key-value entries into the container environment. The first one alone does not configure the app.

۹. کدام مقدار برنده می‌شود؟9. Which value wins?

Dockerfile مقدار APP_MODE=production دارد؛ web.runtime.env می‌گوید development؛ و زیر service در environment مقدار test آمده است. web چه می‌بیند؟

Dockerfile has APP_MODE=production, web.runtime.env says development, and service environment sets test. What does web see?

مقدار صریح serviceThe explicit service value

test. برای این سه منبع، service environment بالاتر از service env_file است و هر دو بالاتر از پیش‌فرض داخل image. اگر مقدار environment خودش interpolation باشد، منبع shell یا .env آن جای‌نگهدار را resolve می‌کند.

test. For these sources, service environment outranks service env_file, and both outrank the image default. If the environment entry uses interpolation, the shell or project .env supplies that placeholder value.

۱۰. رمز در ARG واقعاً ناپدید می‌شود؟10. Does a password in ARG really disappear?

یک هم‌تیمی می‌گوید «ARG مثل ENV به container نمی‌رود، پس برای build token امن است.» این پاسخ چه نکته‌ای را جا می‌اندازد؟

A teammate says, “ARG is not carried into the container like ENV, so it is safe for a build token.” What does this miss?

build metadata هم سطح دسترسی داردBuild metadata also has an audience

ARG معمولاً runtime environment نیست، اما مقدار build argument می‌تواند در history یا provenance نمایان شود. «در فرایند نهایی نیست» با «راز می‌ماند» یکی نیست. اعتبارنامه build را در این درس به Dockerfile یا build argument نمی‌گذاریم.

ARG is generally not a runtime environment value, but a build argument can appear in history or provenance. “Not in the final process” is not the same as “kept secret.” We do not place build credentials in the Dockerfile or build arguments in this lesson.

۱۱. چند service واقعاً رمز لازم دارند؟11. Which services actually need the password?

در پروژهٔ سه‌سرویسی فرضی، web به PostgreSQL پرس‌وجو می‌زند و یک proxy فقط درخواست HTTP را عبور می‌دهد. secret پایگاه‌داده را به کدام serviceها می‌دهی؟

In a three-service project, web queries PostgreSQL while a proxy only forwards HTTP. Which services receive the database secret?

کمترین دسترسی لازمThe minimum necessary access

فقط web و PostgreSQL، چون web برای احراز هویت client و PostgreSQL برای مقداردهی اولیه لازم دارد. proxy فقط ترافیک را می‌بیند و به رمز نیازی ندارد؛ secret را به تمام serviceها broadcast نکن.

Only web and PostgreSQL: web needs it to authenticate its client connection, and PostgreSQL needs it for initialization. The proxy only handles traffic and has no need for the password; do not grant the secret to every service.

۱۲. این secret محلی چه ضمانتی ندارد؟12. What guarantee does this local secret not provide?

Compose از file: ./.env.db-password استفاده می‌کند و فایل را زیر /run/secrets/db_password در container می‌گذارد. آیا همین تعریف فایل را روی میزبان رمز می‌کند؟

Compose uses file: ./.env.db-password and makes it available at /run/secrets/db_password in the container. Does that encrypt the source file on the host?

mount محدودتر، نه گاوصندوق میزبانA narrower mount, not a host vault

نه. اجرای محلی فایل را به صورت bind mount می‌دهد. grant service و بیرون‌بردن رمز از environment مفیدند، اما فایل روی میزبان و دسترسی Docker daemon همچنان مرز اعتمادند؛ رمزنگاری Swarm را فرض نکن.

No. Local Compose makes the file available as a bind mount. Per-service grants and removing the password from the environment help, but the host file and Docker daemon remain in the trust boundary; do not assume Swarm encryption.

۱۳. _FILE ویژگی عمومی Docker است؟13. Is _FILE a general Docker feature?

service دارای DB_PASSWORD_FILE=/run/secrets/db_password است. چه چیزی باعث می‌شود برنامه این فایل را واقعاً بخواند؟

A service has DB_PASSWORD_FILE=/run/secrets/db_password. What makes the application actually read that file?

کدی که قرارداد را پیاده کندCode that implements the convention

هیچ رفتار خودکاری در Docker Engine وجود ندارد. کد منبع باید process.env.DB_PASSWORD_FILE را بخواند، فایل را باز کند و مقدار را به pg بدهد؛ imageهایی مثل PostgreSQL هم فقط متغیرهای _FILE مستند خودشان را می‌فهمند.

Docker Engine does not add automatic behavior. The source must read process.env.DB_PASSWORD_FILE, open the file, and pass its value to pg; images such as PostgreSQL understand only their own documented _FILE variables.

۱۴. secret مجاز است اما مسیر غلط است14. The secret is granted, but the path is wrong

Compose در تنظیمات secret به نام db_password را نشان می‌دهد؛ log برنامه می‌گوید ENOENT روی /run/secrets/db_passwrod. کدام دو نام را مقایسه می‌کنی؟

Config shows a secret named db_password; the app log reports ENOENT for /run/secrets/db_passwrod. Which two names do you compare?

نام mount و مسیر مصرفMount name and consumer path

مقایسه کن secret identifier و مقدار target/مسیر آن با DB_PASSWORD_FILEای که web می‌خواند. typo در مسیر است؛ مقدار رمز را برای حلش چاپ نکن.

Compare the secret identifier and its target/path with the DB_PASSWORD_FILE value web reads. The path contains a typo; printing the password will not help diagnose it.

۱۵. آیا می‌توان خروجی کامل inspect را جای‌گذاری کرد؟15. Is full inspect output safe to paste?

برای دیدن APP_MODE می‌خواهی کل docker inspect را در issue عمومی جای‌گذاری کنی. راه دقیق‌تر و کم‌خطرتر چیست؟

You want to paste the whole docker inspect output into a public issue to check APP_MODE. What is a narrower, safer check?

یک کلید بی‌حساسیت را allowlist کنAllowlist a harmless key

داخل web فقط process.env.APP_MODE را چاپ کن یا در inspect به همان key محدود شو. خروجی کامل ممکن است .Config.Env و متغیرهای حساس را هم آشکار کند؛ آن را عمومی نکن.

Print only process.env.APP_MODE inside web or filter inspect to that key. Full output may expose .Config.Env and sensitive variables; do not publish it.

۱۶. چه چیزی را از .gitignore نتیجه نمی‌گیری؟16. What does .gitignore not prove?

git check-ignore -v .env.db-password نشان می‌دهد فایل ignore می‌شود. آیا این ثابت می‌کند فایل قبلاً track یا در ثبت در Git فرستاده نشده؟

git check-ignore -v .env.db-password says the file is ignored. Does that prove it was never tracked or committed?

نه؛ ignore فقط تصمیم آینده برای فایل untracked استNo; ignore does not untrack history

نه. فایل از قبل trackشده با .gitignore از index بیرون نمی‌آید و ثبت در Git قدیمی را هم پاک نمی‌کند. وضعیت را با git ls-files --error-unmatch یا مرور history بررسی کن؛ اگر secret واقعی ثبت در Git شده، ابتدا آن را باطل یا تعویض کن.

No. A file already tracked stays in the index, and .gitignore does not erase old commits. Check tracking with git ls-files --error-unmatch or inspect history; if a real secret was committed, revoke or rotate it first.

۱۷. رمز را با volume قدیمی عوض کردی؛ چه چیزی ثابت نیست؟17. You changed the password file with an old volume; what is unproven?

فایل secret تازه است، اما PostgreSQL بعد از start هنوز اتصال web را رد می‌کند. data پوشه از اجرای قبلی باقی مانده است. قبل از پاک‌کردن volume چه احتمالی را بررسی می‌کنی؟

The secret file is new, but PostgreSQL still rejects web after startup. The data directory survived from an earlier run. What do you check before deleting the volume?

رمز نقش پایگاه‌داده لزوماً از فایل تازه نمی‌آیدThe stored database role may still have the old password

متغیر آغازین PostgreSQL وقتی volume خالی است پایگاه‌داده را مقداردهی می‌کند؛ volume موجود رمز role را نگه می‌دارد. log و هویت اعتبارنامه را بررسی کن و دربارهٔ تغییر همگام رمز تصمیم بگیر. حذف volume داده را نابود می‌کند و درمان عمومی تعویض دوره‌ای نیست.

PostgreSQL's initialization variable sets up a database when the volume is empty; an existing volume retains the role password. Check the credential identity and plan a coordinated change. Deleting the volume destroys data and is not a general rotation strategy.

۱۸. گزارش کوتاه پیکربندی بنویس18. Write a concise configuration report

تیمی می‌گوید web به DB وصل نمی‌شود، در `.env` مقدار DB_HOST=postgres هست و secret هم در Compose تعریف شده. پیش از restart چه شواهدی می‌خواهی و چطور مراقب می‌مانی رمز چاپ نشود؟

A teammate says web cannot reach the DB, project .env contains DB_HOST=postgres, and Compose declares a secret. What evidence do you want before restarting, and how do you avoid printing the password?

ردیابی منبع تا مصرف‌کنندهTrace from source to consumer

اول docker compose config را برای model و مسیر secret مرور کن، بعد ps -a و logهای دو service را بخوان. در web فقط DB_HOST و DB_PASSWORD_FILE را با یک allowlist بررسی کن؛ readable بودن فایل را بدون چاپ محتوا بسنج و درخواست HTTP را برای نتیجهٔ اتصال امتحان کن. اگر مقدار واقعی جایی لو رفته، پیش از گزارش آن را تعویض کن.

First inspect the model and secret path with docker compose config, then read ps -a and both services' logs. In web, allowlist DB_HOST and DB_PASSWORD_FILE; test file readability without printing content, then make an HTTP request to verify the connection. If a real value escaped anywhere, rotate it before reporting.

آزمایشگاه: همان image را نگه دار، تنظیمات را جابه‌جا کن و یک secret را درست تحویل بدهLab: same application, separate settings, password delivered as a file

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

Run this separately from Chapter 8. A fresh project and volume let us try a disposable credential without touching earlier data. The arc is: keep the image independent, relocate configuration, verify the connection, break one boundary, and repair it with evidence.

اول پروژهٔ تازه و سه منبع محلی را آماده کنFirst prepare a fresh project and three local inputs

از همان ابتدا همهٔ مقدارها را قاطی نکن. یک .env برای جای‌گذاری‌های Compose، یک فایل غیرحساس برای محیط وب و یک فایل جدا برای secret آزمایشی داریم. این جداسازی خودش بخشی از تمرین است: هر فایل فقط یک کار مشخص انجام می‌دهد.

Create compose-lab-env and its web subfolder; place the three application files from the previous section there and use the Dockerfile's non-sensitive defaults. Create the final Compose file, non-sensitive web.runtime.env, and a local project .env. The explicit project name compose-lab-env keeps this volume separate from the earlier project.

local files · values for this disposable exercise only
# .env · interpolation values; no password here
APP_PORT=8080
PORT=3000
DB_HOST=postgres
DB_PORT=5432
DB_USER=lesson
DB_NAME=lesson

# web.runtime.env · only non-secret web settings
APP_MODE=development

# .env.db-password · raw one-line secret file, not a key=value file
env-lab-only

فایل رمز را با ویرایشگر بساز و مطمئن شو یک خط متن آزمایشی دارد؛ هیچ اعتبارنامه واقعی را جای‌گذاری نکن. چون ریشهٔ نام آن .env. است، .gitignore موجود باید آن را بگیرد. پیش از ادامه این را بررسی کن و ببین آیا Git قبلاً آن را track کرده یا نه.

Create the password file in an editor and make sure it contains one disposable line; do not paste any real credential. Because its name begins with .env., the existing .gitignore should match it. Check that, and verify Git is not already tracking it.

confirm ignore and validate without displaying the secret
$ git check-ignore -v .env.db-password
.gitignore:38:.env.*  .env.db-password

$ git ls-files --error-unmatch .env.db-password
error: pathspec '.env.db-password' did not match any files

$ docker compose config
...

اولین فرمان نام قاعدهٔ ignore و فایل matchشده را نشان می‌دهد؛ فرمان دوم می‌گوید secret در index Git نیست. هیچ‌کدام ثابت نمی‌کنند فایل روی میزبان رمز شده یا قبلاً در تاریخچه‌ای بیرون از این repository منتشر نشده است. خروجی config را هم برای مسیرها و placeholderهای غیرحساس بررسی کن؛ secret را در فایل یا خروجی چاپ نکن.

The first command shows the matching ignore rule; the second says the secret is not in Git's index. Neither proves the host file is encrypted or was never published in another history. Review config for paths and non-sensitive values, but do not print the secret in a file or output.

baseline را بالا بیاور و فقط شواهد لازم را ببینBring up the baseline and inspect only useful evidence

حالا image را build و دو service را start کن. اول دنبال secret نمی‌گردیم؛ می‌خواهیم ببینیم کدام فایل به کدام service داده شده، web چه تنظیم غیرحساسی دارد و HTTP واقعاً پایگاه‌داده را پرس‌وجو می‌کند یا نه.

Now build the image and start both services. We will not search for the secret's contents; we want to verify which services receive the file, what non-sensitive settings web sees, and whether HTTP really queries the database.

validate model, build, then check non-secret runtime evidence
$ docker compose config
name: compose-lab-env
services:
  postgres:
    environment:
      POSTGRES_DB: lesson
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_USER: lesson
    secrets:
      - db_password
  web:
    environment:
      APP_MODE: development
      DB_HOST: postgres
      DB_NAME: lesson
      DB_PASSWORD_FILE: /run/secrets/db_password
      PORT: "3000"
    ...
secrets:
  db_password:
    file: ./.env.db-password

$ docker compose up -d --build
[+] Running 4/4
 ✔ Network compose-lab-env_default       Created
 ✔ Volume compose-lab-env_pg-data        Created
 ✔ Container compose-lab-env-postgres-1 Started
 ✔ Container compose-lab-env-web-1      Started

$ docker compose exec web node -e 'for (const k of ["APP_MODE","DB_HOST","DB_NAME","PORT"]) console.log(k + "=" + process.env[k])'
APP_MODE=development
DB_HOST=postgres
DB_NAME=lesson
PORT=3000

$ docker compose exec web node -e 'const fs=require("node:fs"); const p=process.env.DB_PASSWORD_FILE; console.log("secret file readable:",!!p && fs.readFileSync(p).length > 0)'
secret file readable: true

تنظیمات نشان می‌دهد رمز در environment نیست و فقط مسیر منبع file ثبت شده است. فرمان دوم از داخل web نشان می‌دهد مقدارهای غیرحساس از کجا آمده‌اند؛ فرمان سوم فقط readable بودن فایل را می‌سنجد. اگر رمز را در خروجی قبلی ندیدی، این به معنی نبودنش نیست؛ به‌عمد آن را چاپ نکردیم.

Config shows no password environment value, only the file source path. The second command shows where non-sensitive settings came from; the third checks only whether the file can be read. Not seeing the password in output does not mean it is absent—we deliberately did not print it.

verify the whole request path
$ curl.exe --fail http://127.0.0.1:8080
{"name":"lesson","time":"2026-09-24T...","mode":"development"}

$ docker compose logs --tail=8 web postgres
web-1      | web listening on 0.0.0.0:3000; mode=development
postgres-1 | database system is ready to accept connections

پاسخ JSON یعنی web پرس‌وجو را با موفقیت کامل کرده و mode را هم از فایل runtime دیده است. log فقط mode و آمادگی پایگاه‌داده را نشان می‌دهد؛ رمز در هیچ‌کدام نیست. اگر mode برابر پیش‌فرض image بود، اول بررسی می‌کردیم آیا service واقعاً env_file را تعریف کرده است.

The JSON response means web completed its query and also read mode from the runtime file. Logs show only mode and database readiness; neither includes the password. If mode were the image default instead, we would first check that the service actually declares env_file.

حالا یک نام را خراب کن، بعد مسیر را از تنظیمات تا log دنبال کنNow break one name, then trace it from config to logs

در .env مقدار DB_HOST را عمداً از postgres به local-db تغییر بده. این بار syntax درست است، پس validation باید موفق شود؛ اما چنین serviceای وجود ندارد. پیش‌بینی کن شکست در کدام مرز رخ می‌دهد.

In project .env, deliberately change DB_HOST from postgres to local-db. The syntax is valid, so validation should pass, but that service does not exist. Predict which boundary will fail.

resolved value, app failure, then evidence
$ docker compose config
... DB_HOST: local-db ...

$ docker compose up -d
$ curl.exe -i http://127.0.0.1:8080
HTTP/1.1 503 Service Unavailable
database request failed

$ docker compose logs --tail=6 web
web-1 | database request failed: getaddrinfo ENOTFOUND local-db

فایل از نظر Compose معتبر بود؛ مسئله بعد از ساخت مدل رخ داد، وقتی Node می‌خواست نام میزبان را resolve کند. log نامی را می‌دهد که برنامه واقعاً امتحان کرده است. مقدار را در .env به postgres برگردان، تنظیمات را دوباره ببین و با docker compose up -d مقدار تازه را اعمال کن؛ برای این تغییر نیازی به build مجدد نیست.

Compose accepted the file; failure occurred later when Node tried to resolve the hostname. The log tells us which name the app actually attempted. Restore postgres in project .env, inspect config again, and apply it with docker compose up -d; this change does not require rebuilding the image.

مسیر فایل را خراب کن و با دو شاهد تعمیرش کنBreak the file path and repair it with two pieces of evidence

حالا در Compose مقدار DB_PASSWORD_FILE را به /run/secrets/missing تغییر بده. docker compose config باید مسیر اشتباه را نشان دهد؛ پس از اعمال تغییر، web هنگام شروع فایل را پیدا نمی‌کند. سراغ محتویات secret نرو؛ مسیر کد منبع، secret grant و مسیر مصرف را مقایسه کن.

Now change Compose's DB_PASSWORD_FILE to /run/secrets/missing. Config should show the wrong path; after applying the change, web cannot find the file at startup. Do not inspect the secret contents; compare its source, service grant, and consumer path.

diagnose without exposing the value
$ docker compose ps -a
NAME                      SERVICE    STATUS
compose-lab-env-web-1     web        Exited (1)
compose-lab-env-postgres-1 postgres  Up

$ docker compose logs --tail=5 web
web-1 | Error: ENOENT: no such file or directory, open '/run/secrets/missing'

$ docker compose config
... DB_PASSWORD_FILE: /run/secrets/missing ...
... secrets: [db_password] ...

فرایند web خارج شده، اما PostgreSQL زنده است؛ بنابراین پایگاه‌داده را بی‌دلیل دست‌کاری نمی‌کنیم. تنظیمات نشان می‌دهد web secret را درخواست کرده ولی مسیر مورد انتظارش غلط است؛ log هم شکست خواندن همان مسیر را ثابت می‌کند. مسیر را به /run/secrets/db_password اصلاح کن، تنظیمات را ببین و up -d بزن تا web recreate شود.

Web exited while PostgreSQL stayed up, so we do not disturb the database. Config shows that web requested a secret but points at the wrong path; logs confirm the failed read. Restore /run/secrets/db_password, inspect config, and run up -d to recreate web.

در پایان، secret آزمایشی را پاک کن—نه volume فصل قبلی راFinish by removing the demo secret—not Chapter 8's volume

چون این پروژه تازه و داده‌اش آزمایشی است، اول serviceها را پایین بیاور، بعد مطمئن شو secret file دیگر لازم نیست و آن را حذف کن. تنها در همین پروژه جدید down --volumes بزن تا volume آزمایشی هم پاک شود؛ دستور پاک‌سازی را از پوشهٔ Chapter 08 اجرا نکن.

Because this project is fresh and disposable, stop it first, then remove the local secret file when it is no longer needed. Run down --volumes only for this new project to delete its demo volume; do not run the cleanup from the Chapter 8 directory.

scoped cleanup and final check
$ docker compose down --volumes
[+] Running 4/4
 ✔ Container compose-lab-env-web-1      Removed
 ✔ Container compose-lab-env-postgres-1 Removed
 ✔ Network compose-lab-env_default      Removed
 ✔ Volume compose-lab-env_pg-data       Removed

# Delete .env.db-password in your editor.
# Keep .env.example and the non-secret web.runtime.env if useful.

secret آزمایشی از میزبان حذف شد و volume یکتای compose-lab-env هم پاک شد؛ volume پروژهٔ قبلی دست‌نخورده ماند. اگر فایل واقعی رمز را جایی ثبت در Git یا اشتراک‌گذاری کرده بودی، حذف محلی کافی نبود و باید اعتبارنامه را تعویض می‌کردی.

The disposable host file and the unique compose-lab-env volume are gone; the previous project's volume remains untouched. If a real password had been committed or shared, local deletion would not be enough—you would need to rotate it.

حالا می‌توانیم جواب بدهیم: این مقدار کجا زندگی می‌کند و چه کسی باید آن را ببیند؟Now we know where each value travels

در این فصل همان image را نگه داشتیم اما رفتار اجرا را عوض کردیم. آدرس و پورت را هنگام خواندن Compose دادیم، تنظیم غیرحساس را به محیط وب رساندیم و رمز را به‌صورت فایل فقط در اختیار دو سرویس لازم گذاشتیم. هیچ‌کدام از این روش‌ها فقط به‌خاطر اسمشان «امن» نمی‌شوند؛ مهم است مقدار در چه جاهایی دیده می‌شود.

We reused one image across environments, interpolated host and port into the Compose model, passed non-sensitive mode to web through env_file, and delivered the password as a file secret to only web and PostgreSQL. None of these steps becomes secure merely by naming a file; what matters is which process reads it and who can access the host, daemon, image, and history.

در محیط واقعی، منبع اعتبارنامه، سطح دسترسی، تعویض رمز و واکنش به افشا هم باید طراحی شود. اینجا عمداً روی Docker Compose محلی ماندیم. قدم بعدی دیگر محل نگهداری مقدار نیست؛ می‌خواهیم بفهمیم یک container که Up است واقعاً سالم هم هست یا نه.

A team or real environment also needs a deliberate plan for credential sources, access, rotation, and exposure response. This chapter intentionally stayed with local Docker Compose and a file; organizational secret systems and deployment are outside this lab.

سؤالاین فصل چه چیزی ثابت کرد؟مرز باقی‌مانده
برای تغییر حالت، image را دوباره بسازیم؟نه؛ مقدار زمان اجرا می‌تواند پیش‌فرض ENV را override کندمقدار و زمان اعمال آن را مستند کن
.env با env_file یکی است؟نه؛ interpolation در برابر environment سرویسبا خروجی resolved مسیر مقدار را دنبال کن
password را در environment بگذاریم؟برای این lab آن را از process environment خارج کردیمsecret فایل‌محور محلی، فایل host را رمز نمی‌کند
اگر secret لو رفت؟مقدار را از log و image دور نگه دارcredential واقعی را revoke/rotate کن؛ history را هم بررسی کن

قدم بعدی دیگر افزودن متغیر نیست؛ باید ببینیم container چطور سلامت فرایند خود را گزارش می‌دهد و پس از خروجش چه اتفاقی باید بیفتد. فصل ۱۰ سراغ healthcheck و restart policy می‌رود. مدیریت Vault یا secretهای Kubernetes و cloud را برای یک فصل جدا نگه می‌داریم.

The next step is not adding more variables; it is learning how a container reports process health and what should happen after it exits. Chapter 10 covers healthchecks and restart policies. Vault, Kubernetes, and cloud secret management remain topics for a separate course.

مرجع سریعQuick reference

Dockerfile ENV NAME=valueپیش‌فرض غیرحساس image؛ در container قابل override استnon-sensitive image default; overridable at container runtime
docker run --env NAME=value IMAGEمقدار runtime بده، بدون rebuild کردن imageset a runtime value without rebuilding the image
NAME=${NAME:-default}interpolation با پیش‌فرض برای تعریف‌نشده یا خالیinterpolate with a fallback for unset or empty
NAME=${NAME:?required}اگر تعریف‌نشده یا خالی بود تنظیمات را متوقف کنfail configuration when unset or empty
docker compose configmodel نهایی را ببین؛ secret را چاپ یا اشتراک‌گذاری نکنinspect resolved model; never print or share secrets
env_file: ./web.runtime.envkey-valueها را به environment همان service بدهpass key-value entries into that service environment
secrets: /run/secrets/NAMEفایل را فقط به serviceهای لازم grant کنgrant the file only to services that need it
docker history --no-trunc IMAGEدستورهای ثبت‌شده در image history را بررسی کنinspect recorded build instructions in image history
git check-ignore -v FILEقاعدهٔ ignore را ببین؛ افشای قبلی را پاک نمی‌کندsee the ignore rule; it does not erase prior exposure

برای رفتار جاری به مستندات رسمی interpolation و .env، environment و env_file، تقدم متغیرها، Compose secrets و مرز اجرای محلی، Dockerfile ENV، secretهای زمان build، مدل اعتماد Compose، مستندات image رسمی PostgreSQL و مرجع gitignore رجوع کن. syntaxهای پیشرفته به نسخهٔ Compose وابسته‌اند.

For current behavior, see the official docs on interpolation and .env, environment and env_file, variable precedence, Compose secrets and local behavior, Dockerfile ENV, build secrets, the Compose trust model, the official PostgreSQL image docs, and gitignore reference. Advanced syntax can depend on the Compose version.