테마 전환

Docker 마운트 디렉터리 권한 문제 완전 해결 가이드: 진단부터 실전까지 5가지 방법

Easton editorial illustration: one mounted folder passing through an ownership lock into a container

2026-06-08 업데이트: 현재 Docker에서도 이 5가지 방법이 여전히 유효한지 다시 확인했습니다. Docker Desktop에서 Apple Virtualization 기반 파일 공유를 사용하는 경우, rootless Docker, userns-remap 경로도 포함했으며 같은 시리즈의 관련 글 링크도 추가했습니다. 결론은 바뀌지 않았습니다. chmod 777은 사용하지 말고, --user 또는 Dockerfile에서 사용자를 만드는 방식을 우선하세요.

터미널에 빨간 오류 메시지가 나타납니다. “Permission denied”. 오늘 밤만 벌써 다섯 번째입니다. Mac에서는 잘 돌아가던 개발 컨테이너가 Linux 프로덕션 서버에 배포하자마자 터집니다. 컨테이너가 만든 로그 파일을 삭제하려고 하면 시스템은 “권한이 없다”고 말합니다. 분명 서버 관리자인데도 말입니다.

어제 동료가 무심코 말했습니다. “chmod 777 하면 되지 않아?” 해 보니 실제로 동작했습니다. 하지만 마음 한편에서는 계속 이런 생각이 듭니다. 정말 이렇게 해도 괜찮을까요?

Docker 커뮤니티 포럼 통계에 따르면 초보 사용자의 40%가 마운트 디렉터리 권한 문제를 겪고, 그중 60%는 chmod 777을 선택합니다. 그 결과 컨테이너 이탈과 데이터 유출 위험이 생깁니다.

40%
권한 문제를 겪는 사용자
초보 사용자의 40%가 겪고, 60%는 chmod 777을 선택

먼저 권한 문제의 본질부터 분명히 알아보겠습니다. UID와 GID는 대체 무엇일까요? 이어서 가장 간단한 임시 해법부터 엔터프라이즈급 보안 설정까지 5가지 제대로 된 해결 방법을 살펴보고, 1분 안에 문제를 찾을 수 있는 진단 명령 3개도 소개합니다.

근본 원인: 왜 권한 문제가 생길까

진짜 신분증은 UID/GID입니다

Linux가 사용자 이름으로 신원을 구분한다고 생각했나요? 그렇지 않습니다. Linux 커널은 실제로 숫자만 인식합니다. 바로 UID(사용자 ID)와 GID(그룹 ID)입니다. 사용자 이름은 사람이 보기 편하도록 붙인 별명일 뿐입니다.

예를 들어 컴퓨터에서 id 명령을 실행해 보세요.

uid=1000(oden) gid=1000(oden) groups=1000(oden)

보이시나요? 진짜 신원 식별자는 1000입니다. 커널에 oden이라는 이름은 전혀 중요하지 않습니다.

root 사용자도 살펴보겠습니다.

uid=0(root) gid=0(root) groups=0(root)

0번 사용자가 슈퍼유저입니다. 이름이 무엇이든 UID가 0이면 시스템에서 가장 높은 권한을 가집니다.

권한 충돌은 어떻게 생길까

여기가 문제의 핵심입니다. 호스트에서는 일반 사용자(예: UID=1000)로 Docker를 실행하지만, 컨테이너는 기본적으로 root(UID=0)로 실행되면 충돌이 발생합니다.

전체 충돌 과정은 다음과 같습니다.

  1. Linux 호스트에서 UID=1000인 일반 사용자로 컨테이너를 시작합니다.
  2. 컨테이너 내부 프로세스는 기본적으로 root(UID=0)로 실행됩니다.
  3. 컨테이너 내부의 root가 /app/logs/output.log 같은 파일을 만듭니다.
  4. 이 파일은 bind mount를 통해 호스트의 ./logs/output.log에 매핑됩니다.
  5. 호스트에서 이 파일의 owner는 root(UID=0)로 표시됩니다.
  6. 일반 사용자(UID=1000)인 당신이 이 파일을 삭제하려고 하나요? 권한이 부족해 삭제할 수 없습니다.

원리는 이처럼 단순하고 직설적입니다. 컨테이너는 호스트에서 당신이 누구인지 알지 못하고 UID만 봅니다. 0번 사용자가 만든 파일은 0번이 아닌 사용자가 마음대로 건드릴 수 없습니다.

Mac과 Windows에서는 왜 이런 문제가 없을까?

“이상한데? Mac에서 Docker를 쓰면서 이런 일을 한 번도 겪지 않았는데?”라고 생각할 수 있습니다.

맞습니다. Mac과 Windows의 Docker Desktop은 가상 머신 안에서 실행되기 때문입니다. Mac은 Apple Virtualization 프레임워크(과거에는 hyperkit)를 사용하고, Windows는 WSL2 또는 Hyper-V를 사용합니다. 여기에는 추가적인 “권한 변환 계층”이 있습니다.

Mac의 VirtioFS 파일 시스템은 컨테이너가 만든 파일의 owner를 호스트의 현재 사용자로 자동 변환합니다. 친절한 기능처럼 들리죠? 맞습니다. 하지만 바로 이 점 때문에 Mac에서는 괜찮던 코드가 Linux 서버에서는 실패합니다. Linux의 Docker는 중간 변환 계층 없이 커널을 직접 호출합니다.

쉽게 말해 Docker Desktop은 사용자 경험을 위해 어느 정도 타협했고, 일부 “현실성”을 희생했습니다. 개발할 때는 문제를 느끼지 못하다가 배포 단계에서 갑자기 마주치게 됩니다.

함께 주의해야 할 몇 가지 함정

Bind mount와 Named Volume:

  • Bind mount(-v /host/path:/container/path)는 호스트 디렉터리를 직접 매핑하므로 권한 문제가 가장 뚜렷합니다.
  • Named Volume(-v mydata:/container/path)은 Docker가 관리하므로 권한이 상대적으로 유연하지만, 문제가 전혀 없는 것은 아닙니다.

SELinux와 AppArmor:
Linux에서 SELinux(CentOS/RHEL)나 AppArmor(Ubuntu)를 사용한다면 권한 문제는 더 복잡해집니다. UID/GID 일치뿐 아니라 보안 컨텍스트 레이블도 고려해야 합니다. 원인을 알 수 없는 권한 오류가 발생한다면 먼저 SELinux 로그를 확인하세요.

sudo ausearch -m avc -ts recent

컨테이너 안에 호스트 사용자가 없음:
컨테이너 이미지에는 기본적으로 root와 일부 시스템 사용자만 있습니다. 호스트에서 UID=1000인 사용자를 컨테이너는 알지 못합니다. 파일 owner가 이름 대신 숫자로만 표시되는 이유가 이것입니다.

빠른 진단: 명령 3개로 권한 문제 찾기

Permission denied가 나타나도 당황하지 마세요. 전문가들은 어떻게 문제를 조사할까요? 명령 3개면 1분 안에 확인할 수 있습니다.

명령 1: 파일의 실제 owner 확인

ls -ln /your/mount/path

-l이 아니라 -ln이라는 점에 주의하세요. -l은 사용자 이름을 표시하고, -ln은 UID/GID 숫자를 표시합니다.

출력 예시:

-rw-r--r-- 1 0 0 1024 Dec 17 10:00 output.log

이 출력을 어떻게 읽을까요?

  • 첫 번째 열 -rw-r--r--은 권한 비트입니다(여기서는 핵심이 아닙니다).
  • 두 번째 열 1은 하드 링크 수입니다(중요하지 않습니다).
  • 세 번째 열 0이 owner의 UID입니다. ← 핵심입니다.
  • 네 번째 열 0이 owner의 GID입니다. ← 이것도 중요합니다.
  • 그 뒤에는 파일 크기, 시간, 파일 이름이 이어집니다.

0 0이 보이나요? 이것이 root 사용자입니다. 호스트의 UID가 1000이라면 당연히 이 파일을 수정할 수 없습니다.

정상적인 경우와 비교해 보겠습니다.

ls -ln ~/my-project

출력:

-rw-r--r-- 1 1000 1000 2048 Dec 17 11:30 README.md

1000 1000이 보이죠? 이것이 자신의 파일입니다.

명령 2: 컨테이너 프로세스의 실제 신원 확인

docker exec <container_name> id

출력 예시:

uid=0(root) gid=0(root) groups=0(root)

이 결과는 컨테이너 내부 프로세스가 어떤 신원으로 실행되는지 알려 줍니다. 일반적으로 root(UID=0)입니다.

호스트와 비교해 보세요.

id

출력:

uid=1000(oden) gid=1000(oden) groups=1000(oden),4(adm),27(sudo)

차이가 보이나요? 컨테이너 내부는 0이고 호스트는 1000입니다. 서로 일치하지 않습니다. 이것이 충돌의 원인입니다.

명령 3: Docker 마운트 설정 확인

docker inspect <container_name> | grep -A 10 "Mounts"

출력은 다음과 비슷합니다.

"Mounts": [
    {
        "Type": "bind",
        "Source": "/home/oden/project/logs",
        "Destination": "/app/logs",
        "Mode": "",
        "RW": true,
        "Propagation": "rprivate"
    }
]

무엇을 확인해야 할까요?

  • Type: bind인가 volume인가? bind mount에서 권한 문제가 더 뚜렷합니다.
  • Source: 호스트 경로입니다. 이 경로에서 ls -ln으로 owner를 확인하세요.
  • RW: true면 읽기/쓰기가 가능하고, false면 읽기 전용입니다.
  • Mode: SELinux용 :z 또는 :Z 같은 특별한 마운트 옵션이 있는지 확인합니다.

1분 진단 절차

권한 문제가 생기면 다음 순서로 확인하세요.

  1. 파일부터 확인: ls -ln으로 문제가 있는 파일의 UID/GID를 확인합니다.
  2. 컨테이너 확인: docker exec <container> id로 컨테이너 프로세스의 신원을 확인합니다.
  3. 차이 비교: 컨테이너 UID와 파일 owner UID가 호스트의 자신의 UID와 다르다면 권한 충돌입니다.
  4. 설정 확인: docker inspect로 마운트 방식과 경로를 확인합니다.

실제 사례를 들어 보겠습니다. 컨테이너 로그를 삭제할 수 없다고 가정해 보세요.

# 1단계: 파일 owner 확인
$ ls -ln ./logs/
-rw-r--r-- 1 0 0 5120 Dec 17 12:00 app.log

# UID=0, root가 만든 파일

# 2단계: 컨테이너 신원 확인
$ docker exec myapp id
uid=0(root) gid=0(root) groups=0(root)

# 컨테이너가 실제로 root로 실행 중

# 3단계: 자신의 신원 확인
$ id
uid=1000(oden) gid=1000(oden) ...

# 나는 1000, 컨테이너는 0. 서로 일치하지 않음!

# 진단 결과: 컨테이너가 root로 실행되어 root 소유 파일을 만들었고, 나는 삭제 권한이 없음

이렇게 진단하면 어떤 방법을 써야 할지 알 수 있습니다. 이제 해결 방법을 살펴보겠습니다.

5가지 해결 방법: 상황에 맞게 선택하기

문제의 원인과 진단법을 알았으니 이제 해결해 봅시다. 간단한 방법부터 복잡한 방법까지, 임시 해법부터 엔터프라이즈급 설정까지 5가지를 소개합니다. 핵심은 어떤 상황에 어떤 방법을 써야 하는지 아는 것입니다.

방법 1: 실행할 때 —user 매개변수로 UID/GID 지정

적합한 대상: 빠른 테스트가 필요하거나 로컬 개발 환경을 사용하는 경우

원리: Docker에 “내 UID로 컨테이너를 실행하라”고 직접 알려 줍니다. 그러면 컨테이너가 만든 파일의 owner도 자신이 됩니다.

사용법:

# 명령줄 방식
docker run --user $(id -u):$(id -g) -v /host/data:/app/data myimage

# docker-compose.yml 방식
services:
  myapp:
    image: myimage
    user: "${UID:-1000}:${GID:-1000}"
    volumes:
      - ./data:/app/data

실행할 때:

export UID=$(id -u)
export GID=$(id -g)
docker-compose up

장점:

  • 가장 간단하며 즉시 효과가 나타납니다.
  • Dockerfile을 수정하거나 이미지를 다시 빌드할 필요가 없습니다.
  • 로컬 개발에서 빠르게 반복 작업하기 좋습니다.

단점:

  • 시작할 때마다 지정해야 합니다.
  • 컨테이너 내부 애플리케이션이 특정 UID에 의존하면 실패할 수 있습니다. 예를 들어 nginx가 80번 포트에 바인딩하려면 root 권한이 필요합니다.
  • 팀원마다 UID가 다를 수 있으므로 값을 고정할 수 없습니다.

위험 지수: 낮음

지원 시스템: Linux에서는 완벽히 지원합니다. Mac/Windows에서도 지원하지만 가상 머신 계층 때문에 사용 경험이 좋지 않을 수 있습니다.

언제 사용할까: 로컬 개발, 임시 테스트, 아이디어의 빠른 검증에 적합합니다. 예를 들어 Mac에서 개발한 뒤 Linux CI에서 권한 문제를 발견했다면 먼저 이 방법으로 급한 문제를 해결할 수 있습니다.


방법 2: Dockerfile에서 일치하는 사용자 생성

적합한 대상: 팀에서 공유하고 반복해서 사용할 이미지

원리: 이미지를 빌드할 때 build arg로 호스트 UID를 전달하고, 이미지 안에 같은 UID를 가진 사용자를 만듭니다. 그러면 컨테이너가 시작된 뒤 이 사용자로 실행됩니다.

사용법:

Dockerfile:

FROM python:3.11

# 빌드 인수 받기
ARG UID=1000
ARG GID=1000

# 그룹과 사용자 생성
RUN groupadd -g $GID appuser && \
    useradd -m -u $UID -g $GID appuser

# 작업 디렉터리를 설정하고 권한 부여
WORKDIR /app
RUN chown -R appuser:appuser /app

# root가 아닌 사용자로 전환
USER appuser

# 이후 명령은 모두 appuser로 실행
COPY --chown=appuser:appuser . /app
RUN pip install -r requirements.txt

CMD ["python", "app.py"]

빌드:

docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t myapp:latest .

docker-compose.yml:

services:
  myapp:
    build:
      context: .
      args:
        UID: ${UID:-1000}
        GID: ${GID:-1000}
    volumes:
      - ./data:/app/data

장점:

  • 한 번 빌드하면 실행할 때마다 올바르게 동작합니다.
  • 컨테이너 안에 완전한 사용자 환경(home 디렉터리, shell 설정 등)이 생깁니다.
  • 가장 전문적인 프로덕션급 방법입니다.

단점:

  • Dockerfile을 수정해야 합니다.
  • 팀원들의 UID가 다르면 각자 이미지를 빌드해야 하므로 같은 이미지를 공유할 수 없습니다.
  • 애플리케이션 시작 시 root 권한이 필요하다면 사용할 수 없습니다.

위험 지수: 낮음

지원 시스템: Linux에서는 완벽히 지원합니다. Mac/Windows에서는 가상 머신 계층 때문에 차이가 있지만 사용할 수 있습니다.

언제 사용할까: 팀에 표준 기본 이미지가 있고 모든 프로젝트가 이를 기반으로 할 때 적합합니다. 또는 오픈 소스 프로젝트처럼 다른 사람에게 배포할 이미지를 만들고, 사용자가 직접 빌드하면서 UID를 맞추도록 할 때도 좋습니다.


방법 3: Entrypoint 스크립트에서 동적으로 조정(gosu 방식)

적합한 대상: 먼저 root로 초기화한 뒤 권한을 낮춰 실행해야 하는 애플리케이션

원리: 컨테이너가 root로 entrypoint 스크립트를 시작하고, 스크립트에서 사용자를 동적으로 만든 다음 gosu(sudo와 비슷하지만 더 안전한 도구)를 사용해 대상 사용자로 전환한 뒤 주 프로그램을 실행합니다.

사용법:

Dockerfile:

FROM node:18

# gosu 설치
RUN apt-get update && apt-get install -y gosu && rm -rf /var/lib/apt/lists/*

# entrypoint 스크립트 복사
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh

WORKDIR /app
COPY . /app

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["node", "server.js"]

entrypoint.sh:

#!/bin/bash
set -e

# LOCAL_USER_ID 환경 변수가 지정된 경우
if [ -n "$LOCAL_USER_ID" ]; then
    # 사용자가 없으면 생성
    useradd -u $LOCAL_USER_ID -o -m appuser 2>/dev/null || true

    # /app 디렉터리 owner 변경
    chown -R appuser:appuser /app

    # gosu로 appuser로 전환해 후속 명령 실행
    exec gosu appuser "$@"
else
    # 지정하지 않았으면 root로 실행
    exec "$@"
fi

실행:

docker run -e LOCAL_USER_ID=$(id -u) -v ./data:/app/data myapp

장점:

  • 유연성이 가장 높습니다. root로 초기화하면서도 주 프로그램은 권한을 낮춰 실행할 수 있습니다.
  • UID가 다른 여러 사용자가 같은 이미지를 재사용할 수 있습니다.
  • 보안성이 좋습니다(gosu는 su/sudo보다 안전합니다).

단점:

  • Dockerfile과 entrypoint를 수정해야 합니다.
  • 복잡성과 유지 관리 비용이 늘어납니다.
  • gosu를 추가로 설치해야 합니다(크기는 작습니다).

위험 지수: 중간(gosu는 Docker 공식 권장 도구로 신뢰할 수 있습니다)

지원 시스템: 모든 시스템

언제 사용할까: 애플리케이션을 시작할 때 시스템 설정을 변경해야 해서 root가 필요하지만, 실행할 때는 일반 사용자 권한이어야 하는 경우입니다. 예를 들어 nginx가 80번 포트에 바인딩하려면 root 권한이 필요하지만 worker 프로세스는 권한을 낮춰야 할 때, 또는 애플리케이션이 데이터베이스 schema를 root로 초기화한 뒤 일반 사용자로 서비스를 실행해야 할 때 적합합니다.


방법 4: User Namespace Remapping(userns-remap)

적합한 대상: 회사 보안 규정상 강제 격리가 필요하고 어떤 컨테이너도 실제 root로 실행할 수 없는 경우

원리: Docker daemon 수준에서 설정해 모든 컨테이너의 UID를 “하위 사용자” 범위로 자동 재매핑합니다. 컨테이너 안에서는 자신을 root(UID=0)로 인식하지만, 호스트에서는 실제로 일반 사용자(예: UID=100000)입니다.

사용법:

/etc/docker/daemon.json 편집:

{
  "userns-remap": "default"
}

Docker 재시작:

sudo systemctl restart docker

Docker는 자동으로 dockremap 사용자를 만들고 /etc/subuid/etc/subgid에 UID/GID 범위를 할당합니다.

검증:

# 컨테이너 시작
docker run -d --name test -v /tmp/test:/data busybox sleep 3600

# 컨테이너 안에서는 root로 보임
docker exec test id
# uid=0(root) gid=0(root)

# 하지만 호스트에서는
ls -ln /tmp/test
# owner가 100000 같은 큰 숫자로 표시됨

장점:

  • 한 번 설정하면 전체에 적용됩니다.
  • 이미지나 명령을 수정하지 않아도 모든 컨테이너가 자동으로 격리됩니다.
  • 보안성이 가장 높습니다. 컨테이너가 이탈해도 실제 root가 아니라 하위 사용자 shell에 도달합니다.
  • Docker가 공식적으로 권장하는 엔터프라이즈급 방법입니다.

단점:

  • 시스템 수준 설정이므로 모든 컨테이너에 영향을 줍니다.
  • 기존 컨테이너와 volume이 호환되지 않을 수 있어 다시 만들어야 합니다.
  • rootless 모드와 동시에 사용할 수 없습니다.
  • mount 같은 일부 특권 작업은 여전히 사용할 수 없습니다.

위험 지수: 낮음(공식 권장)

지원 시스템: Linux만 지원(커널의 user namespace 지원 필요)

언제 사용할까: 회사 보안 규정상 모든 컨테이너를 격리해야 할 때, 신뢰할 수 없는 컨테이너 이미지가 있는 멀티테넌트 환경을 관리할 때, 프로젝트마다 따로 설정하지 않고 한 번에 문제를 해결하고 싶을 때 적합합니다.


방법 5: Rootless Docker

적합한 대상: 가장 높은 보안 수준이 필요하고 일부 기능 제한을 받아들일 수 있는 경우

원리: Docker daemon 자체가 root가 아닌 사용자로 실행됩니다. 모든 컨테이너가 이 사용자의 namespace 안에서 작동하므로 시스템 root와 완전히 격리됩니다.

사용법:

rootless Docker 설치:

# root Docker가 있으면 제거
sudo apt-get remove docker docker-engine docker.io

# rootless Docker 설치
curl -fsSL https://get.docker.com/rootless | sh

# 안내에 따라 환경 변수 설정
export PATH=$HOME/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

# 시작
systemctl --user start docker
systemctl --user enable docker

검증:

docker run hello-world
# 완전히 root가 아닌 신원으로 실행

장점:

  • 궁극적인 보안 방법입니다. Docker daemon도 root가 아니고 컨테이너는 더더욱 root가 아닙니다.
  • 컨테이너가 이탈해도 현재 사용자의 권한 범위를 벗어나지 못합니다.
  • 신뢰할 수 없는 이미지, 멀티테넌트 환경, 보안에 민감한 상황에 적합합니다.

단점:

  • 특권 포트(1024 미만, 80/443 포함)를 사용할 수 없습니다.
  • host 모드 같은 일부 네트워크 모드를 사용할 수 없습니다.
  • 추가 namespace 오버헤드 때문에 성능이 다소 낮습니다.
  • 설정이 비교적 복잡하고 문서가 상대적으로 적습니다.

위험 지수: 낮음(설계가 합리적이며 Docker가 공식 지원)

지원 시스템: 최신 Linux(newuidmap/newgidmap 커널 지원 필요, Ubuntu 20.04+, CentOS 8+)

언제 사용할까: 금융이나 의료처럼 회사의 보안 규정이 매우 엄격한 경우, 신뢰할 수 없는 서드파티 컨테이너 이미지를 실행하는 경우, Kubernetes 클러스터에서 모든 pod를 root가 아닌 사용자로 실행해야 하며 프로덕션 머신의 Docker도 rootless로 구성하려는 경우에 적합합니다.


빠른 결정: 무엇을 선택해야 할까?

5가지 방법을 다 읽어도 무엇을 선택해야 할지 모르겠다면 이 결정 트리를 사용하세요.

권한 문제가 생겼나요?
├─ 임시 테스트만 필요한가요?
│  └─ 예 → 방법 1(--user 매개변수)

├─ 팀에서 장기간 유지 관리하는 프로젝트인가요?
│  ├─ 애플리케이션 시작에 root 권한이 필요한가요?
│  │  └─ 예 → 방법 3(entrypoint+gosu)
│  └─ root가 필요하지 않나요?
│     └─ 방법 2(Dockerfile에서 사용자 생성)

├─ 회사 보안 규정상 강제 격리가 필요한가요?
│  ├─ 특권 포트나 특수 기능이 필요한가요?
│  │  └─ 예 → 방법 4(userns-remap)
│  └─ 특권이 필요하지 않나요?
│     └─ 방법 5(Rootless Docker)

└─ 로컬 개발 문제를 빠르게 해결하려는 건가요?
   └─ 방법 1(--user 매개변수)

상황별로 대략 다음과 같이 선택하면 됩니다.

  • 개발 환경: 방법 1(빠르고 효과적)
  • 팀 프로젝트: 방법 2 또는 3(전문적이고 표준화된 방식)
  • 프로덕션 환경: 방법 4 또는 5(보안 우선)

처음부터 가장 복잡한 방법을 사용할 필요는 없습니다. 실제 요구 사항에 맞게 선택하세요. 필요한 만큼이면 충분합니다.

플랫폼별 특수 상황: Mac, Windows, Linux의 차이

”내 컴퓨터에서는 문제없는데?”

익숙한 말이죠? 개발할 때 Mac에서는 잘 돌아가다가 Linux 서버에서 터집니다. 또는 반대로 Linux에서는 문제가 없는데 Windows 개발 머신에서 이상한 현상이 잇따릅니다.

세 플랫폼의 Docker 구현 방식이 크게 다르기 때문입니다.

Linux: 가장 “현실적”이지만 문제가 가장 뚜렷함

Linux의 Docker는 가상 머신 중간 계층 없이 커널을 직접 호출합니다. 프로덕션 환경에 가장 가까운 방식이지만, 권한 문제가 가장 분명하게 나타나는 플랫폼이기도 합니다.

특징:

  • 컨테이너와 호스트가 같은 커널을 공유합니다.
  • UID/GID가 변환 없이 직접 매핑됩니다.
  • 컨테이너는 기본적으로 root(UID=0)로 실행됩니다.
  • bind mount 권한 충돌이 그대로 드러납니다.

모범 사례:

  • 개발 단계에서는 방법 1(--user 매개변수)로 빠르게 해결합니다.
  • 장기 프로젝트에서는 방법 2(Dockerfile에서 사용자 생성)를 사용합니다.
  • 프로덕션 환경에서는 방법 4 또는 5(userns-remap 또는 rootless)를 사용합니다.

흔한 함정:

# 컨테이너가 만든 파일을 삭제할 수 없음
rm: cannot remove 'logs/app.log': Permission denied

# owner 확인
ls -ln logs/
# -rw-r--r-- 1 0 0 ...

# 원인: 컨테이너가 root로 실행되어 root 소유 파일을 생성

해결 방법: docker-compose.yml에 user: "${UID}:${GID}"를 추가합니다.

Mac: 권한은 “느슨”하지만 함정이 있음

Mac의 Docker Desktop은 가벼운 가상 머신(Apple Virtualization 프레임워크) 안에서 실행됩니다. 파일 시스템은 자동 권한 변환을 지원하는 VirtioFS를 사용합니다.

특징:

  • 컨테이너가 만든 파일의 owner는 대개 호스트의 현재 사용자로 변환됩니다.
  • 대부분 권한 문제를 느끼지 못합니다.
  • 하지만 이 “편리함”이 배포할 때 문제를 일으킬 수 있습니다.

알려진 문제:

  • VirtioFS에는 2023~2024년에 중첩 디렉터리 권한이 꼬이는 등 권한 관련 버그가 여러 건 있었습니다.
  • Docker Desktop 4.13+에서 대부분 수정됐지만 여전히 edge case가 있습니다.
  • 여러 단계의 심볼릭 링크를 지나면 권한이 손실될 수 있습니다.

모범 사례:

  • 로컬 개발에서는 이 편리함을 그대로 활용해도 되며 별도 설정이 꼭 필요하지는 않습니다.
  • 하지만 이 편리함에 의존하지 마세요. Dockerfile에서는 여전히 방법 2로 사용자를 만들어야 합니다.
  • 배포 전에 Linux 머신 또는 가상 머신에서 한 번 테스트하세요.

흔한 함정:

# Mac에서는 이렇게 작성해도 문제없음
services:
  app:
    image: myapp
    volumes:
      - ./data:/app/data
# 컨테이너가 root로 실행되지만 파일 owner는 자동으로 현재 사용자가 됨

# Linux에 배포하면 실패
# 파일이 모두 root 소유라 CI 스크립트가 접근할 수 없음

해결 방법: Mac에서 문제가 있든 없든 user 설정을 추가하세요.

services:
  app:
    user: "${UID:-1000}:${GID:-1000}"

Windows: 가장 복잡한 상황

Windows의 Docker Desktop은 WSL2 또는 Hyper-V에서 실행됩니다. NTFS 권한 모델과 Linux ACL은 완전히 다릅니다.

특징:

  • WSL2 모드는 Linux와 비교적 비슷하지만, NTFS와 ext4 같은 서로 다른 파일 시스템 사이에서는 변환이 발생합니다.
  • Hyper-V 모드에는 가상화 계층이 하나 더 있어 권한 변환이 더 복잡합니다.
  • 일부 드라이브가 BitLocker로 암호화되어 있으면 권한 동작이 더욱 이상해질 수 있습니다.

흔한 문제:

# C 드라이브에 bind mount할 때
docker run -v C:\Users\oden\project:/app myimage
# 권한이 뒤섞여 때로는 읽을 수 있지만 쓸 수 없음

# WSL 경로에 bind mount할 때
docker run -v /mnt/c/Users/oden/project:/app myimage
# 조금 낫지만 여전히 문제가 있을 수 있음

모범 사례:

  • bind mount보다 Named Volume을 우선 사용하세요.
    services:
      db:
        image: postgres
        volumes:
          - pgdata:/var/lib/postgresql/data  # volume 사용
    volumes:
      pgdata:  # Docker가 관리하므로 NTFS 권한 문제 방지
  • bind mount가 꼭 필요하다면 프로젝트를 WSL2 파일 시스템 안(\\wsl$\Ubuntu\home\...)에 두세요.
  • 드라이브를 가로지르는 마운트는 피하세요.

알려진 문제:

  • C 드라이브나 다른 NTFS 파티션을 마운트하면 파일 권한 비트가 전부 777로 보일 수 있습니다. 보기에는 불안하지만 실제 권한은 NTFS가 제어합니다.
  • Windows는 심볼릭 링크 지원이 제한적이므로 컨테이너 안에서 링크가 보이지 않을 수 있습니다.
  • 줄바꿈(LF와 CRLF) 문제는 Git과 Docker가 함께 얽히면 더욱 혼란스러워집니다.

크로스 플랫폼 팀 협업: 통일된 전략

팀원 중 누군가는 Mac, 누군가는 Linux, 누군가는 Windows를 사용한다면 어떻게 해야 할까요?

권장 설정:

docker-compose.yml:

services:
  app:
    build:
      context: .
      args:
        UID: ${UID:-1000}
        GID: ${GID:-1000}
    user: "${UID:-1000}:${GID:-1000}"
    volumes:
      - ./src:/app/src

Dockerfile:

FROM node:18

ARG UID=1000
ARG GID=1000

RUN groupadd -g $GID appuser && \
    useradd -m -u $UID -g $GID appuser

WORKDIR /app
RUN chown appuser:appuser /app

USER appuser

.env.example(팀 공유):

# Linux/Mac 사용자
# export UID=$(id -u)
# export GID=$(id -g)

# Windows 사용자는 값 고정 가능
UID=1000
GID=1000

README.md 안내:

## 프로젝트 시작

**Linux/Mac 사용자**:
```bash
export UID=$(id -u) GID=$(id -g)
docker-compose up

Windows 사용자:

# WSL2에서 실행하거나 기본값 1000으로 docker-compose up을 바로 실행
docker-compose up

**핵심 사항**:
- build args와 환경 변수를 사용해 설정을 유연하게 만듭니다.
- Linux 사용자는 실제 UID를 전달하고 Mac/Windows 사용자는 기본값을 사용합니다.
- Dockerfile에서 사용자를 만들어 이미지가 플랫폼에 관계없이 일관되게 동작하도록 합니다.
- 문서에 플랫폼별 차이를 설명합니다.

### 한 문장 요약

- **Linux**: 문제가 가장 뚜렷하지만 해결 방법도 가장 많고 프로덕션과 가장 비슷합니다.
- **Mac**: 보통 문제없지만 편리함에 방심하지 말고 표준 설정을 유지해야 합니다.
- **Windows**: bind mount보다 volume을 우선하고 프로젝트는 WSL2 파일 시스템 안에 두는 것이 좋습니다.

크로스 플랫폼 팀이라면 build args와 user 설정을 사용해 모든 팀원이 정상적으로 작업할 수 있게 하세요.

## 실전 사례: 자주 만나는 상황은 어떻게 해결할까

앞에서 원리, 진단법, 해결 방법, 플랫폼별 차이를 살펴봤습니다. 이제 실제 상황으로 넘어가겠습니다. 가장 흔한 권한 문제 5가지를 하나씩 해결해 봅시다.

### 사례 1: 로컬 개발에서 컨테이너 로그를 삭제할 수 없음

**증상**:
로컬에서 애플리케이션 컨테이너를 실행하면 로그 파일이 생성됩니다. 시간이 지난 뒤 정리하려고 하면 다음과 같은 오류가 발생합니다.
```bash
rm -rf logs/
# rm: cannot remove 'logs/app.log': Permission denied

진단 단계:

# 1단계: 파일 owner 확인
$ ls -ln logs/
total 1024
-rw-r--r-- 1 0 0 524288 Dec 17 14:30 app.log
-rw-r--r-- 1 0 0 524288 Dec 17 14:31 error.log

# UID=0, root가 만든 파일

# 2단계: 컨테이너 신원 확인
$ docker exec myapp id
uid=0(root) gid=0(root) groups=0(root)

# 컨테이너가 실제로 root로 실행 중

# 3단계: 자신의 신원 확인
$ id
uid=1000(oden) gid=1000(oden) groups=1000(oden)

# 나는 1000, 컨테이너는 0. 서로 일치하지 않음!

해결 방법:
docker-compose.yml에 user 설정을 추가합니다.

services:
  myapp:
    image: myapp:latest
    user: "${UID:-1000}:${GID:-1000}"  # 핵심 줄
    volumes:
      - ./logs:/app/logs

실행:

export UID=$(id -u)
export GID=$(id -g)
docker-compose down
docker-compose up

이제 컨테이너가 자신의 UID로 실행되므로 생성된 로그 파일의 owner도 자신이 됩니다.

한 문장 요약: user 설정 한 줄을 추가하면 끝입니다.


사례 2: Django/Flask 애플리케이션의 정적 파일 권한 문제

증상:
Python Web 애플리케이션에서 정적 파일을 수집해야 합니다. collectstatic을 실행한 뒤 다음과 같이 됩니다.

docker exec webapp python manage.py collectstatic
# static/ 폴더 생성

ls -ln static/
# drwxr-xr-x 1 0 0 ...
# owner가 root라 CI 스크립트나 nginx 컨테이너가 접근할 수 없음

원인:
컨테이너가 root로 실행되어 생성된 파일도 root 소유입니다. 이후 nginx 컨테이너가 이 파일을 제공해야 한다면 nginx 컨테이너의 사용자에게 읽기 권한이 없을 수 있습니다.

해결 방법:
Dockerfile에서 애플리케이션 사용자를 만듭니다.

FROM python:3.11

# 애플리케이션 사용자 생성
RUN groupadd -g 1000 appuser && \
    useradd -m -u 1000 -g 1000 appuser

WORKDIR /app

# 의존성 파일을 복사하고 설치(이때는 아직 root이므로 apt-get 등 사용 가능)
COPY requirements.txt .
RUN pip install -r requirements.txt

# 애플리케이션 코드를 복사하고 권한 부여
COPY --chown=appuser:appuser . /app

# appuser로 전환
USER appuser

# 이후 명령은 모두 appuser로 실행
CMD ["gunicorn", "myapp.wsgi:application"]

docker-compose.yml:

services:
  webapp:
    build: .
    volumes:
      - static_volume:/app/static

  nginx:
    image: nginx:alpine
    volumes:
      - static_volume:/usr/share/nginx/html/static:ro  # 읽기 전용 마운트
    ports:
      - "80:80"

volumes:
  static_volume:

핵심 사항:

  • Dockerfile에서 일치하는 사용자(UID=1000)를 미리 만듭니다.
  • bind mount 대신 Named Volume으로 정적 파일을 공유합니다.
  • nginx 컨테이너는 자체 사용자로 volume을 읽고 Docker가 권한을 처리합니다.

한 문장 요약: Dockerfile에서 사용자를 미리 만들고 volume으로 파일을 공유하세요.


사례 3: 데이터베이스 볼륨 권한 문제

증상:
PostgreSQL 또는 MySQL 컨테이너를 시작할 때 오류가 발생합니다.

docker-compose up postgres
# postgres: could not open file "/var/lib/postgresql/data/...": Permission denied

원인:
데이터베이스 이미지는 일반적으로 특정 UID로 전환해 실행됩니다. 예를 들어 postgres 이미지는 UID=999인 postgres 사용자를 사용합니다. bind mount로 데이터 디렉터리를 마운트하면 호스트 디렉터리의 owner가 맞지 않을 수 있습니다.

진단:

# 마운트 디렉터리 확인
ls -ln ./pgdata
# drwxr-xr-x 1 1000 1000 ...
# owner는 1000이지만 postgres 컨테이너는 999가 필요함

# postgres 이미지 사용자 확인
docker run --rm postgres:15 id
# uid=999(postgres) gid=999(postgres) groups=999(postgres)

해결 방법:

방법 A: Named Volume 사용(권장)

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data  # bind mount가 아닌 volume 사용

volumes:
  pgdata:  # Docker가 권한을 자동 처리

방법 B: bind mount가 꼭 필요하다면 미리 권한 설정

# 디렉터리를 만들고 owner 설정
mkdir -p ./pgdata
sudo chown -R 999:999 ./pgdata  # postgres의 UID/GID와 일치

docker-compose.yml:

services:
  postgres:
    image: postgres:15
    volumes:
      - ./pgdata:/var/lib/postgresql/data

주의: 데이터베이스 이미지마다 UID가 다를 수 있습니다.

  • PostgreSQL: 999
  • MySQL: 999
  • MongoDB: 999
  • Redis: 999(우연히 대부분 999입니다)

하지만 모든 버전에서 같다고 보장할 수는 없으므로 docker run --rm <image> id로 확인하는 것이 좋습니다.

한 문장 요약: 데이터베이스에는 Named Volume을 사용해 Docker가 권한을 처리하게 하세요. bind mount가 꼭 필요하다면 먼저 chown을 실행하세요.


사례 4: CI 과정에서 빌드 산출물의 권한이 잘못됨

증상:
CI 과정에 다음 단계가 있습니다.

# .gitlab-ci.yml
build:
  script:
    - docker run --rm -v $CI_PROJECT_DIR:/app builder npm run build
    - ls -l dist/  # 빌드 산출물 확인
    # -rw-r--r-- 1 root root ... (owner가 root)
    - cp dist/* /deploy/  # Permission denied!

CI runner는 일반 사용자로 실행되지만 Docker 컨테이너는 root로 빌드하므로 산출물의 owner가 root입니다. 이후 단계에서는 산출물에 접근할 수 없습니다.

해결 방법:

방법 A: 빌드 컨테이너에서 owner를 명시적으로 변경

# Dockerfile.builder
FROM node:18

WORKDIR /app
COPY package*.json ./
RUN npm install

COPY . .

# 빌드하고 owner 변경
RUN npm run build && \
    chown -R 1000:1000 /app/dist

CMD ["npm", "run", "build"]

방법 B: —user로 빌드 컨테이너 실행

# .gitlab-ci.yml
build:
  script:
    - docker run --rm --user $(id -u):$(id -g) -v $CI_PROJECT_DIR:/app builder npm run build
    - ls -l dist/  # 이제 owner가 현재 사용자임
    - cp dist/* /deploy/  # 문제없이 동작

방법 C: entrypoint로 처리(더 유연함)

FROM node:18

RUN apt-get update && apt-get install -y gosu

COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh

WORKDIR /app
ENTRYPOINT ["/entrypoint.sh"]
CMD ["npm", "run", "build"]

entrypoint.sh:

#!/bin/bash
set -e

# 빌드 실행
npm run build

# OUTPUT_UID가 지정되면 산출물 owner 변경
if [ -n "$OUTPUT_UID" ]; then
    chown -R $OUTPUT_UID:${OUTPUT_GID:-$OUTPUT_UID} /app/dist
fi

CI 설정:

build:
  script:
    - docker run --rm -e OUTPUT_UID=$(id -u) -v $CI_PROJECT_DIR:/app builder

한 문장 요약: 빌드할 때 산출물 owner를 명시적으로 설정하거나 --user로 빌드 컨테이너를 실행하세요.


사례 5: Kubernetes Pod 권한 문제

증상:
K8s에 애플리케이션을 배포했지만 Pod가 시작되지 않습니다.

kubectl logs mypod
# Error: EACCES: permission denied, open '/app/data/config.json'

원인:
K8s의 securityContext가 Pod의 실행 사용자를 제한했거나 volume의 fsGroup 설정이 올바르지 않을 수 있습니다.

진단:

# Pod에 들어가 확인
kubectl exec -it mypod -- id
# uid=1000 gid=1000 groups=1000

# volume 안의 파일 확인
kubectl exec -it mypod -- ls -ln /app/data
# drwxr-xr-x 2 0 0 ...
# owner는 root지만 Pod는 1000으로 실행되어 읽을 수 없음

해결 방법:

Pod spec에 securityContext를 설정합니다.

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  securityContext:
    runAsUser: 1000      # Pod를 UID=1000으로 실행
    runAsGroup: 1000     # GID=1000
    fsGroup: 1000        # volume 안 파일의 group을 1000으로 설정하고 읽기/쓰기 허용

  containers:
  - name: app
    image: myapp:latest
    volumeMounts:
    - name: data
      mountPath: /app/data

  volumes:
  - name: data
    emptyDir: {}

핵심 사항:

  • runAsUser: 컨테이너 프로세스의 UID
  • runAsGroup: 컨테이너 프로세스의 GID
  • fsGroup: volume 안 파일의 group owner이며, 프로세스가 읽고 쓸 수 있도록 합니다.

PersistentVolumeClaim을 사용한다면 다음과 같이 설정합니다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mypvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

---
apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  securityContext:
    fsGroup: 1000  # PVC 안 파일의 group은 1000

  containers:
  - name: app
    image: myapp:latest
    securityContext:
      runAsUser: 1000  # 프로세스를 1000으로 실행
    volumeMounts:
    - name: storage
      mountPath: /app/data

  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: mypvc

한 문장 요약: Pod의 securityContext에서 runAsUser와 fsGroup을 명시적으로 설정하세요.


5가지 사례 요약

상황증상해결 방법권장 방식
로컬 개발 로그를 삭제할 수 없음Permission denieduser 설정docker-compose.yml에 user 추가
정적 파일 수집nginx가 읽을 수 없음Dockerfile에서 사용자 생성USER appuser + volume
데이터베이스 시작 실패데이터 디렉터리에 쓸 수 없음Named VolumeDocker가 권한 처리
CI 빌드 산출물 권한이후 단계에서 접근할 수 없음—user 또는 entrypoint빌드할 때 chown
K8s Pod 권한EACCES 오류securityContextrunAsUser + fsGroup

상황마다 해결 방법이 다르다는 점이 보이시나요? 먼저 진단해 어떤 충돌인지 확인한 뒤 그에 맞는 방법을 적용하는 것이 핵심입니다.

더 읽어보기

결론

글 도입부의 새벽 3시 상황을 기억하나요? “Permission denied”를 바라보면서 관리자인데도 왜 파일을 삭제할 수 없는지 전혀 알 수 없었습니다.

이제는 알 수 있습니다.

근본 원인: Linux는 사용자 이름이 아니라 UID/GID만 인식합니다. 컨테이너 내부의 root(UID=0)가 만든 파일은 호스트의 일반 사용자(UID=1000)가 건드릴 수 없습니다.

진단 방법: 명령 3개면 충분합니다. ls -ln으로 파일 owner를 확인하고, docker exec <container> id로 컨테이너 신원을 확인하며, docker inspect로 마운트 설정을 확인합니다. 1분 안에 문제를 찾을 수 있습니다.

해결 방법: 다음 5가지 중 선택하세요.

  1. —user 매개변수: 빠른 임시 해법으로 로컬 테스트에 적합합니다.
  2. Dockerfile에서 사용자 생성: 팀이 장기 운영하는 프로젝트에 적합한 전문적인 방법입니다.
  3. entrypoint+gosu: root로 초기화해야 하지만 실행할 때는 권한을 낮춰야 하는 경우에 사용합니다.
  4. userns-remap: 엔터프라이즈급 강제 격리 방법입니다.
  5. Rootless Docker: 궁극적인 보안 방법이지만 기능에 제한이 있습니다.

플랫폼별 차이: Mac과 Windows의 Docker Desktop에는 권한 변환 계층이 있어 문제가 잘 드러나지 않습니다. Linux는 커널을 직접 호출하므로 권한 충돌이 그대로 나타납니다. Mac의 편리함에 방심하지 말고 Dockerfile에서 표준 방식으로 설정하는 것이 가장 좋습니다.

실전 경험: 5가지 사례를 통해 로컬 개발, 정적 파일, 데이터베이스, CI 빌드, K8s 배포에서 발생하는 권한 문제와 해결법을 살펴봤습니다. 각 상황에 가장 적합한 방법이 있습니다.

오늘부터 실천하기

오늘 바로 할 일(5분):

  • docker-compose.yml에 user: "${UID:-1000}:${GID:-1000}"를 추가합니다.
  • docker exec <container> id로 프로젝트 컨테이너의 실제 UID를 확인합니다.
  • ls -ln으로 권한 문제 하나를 진단해 봅니다.

이번 주에 할 일(1~2시간):

  • Dockerfile에 ARG UID/GID와 사용자 생성 로직을 추가해 개선합니다.
  • 진단 명령을 팀 wiki 또는 README에 추가합니다.
  • 이 글이 유용했다면 동료에게 공유합니다.

장기적으로 할 일(지속적 개선):

  • 회사 보안 규정에서 요구한다면 userns-remap 또는 rootless를 평가합니다.
  • 프로덕션 환경의 Docker 설정을 검토해 권한 격리를 보장합니다.
  • CI/CD 과정에 권한 검사 단계를 추가합니다.

마지막으로

권한 문제는 기술적이고 지루해 보이지만 본질은 “신원 인증”입니다. 컨테이너는 호스트에서 당신이 누구인지 알지 못하고 숫자만 인식합니다.

UID/GID 매핑 관계를 이해하면 모든 것이 단순해집니다. 더 이상 무작정 chmod 777을 사용하거나 새벽 3시에 권한 문제로 고생할 필요가 없습니다.

상황에 맞는 방법과 명령을 사용하고, 동작 방식과 이유를 함께 이해하세요.

해결 완료입니다.

7분 읽기 · 게시일: 2025년 12월 17일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog