Phần 51/60 của sê-ri Docker và Docker Compose. Phần 49 dựng file
docker-compose.ymlđầu tiên với hai dịch vụ; bài này ở lại đúng file đó nhưng đào sâu một chỗ rất hay bị hiểu nhầm — nơi biến môi trường thật sự đến từ đâu khi có tới bốn chỗ cùng khai một cái tên.
Giới thiệu
DATABASE_URL xuất hiện trong Dockerfile bằng ENV, trong env_file: .env.prod, và cả trong environment: của service — ba giá trị khác nhau, container chạy giá trị nào? Câu hỏi gặp thường xuyên khi project lớn dần: ai đó thêm .env cho tiện dev, ai đó khác thêm env_file: cho staging, Dockerfile vẫn giữ giá trị mặc định từ ngày đầu. Không đo thật thì chỉ đoán.
Bài này dựng đúng bốn lớp đó, cho chúng đè lên nhau từng cặp, đọc kết quả bằng env chạy thật bên trong container — không suy luận từ tài liệu.
Bốn chỗ có thể khai cùng một biến
ENVtrong Dockerfile — giá trị mặc định đóng cứng vào image, thấp nhất trong chuỗi ghi đè.env_file:trong service — nạp một fileKEY=valuelàm biến môi trường của container.environment:trong service — khai trực tiếp trongdocker-compose.yml, có thể là giá trị tĩnh hoặc tham chiếu${TEN_BIEN}..envở cùng thư mục vớidocker-compose.yml— không phải biến môi trường của container. Nó chỉ là bảng thế chỗ cho cú pháp${...}ngay trong chính filedocker-compose.yml, hoàn toàn khác nhóm ba cái trên.
Bốn cái nghe giống nhau ("biến môi trường") nhưng hai nhóm đầu và cuối phục vụ hai việc khác hẳn nhau. Nhầm .env là "cứ khai là vào container" là nguồn gốc phần lớn câu hỏi "sao tôi đặt biến rồi mà os.environ vẫn không thấy?".
Thiết lập đo
Môi trường: Docker Compose v5.4.0, Engine 29.7.2. Image dùng chung cho mọi phép đo:
FROM alpine:3.20
ENV FOO=from-dockerfile
ENV ONLY_DOCKERFILE=only-dockerfile-value
CMD ["env"]
Mọi lần chạy đều đọc kết quả bằng docker compose run --rm app env rồi lọc đúng biến đang xét.
environment: đè env_file: khi trùng tên
services:
app:
image: env-test:local
env_file:
- envfile.env # FOO=from-envfile
environment:
FOO: from-environment
FOO=from-environment
ONLY_ENVFILE=only-envfile-value
FOO thắng về tay environment:, còn ONLY_ENVFILE (biến không bị đụng tới) vẫn lọt vào bình thường. environment: luôn đứng trên env_file: khi cả hai cùng khai một tên trong cùng một service — không phụ thuộc thứ tự viết trước sau trong file YAML.
env_file: đè ENV trong Dockerfile
services:
app:
image: env-test:local # ENV FOO=from-dockerfile
env_file:
- envfile.env # FOO=from-envfile
FOO=from-envfile
ONLY_ENVFILE=only-envfile-value
ONLY_DOCKERFILE=only-dockerfile-value
FOO đổi thành giá trị từ env_file:, còn ONLY_DOCKERFILE — biến chỉ có trong Dockerfile, không bị env_file: đụng tới — vẫn giữ nguyên. Kết luận: ENV trong Dockerfile là giá trị mặc định, mọi lớp bên ngoài đều ghi đè được nó, kể cả lớp thấp nhất trong ba lớp còn lại.
Nhiều file trong env_file:: file sau đè file trước
services:
app:
image: env-test:local
env_file:
- envfile.env # FOO=from-envfile
- envfile2.env # FOO=from-envfile2
FOO=from-envfile2
Danh sách env_file: được đọc từ trên xuống, file đọc sau đè file đọc trước — giống hệt cách environment: đè env_file:. Hữu ích khi tách file base (common.env) và file riêng môi trường (prod.env), miễn nhớ đặt file cụ thể hơn ở dưới.
.env: không phải biến môi trường, chỉ là bảng thế chỗ
Đây là chỗ hay bị hiểu nhầm nhất. Tạo .env ở thư mục gốc:
SUB_VAR=from-dotenv
NOT_REFERENCED=should-not-appear-in-container
Và một service không hề tham chiếu ${SUB_VAR} hay ${NOT_REFERENCED} ở đâu cả:
services:
app:
image: env-test:local
FOO=from-dockerfile
SUB_VAR và NOT_REFERENCED không xuất hiện trong container, dù cả hai đều nằm trong .env. Đây không phải lỗi — .env chỉ nạp vào bộ nhớ của lệnh docker compose để thế chỗ cho ${...} viết trong chính docker-compose.yml, và không có ${...} nào tham chiếu tới chúng thì chúng chỉ nằm im trong file, không đi đâu cả.
Giờ tham chiếu tường minh bằng environment: SUB_VAR: ${SUB_VAR}:
SUB_VAR=from-dotenv
Biến xuất hiện — nhưng đi qua ngả environment:, không phải tự động từ .env. Và khi biến cùng tên được export ở shell:
export SUB_VAR=from-shell
docker compose run --rm app env | grep SUB_VAR
SUB_VAR=from-shell
Shell thắng .env khi cả hai cùng cấp cho phép thế chỗ ${SUB_VAR} — đúng như tài liệu Compose mô tả, và ở đây đo lại y hệt trên bản v5.4.0.
Truyền thẳng biến shell bằng environment: - TEN (không giá trị)
Cách viết environment: dạng danh sách, chỉ ghi tên biến không kèm =giá-trị, kéo nguyên giá trị từ shell đang chạy docker compose:
services:
app:
image: env-test:local
environment:
- FOO
export FOO=truyen-tu-shell-khong-gia-tri
docker compose run --rm app env | grep FOO
FOO=truyen-tu-shell-khong-gia-tri
Cách này khác ${FOO} ở chỗ không cần dấu ngoặc nhọn, nhưng bản chất vẫn là lấy từ shell — nếu FOO chưa export thì biến này rỗng trong container chứ không lỗi, không cảnh báo gì.
Bảng tổng hợp thứ tự ưu tiên
| Ưu tiên | Lớp | Vào container khi nào |
|---|---|---|
| 1 (cao nhất) | environment: |
Luôn vào, ghi đè mọi lớp dưới nếu trùng tên |
| 2 | env_file: (file sau đè file trước) |
Vào nếu không bị environment: ghi đè |
| 3 (thấp nhất) | ENV trong Dockerfile |
Chỉ còn lại khi không lớp nào ở trên khai cùng tên |
| — | .env ở thư mục gốc |
Không tự vào — chỉ thế chỗ ${...} trong docker-compose.yml, và bản thân bị shell export đè khi thế chỗ |
Khi nào không nên dùng .env hay environment: cho bí mật
environment: viết thẳng giá trị vào docker-compose.yml — file này thường nằm trong Git, nên mật khẩu hay API key đặt ở đây coi như đã commit. .env đỡ hơn vì thường được .gitignore, nhưng vẫn là plaintext trên đĩa và docker compose config in thẳng ra màn hình khi debug. Với secret thật sự (khoá ký, mật khẩu production), dùng env_file: trỏ tới file bị .gitignore chặn, hoặc Docker secrets/trình quản lý bí mật riêng — không phải hai cách đo trong bài này.
Lỗi thường gặp
Nhầm .env với env_file:
Hai tên gần giống nhau, một là quy ước riêng của docker compose (không cần khai trong YAML, tự động đọc nếu nằm cạnh docker-compose.yml), một là khoá phải khai tường minh trong từng service. Đặt biến trong .env rồi ngạc nhiên sao container không thấy — chính là số đo ở mục "bảng thế chỗ" bên trên.
Số dạng phiên bản bị rụng số 0 nếu không quote
Đo được khi thử khai VERSION_UNQUOTED: 1.20 trong environment::
environment:
VERSION_UNQUOTED: 1.20
VERSION_QUOTED: "1.20"
VERSION_QUOTED=1.20
VERSION_UNQUOTED=1.2
1.20 không có dấu ngoặc bị YAML đọc thành số thực 1.2 — số 0 ở cuối không có ý nghĩa với một số thực nên bị rụng khi ghi lại thành chuỗi. Có ngoặc kép thì giữ nguyên. Biến môi trường luôn là chuỗi, nhưng giá trị trong YAML thì không — bất cứ thứ gì trông giống số hoặc giống boolean (yes, no, true) nên quote nếu ý định là giữ nguyên văn bản, đặc biệt với số phiên bản hay hash commit rút gọn có thể toàn chữ số.
Thử ba mươi giây
Dán đoạn này vào một thư mục có sẵn docker-compose.yml bất kỳ, kiểm tra ngay biến nào thật sự đang chảy vào container đang chạy:
docker compose run --rm <ten-service> env | sort
So kết quả với những gì bạn nghĩ là đang khai trong .env, env_file: và environment: — chỗ nào lệch với dự đoán chính là chỗ đang có một lớp khác thắng thế mà chưa để ý.