빌드노트
USER를 바꿨는데 COPY한 파일은 여전히 root 소유다
USER는 실행 사용자를 바꾸지만 COPY 파일의 소유권을 바꾸지는 않는다. UID/GID와 모드를 따로 조회하고, 비root 읽기·쓰기 비교를 통해 COPY --chown과 최소 권한을 적용할 위치를 확인한다.
Linux 컨테이너를 비root 사용자로 실행하도록 바꾼 뒤 설정 파일에서 Permission denied가 나거나 작업 파일을 저장하지 못할 수 있다. Dockerfile에 USER 10001:10001이 있어도 복사한 파일의 소유권까지 바뀌지는 않는다. 프로세스의 UID와 파일의 소유자·모드를 따로 확인해야 한다.
Docker 공식 문서에서 USER는 이후 RUN과 컨테이너의 ENTRYPOINT·CMD를 실행할 기본 사용자다. COPY의 소유권은 별도 옵션인 --chown으로 지정하며, 이를 생략한 파일의 UID/GID 기본값은 0으로 설명한다. 따라서 USER를 COPY 앞으로 옮기는 것만으로 소유권 문제가 해결되지는 않는다. USER 설명 (새 탭에서 열림), COPY --chown 설명 (새 탭에서 열림)
실행 사용자와 실패한 경로부터 확인한다
아래는 실행 중인 Linux 컨테이너를 PowerShell에서 조회하는 명령이다. app-container는 자신의 컨테이너 이름으로, /app/app.conf와 작업 경로는 실제로 실패한 경로로 바꾼다. 컨테이너에 id, stat, sh, cat이 있어야 한다. 먼저 사용자와 경로별 권한을 보고, 마지막 명령으로 설정 파일의 실제 읽기를 확인한다.
$appContainer = 'app-container'
docker exec $appContainer id
docker exec $appContainer stat -c '%n uid=%u gid=%g mode=%a' / /app /app/app.conf /app/work /app/work/state.txt
docker exec $appContainer sh -c 'cat /app/app.conf >/dev/null'
id가 uid=10001 gid=10001인데 설정 파일은 uid=0 gid=0 mode=400이면, 소유자에게만 주어진 읽기 권한을 이 사용자로는 쓸 수 없다. 반면 root 소유라도 모드가 644이면 다른 사용자에게 읽기 권한이 있다. root 소유라는 사실만으로 읽기 실패를 판단하지 않는다. 모드의 첫 자리는 소유자, 다음은 그룹, 마지막은 그 밖의 사용자 권한이며, 읽기·쓰기·실행 비트는 각각 4·2·1이다. Linux 파일 모드 설명 (새 탭에서 열림)
파일 자체를 stat으로 조회하지 못한다면 상위 디렉터리도 본다. 경로를 따라가려면 각 디렉터리에 탐색 권한인 x가 필요하다. 파일을 읽을 수 있는 모드여도 부모 경로에서 막힐 수 있다. 앱이 시작한 뒤 스스로 사용자를 전환하는 구성이라면 위 id는 exec 프로세스의 신원일 뿐이므로 앱의 실제 UID와도 대조한다. Linux 경로 탐색 규칙 (새 탭에서 열림)
USER 전후에 복사해도 소유자는 같았다
이를 확인하려고 BusyBox 1.37.0으로 작은 이미지를 따로 만들었다. 실제 장애 기록이 아니라 합성 파일을 사용한 비교 실험이다. 상위 디렉터리는 root 소유 0755로 미리 만들고, 같은 설정을 USER 전후에 복사했다. 다음 세 줄이 비교한 부분이다.
COPY --chmod=0400 config.ini /before/config.ini
USER 10001:10001
COPY --chmod=0400 config.ini /after/config.ini
실행한 사용자는 10001이었지만 두 파일은 모두 0:0이었다. 아래는 조회 결과 중 해당 부분이다.
uid=10001 gid=10001 groups=10001
/before/config.ini uid=0 gid=0 mode=400
/after/config.ini uid=0 gid=0 mode=400
두 파일 모두 cat이 Permission denied로 실패했다. 같은 이미지에 root 소유 0644로 넣은 별도 설정은 읽혔다. USER 뒤의 RUN에서 만든 파일은 소유자가 10001이었다. 이 비교에서는 USER가 프로세스 실행에는 적용되지만, 그 뒤의 COPY 파일을 실행 사용자 소유로 바꾸지는 않았다.
쓰기 조건도 나눠 봤다. root 소유 0600인 상태 파일에 내용을 덧붙이는 작업과 root 소유 0700인 작업 디렉터리에 새 파일을 만드는 작업은 모두 거부됐다. 전자는 기존 파일의 쓰기 권한, 후자는 디렉터리의 쓰기·탐색 권한을 확인해야 하는 경우다.
소유권을 맞추고 필요한 모드는 유지한다
비교 이미지에서는 설정과 상태 파일의 COPY에 --chown=10001:10001을 넣었다. 작업 디렉터리도 10001 소유로 만들었다. 파일 모드는 각각 0400, 0600, 디렉터리 모드는 0700으로 유지했다. 그 결과 설정 읽기, 상태 파일 덧붙이기, 새 작업 파일 생성이 모두 성공했다. 수정 전후를 각각 새 컨테이너 두 개에서 실행해 같은 결과를 확인했다.
다음은 같은 수정을 /app 경로에 적용하는 예다. 기존 Dockerfile의 FROM 뒤, 빌드 사용자가 root인 구간에 넣는다. 빌드 컨텍스트에 config.ini와 state.txt가 있어야 하며, 10001은 실제 앱의 UID/GID로 바꾼다. 실험 입력은 각각 mode=synthetic과 seed 한 줄이었다. 디렉터리를 먼저 만들어 탐색 모드도 명시한다.
RUN mkdir -p /app/work && chmod 0755 /app \
&& chown 10001:10001 /app/work && chmod 0700 /app/work
COPY --chown=10001:10001 --chmod=0400 config.ini /app/app.conf
COPY --chown=10001:10001 --chmod=0600 state.txt /app/work/state.txt
USER 10001:10001
읽기만 필요한 설정에는 소유자의 읽기 비트, 갱신할 상태 파일에는 읽기·쓰기 비트, 앱 전용 작업 디렉터리에는 읽기·쓰기·탐색 비트를 줬다. 이 예의 0400 설정은 소유권을 맞춘 뒤에도 덧붙이기가 거부됐다. --chown은 소유자를 바꾸는 옵션이고, 쓰기 비트를 추가하는 옵션은 아니다. 소유자가 직접 모드를 다시 바꿀 수 있으므로 0400을 파일 변경 자체를 금지하는 장치로 해석하지는 않는다.
숫자 UID/GID를 쓰면 이미지의 사용자 이름 조회가 필요 없다. --chown=app:app처럼 이름을 쓰려면 대상 이미지의 /etc/passwd와 /etc/group에서 이름을 찾을 수 있어야 한다. 위의 8진수 --chmod는 Dockerfile 문법 1.2 이상에서 지원하며, 이 두 옵션은 Linux 이미지에 적용하는 기능이다. COPY 옵션과 지원 조건 (새 탭에서 열림)
수정한 이미지로 새 컨테이너를 만든 뒤 처음의 조회·읽기 명령을 다시 실행한다. 위 /app 예제도 별도로 빌드해 PowerShell에서 확인했다. 설정은 10001:10001 / 400, 상태 파일은 10001:10001 / 600, 작업 디렉터리는 10001:10001 / 700이었다. 설정 읽기는 종료 코드 0이었고 상태 파일 갱신과 새 파일 생성도 성공했다. 자신의 앱에서는 실제 저장 동작까지 확인한다. 설정 파일의 소유권을 맞춰도 저장 대상 디렉터리가 다른 곳이면 그 경로를 다시 조회해야 한다.
검증 환경은 Windows 호스트의 Docker Desktop 4.92.0, Docker Engine 29.8.0, Linux 컨테이너였다. 네트워크와 마운트 없이 실행했다. 위의 기본 소유권 0:0 관찰은 로컬 디렉터리 빌드 컨텍스트의 결과다. 초기 tar 컨텍스트 비교에서는 파일에 넣은 UID/GID 22222가 남는 다른 결과도 관찰했으며 그 구현 원인까지 확인하지 못했다. 또한 이 환경에서 COPY --chmod로 새 상위 경로까지 만들면 그 디렉터리에도 같은 모드가 적용되는 값을 확인해, 최종 비교에서는 부모 디렉터리를 먼저 준비했다. 다른 입력 형식이나 빌더의 결과를 추정하기보다 최종 이미지의 UID/GID·모드와 실제 앱 사용자로의 접근 결과를 함께 확인하는 것이 이 글의 적용 범위다.