متغیرهای محیطی و 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?
یک 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:
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?
فلش اول یعنی همان 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 زدن به PostgreSQL | source و image |
| image | چه artifactای را در همهجا اجرا میکنیم؟ | Node، packageها و server | registry یا ماشین میزبان |
| تنظیم غیرحساس | این اجرا باید با چه رفتاری کار کند؟ | APP_MODE، DB_HOST، port | Compose، .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?”
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.
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.
فلش از 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 و container | PORT=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.
$ 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.
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.
اول اعتبارنامه را باطل یا عوض کن و مصرفکنندههایش را به مقدار تازه منتقل کن. حذف فایل از ثبت در 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.
فلشهای سمت چپ کاندیداهای 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 میشود؟ | نقش این فصل |
|---|---|---|---|
پروژه .env | Docker Compose CLI برای interpolation | نه؛ فقط اگر در service ارجاعش بدهی | مقدارهای محلی برای ${...} |
service env_file: | Compose برای پیکربندی همان service | بله، key-valueهایش به process میرسند | چند تنظیم غیرحساس و تکرارشونده |
service environment: | Compose | بله، همان نامهای تعریفشده | مقدار صریح یا interpolationشده |
docker compose --env-file | Compose 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.
# .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.
$ 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.
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 ENV | default نهایی وقتی 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.
# 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.
| مقایسه | تنظیم environment | Compose 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.
فلش اول یعنی 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.
برای اجرای محلی با 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.
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.
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.
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.
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.
APP_MODE=development
اکنون APP_MODE را از بخش environment بردار و زیر همان service بنویس:
Now remove APP_MODE from environment and place this under the same service instead:
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.
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.
فلش نخست میگوید 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.
$ 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.
$ 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.
# .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.
# .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.
$ 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.
$ 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 readiness | logs 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.
# .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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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 runtimedocker run --env NAME=value IMAGEمقدار runtime بده، بدون rebuild کردن imageset a runtime value without rebuilding the imageNAME=${NAME:-default}interpolation با پیشفرض برای تعریفنشده یا خالیinterpolate with a fallback for unset or emptyNAME=${NAME:?required}اگر تعریفنشده یا خالی بود تنظیمات را متوقف کنfail configuration when unset or emptydocker compose configmodel نهایی را ببین؛ secret را چاپ یا اشتراکگذاری نکنinspect resolved model; never print or share secretsenv_file: ./web.runtime.envkey-valueها را به environment همان service بدهpass key-value entries into that service environmentsecrets: /run/secrets/NAMEفایل را فقط به serviceهای لازم grant کنgrant the file only to services that need itdocker history --no-trunc IMAGEدستورهای ثبتشده در image history را بررسی کنinspect recorded build instructions in image historygit 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.