SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-07

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 được gọi là env file

Docker Compose có ba cơ chế riêng biệt với tên gọi khá giống nhau. File .env điền các placeholder ${VARIABLE} bên trong chính compose.yaml trước khi Compose phân tích file đó. Thuộc tính env_file: nạp một file chứa 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 khai báo trong compose file. Chúng không thể thay thế cho nhau. Khi hai cơ chế cùng đặt một key, giá trị cuối cùng được quyết định 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 điểm quan trọng hơn: bất kỳ ai có thể chạy docker inspect đều đọc được environment variables, vì vậy không nên đặt password vào đó. Nếu bạn mới làm quen với compose file nói chung, hãy bắt đầu với Những kiến thức cơ bản về Docker Compose trên VPS, rồi quay lại đây để tìm hiểu về cấu hình.

Tệp .env dùng cho file compose, không phải 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' > .env
services:
  demo:
    image: alpine:${ALPINE_TAG}
    command: printenv ALPINE_TAG

Bây giờ yêu cầu Compose hiển thị chính xác nội dung mà nó đã phân tích.

docker compose config

Kết quả hiển thị image: alpine:3.20. Placeholder đã biến mất vì quá trình nội suy diễn ra tại thời điểm phân tích. Compose tìm .env trong thư mục dự án, tức thư mục chứa file compose, rồi thay thế mọi ${NAME} mà nó tìm thấy.

Sau đó chạy service.

docker compose run --rm demo

printenv 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 file compose, không cấu hình 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 của file compose tham chiếu đến file đó.

${NAME:-default} cung cấp giá trị dự phòng khi biến chưa được đặt hoặc có giá trị rỗng. ${NAME:?message} khiến Compose từ chối khởi động và in thông báo của bạn. Đây là lựa chọn phù hợp với giá trị không có giá trị mặc định an toàn.

env_file nạp biến vào container

Thuộc tính env_file: chỉ định một hoặc nhiều file. Nội dung của các file này sẽ trở thành biến môi trường của container.

printf 'GREETING=from_env_file\nAPP_MODE=production\n' > app.env
services:
  demo:
    image: alpine:3.20
    command: printenv GREETING
    env_file:
      - ./app.env
docker compose run --rm demo

Lệnh này in ra from_env_file. Định dạng file là các dòng KEY=value thuần túy, mỗi dòng một biến, trong đó # ở đầu dòng để bắt đầu comment. Đây không phải shell. Trong hầu hết trường hợp, dấu ngoặc kép được giữ lại như một phần của giá trị và không cần tiền tố export. Không đặt khoảng trắng quanh dấu =, vì KEY = value sẽ tạo một biến có tên đúng là KEY và giá trị có khoảng trắng ở đầu.

Đường dẫn env_file không tồn tại là lỗi và Compose sẽ dừng. Đánh dấu file là tùy chọn nếu file có thể hợp lệ khi không tồn tại:

    env_file:
      - path: ./app.env
        required: false

Thiết lập biến trong dòng lệnh

services:
  demo:
    image: alpine:3.20
    command: printenv GREETING
    environment:
      GREETING: from_environment

Có 2 cú pháp được chấp nhận: dạng ánh xạ ở trên và dạng danh sách dùng - GREETING=from_environment. Hai dạng này hoạt động giống hệt nhau. Dạng danh sách có thêm một tính năng: một key không có giá trị sẽ truyền biến đó từ shell nơi bạn chạy docker compose.

    environment:
      - GREETING
GREETING=from_my_shell docker compose run --rm demo

Lệnh này 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 cảnh báo. Cần lưu ý lỗi truyền biến âm thầm này, vì service khởi động với biến mật khẩu rỗng thường vẫn khởi động thành công nhưng 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_environment
docker compose run --rm demo
docker compose run --rm -e GREETING=from_cli demo printenv GREETING

Lệ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 trong cấu hình của container trên đĩa và mọi người dùng thuộc nhóm 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}}'

Đầu ra chứa "DB_PASSWORD=hunter2" ở dạng văn bản thuần túy. Ba đường dẫn 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 diễn đàn hỗ trợ. Mọi process bên trong container đều có thể đọc /proc/1/environ, và mọi process con đều kế thừa biến này. Ngoài ra, trình xử lý crash của ứng dụng thường ghi toàn bộ environment vào log hoặc báo cáo lỗi.

Thành viên của nhóm docker về cơ bản có quyền root trên host, nên đâ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 người dùng với quyền tối thiểu trên VPS giải thích vì sao nên hạn chế nhóm 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.txt

Secret đượ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, mysqlmariadb. 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_PASSWORD

Lệ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.txt

Mứ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.env

install -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.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 config

Compose đọ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 chỉ thay thế các placeholder ${NAME} trong compose file. Nó không bao giờ thiết lập biến bên trong container. Để đưa giá trị vào container, hãy tham chiếu đến nó bằng environment: { KEY: "${NAME}" }, hoặc dùng env_file: ./that-file.env thay thế.

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 cách nào để xem giá trị cuối cùng Compose sẽ dùng?

Chạy docker compose config để in compose file đã được phân giải hoàn toàn, với toàn bộ phép nội suy đã áp dụng. 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ựa trên 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 hiển thị, không phải mã hóa: giá trị không xuất hiện trong environment của container, trong output của docker inspect hoặc 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?

Hãy dùng KEY=value with spaces và bỏ dấu ngoặc kép. Compose xem toàn bộ phần còn lại của dòng là giá trị, nên dấu ngoặc kép thường trở thành ký tự literal trong giá trị. Không bao giờ đặt khoảng trắng quanh =, vì khi đó key sẽ có thêm một khoảng trắng ở cuối và không có giá trị nào khớp với nó.