Docker 볼륨 마운트 권한 문제 해결

컨테이너 프로세스의 UID와 호스트 디렉터리 소유자가 달라서 발생하는 Permission Denied를 chown, --user 플래그, Dockerfile ARG, ENTRYPOINT gosu 패턴으로 해결합니다.

· 5 min read · PALDYN Team

지난 글에서 포트 충돌 문제를 해결했다. 이번에는 볼륨을 마운트했을 때 컨테이너 내부에서 파일을 읽거나 쓰지 못하는 마운트 권한 문제를 다룬다. 보안을 위해 non-root 유저로 실행하도록 Dockerfile을 작성하면 필연적으로 마주치는 문제다.

에러 패턴

open /app/data/config.json: permission denied

또는

mkdir: cannot create directory '/app/data': Permission denied

컨테이너는 정상 실행됐지만 마운트된 디렉터리나 파일에 접근할 때 권한 에러가 발생한다.

원인: UID 불일치

UID 불일치로 인한 마운트 권한 문제

Docker는 이름이 아닌 UID/GID 번호로 권한을 판단한다. 호스트의 alice(UID 1000)와 컨테이너의 appuser(UID 999)는 이름이 달라도 번호가 다르면 별개의 사용자다.

# 호스트 사용자 UID 확인
id
# uid=1000(alice) gid=1000(alice)

# 컨테이너 내부에서 확인
docker exec <컨테이너> id
# uid=999(appuser) gid=999(appgroup)

# 마운트된 디렉터리 권한
docker exec <컨테이너> ls -la /app/data
# drwxr-xr-x 1000 1000 data  ← 컨테이너의 999가 쓸 수 없음

해결 방법

방법 1: 호스트 디렉터리 소유권 변경

# 컨테이너의 appuser UID(999)로 소유권 변경
sudo chown -R 999:999 ./data

# 실행
docker run -v $(pwd)/data:/app/data myapp

가장 직접적인 방법이지만 호스트 파일의 소유자가 바뀐다.

방법 2: —user 플래그로 호스트 UID 사용

# 현재 호스트 사용자 UID로 컨테이너 실행
docker run --user $(id -u):$(id -g) \
  -v $(pwd)/data:/app/data \
  myapp

호스트 UID와 컨테이너 프로세스 UID를 맞춰서 권한 충돌을 없앤다. 단, 컨테이너 내 앱이 루트 권한을 요구하는 경우에는 쓸 수 없다.

방법 3: Dockerfile에서 ARG로 UID를 빌드 시 주입

FROM node:20-alpine

ARG UID=1000
ARG GID=1000

RUN addgroup -g $GID appgroup \
    && adduser -u $UID -G appgroup -S appuser

WORKDIR /app
RUN chown appuser:appgroup /app

USER appuser
# 빌드 시 호스트 UID 전달
docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t myapp .

빌드 시점에 UID를 맞추는 가장 깔끔한 방법이다. 팀원마다 UID가 다를 수 있는 환경에서 유용하다.

방법 4: ENTRYPOINT에서 gosu로 권한 처리

# gosu 설치
RUN apk add --no-cache gosu  # alpine
# 또는
RUN apt-get install -y gosu   # debian
#!/bin/sh
# entrypoint.sh
set -e

# Named volume이 root로 생성됐을 때 소유자 수정
chown -R appuser:appgroup /app/data

# 이후 appuser로 프로세스 전환 (PID 1 유지)
exec gosu appuser "$@"
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["node", "server.js"]

Named Volume은 처음 생성될 때 root 소유로 만들어진다. entrypoint에서 chown으로 소유권을 바꾸고 gosu로 non-root 사용자로 전환하면 이 문제를 해결할 수 있다.

Named Volume 권한 문제

Named Volume 권한 초기화 패턴

# Named Volume 내부 권한 확인
docker run --rm -v myvolume:/data alpine ls -la /data

# root 소유인 경우
# drwxr-xr-x    2 root     root          4096 ...

# 임시 컨테이너로 소유권 변경
docker run --rm -v myvolume:/data alpine chown -R 999:999 /data

Compose에서의 권한 설정

services:
  app:
    image: myapp
    user: "${UID:-1000}:${GID:-1000}"
    volumes:
      - ./data:/app/data
# 실행 시 호스트 UID/GID 전달
UID=$(id -u) GID=$(id -g) docker compose up

또는 .env 파일에 미리 저장한다:

# .env
UID=1000
GID=1000

SELinux 환경에서의 마운트

RHEL/Fedora에서 SELinux를 사용하는 경우 bind mount에 :z 또는 :Z 레이블을 추가해야 한다.

# :z — 볼륨을 컨테이너간 공유 가능하게 레이블
docker run -v $(pwd)/data:/app/data:z myapp

# :Z — 볼륨을 이 컨테이너 전용으로 레이블
docker run -v $(pwd)/data:/app/data:Z myapp

SELinux 없는 환경에서는 :z, :Z를 쓰지 않아도 된다.


지난 글: Docker Port Already in Use 에러 해결

다음 글: Docker OOM Kill 해결 — 컨테이너 메모리 부족


읽어주셔서 감사합니다. 😊