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

빌드노트

Docker에서 root·777인데도 Read-only file system이 나는 이유

root이고 파일 모드가 777이어도 읽기 전용 bind mount에는 쓸 수 없다. Mounts.RW로 마운트 정책을 확인하고, ro/rw의 호스트 파일 변화와 Permission denied 대조를 통해 수정할 컨테이너 생성 설정을 찾는다.

Docker 컨테이너에서 파일에 쓰려는데 Read-only file system이 나온다. 실행 사용자는 root이고 파일 모드도 777이다. 이때는 파일 권한을 더 바꾸기 전에 실패한 경로에 적용되는 bind mount의 RW 값을 확인한다. 읽기 전용으로 연결한 경로는 root로 접근해도 쓰기가 막힌다.

파일 모드는 사용자에게 허용할 접근을 정하고, 마운트의 읽기 전용 설정은 그 연결을 통한 쓰기를 제한한다. chmod로 모드를 바꿔도 마운트 설정은 바뀌지 않는다. Docker bind mount 문서 (새 탭에서 열림)는 readonly 또는 ro로 읽기 전용 연결을 만들고, inspect의 Mounts에서 RW: false를 확인하는 방법을 설명한다.

실패한 파일이 어느 마운트에 속하는지 확인한다

다음은 PowerShell에서 기존 컨테이너의 마운트와 실행 사용자·파일 모드를 조회하는 명령이다. Docker CLI가 그 컨테이너를 관리하는 context에 연결돼 있어야 한다. my-app은 조사할 컨테이너 이름, /data/input.txt는 실제 쓰기에 실패한 파일, /data는 그 상위 폴더로 바꾼다. 생성에 실패해 파일이 아직 없다면 ls의 파일 경로를 빼고 상위 폴더만 조회한다. 두 번째 명령은 실행 중인 Linux 컨테이너에 sh, id, ls가 있어야 한다.

$container = 'my-app'
docker container inspect --format '{{json .Mounts}}' $container
docker exec $container sh -c 'id; ls -ld /data /data/input.txt'

첫 출력에서 실패 경로를 포함하는 Destination을 찾는다. /data/input.txt에 쓰다가 실패했다면 /data나 / 같은 상위 경로의 마운트도 대상이다. 여러 항목이 겹치면 실제 경로에 더 가깝게 연결된 마운트를 확인한다. 그 항목이 Type: bind, RW: false라면 해당 bind가 읽기 전용으로 설정돼 있다. Source는 연결한 호스트 경로이며, 원격 Docker를 쓴다면 내 PC가 아닌 daemon 쪽 경로다. bind mount의 호스트 조건 (새 탭에서 열림)

Mode에 ro가 적혀 있는지만 보면 놓칠 수 있다. 이번 실험에서는 --mount ...,readonly로 만든 연결의 Mode가 빈 문자열이었지만 RW는 false였다. inspect의 형식 지정 (새 탭에서 열림)으로 꺼낸 Mounts에서 읽기 전용 여부를 직접 확인하는 편이 분명하다.

id와 ls는 권한 쪽 단서를 준다. 다만 docker exec로 새로 띄운 프로세스의 사용자가 앱의 실제 실행 사용자와 같다는 보장은 없다. 앱이 시작한 뒤 사용자를 바꾸는 구성이라면 실패한 앱 프로세스의 UID도 따로 확인해야 한다. 컨테이너에 셸이 없어 두 번째 명령이 실패해도 첫 번째 마운트 조회 결과까지 무효가 되는 것은 아니다.

해당 경로의 bind가 RW: true라면 Docker가 그 연결을 읽기 전용으로 설정하지 않았다는 정보다. 그것만으로 쓰기 성공이 보장되지는 않는다. Permission denied라면 실패한 앱 사용자의 파일·상위 디렉터리 권한을 확인한다. Read-only file system이 계속된다면 호스트 원본 파일시스템이나 별도의 하위 마운트까지 조사 범위를 넓힌다. 적용되는 bind 항목 자체가 없다면 이 글의 사례로 원인을 확정할 수 없다. 다른 마운트나 컨테이너 루트 파일시스템의 읽기 전용 설정을 별도로 조사한다.

같은 폴더에서 읽기는 같고 쓰기만 달라졌다

운영 장애 기록이 아니라, 새 호스트 폴더와 합성 파일로 비교한 결과다. input.txt에 host-seed 한 줄을 넣고 같은 폴더를 두 컨테이너의 /lab에 연결했다. 하나에는 readonly를 붙였고 다른 하나에는 붙이지 않았다. 두 프로세스 모두 UID 0이었다. 쓰기 가능 연결에서 실험 폴더와 파일을 777로 맞춘 뒤, 두 연결에서 실제 표시 모드가 모두 777인지 확인했다.

  • 읽기 전용 연결은 RW: false였고 cat /lab/input.txt는 host-seed를 읽었다. 기존 파일에 한 줄 추가하기와 새 파일 생성은 모두 종료 코드 1로 실패했다. 호스트의 파일 내용·SHA-256은 그대로였고 새 파일도 생기지 않았다.
  • 쓰기 가능 연결은 RW: true였고 처음 읽은 내용도 같았다. 한 줄 추가와 파일 생성은 모두 종료 코드 0이었다. 호스트의 input.txt에는 host-seed 다음에 rw-write가 생겼고, created.txt에는 rw-created가 들어갔다.

읽기 전용 연결에서 실행한 추가 입력은 printf 'ro-write\n' >> /lab/input.txt였다. 이어 같은 파일에 chmod 777도 시도했다. 각 명령이 남긴 오류는 다음과 같다.

sh: can't create /lab/input.txt: Read-only file system
chmod: /lab/input.txt: Read-only file system

이미 쓰기 비트가 열려 있어도 연결의 쓰기 제한은 남았다. chmod 시도 자체도 읽기 전용 오류로 끝났다. 모드를 바꾸는 시도로 이 연결을 쓰기 가능하게 만들 수 없다는 관찰이다.

또 하나 구분할 점이 있다. 쓰기 가능 연결에서 파일을 바꾼 뒤 읽기 전용 컨테이너로 다시 읽었더니 rw-write 줄도 보였다. 읽기 전용 bind는 그 연결을 통한 쓰기를 막는다. 호스트나 다른 쓰기 가능 연결에서 일어난 변경까지 막거나, 처음 내용을 고정해 두는 스냅샷을 만드는 것은 아니다.

Permission denied도 별도로 비교했다. Windows 공유 폴더의 모드 표시를 Linux 파일 권한 동작과 같은 것으로 가정하지 않기 위해, 이 대조는 컨테이너 내부 Linux 파일에서 했다. root 소유·모드 444인 파일에 UID 1000으로 추가하려고 하자 종료 코드 1과 Permission denied가 나왔다. root로 그 실험 파일의 모드만 666으로 바꾸자 같은 UID 1000의 추가가 성공했다. 이 값은 권한 차이를 드러내기 위한 실험 조건이다.

Linux의 open 오류 설명 (새 탭에서 열림)은 접근 권한 거부인 EACCES와 읽기 전용 파일시스템에 쓰기를 요청한 EROFS를 구분한다. 위 실험에서는 두 조건이 각각 Permission denied, Read-only file system으로 나타났다. 오류 문구가 권한 거부라면 실패한 앱 사용자의 권한과 경로 접근을 확인하고, 읽기 전용 오류라면 쓰기 대상의 마운트 상태부터 확인할 근거가 된다. 두 조건이 동시에 잘못된 경우까지 오류 순서를 실험한 것은 아니다.

쓰기가 필요한 경로의 생성 설정을 고친다

실패한 경로의 bind가 읽기 전용임을 확인했다면, 그 경로에 앱이 써야 하는지 먼저 정한다. 입력 설정을 보호하려고 연결한 폴더라면 읽기 전용을 유지하고 앱의 출력 경로를 별도의 쓰기 가능한 데이터 경로로 바꾼다. 실제 저장 폴더를 실수로 읽기 전용에 둔 경우에는 그 bind 항목의 생성 설정을 고친다.

docker run을 쓴다면 해당 --mount 항목의 readonly를 제거하거나 -v 항목의 :ro를 :rw로 바꾼다. Compose라면 services.<서비스명>.volumes 안에서 같은 target의 항목을 찾는다. 긴 문법의 read_only: true를 false로 바꾸거나 짧은 문법의 :ro를 :rw로 바꾼다. Compose volumes 문법 (새 탭에서 열림)

변경한 구성으로 컨테이너를 재생성해야 한다. 아래 명령은 현재 폴더의 기본 Compose 파일을 쓰고 서비스 이름이 app인 경우다. 평소 -f, -p, --project-directory를 지정했다면 같은 옵션을 유지하고 서비스 이름도 자신의 것으로 바꾼다. compose up 문서 (새 탭에서 열림)는 생성 이후 구성에 변경이 있으면 컨테이너를 재생성한다고 설명한다.

docker compose up -d --no-deps app

이 수정도 별도의 합성 Compose 서비스에서 실행했다. 같은 bind의 read_only를 true에서 false로 바꾼 뒤 위 명령을 실행하자 컨테이너 ID가 달라졌고, 새 컨테이너의 RW가 true로 바뀌었다. 이전에 실패하던 파일 추가도 성공해 호스트 파일에 compose-write 줄이 생겼다. 재생성 뒤에는 앞의 inspect로 새 연결을 다시 확인한다. 단순 compose restart는 구성 파일 변경을 반영하지 않는다 (새 탭에서 열림).

검증 환경은 2026년 10월 21일 KST의 Windows 11, 로컬 Docker Desktop Linux 엔진 29.8.0, BusyBox 1.37.0, Compose 5.5.1이었다. 본문의 PowerShell 명령도 실험 대상 이름과 경로로 바꿔 실행했다. 기존 컨테이너·폴더는 연결하지 않았다. 확인한 범위는 일반 bind의 읽기·쓰기 정책과 별도 Linux 파일 모드 대조다. 중첩 마운트, ACL·SELinux 정책, rootless Docker와 원격 daemon은 실행하지 않았으므로 RW: true인데도 계속 실패하는 환경의 원인까지 이 결과로 단정할 수는 없다.

← 전체 글 보기