Hình dung một công trường. ARG là những dòng phấn thợ nguệch ngoạc lên tường trong lúc xây — "cột này cao 3m", "dùng gạch loại B" — hữu ích lúc thi công, rồi lau sạch khi bàn giao, chẳng ai ở còn thấy. ENV là tấm biển đồng bắt vít lên cửa toà nhà đã hoàn thiện: nó ở lại, và bất kỳ ai đi ngang cũng đọc được. Hai thứ trông na ná nhau tới mức nhiều Dockerfile dùng lẫn lộn, nhưng chúng khác ở ba điểm — thời điểm (lúc xây hay sau khi bàn giao), phạm vi (phấn phòng này không tự sang phòng khác), và chỗ rò rỉ (viết mật khẩu lên biển đồng thì ai cũng thấy) — và cả ba đều đo được.
Phần 5 đã cho thấy điểm đầu tiên: ARG chỉ tồn tại lúc build, ENV đi vào image và có mặt lúc chạy. Bài này đào hai điểm còn lại.
Phạm vi: ARG trước FROM không dùng được bên trong tầng
Cách viết trông hoàn toàn hợp lý này không chạy:
ARG PHIEN_BAN=3.20
FROM alpine:${PHIEN_BAN} AS mot
RUN echo "tang MOT thay PHIEN_BAN='${PHIEN_BAN}'" >> /kq
FROM alpine:${PHIEN_BAN} AS hai
ARG PHIEN_BAN
RUN echo "tang HAI (co khai lai ARG) thay PHIEN_BAN='${PHIEN_BAN}'" >> /kq
tang MOT thay PHIEN_BAN=''
tang HAI (co khai lai ARG) thay PHIEN_BAN='3.20'
ARG khai trước FROM chỉ dùng được trong chính dòng FROM. Bên trong tầng, nó là chuỗi rỗng cho tới khi bạn khai lại ARG PHIEN_BAN (không cần giá trị mặc định — nó kế thừa giá trị ngoài).
Kiểu hỏng này đặc biệt khó chịu vì không có lỗi nào cả. Biến rỗng đi vào một lệnh RUN, và bạn được một image cài nhầm phiên bản, hoặc tải nhầm URL, hoặc gắn nhãn sai — im lặng.
ENV không đi qua ranh giới tầng
FROM alpine:3.20 AS mot
ENV BIEN_ENV=tu-tang-mot
RUN echo "tang mot: BIEN_ENV=$BIEN_ENV" > /kq
FROM alpine:3.20 AS hai
RUN echo "tang hai: BIEN_ENV='${BIEN_ENV:-<khong co>}'" > /kq2
tang mot: BIEN_ENV=tu-tang-mot
tang hai: BIEN_ENV='<khong co>'
luc chay: BIEN_ENV='<khong co>'
Mỗi tầng bắt đầu từ image nền của nó với môi trường sạch. ENV của tầng trước chỉ theo image nếu tầng đó là tầng cuối, hoặc nếu bạn dùng FROM mot AS hai để kế thừa.
Đây là chỗ hay sai khi chuyển một Dockerfile một tầng sang multi-stage: mọi ENV đặt ở tầng build biến mất, và ứng dụng khởi động với cấu hình mặc định mà không ai để ý.
Bí mật rò rỉ ở đâu
Tôi truyền một giá trị qua ARG và một giá trị qua ENV, rồi tìm chúng ở ba chỗ:
docker history |
docker inspect .Config.Env |
Biến môi trường lúc chạy | |
|---|---|---|---|
ARG |
có | không | không |
ENV |
có | có | có |
ENV tệ hơn ARG ở hai chỗ nữa: nó nằm trong cấu hình image (ai docker inspect cũng thấy) và nó có mặt trong môi trường của mọi tiến trình bên trong container. Nghĩa là bất cứ thư viện nào ghi log môi trường khi có sự cố, bất cứ công cụ báo lỗi nào gửi biến môi trường về máy chủ, đều mang bí mật của bạn đi.
Không cách nào trong hai cách là chỗ để đặt mật khẩu hay token. Cách đúng là --mount=type=secret của BuildKit cho lúc build, và tệp bí mật gắn vào lúc chạy — chủ đề của phần 21.
ARG đặt ở đâu thì quyết định cache
Phần 6 đã cho thấy mất cache ở dòng nào thì mất từ đó trở đi. ARG thay đổi giá trị mỗi lần build — GIT_SHA, BUILD_DATE, số hiệu bản dựng — nên vị trí của nó rất quan trọng:
FROM alpine:3.20
ARG MOC_THOI_GIAN # o DAU
RUN sleep 1 && echo buoc-1 > /1
RUN sleep 1 && echo buoc-2 > /2
RUN sleep 1 && echo buoc-3 > /3
Sau khi đổi giá trị ARG |
|
|---|---|
ARG ở đầu tệp |
3,9 s — 1 bước CACHED |
ARG ở cuối, ngay trước lệnh cần nó |
1,6 s — 2 bước CACHED |
Luật: khai ARG ngay trước lệnh đầu tiên thật sự dùng nó, không phải ở đầu Dockerfile cho "gọn".
Biến dựng sẵn của BuildKit
Có một nhóm ARG bạn không cần truyền vào, BuildKit tự điền:
TARGETPLATFORM=linux/arm64
TARGETOS=linux
TARGETARCH=arm64
TARGETVARIANT=
BUILDPLATFORM=linux/arm64
Đổi --platform linux/amd64 thì TARGETARCH thành amd64 trong khi BUILDPLATFORM giữ nguyên kiến trúc của máy đang build. Đây là cơ chế để một Dockerfile dựng được ảnh cho nhiều kiến trúc, và là chủ đề của phần 20.
Vẫn phải khai ARG TARGETPLATFORM trong tầng cần dùng — luật phạm vi ở đầu bài áp cho cả chúng.
HTTP_PROXY không còn như tài liệu cũ
Tài liệu Docker đời trước nói rằng HTTP_PROXY, HTTPS_PROXY, NO_PROXY là các ARG dựng sẵn: truyền vào là dùng được ngay, không cần khai, và không xuất hiện trong docker history.
Đo trên Docker 29.3.1 với BuildKit thì cả hai vế đều không đúng:
--build-arg HTTP_PROXY=..., KHONG khai ARG : HTTP_PROXY='<khong co>'
--build-arg HTTP_PROXY=..., CO khai ARG : HTTP_PROXY='http://vidu:3128'
Và giá trị đó có trong history:
$ docker history --no-trunc ae:px3 | grep -oE 'HTTP_PROXY=http[^ ]*'
HTTP_PROXY=http://vidu:3128
HTTP_PROXY=http://vidu:3128
Nó cư xử đúng như một ARG bình thường. Điểm khác duy nhất tôi đo được là nó không vào Config.Env.
Hệ quả thực tế: nếu URL proxy của bạn có chứa thông tin đăng nhập — http://nguoi:matkhau@proxy:3128, một cấu hình rất phổ biến trong mạng doanh nghiệp — thì thông tin đó nằm trong image và đi cùng nó lên registry.
Bảng quyết định
| Muốn gì | Dùng |
|---|---|
| Tham số chỉ cần lúc build (phiên bản, cờ biên dịch) | ARG |
| Cấu hình mặc định cho lúc chạy, người dùng ghi đè được | ENV |
| Cấu hình khác nhau theo môi trường | không phải cả hai — truyền -e lúc chạy |
| Bí mật lúc build | --mount=type=secret |
| Bí mật lúc chạy | tệp gắn vào, hoặc trình quản lý bí mật |
Và một mẫu hay dùng, kết hợp cả hai đúng cách:
ARG PHIEN_BAN_NODE=22
FROM node:${PHIEN_BAN_NODE}-alpine
ARG PHIEN_BAN_NODE # khai lai de dung ben trong tang
ENV NODE_ENV=production # mac dinh luc chay, ghi de duoc bang -e
RUN echo "dung Node ${PHIEN_BAN_NODE}" > /thong-tin
Muốn biết ngay bí mật nào đang nằm sẵn trong image của mình, soi hai chỗ nó hay lộ nhất:
docker history --no-trunc <anh> | grep -iE 'password|passwd|token|secret|key|apikey|_pw'
docker image inspect <anh> --format '{{json .Config.Env}}' | tr ',' '\n' | grep -iE 'password|token|secret|key'
Dòng nào hiện ra là dòng ai kéo được image cũng đọc được — và nếu image đã lên registry thì việc xoá nó khỏi Dockerfile hôm nay không lấy lại được gì. Bí mật đó phải coi như đã lộ và cần đổi.
Mẫu số chung
Cái bẫy đầu tiên, và là cội nguồn của hầu hết lỗi im lặng trong Dockerfile: build-time và runtime là hai pha khác nhau, và một giá trị sống ở nhầm pha thì hỏng mà không kêu. ARG là chuyện của lúc xây, ENV là chuyện của lúc chạy; lẫn hai cái là được một biến rỗng đi vào RUN, một ENV bốc hơi qua ranh giới tầng, một image cài nhầm phiên bản — tất cả không lỗi. Cùng cái ranh giới pha ấy ở khắp nơi trong nghề: hằng số biên dịch khác biến đọc lúc chạy, macro C khác getenv, generic bị xoá lúc biên dịch khác reflection lúc chạy, static final được nội tuyến khác giá trị đọc từ cấu hình. Nguyên tắc: với mỗi giá trị, hỏi trước "nó được quyết định khi nào" — và biết rằng lỗi pha hầu như luôn im lặng, vì mỗi pha đều "chạy được" theo cách của nó, chỉ là ra sai kết quả.
Điều thứ hai, về bí mật: thứ gì được ghi lại "cho tiện" đều là một chỗ bí mật sống mãi. Bảng đo cho thấy giá trị rò ra docker history, docker inspect, môi trường mọi tiến trình — không phải vì Docker cẩu thả, mà vì mỗi cái được thiết kế để ghi nhớ trạng thái nhằm tái lập và gỡ lỗi. Đó đúng là chỗ tệ nhất để đặt một bí mật. Cùng cái bẫy ở lịch sử Git giữ lại file .env đã xoá, ở log ghi cả request có token, ở bản sao lưu cơ sở dữ liệu, ở ảnh chụp màn hình dán vào ticket. Và một hệ quả cứng rắn: một khi bí mật đã đi vào một tầng bất biến rồi được đẩy đi (registry, remote, backup), xoá khỏi nguồn hôm nay không thu hồi được gì — nó phải bị coi như đã lộ và bị xoay vòng, không phải bị ẩn đi. Bí mật thật thì không bao giờ nằm trong thứ được lưu để tái lập; nó được tiêm vào đúng lúc cần và không để lại vết.
Phần sau đo HEALTHCHECK: độ trễ phát hiện thật, và chỗ chính healthcheck gây tải.