---
id: docker-env
title: "Biến môi trường"
shortTitle: "Biến môi trường"
track: docker
trackTitle: "Docker"
order: 6
level: core
status: published
estimatedMinutes: 35
prerequisites:
  - docker-entrypoint
next: docker-compose
reviewedAgainst: 2026-10-19
sources:
  - title: "KodeKloud — Docker Training Course for the Absolute Beginner"
    url: https://learn.kodekloud.com/learn/courses/docker-training-course-for-the-absolute-beginner
  - title: "docker run environment"
    url: https://docs.docker.com/reference/cli/docker/container/run/#env
  - title: "Dockerfile ENV"
    url: https://docs.docker.com/reference/dockerfile/#env
  - title: "Compose variable interpolation"
    url: https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/
  - title: "Compose environment precedence"
    url: https://docs.docker.com/compose/how-tos/environment-variables/envvars-precedence/
---

# Biến môi trường

Màu, port, tên host database đổi theo môi trường. Sửa trong source rồi build lại image cho mỗi máy là việc thừa. App đọc biến môi trường. Việc còn lại là để file chứa giá trị ở trên host, không copy vào layer.

## Để làm gì

Tách giá trị cấu hình khỏi image. Biết file `.env` nằm ở đâu, lệnh nào đọc file đó, và chỗ nào làm giá trị lộ cho người pull image.

## Gồm những gì

- App đọc biến, có giá trị mặc định khi biến trống.
- `ENV` trong Dockerfile: mọi container của image kế thừa, `docker inspect` thấy.
- `docker run -e` và `docker run --env-file` ghi đè biến cùng tên đã có trong image.
- File `.env` trên host: `.gitignore`, `.dockerignore`. Không `COPY`.
- Compose: file `.env` cạnh `compose.yaml` thay `${TÊN}` trong yaml. Process trong container chỉ thấy biến khi có `env_file` hoặc `environment`.

Token chỉ cần trong một lệnh `RUN` lúc build là secret mount, bài BuildKit. Mật khẩu database trong lab Compose đọc từ Docker secret, không từ `ENV`.

## Dùng khi nào

Cùng một image, máy dev và máy CI khác port hoặc khác tên host. Giá trị đó không phải bí mật thì `ENV` trong image cũng được, ví dụ `NODE_ENV=production`.

API token, mật khẩu, connection string: file trên host hoặc secret của nền tảng, đưa vào lúc tạo container. Không ghi vào Dockerfile.

## Kiến thức cốt lõi

Ba chỗ giá trị có thể nằm:

<div class="osi">
<div class="osi-row" style="--layer:#dcfce7"><div class="osi-num">1</div><div class="osi-body"><div class="osi-name">File trên host</div><div class="osi-bits"><span class="osi-bit">.env</span><span class="osi-bit">--env-file</span><span class="osi-bit">env_file</span></div><p class="osi-note">Không vào layer. `.gitignore` và `.dockerignore`. Compose còn dùng file `.env` cạnh yaml để thay `${TÊN}`.</p></div></div>
<div class="osi-row" style="--layer:#dbeafe"><div class="osi-num">2</div><div class="osi-body"><div class="osi-name">ENV trong Dockerfile</div><div class="osi-bits"><span class="osi-bit">image config</span><span class="osi-bit">mọi container</span></div><p class="osi-note">`docker inspect` và `docker history` đọc được. Hợp với chế độ chạy, không hợp với mật khẩu.</p></div></div>
<div class="osi-row is-memo" style="--layer:#fee2e2"><div class="osi-num">3</div><div class="osi-body"><div class="osi-name">COPY .env</div><div class="osi-bits"><span class="osi-bit">layer</span></div><p class="osi-note">Người pull image lấy được file. Không phải chỗ giữ cấu hình riêng của từng máy.</p></div></div>
</div>

`docker run -e TÊN=giá` và `--env-file` ghi đè biến cùng tên mà `ENV` đã đặt trong image. File cho `--env-file` gồm dòng `TÊN=giá`. Dòng bắt đầu bằng `#` là chú thích. Dòng chỉ có tên biến, không có dấu `=`, lấy giá trị từ môi trường của client. Không viết `export` ở đầu dòng.

`docker inspect` của container có `Config.Env`: biến từ image cộng biến vừa truyền. Mẫu gọn:

```bash
docker inspect --format '{{json .Config.Env}}' CONTAINER
```

Trên PowerShell, ngoặc nhọn của `--format` dễ bị nuốt. Dùng dấu nháy đơn của bash trên WSL, hoặc bỏ `--format` và tìm khối `Env` trong JSON.

Compose đọc file `.env` đặt cạnh `compose.yaml` để thay `${TÊN}` trong file yaml. Bước đó xảy ra trước khi tạo container, và nó không tự nhét mọi dòng của file vào process. Muốn process thấy biến thì khai `env_file:` hoặc `environment:`. Một file có thể làm cả hai việc nếu yaml vừa có `${TÊN}` vừa có `env_file`.

Khi cùng một tên xuất hiện nhiều chỗ, Compose lấy giá trị theo thứ tự sau, trên cùng thắng: `docker compose run -e`, rồi giá trị đã nội suy từ shell hoặc từ file `.env` vào `environment` / `env_file`, rồi khóa `environment` viết thẳng, rồi `env_file`, rồi `ENV` trong image.

`ARG` chỉ có lúc build. Nó không thành biến của process lúc chạy, trừ khi một dòng `ENV` chép nó sang. Docs build secrets nói cả `ARG` lẫn `ENV` đều để lại dấu trong image, nên token không đi qua hai chỉ thị đó.

## Lệnh và code mẫu

App đọc biến. `server.js` của lab đã đọc `PORT`. Thêm một dòng cùng kiểu khi cần một chuỗi riêng, ví dụ `process.env.GREETING`.

File trên host, không commit, không đưa vào context:

```bash
cd "$HOME/devops-lab/docker/app"
printf 'GREETING=xin-chao\n' > .env
printf '\n.env\n' >> .gitignore
```

`.dockerignore` đã có `.env` từ bài Dockerfile. `COPY . .` bỏ qua file này.

Chạy một lần, không cần build, để thấy file đi vào process:

```bash
docker run --rm --env-file .env alpine:3.20 sh -c 'echo "$GREETING"'
docker run --rm -e GREETING=khac alpine:3.20 sh -c 'echo "$GREETING"'
```

Lệnh đầu in `xin-chao`. Lệnh sau in `khac`. Image alpine không có `GREETING`. Biến chỉ có trong container vừa tạo.

`ENV` cho giá trị mọi bản chạy đều giống nhau:

```dockerfile
ENV NODE_ENV=production
```

`docker run -e NODE_ENV=development img` ghi đè cho container đó. Image vẫn mang `production` cho lần chạy không có `-e`.

Compose, sau bài này, khi app cần biến mà không muốn ghi giá trị vào yaml:

```yaml
services:
  web:
    build: .
    env_file:
      - .env
    environment:
      DB_HOST: db
```

`DB_HOST` viết trong yaml vì nó là tên service, không phải bí mật. `GREETING` nằm trong `.env` và vào process qua `env_file`. Mật khẩu database của lab vẫn là file secret, như mẫu Compose, không thêm vào `.env` rồi `ENV`.

Xem biến của container đang chạy:

```bash
docker inspect --format '{{json .Config.Env}}' lab-node
```

## Thực hành

1. Trong `$HOME/devops-lab/docker/app`, tạo `.env` với `GREETING=xin-chao` và thêm `.env` vào `.gitignore` nếu dòng đó chưa có.
2. `docker run --rm --env-file .env alpine:3.20 sh -c 'echo "$GREETING"'`.
3. `docker run --rm -e GREETING=khac alpine:3.20 sh -c 'echo "$GREETING"'`.
4. `docker image history node:24-alpine` và tìm các dòng `ENV` của image gốc. Đó là biến đóng trong image, trước khi bạn truyền thêm lúc `run`.

## Kết quả mong đợi

- Bước 2 in `xin-chao`.
- Bước 3 in `khac`.
- History của image `node` có các dòng `ENV`. File `.env` của bạn không nằm trong history đó, vì chưa `COPY` và chưa `ENV GREETING=...`.

## Checklist

- [ ] App đọc biến môi trường cho giá trị đổi theo máy.
- [ ] `ENV` trong Dockerfile cho giá trị được phép nằm trong image.
- [ ] File `.env` ở trên host, có trong `.gitignore` và `.dockerignore`.
- [ ] `docker run --env-file` hoặc Compose `env_file` để process thấy file đó.
- [ ] File `.env` cạnh `compose.yaml` còn để thay `${}` trong yaml. Nó không tự vào container.
- [ ] `docker inspect` khi cần xem `Config.Env` của một container.

## Thẻ ôn

### docker-env-where
kind: distinguish
cue: `ENV` nằm trong image, mọi container kế thừa. `-e` và `--env-file` chỉ gắn vào container lúc `docker run`, và ghi đè cùng tên.

Q: Image có `ENV NODE_ENV=production`. Một lần chạy bạn cần `development`, những lần khác vẫn production. Ghi vào đâu để không phải build lại image?

A: `docker run -e NODE_ENV=development img`, hoặc một dòng trong file rồi `docker run --env-file`. Cả hai ghi đè biến cùng tên đã khai bằng `ENV` trong image, và chỉ cho container đó. Image không đổi. `docker inspect` của container thấy giá trị đã ghi đè trong `Config.Env`.

### docker-env-file
kind: trap
cue: `COPY .env` hoặc `ENV TOKEN=...` để giá trị trong image. File `.env` ở trên host, khai trong `.dockerignore` và `.gitignore`.

Q: Máy dev có `.env` chứa token. Dockerfile `COPY . .` và `.dockerignore` không có `.env`. Vì sao người pull image đọc được token dù bạn không viết dòng `ENV` nào?

A: `COPY` đưa file vào layer. `.env` là một file trong context, nên nó thành file trong image. `docker history` không cần in nội dung thì người có image vẫn đọc file trong container. Thêm `.env` vào `.dockerignore` để file không vào context, và thêm `.gitignore` để không commit. Lúc chạy, `docker run --env-file .env` đọc file từ host. Token lúc build thì dùng secret mount, không `COPY` và không `ENV`.

### docker-env-compose
kind: distinguish
cue: `.env` cạnh `compose.yaml` thay `${TÊN}` trong file. Process chỉ thấy biến khi service có `env_file` hoặc `environment`.

Q: Cạnh `compose.yaml` có `.env` với `GREETING=xin-chao`. File yaml không có `env_file` và không có `environment`. Process trong container có `GREETING` không?

A: Không. Compose dùng `.env` đó để thay `${GREETING}` nếu yaml viết `${GREETING}`. Không có chỗ nào đưa biến vào service thì process không có `GREETING`. Thêm `env_file: .env` hoặc `environment:` thì container mới nhận. `docker compose run -e` thắng các nguồn kia. `ENV` trong image đứng cuối, bị các nguồn trên ghi đè khi trùng tên.

### docker-env-inspect
kind: command
cue: `docker inspect --format '{{json .Config.Env}}'` liệt kê biến của container, gồm biến từ image và biến truyền lúc chạy.

Q: Container đang chạy và app đọc sai host database. Bạn cần xem đúng giá trị process nhận, không đoán từ file trên máy. Lệnh nào in danh sách biến?

A: `docker inspect --format '{{json .Config.Env}}' CONTAINER`. Khối `Env` là những gì container nhận: `ENV` của image, rồi giá trị `-e`, `--env-file`, hoặc Compose đã ghi đè. Sửa file `.env` trên host không đổi container đã tạo. Tạo lại container sau khi sửa file.

## Lỗi thường gặp

**`ENV DB_PASSWORD=...` cho đỡ phải truyền lúc chạy.** Mật khẩu nằm trong image. Ai pull cũng xem được. Đưa lúc tạo container, hoặc dùng secret như lab Compose.

**`export GREETING=xin-chao` trong file đưa cho `--env-file`.** Client tìm biến tên `export GREETING`. Viết `GREETING=xin-chao`.

**Sửa `.env` rồi tưởng container đang chạy đổi theo.** Biến gắn lúc tạo container. `docker compose up` lại, hoặc `docker run` lại, sau khi sửa file.

**Tin file `.env` cạnh Compose đã vào được process vì `docker compose config` hiện giá trị.** `config` hiện kết quả nội suy yaml. Process còn cần `env_file` hoặc `environment`.

## Nguồn

1. [KodeKloud — Docker for the Absolute Beginner](https://learn.kodekloud.com/learn/courses/docker-training-course-for-the-absolute-beginner) — module Docker Images: Environment Variables. Bài viết lại, không chép lab.
2. [docker run, môi trường](https://docs.docker.com/reference/cli/docker/container/run/#env) — `-e`, `--env-file`, ghi đè `ENV` của image, cú pháp file.
3. [ENV](https://docs.docker.com/reference/dockerfile/#env) — biến đóng trong image.
4. [Nội suy biến Compose](https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/) — file `.env` thay `${}` trong yaml, không phải biến của Swarm.
5. [Thứ tự biến Compose](https://docs.docker.com/compose/how-tos/environment-variables/envvars-precedence/) — `run -e`, rồi `environment` / `env_file`, rồi `ENV` trong image.

## Tiếp theo

[Docker Compose](./03-compose.md)
