본문 바로가기
빌드노트 검색

빌드노트

FROM 위 ARG는 이미지 태그에 쓰이는데 RUN에서는 비어 있다

Dockerfile의 첫 FROM 위 ARG는 이미지 선택에 쓰여도 RUN에 자동 전달되지 않는다. 단계 안에 ARG 이름을 다시 선언하는 전후 결과와 독립 단계의 범위를 비교하고, --build-arg와 ENV가 맡는 역할을 구별한다.

Dockerfile 맨 위의 ARG로 이미지 태그를 골랐는데, 같은 이름을 바로 아래 RUN에서 읽으면 빈칸이 나올 수 있다. 첫 FROM 이전의 선언은 전역 범위에 있다. 그 값을 RUN에서도 쓰려면 해당 단계 안에 ARG 이름을 다시 선언해야 한다. 빌드 인자를 다시 전달하기 전에 값을 읽는 RUN에서 위로 올라가 가장 가까운 FROM과 선언 위치부터 확인한다.

전역 선언을 아래로 옮기는 것은 해결이 아니다. FROM의 이미지 선택에도 필요하므로 전역 선언은 남기고, 값을 쓰는 단계의 RUN 앞에 선언을 추가한다. 단계 안에서는 기본값 없이 이름만 적어 전역 기본값이나 빌드 때 전달한 값을 사용한다. Dockerfile의 ARG와 FROM 설명 (새 탭에서 열림)에 나온 방식이다.

같은 Dockerfile에 한 줄을 추가해 비교한다

다음은 로컬 alpine:3.22를 사용하는 작은 재현이다. Linux 컨테이너 엔진이 실행 중이고 docker image inspect alpine:3.22가 성공해야 한다. 이미지가 없으면 이 재현의 준비 조건을 충족하지 못한 것이므로 먼저 로컬 이미지부터 준비한다. 프로젝트 소스가 없는 새 빈 폴더에 아래 내용을 UTF-8 파일 Dockerfile로 저장한다. 아직 단계 안에는 ARG가 없다.

ARG ALPINE_TAG=3.22
FROM alpine:${ALPINE_TAG} AS probe
RUN printf 'probe ALPINE_TAG=<%s>\n' "$ALPINE_TAG"

그 폴더의 PowerShell에서 다음 명령으로 probe 단계를 빌드한다. Docker Desktop의 desktop-linux 빌더 기준이며, 다른 환경에서는 docker buildx ls에서 확인한 로컬 이미지를 읽는 docker 드라이버 빌더 이름으로 바꾼다. 빌드가 실패하면 출력 비교로 넘어가지 말고 이미지와 빌더 조건부터 확인한다.

docker build --builder desktop-linux --pull=false --network=none --no-cache --progress=plain --target probe .

실제 로그에는 FROM docker.io/library/alpine:3.22가 있었지만, 실행 출력은 probe ALPINE_TAG=<>였다. 전역 기본값으로 이미지 선택은 끝났어도 이 단계의 셸에는 그 인자가 전달되지 않은 것이다. 여기서 <>는 셸이 빈 문자열을 출력했다는 뜻이다.

같은 파일에서 FROM과 RUN 사이에 ARG ALPINE_TAG 한 줄을 추가한다. 수정한 파일 전체는 다음과 같다.

ARG ALPINE_TAG=3.22
FROM alpine:${ALPINE_TAG} AS probe
ARG ALPINE_TAG
RUN printf 'probe ALPINE_TAG=<%s>\n' "$ALPINE_TAG"

위와 같은 빌드 명령을 다시 실행하자 probe ALPINE_TAG=<3.22>가 나왔다. 값 없는 재선언이 입력을 비우는 것이 아니라, 이 단계에서 그 값을 사용할 수 있게 했다.

다른 태그로 덮어쓰려면 빌드 명령의 마지막 . 앞에 --build-arg ALPINE_TAG=태그이름을 넣는다. 태그이름은 자신의 로컬 alpine 이미지에 실제로 있는 태그로 바꿔야 한다. 별도 비교에는 같은 Alpine 이미지의 base·override 별칭을 사용했다. 두 태그는 서로 다른 릴리스가 아니다. --build-arg ALPINE_TAG=override를 주면 FROM은 :override를 골랐지만, 단계 안 선언이 없을 때 RUN은 여전히 빈칸이었다. 선언을 추가한 쪽에서는 기본값 base와 전달값 override가 각각 읽혔다. 인자는 값을 정하고, 단계 안의 선언은 그 값을 사용할 범위를 정한다.

이 비교에서 --no-cache는 출력 명령을 다시 실행하려고 넣었다. 캐시를 지우는 것이 범위 문제의 해결책은 아니다. --network=none도 빌드의 RUN에 적용되는 옵션이며 이미지 조회까지 차단하지 않는다. 로컬 이미지와 내장 Dockerfile 프런트엔드를 사용했고 외부 # syntax 지시문은 넣지 않았다. 빌드 명령의 네트워크 옵션 (새 탭에서 열림)

새 FROM이 앞 단계에서 파생했는지 확인한다

독립 단계에서도 같은 값을 쓴다면 그 단계에 선언이 필요하다. 수정한 Dockerfile 끝에 다음 부분을 붙여 선언 전후를 비교할 수 있다.

FROM alpine:${ALPINE_TAG} AS fresh
RUN printf 'fresh-before ALPINE_TAG=<%s>\n' "$ALPINE_TAG"
ARG ALPINE_TAG
RUN printf 'fresh-after ALPINE_TAG=<%s>\n' "$ALPINE_TAG"

앞의 빌드 명령에서 --target probe만 --target fresh로 바꿔 실행했다. fresh-before ALPINE_TAG=<> 다음에 fresh-after ALPINE_TAG=<3.22>가 나왔다. 새 FROM도 전역 인자로 이미지를 골랐지만, probe와 독립된 단계라 자체 선언이 필요했다. 뒤의 선언이 앞선 RUN에 소급 적용되지 않는 점도 보인다. 별칭 비교에서도 선언 전은 빈칸이고 선언 후는 base 또는 override였다.

반면 FROM probe AS child는 앞 단계를 베이스로 삼는다. 부모 단계에서 선언하거나 소비한 ARG를 자식이 상속한다는 것이 Docker의 범위 규칙 (새 탭에서 열림)이다. 자식 단계 상속은 문서로 확인했으며 이번에는 직접 실행하지 않았다. 같은 이미지를 다시 쓰는 독립 단계와 앞 단계에서 파생한 자식을 구별해서 선언 위치를 정한다.

또한 BuildKit은 대상에 필요한 단계만 빌드한다 (새 탭에서 열림). fresh를 골랐을 때 probe 출력이 생략돼도 변수 전달 실패의 근거가 되지는 않는다. 특정 단계의 값을 확인하려면 그 단계를 --target으로 지정한다.

RUN에서 읽힌다고 이미지 ENV가 되는 것은 아니다

재선언을 추가한 probe 이미지도 Config.Env에는 ALPINE_TAG가 없었고, 컨테이너를 실행해 확인했을 때도 미설정이었다. 별도 대조에서 ENV SAVED_TAG=$ALPINE_TAG를 명시한 이미지에만 SAVED_TAG=base 또는 SAVED_TAG=override가 남았다. ARG가 이미지의 환경변수 기본값으로 자동 저장되지는 않는다는 Docker의 빌드 변수 설명 (새 탭에서 열림)과 맞는다. 빌드의 RUN에서만 필요한 값이면 단계 안의 ARG로 해결하고, 실행할 컨테이너에도 제공할 기본값일 때 ENV를 별도로 정한다.

이미 해당 단계의 RUN 앞에 ARG가 있는데도 값이 다르다면 전달한 인자 이름과 같은 이름의 ENV 설정을 살핀다. 같은 이름의 ENV가 ARG보다 우선하는 규칙은 ARG 변수 사용 설명 (새 탭에서 열림)에서 확인했으며 이 조건은 따로 실행하지 않았다.

직접 실행한 환경은 Windows 11·PowerShell 7.6.6, Docker Engine 29.8.0·BuildKit 0.33.0의 Linux/amd64이며 이미지 안 Alpine은 3.22.7이었다. 여기서 확인한 것은 같은 로컬 이미지의 전역·단계 범위와 명시적 ENV의 차이다. 다른 Alpine 릴리스, 원격 빌더, 레지스트리 통신의 부재는 검증하지 않았다. Config.Env에 인자가 없다는 관찰을 이력이나 메타데이터에도 흔적이 없다는 뜻으로 해석해서는 안 된다.

← 전체 글 보기