SSD Nodes Learn 8GB RAM — $66/năm
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-01

Phân biệt .env, env_file và environment trong Docker

Bạn đang nhầm lẫn giữa .env, env_file và environment? Bài viết này giải thích thứ tự ưu tiên của chúng và cách quản lý secrets an toàn thay vì để lộ trong biến môi trường.

Ba khái niệm mà mọi người gọi là file env

Docker Compose có ba cơ chế riêng biệt với những cái tên dễ gây nhầm lẫn. File .env điền vào 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: tải một file chứa các cặp khóa/giá trị vào môi trường của container. Thuộc tính environment: thiết lập trực tiếp các biến cho container, được viết ngay trong file compose. Chúng không thể thay thế cho nhau, và khi hai trong số đó cùng thiết lập một khóa, bên thắng cuộc được xác định bởi thứ tự ưu tiên đã được tài liệu hóa.

Hướng dẫn này trình bày cách hoạt động của từng loại, chứng minh thứ tự ưu tiên bằng một lệnh bạn có thể chạy, sau đó đề cập đến phần quan trọng hơn: các biến môi trường có thể đọc được bởi bất kỳ ai có quyền chạy docker inspect, vì vậy mật khẩu không nên nằm trong đó. Nếu bạn mới làm quen với các file compose nói chung, hãy bắt đầu với Các kiến thức cơ bản về Docker Compose trên VPS rồi quay lại đây để cấu hình.

Tệp .env dành cho tệp compose, không phải cho container

Tạo một thư mục và đặt hai tệp 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ờ hãy yêu cầu Compose cho biết những gì nó thực sự đã phân tích cú pháp.

docker compose config

Đầu ra hiển thị image: alpine:3.20. Trình giữ chỗ đã biến mất vì quá trình nội suy (interpolation) đã xảy ra tại thời điểm phân tích cú pháp. Compose tìm kiếm .env trong thư mục dự án, đây là thư mục chứa tệp compose, và 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 trạng thái 1 và không in ra gì cả. Biến này không tồn tại bên trong container. Đó là hiểu lầm phổ biến nhất: .env cấu hình tệp compose, không phải tiến trình. Một tệp .env chứa POSTGRES_PASSWORD=hunter2 hoàn toàn không có tác dụng gì đối với cơ sở dữ liệu của bạn trừ khi một phần nào đó của tệp compose tham chiếu đến nó.

${NAME:-default} cung cấp giá trị dự phòng khi biến không được thiết lập hoặc trố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 đúng đắn cho một giá trị không có mặc định 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.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 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. Dấu ngoặc kép được giữ nguyên như một phần của giá trị trong hầu hết các trường hợp và các tiền tố export là không cần thiết. Đừng đặt khoảng trắng xung quanh dấu =, vì KEY = value sẽ tạo ra một biến có tên chính xác là KEY với một khoảng trắng ở đầu giá trị.

Đườ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ể không tồn tại một cách hợp lệ:

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

thiết lập biến môi trường inline

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

Có hai cú pháp được chấp nhận, dạng mapping ở trên và dạng danh sách sử dụng - GREETING=from_environment. Chúng hoạt động giống hệt nhau. Dạng danh sách có thêm một thủ thuật: một key trống 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 đó in ra from_my_shell. Nếu chạy mà không thiết lập GREETING trong shell, Compose sẽ không thiết lập gì cả và không đưa ra cảnh báo. Bạn cần lưu ý về việc truyền biến thất bại một cách âm thầm, vì một service khởi chạy với biến mật khẩu trống thường vẫn khởi động thành công nhưng lại để hở hoàn toàn.

Cái nào được ưu tiên

Docker ghi lại thứ tự ưu tiên, cao nhất trước: docker compose run -e trên dòng lệnh, sau đó là environment hoặc env_file với giá trị được nội suy từ shell của bạn hoặc từ một file env, tiếp đến là environment thuần túy trong file compose, sau đó là env_file, và cuối cùng là chỉ thị ENV được tích hợp sẵn trong image.

Tóm tắt ngắn gọn cho công việc hàng ngày: environment: thắng env_file:, và -e trên dòng lệnh thắng cả hai. Hãy kiểm chứng trong 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, vì vậy environment: đã ghi đè giá trị trong app.env. Lệnh thứ hai in ra from_cli. Không có gì trong file compose có thể ghi đè dòng lệnh.

Khi một container hoạt động như thể cấu hình của bạn không được áp dụng, đừng đoán mò. docker compose config in ra file đã được phân giải hoàn toàn, và docker compose config --environment in ra các biến nội suy mà Compose đang sử dụng. Hầu hết các báo cáo kiểu "file env của tôi bị bỏ qua" đều do một giá trị được thiết lập hai lần ở hai cấp độ khác nhau.

Tại sao các biến môi trường bị rò rỉ

Thiết lập mật khẩu trong environment: và nó sẽ được lưu trữ trong cấu hình của container trên đĩa, người dùng bất kỳ thuộc nhóm docker đều có thể nhìn thấy.

docker compose run -d --name leaky -e DB_PASSWORD=hunter2 demo sleep 300
docker inspect leaky --format '{{json .Config.Env}}'

Kết quả đầu ra chứa "DB_PASSWORD=hunter2" dưới dạng văn bản thuần. Ba đường dẫn khác cũng làm lộ giá trị tương tự. docker compose config in giá trị này ra terminal, đây là cách nó bị dán vào các diễn đàn hỗ trợ. Mọi tiến trình bên trong container đều có thể đọc /proc/1/environ và mọi tiến trình con đều kế thừa biến này. Ngoài ra, các trình xử lý lỗi ứng dụng thường xuyên kết xuất toàn bộ môi trường vào log hoặc báo cáo lỗi.

Việc là thành viên của nhóm docker tương đương với 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 về các tài khoản người dùng có đặc quyền tối thiểu trên VPS giải thích lý do tại sao cần hạn chế nhóm đó trên bất kỳ máy chủ dùng chung nào.

Compose secrets lưu giá trị trong một tệp

Compose hỗ trợ các secret dựa trên tệp. Giá trị được mount vào container dưới dạng một tệp thay vì được đưa vào môi trường (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 từ khối secrets: ở cấp cao nhất.

Hậu tố _FILE là một quy ước được sử dụng bởi các Docker Official Images, bao gồm postgres, mysqlmariadb. Các tập lệnh entrypoint đó kiểm tra VARNAME_FILE, đọc tệp và sử dụng nội dung của nó. Đây không phải là một tính năng của Docker, vì vậy nó chỉ hoạt động khi image triển khai tính năng này. Hãy kiểm tra tài liệu của image trước khi giả định rằng SOMETHING_FILE sẽ được hỗ trợ. Các ứng dụng không hỗ trợ tính năng này thường có thể tự đọc tệp khi khởi động, hoặc bạn có thể truyền đường dẫn và để entrypoint của riêng bạn thực hiện việc đó.

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 mật khẩu. Lệnh thứ hai không in ra gì cả, vì giá trị không bao giờ được đưa vào môi trường. Đó chính là mục đích: docker inspect trên container này chỉ hiển thị đường dẫn vô hại.

Hãy bảo vệ tệp nguồn trên host, vì tính bảo mật của secret chỉ tương đương với tệp chứa nó:

chmod 600 db_password.txt

Giải pháp trung gian thực tế trên VPS

Nhiều image tự host không hỗ trợ các biến _FILE, vì vậy biến môi trường là cách duy nhất để cấu hình. Trên một VPS chỉ có một quản trị viên, mục tiêu thực tế là ngăn các giá trị này nằm trong một tệp mà mọi người đều có thể đọc được trong thư mục dự án, và giữ chúng không bị đưa 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 tệp với quyền truy cập được thiết lập sẵn, vì vậy không có khoảng thời gian nào mà tệp đó có thể bị người khác đọc. root sở hữu tệp này, nên người dùng không phải root trên máy không thể đọc được, mặc dù bất kỳ ai có thể chạy docker vẫn có thể đọc giá trị từ bên trong container. Hãy thêm *.env.env vào .gitignore và commit một app.env.example chứa tên các khóa với giá trị trống thay thế. Một mật khẩu đã bị commit là một mật khẩu cần phải xoay vòng.

Xoay vòng một giá trị đồng nghĩa với việc khởi động lại dịch vụ. Các biến môi trường chỉ được đọc một lần khi tiến trình container bắt đầu, vì vậy việc chỉnh sửa tệp sẽ không có tác dụng cho đến khi bạn chạy docker compose up -d --force-recreate db. Đây là mô hình tương tự được sử dụng trong hướng dẫn n8n đằng sau HTTPS trên VPS, nơi khóa mã hóa nằm bên ngoài tệp compose.

Tách biệt cấu hình theo môi trường

Theo mặc định, Compose đọc .env từ thư mục dự án. Hãy trỏ nó đến một vị trí khác bằng --env-file.

docker compose --env-file .env.staging config

Nhiều tệp được đọc theo thứ tự, và các tệp phía sau sẽ ghi đè lên các tệp phía trước. Hãy giữ các thiết lập mặc định không chứa thông tin nhạy cảm trong một tệp được commit, còn các thông tin bí mật thì để trong một tệp không bao giờ rời khỏi server. Điều tương tự cũng áp dụng cho env_file:, trong đó tệp được liệt kê cuối cùng sẽ có quyền ưu tiên nếu có khóa trùng lặp.

FAQ

Tại sao tệp .env của tôi bị bỏ qua bên trong container?

Nó không bị bỏ qua. Tệp .env chỉ thay thế các placeholder ${NAME} trong tệp compose. Nó không bao giờ thiết lập các biến bên trong container. Để đưa giá trị vào container, hãy tham chiếu đến nó: environment: { KEY: "${NAME}" }, hoặc sử dụng env_file: ./that-file.env thay thế.

environment ghi đè env_file, hay ngược lại?

environment: có quyền ưu tiên cao hơn. Thứ tự được tài liệu hóa của Docker đặ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 dòng lệnh. Nếu một key được thiết lập ở 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 để tôi xem giá trị cuối cùng mà Compose sẽ sử dụng?

Chạy docker compose config để in tệp compose đã được phân giải hoàn toàn với tất cả các nội suy được áp dụng. Đối với một container đang chạy, docker inspect <container> --format '{{json .Config.Env}}' hiển thị chính xác những gì tiến trình của nó đã nhận được.

Các secret của Compose có được mã hóa không?

Không. Một secret dựa trên tệp được mount vào container dưới dạng tệp văn bản thuần tại /run/secrets/<name>, và tệp nguồn nằm trên đĩa của host mà không được mã hóa. Lợi ích ở đây là phạm vi, không phải mã hóa: giá trị nằm ngoài môi trường container, ngoài đầu ra của docker inspect, và ngoài các bản ghi crash dump vốn thường in ra môi trường.

Tôi có thể sử dụng dấu ngoặc kép và khoảng trắng trong tệp env không?

Sử dụng KEY=value with spaces và bỏ dấu ngoặc kép. Compose coi toàn bộ phần còn lại của dòng là giá trị, vì vậy dấu ngoặc kép thường trở thành các ký tự theo nghĩa đen trong giá trị. Không bao giờ đặt khoảng trắng xung quanh =, vì khi đó key sẽ mang theo một khoảng trắng ở cuối và không có gì khớp với nó.