Docker Compose: .env, env_file và environment khác gì?
Phân biệt .env, env_file và environment trong Docker Compose, biết thứ tự ưu tiên khi trùng key và vì sao password không nên đặt trong environment của container.
Ba thứ thường bị gọi là file env
Docker Compose có 3 cơ chế riêng biệt với tên gọi gần giống nhau. File .env thay thế các placeholder ${VARIABLE} bên trong chính compose.yaml, sau khi Compose đọc YAML và trước khi kiểm tra hoặc chạy bất kỳ thứ gì. Thuộc tính env_file: nạp một file gồm các cặp key/value vào environment của container. Thuộc tính environment: đặt trực tiếp các biến trên container và được viết trong compose file. Chúng không thể thay thế cho nhau. Khi 2 cơ chế cùng đặt một key, giá trị được chọn theo thứ tự ưu tiên đã được tài liệu hóa.
Hướng dẫn này trình bày cách dùng từng cơ chế, chứng minh thứ tự ưu tiên bằng một command bạn có thể chạy, sau đó đề cập đến phần quan trọng hơn: bất kỳ ai có thể chạy docker inspect đều có thể đọc environment variables. Vì vậy, không đặt password trong các biến này. Nếu bạn chưa quen với compose file, hãy bắt đầu bằng Những điều cơ bản về Docker Compose trên VPS, rồi quay lại đây để tìm hiểu về configuration.
Tệp .env dùng cho compose file, không phải cho container
Tạo một thư mục rồi đặt 2 file vào đó.
mkdir -p ~/envdemo && cd ~/envdemo
printf 'ALPINE_TAG=3.20\n' > .envservices:
demo:
image: alpine:${ALPINE_TAG}
command: printenv ALPINE_TAGBây giờ yêu cầu Compose hiển thị nội dung đã parse.
docker compose configOutput hiển thị image: alpine:3.20. Placeholder đã biến mất vì quá trình interpolation diễn ra khi Compose load file. Compose tìm .env trong project directory: thư mục được chỉ định bằng --project-directory nếu bạn thiết lập, nếu không thì dùng thư mục của file đầu tiên được truyền bằng -f, và nếu vẫn không có thì dùng thư mục hiện tại. Compose thay thế các placeholder ${NAME} trong những giá trị YAML mà nó tìm thấy. Key không bao giờ được interpolation, còn $$ lặp đôi tạo ra một ký tự dollar literal thay vì substitution.
Sau đó chạy service.
docker compose run --rm demoprintenv ALPINE_TAG thoát với status 1 và không in gì. Biến này không tồn tại bên trong container. Đây là hiểu nhầm phổ biến nhất: .env đã cấu hình compose file, không phải process. Một file .env chứa POSTGRES_PASSWORD=hunter2 hoàn toàn không có tác dụng với database của bạn, trừ khi một phần nào đó trong compose file tham chiếu đến file này.
${NAME:-default} cung cấp giá trị fallback khi biến chưa được set hoặc có giá trị rỗng. ${NAME:?message} khiến Compose từ chối khởi động và in message của bạn. Đây là lựa chọn phù hợp với giá trị không có default an toàn.
env_file tải các biến vào container
Thuộc tính env_file: chỉ định một hoặc nhiều tệp mà nội dung của chúng sẽ trở thành các biến môi trường của container.
printf 'GREETING=from_env_file\nAPP_MODE=production\n' > app.envservices:
demo:
image: alpine:3.20
command: printenv GREETING
env_file:
- ./app.envdocker compose run --rm demoLệnh này in ra from_env_file. Định dạng tệp là các dòng KEY=value thuần túy, mỗi dòng một biến, với # bắt đầu một chú thích. Đây không phải là shell, mặc dù Compose phân tích cú pháp tệp này với một vài quy tắc giống shell. Các dấu ngoặc bao quanh sẽ bị loại bỏ, vì vậy KEY="value" và KEY=value cho kết quả như nhau. Các giá trị trong dấu ngoặc kép và không có dấu ngoặc sẽ được nội suy các placeholder ${...}, trong khi các giá trị trong dấu ngoặc đơn được sử dụng theo nghĩa đen. Tiền tố export được chấp nhận nhưng không bắt buộc. Các khoảng trắng xung quanh dấu = sẽ bị cắt bỏ. Điểm cần lưu ý là chú thích nội dòng: trong một giá trị không có dấu ngoặc, một khoảng trắng theo sau bởi # sẽ bắt đầu một chú thích, vì vậy hãy đặt trong dấu ngoặc bất kỳ giá trị nào chứa #.
Đường dẫn env_file bị thiếu là một lỗi và Compose sẽ dừng lại. Hãy đánh dấu là tùy chọn nếu tệp có thể vắng mặt một cách hợp lệ:
env_file:
- path: ./app.env
required: falseenvironment thiết lập biến inline
services:
demo:
image: alpine:3.20
command: printenv GREETING
environment:
GREETING: from_environmentCó 2 cú pháp được chấp nhận: dạng mapping ở trên và dạng list dùng - GREETING=from_environment. Hai dạng này hoạt động giống hệt nhau. Cả hai còn có một cách dùng đặc biệt: key không có value sẽ truyền nguyên biến từ shell nơi bạn chạy docker compose. Ở dạng list, đó là một - GREETING đứng riêng. Ở dạng mapping, đó là GREETING: không có gì sau dấu hai chấm.
environment:
- GREETINGGREETING=from_my_shell docker compose run --rm demoLệnh đó in ra from_my_shell. Nếu chạy lệnh mà không thiết lập GREETING trong shell, Compose sẽ không thiết lập gì và cũng không đưa ra cảnh báo. Cần lưu ý lỗi truyền biến âm thầm này, vì một service khởi động với biến password rỗng thường vẫn chạy thành công và hoàn toàn không được bảo vệ.
Thứ tự nào được ưu tiên
Docker ghi rõ thứ tự ưu tiên, từ cao xuống thấp: docker compose run -e trên command line, tiếp đến environment hoặc env_file có giá trị được nội suy từ shell hoặc từ env file, sau đó là environment thông thường trong compose file, rồi env_file, cuối cùng là directive ENV được tích hợp sẵn trong image.
Tóm tắt cho công việc hằng ngày: environment: được ưu tiên hơn env_file:, còn -e trên command line được ưu tiên hơn cả hai. Hãy kiểm tra bằng một file.
services:
demo:
image: alpine:3.20
command: printenv GREETING
env_file:
- ./app.env
environment:
GREETING: from_environmentdocker compose run --rm demo
docker compose run --rm -e GREETING=from_cli demo printenv GREETINGLệnh đầu tiên in ra from_environment, nên environment: đã ghi đè giá trị trong app.env. Lệnh thứ hai in ra from_cli. Không có nội dung nào trong compose file ghi đè được giá trị trên command line.
Khi container hoạt động như thể cấu hình của bạn chưa được áp dụng, đừng đoán. docker compose config in ra file đã được phân giải đầy đủ, còn docker compose config --environment in ra các biến nội suy mà Compose đang sử dụng. Phần lớn các báo cáo kiểu “env file của tôi bị bỏ qua” xảy ra vì một giá trị được đặt hai lần ở hai cấp khác nhau.
Vì sao biến môi trường bị lộ
Đặt mật khẩu trong environment: thì mật khẩu được lưu vào file cấu hình của container trên disk và mọi user thuộc group docker đều có thể xem.
docker compose run -d --name leaky -e DB_PASSWORD=hunter2 demo sleep 300
docker inspect leaky --format '{{json .Config.Env}}'Output chứa "DB_PASSWORD=hunter2" ở dạng plain text. Ba path khác cũng làm lộ cùng giá trị này. docker compose config in giá trị đó ra terminal, khiến nó có thể bị dán vào một support forum. Theo mặc định, process con kế thừa biến này, trừ khi process cha cố ý khởi chạy chúng với một environment thay thế. Process chạy cùng user với PID 1 hoặc có CAP_SYS_PTRACE cũng có thể đọc biến này từ /proc/1/environ. Trong container mà mọi thứ chạy dưới root, đó là mọi process. Nếu application chạy dưới một user không có đặc quyền còn PID 1 chạy dưới root, application sẽ nhận lỗi permission khi đọc file đó, nhưng biến này vẫn nằm trong environment riêng của nó. Ngoài ra, crash handler của application thường ghi toàn bộ environment vào log hoặc error report.
Việc là thành viên của group docker về cơ bản tương đương có quyền root trên host. Vì vậy, đây không phải là ranh giới đặc quyền mà bạn có thể dựa vào. Hướng dẫn tài khoản user có đặc quyền tối thiểu trên VPS giải thích vì sao nên hạn chế group này trên mọi máy chủ dùng chung.
Compose secrets lưu giá trị trong file
Compose hỗ trợ secrets dựa trên file. Giá trị được mount vào container dưới dạng file thay vì được inject vào environment.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtSecret được mount tại /run/secrets/db_password bên trong container. Tên sau dấu gạch chéo là tên secret trong block secrets: cấp cao nhất.
Hậu tố _FILE là quy ước được Docker Official Images sử dụng, bao gồm postgres, mysql và mariadb. Các entrypoint script đó kiểm tra VARNAME_FILE, đọc file rồi sử dụng nội dung của file. Đây không phải tính năng của Docker, nên chỉ hoạt động khi image có triển khai quy ước này. Kiểm tra tài liệu của image trước khi giả định rằng SOMETHING_FILE sẽ được xử lý. Những ứng dụng không hỗ trợ quy ước này thường vẫn có thể tự đọc file khi khởi động, hoặc bạn có thể truyền đường dẫn và để entrypoint của mình xử lý.
Xác minh từ bên trong container đang chạy:
docker compose exec db cat /run/secrets/db_password
docker compose exec db printenv POSTGRES_PASSWORDLệnh đầu tiên in ra password. Lệnh thứ hai không in gì, vì giá trị chưa bao giờ được đưa vào environment. Đó là mục đích chính: docker inspect trên container này chỉ hiển thị đường dẫn không nhạy cảm.
Bảo vệ file nguồn trên host, vì secret chỉ riêng tư ở mức độ file chứa nó được bảo vệ:
chmod 600 db_password.txtMức trung gian thực tế trên VPS
Nhiều image tự host không hỗ trợ biến _FILE, nên biến môi trường là cách duy nhất để truyền giá trị vào. Với một VPS chỉ có một quản trị viên, mục tiêu thực tế là không để các giá trị này nằm trong một file mà mọi người dùng đều có thể đọc trong thư mục project, đồng thời không đưa chúng vào git.
sudo install -o root -g root -m 600 /dev/null /etc/myapp/app.env
sudo nano /etc/myapp/app.env env_file:
- /etc/myapp/app.envinstall -m 600 tạo file với mode đã được đặt sẵn, nên không có khoảng thời gian nào file có thể bị mọi người đọc. Root sở hữu file, vì vậy user không phải root trên máy không thể đọc file này, dù bất kỳ ai chạy được docker vẫn có thể đọc giá trị từ container. Thêm *.env và .env vào .gitignore, rồi commit một app.env.example chỉ chứa tên các key với giá trị trống. Password đã commit thì phải được rotate.
Rotate một giá trị nghĩa là restart service. Biến môi trường chỉ được đọc một lần khi process của container khởi động, nên sửa file không có tác dụng cho đến khi chạy docker compose up -d --force-recreate db. Đây cũng là pattern được dùng trong hướng dẫn chạy n8n phía sau HTTPS trên VPS, trong đó encryption key nằm bên ngoài compose file.
Tách cấu hình theo từng môi trường
Compose mặc định đọc .env từ thư mục project. Dùng --env-file để chỉ định một vị trí khác.
docker compose --env-file .env.staging configCompose đọc nhiều file theo thứ tự. Các file đọc sau sẽ ghi đè các file đọc trước. Đặt các giá trị mặc định không chứa secret trong một file được commit. Đặt secret trong một file không bao giờ rời khỏi server. Quy tắc tương tự áp dụng cho env_file:: nếu một key bị trùng, giá trị trong file được liệt kê cuối cùng sẽ được dùng.
FAQ
Vì sao file .env của tôi bị bỏ qua bên trong container?
File này không bị bỏ qua. File .env thay thế các placeholder ${NAME} trong compose file, đồng thời có thể đặt các biến riêng của Compose như COMPOSE_PROJECT_NAME hoặc COMPOSE_FILE. File này không bao giờ đặt biến bên trong container. Để đưa giá trị vào container, hãy tham chiếu đến biến đó: environment: { KEY: "${NAME}" }, hoặc dùng env_file: ./that-file.env.
environment có ghi đè env_file không, hay ngược lại?
environment: được ưu tiên. Thứ tự được Docker ghi nhận đặt thuộc tính environment cao hơn thuộc tính env_file, và cả hai đều thấp hơn docker compose run -e trên command line. Nếu một key được đặt ở cả hai nơi, giá trị trong env_file sẽ bị bỏ qua mà không có thông báo.
Làm thế nào để xem giá trị cuối cùng mà Compose sẽ sử dụng?
Chạy docker compose config để in compose file đã được resolve hoàn chỉnh, sau khi áp dụng toàn bộ interpolation. Với container đang chạy, docker inspect <container> --format '{{json .Config.Env}}' hiển thị chính xác những gì process của container đã nhận.
Compose secrets có được mã hóa không?
Không. Secret dạng file được mount vào container dưới dạng file plain text tại /run/secrets/<name>, còn file nguồn nằm trên disk của host và không được mã hóa. Lợi ích nằm ở phạm vi truy cập, không phải ở mã hóa: giá trị không xuất hiện trong environment của container, không xuất hiện trong output của docker inspect và không xuất hiện trong crash dump có in environment.
Tôi có thể dùng dấu ngoặc kép và khoảng trắng trong env file không?
Có. Compose loại bỏ dấu ngoặc kép bao quanh, nên KEY="value with spaces" và KEY=value with spaces đều cho kết quả là value with spaces. Dùng dấu ngoặc đơn khi giá trị chứa $ và không được phép interpolation, đồng thời đặt trong ngoặc kép mọi giá trị chứa # vì khi không có ngoặc kép, ký tự đó bắt đầu một comment. Khoảng trắng xung quanh = sẽ được loại bỏ, nên KEY = value vẫn đặt KEY.