Docker 볼륨의 UID·GID 권한 문제 이해하기
Docker로 애플리케이션을 운영하다 보면 볼륨 마운트 후 컨테이너가 파일을 읽거나 쓰지 못하는 상황을 자주 마주칩니다. 로그에는 Permission denied 오류가 찍히고, 컨테이너를 재시작해도 해결되지 않아 당황하는 경우가 많습니다. 이 문제의 핵심은 대부분 UID(User ID)와 GID(Group ID)의 불일치에 있습니다.
UID·GID란 무엇인가
Linux 시스템에서 모든 프로세스와 파일은 소유자를 나타내는 UID와 그룹을 나타내는 GID를 가집니다. 파일 시스템은 이 숫자값을 기준으로 읽기, 쓰기, 실행 권한을 판단합니다. /etc/passwd나 /etc/group 파일에 저장된 사용자 이름은 사람이 읽기 편하도록 붙인 레이블일 뿐이며, 실제 권한 판단은 항상 숫자 ID로 이루어집니다.
예를 들어 호스트의 ubuntu 사용자가 UID 1000이고, 컨테이너 내부의 appuser도 UID 1000이라면 이름이 달라도 파일 권한은 문제없이 동작합니다. 반대로 이름이 같아도 UID가 다르면 권한 충돌이 발생합니다.
Docker 볼륨에서 권한 문제가 생기는 이유
Docker 컨테이너는 호스트와 커널을 공유하지만, 사용자 네임스페이스를 별도로 설정하지 않는 한 UID·GID 공간도 호스트와 공유합니다. 즉, 컨테이너 안에서 UID 1000으로 실행되는 프로세스는 호스트 관점에서도 UID 1000으로 동작합니다.
문제는 컨테이너 이미지가 어떤 UID로 프로세스를 실행하도록 설계되었느냐에 따라 달라집니다.
공식 이미지의 기본 사용자
많은 공식 Docker 이미지는 보안을 위해 루트가 아닌 별도 사용자로 프로세스를 실행합니다. 예를 들어 nginx 이미지는 내부적으로 nginx 사용자를 사용하고, postgres 이미지는 postgres 사용자를 사용합니다. 이 사용자들의 UID는 이미지마다 다르게 설정되어 있습니다.
호스트에서 특정 디렉터리를 볼륨으로 마운트할 때, 그 디렉터리의 소유자 UID가 컨테이너가 사용하는 UID와 다르면 쓰기 권한이 거부됩니다.
bind mount와 named volume의 차이
Docker 볼륨에는 크게 두 가지 방식이 있습니다.
- bind mount: 호스트의 특정 경로를 컨테이너에 직접 연결합니다. 호스트 디렉터리의 소유권과 권한이 그대로 컨테이너에 노출됩니다.
- named volume: Docker가 관리하는 전용 스토리지 영역을 사용합니다. 볼륨이 처음 생성될 때 컨테이너 이미지 내부의 해당 경로 소유권이 복사됩니다.
bind mount는 호스트 파일 시스템을 직접 다루기 때문에 UID·GID 불일치 문제가 더 자주 발생합니다. named volume은 초기 설정이 이미지 기준으로 맞춰지지만, 이후 다른 사용자가 파일을 만들면 역시 충돌이 생길 수 있습니다.
권한 문제를 확인하는 방법
문제를 진단하려면 먼저 컨테이너 내부에서 실행 중인 프로세스의 UID를 확인해야 합니다.
docker exec <컨테이너명> id
이 명령은 컨테이너 내부의 현재 사용자 UID, GID, 그리고 소속 그룹을 출력합니다.
다음으로 마운트된 볼륨 경로의 소유권을 확인합니다.
docker exec <컨테이너명> ls -ln /path/to/volume
-l 옵션은 상세 정보를, -n 옵션은 사용자 이름 대신 UID·GID 숫자를 출력합니다. 여기서 파일 소유자의 UID와 프로세스의 UID가 일치하는지 비교하면 문제 여부를 바로 파악할 수 있습니다.
호스트 쪽에서도 마운트 디렉터리의 소유권을 확인합니다.
ls -ln /host/path/to/volume
권한 문제를 해결하는 방법
원인을 파악했다면 상황에 맞는 방법으로 해결할 수 있습니다.
호스트 디렉터리의 소유권 변경
bind mount를 사용할 때 가장 직접적인 방법은 호스트 디렉터리의 소유권을 컨테이너가 사용하는 UID·GID에 맞추는 것입니다.
sudo chown -R 1000:1000 /host/path/to/volume
컨테이너가 사용하는 UID를 먼저 확인한 뒤, 그 값으로 호스트 디렉터리를 변경합니다. 이 방법은 간단하지만 호스트 사용자와의 관계를 고려해야 합니다. 변경된 UID가 호스트의 다른 사용자와 겹치면 의도치 않은 접근이 허용될 수 있습니다.
Dockerfile에서 사용자 UID 지정
직접 이미지를 빌드하는 경우라면 Dockerfile에서 사용할 UID를 명시적으로 설정할 수 있습니다.
ARG UID=1000
ARG GID=1000
RUN groupadd -g ${GID} appgroup && \
useradd -u ${UID} -g appgroup appuser
USER appuser
이렇게 하면 빌드 시 인자로 UID를 전달할 수 있어 유연하게 관리할 수 있습니다.
docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t myapp .
$(id -u)와 $(id -g)는 현재 호스트 사용자의 UID·GID를 자동으로 전달합니다. 개발 환경에서 호스트 사용자와 컨테이너 사용자의 UID를 일치시키는 데 유용한 패턴입니다.
Docker Compose에서 user 옵션 사용
docker-compose.yml에서 user 필드를 사용하면 컨테이너가 특정 UID·GID로 실행되도록 강제할 수 있습니다.
services:
app:
image: myapp
user: "1000:1000"
volumes:
- ./data:/app/data
이 방법은 이미지를 수정하지 않고도 실행 시점에 사용자를 바꿀 수 있어 편리합니다. 다만 해당 UID가 컨테이너 내부에 존재하지 않으면 일부 애플리케이션이 홈 디렉터리나 환경 변수를 제대로 찾지 못할 수 있으니 주의해야 합니다.
entrypoint에서 동적으로 처리하기
컨테이너가 시작될 때 entrypoint 스크립트에서 볼륨 경로의 소유권을 동적으로 수정하는 방법도 있습니다. 이 패턴은 일부 공식 이미지에서도 사용합니다.
#!/bin/sh
chown -R appuser:appgroup /app/data
exec gosu appuser "$@"
gosu는 권한을 낮춰 특정 사용자로 프로세스를 전환하는 도구입니다. 루트로 컨테이너를 시작해 소유권을 조정한 뒤 일반 사용자로 전환하는 패턴입니다. 하지만 이 방법은 컨테이너가 초기에 루트 권한으로 실행되어야 하므로 보안 정책에 따라 허용 여부를 확인해야 합니다.
사용자 네임스페이스 리매핑
Docker는 사용자 네임스페이스 리매핑(user namespace remapping) 기능을 지원합니다. 이를 활성화하면 컨테이너 내부의 루트(UID 0)가 호스트에서는 일반 사용자 UID로 매핑되어 보안이 크게 향상됩니다.
/etc/docker/daemon.json에서 설정할 수 있습니다.
{
"userns-remap": "default"
}
default를 지정하면 Docker가 자동으로 dockremap 사용자를 생성하고 서브UID·서브GID를 할당합니다. 이 설정을 적용하면 볼륨 파일의 소유권이 호스트에서 다른 UID로 보이게 되므로, 기존 볼륨 데이터가 있다면 소유권을 다시 정리해야 합니다.
실제 운영에서 주의할 점
권한 문제를 해결할 때 chmod 777처럼 모든 사용자에게 쓰기 권한을 주는 방식은 빠른 해결책처럼 보이지만 보안상 좋지 않습니다. 특히 웹 서버나 데이터베이스 데이터 디렉터리에 이런 권한을 주면 컨테이너 탈출 공격이나 다른 프로세스에 의한 파일 조작 위험이 높아집니다.
가능하면 최소 권한 원칙을 따르는 것이 좋습니다. 컨테이너 프로세스가 필요한 디렉터리에만 적절한 소유권을 부여하고, 읽기 전용 볼륨이 가능한 경우에는 ro 옵션을 사용하는 것을 권장합니다.
volumes:
- ./config:/app/config:ro
또한 CI/CD 파이프라인이나 쿠버네티스 환경에서는 호스트 사용자가 고정되지 않을 수 있으므로, 빌드 인자나 환경 변수로 UID를 주입하는 방식을 미리 설계해두는 것이 나중에 유지보수를 훨씬 편하게 만들어 줍니다.
정리
Docker 볼륨의 UID·GID 권한 문제는 Linux 파일 시스템의 기본 동작 방식에서 비롯됩니다. 컨테이너와 호스트가 같은 UID 공간을 공유한다는 사실을 이해하면 문제의 원인을 빠르게 파악할 수 있습니다.
상황에 따라 호스트 디렉터리 소유권 변경, Dockerfile에서의 UID 지정, Docker Compose의 user 옵션, entrypoint 스크립트를 통한 동적 처리 등 다양한 방법 중 적절한 것을 선택할 수 있습니다. 중요한 것은 문제를 우회하는 것이 아니라 UID·GID가 왜 맞지 않는지를 파악하고, 보안을 고려한 방식으로 해결하는 것입니다.