ARG và ENV trông giống nhau tới mức nhiều Dockerfile dùng lẫn lộn. Chúng khác nhau ở ba điểm — thời điểm, phạm vi, và chỗ rò rỉ — 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
Thử ba mươi giây
Tìm bí mật đang nằm trong image của bạn:
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.
Phần sau đo HEALTHCHECK: độ trễ phát hiện thật, và chỗ chính healthcheck gây tải.