빌드노트
read-only 컨테이너에서 root도 /tmp에 쓰지 못하는 이유
root여도 읽기 전용 rootfs에는 임시 파일을 만들 수 없다. HostConfig.ReadonlyRootfs와 두 마운트 설정을 확인하고, /tmp에만 tmpfs를 연결해 쓰기를 허용하면서 나머지 rootfs는 읽기 전용으로 유지하는 방법을 실제 비교로 설명한다.
--read-only로 실행한 Docker 컨테이너에서 앱이 /tmp에 파일을 만들다가 Read-only file system으로 실패한다. root로 바꿔 실행해도 같은 오류가 난다면 파일 모드보다 루트 파일시스템(rootfs)의 읽기 전용 설정과 /tmp의 마운트를 먼저 확인한다. 임시 파일이 필요한 경우에는 rootfs를 읽기 전용으로 유지하면서 /tmp에만 쓰기 가능한 tmpfs를 연결할 수 있다.
root는 실행 사용자를 가리키고, --read-only는 rootfs를 읽기 전용으로 마운트하는 옵션이다. 사용자에게 쓰기 권한이 있어도 읽기 전용 파일시스템에는 파일을 만들 수 없다. Docker run 문서 (새 탭에서 열림)도 rootfs의 쓰기 제한과 별도 저장 경로를 함께 사용하는 구성을 설명한다.
rootfs와 실패 경로의 마운트를 함께 본다
다음은 기존 컨테이너의 설정만 읽는 PowerShell 명령이다. Docker CLI가 대상 엔진에 연결돼 있어야 하며, my-app을 조사할 컨테이너 이름이나 ID로 바꾼다. 중지된 컨테이너도 조회할 수 있고 컨테이너 안에 셸이 없어도 된다. 조회 자체가 실패한다면 먼저 대상 이름과 Docker context를 확인한다.
$container = 'my-app'
docker container inspect --format 'rootfs_ro={{.HostConfig.ReadonlyRootfs}} user={{.Config.User}}' $container
docker container inspect --format '{{json .Mounts}}' $container
docker container inspect --format '{{json .HostConfig.Tmpfs}}' $container
첫 출력의 rootfs_ro=true는 rootfs를 읽기 전용으로 만들었다는 뜻이다. 필드 위치는 HostConfig.ReadonlyRootfs다. Config.User는 설정된 실행 사용자이며, rootfs의 읽기 전용 여부를 알려 주는 값은 아니다. 아래 실험에서 명시한 --user 0:0은 user=0:0으로 나왔다. 앱이 기동 후 사용자를 바꾸는 경우까지 이 설정값으로 알 수는 없다. 필요한 필드를 골라 읽는 문법은 container inspect 문서 (새 탭에서 열림)에 있다.
이어서 실패한 경로를 포함하는 마운트를 찾는다. /tmp/work/file이라면 /tmp나 그 아래 디렉터리에 연결된 항목을 본다. 심볼릭 링크라면 실제 대상 경로를 따라가고, 마운트가 겹치면 그 경로에 가장 가깝게 적용되는 항목을 확인한다. Mounts에 /tmp의 Type: tmpfs, RW: true가 있으면 그 경로는 rootfs와 별개로 쓰기 가능하게 연결돼 있다. 다만 앱 사용자의 디렉터리 접근 권한까지 보장하는 값은 아니다.
Mounts가 비어 있다는 이유만으로 tmpfs가 없다고 결론 내리면 안 된다. 이번 Engine 29.8.0에서 --tmpfs /tmp:rw,size=1m,mode=1777로 만든 경우 Mounts는 []였고, HostConfig.Tmpfs에 {"/tmp":"rw,size=1m,mode=1777"}가 나왔다. 반면 뒤에서 사용할 --mount type=tmpfs,... 방식은 Mounts에 표시됐다. 두 필드를 함께 보는 이유다.
rootfs_ro=true이고 /tmp에 적용되는 별도 마운트도 없다면 /tmp는 읽기 전용 rootfs 안에 있다. 반대로 rootfs_ro=false이거나 이미 쓰기 가능한 마운트가 있다면 이 설정만으로 실패 원인을 확정할 수 없다. 실제 파일 경로와 하위 마운트를 확인한다. 오류가 Permission denied라면 실패한 앱 프로세스의 UID와 디렉터리 권한도 확인해야 한다.
같은 root로 실행해도 쓰기 결과가 달랐다
운영 장애 기록이 아니라 새 Linux 컨테이너에서 합성 파일로 비교한 결과다. 2026년 10월 24일 KST에 Windows 11의 Docker Desktop 4.92.0, Linux/amd64 Engine 29.8.0, Alpine 3.22.7로 실행했다. 모든 조건에서 실행 사용자는 UID 0, /tmp의 모드는 1777이었다. 기존 데이터나 볼륨은 연결하지 않았고 네트워크도 차단했다.
동일한 printf temporary > /tmp/rootfs-probe.txt로 파일을 만들고 내용을 읽었다. 대조 경로는 별도 마운트가 없는 /rootfs-probe.txt였으며, 여기에는 printf outside > /rootfs-probe.txt를 실행했다.
- 일반 컨테이너에서는 두 쓰기가 모두 성공했다.
/tmp파일을 읽으면temporary, 대조 파일을 읽으면outside가 나왔다. --read-only만 추가하면 두 쓰기가 모두 실패했다./tmp에chmod 1777을 실행하는 시도도 실패했다.--read-only를 유지하고/tmp에 tmpfs를 연결하면/tmp쓰기와 읽기는 성공했다./rootfs-probe.txt쓰기는 계속 실패했다.
--read-only만 준 컨테이너에서 /tmp 쓰기와 권한 변경이 각각 남긴 오류 원문은 다음과 같다.
sh: can't create /tmp/rootfs-probe.txt: Read-only file system
chmod: /tmp: Read-only file system
Linux의 open(2) 오류 설명 (새 탭에서 열림)은 읽기 전용 파일시스템에 쓰기를 요청하는 경우를 EROFS로 구분한다. 위 결과에서는 이미 root였고 디렉터리의 쓰기 비트도 열려 있었다. 모드를 더 넓히는 조치로 해결할 조건이 아니었다.
tmpfs를 추가한 뒤에도 HostConfig.ReadonlyRootfs는 true였다. 컨테이너 안의 /proc/mounts에서도 /는 ro, /tmp의 tmpfs는 rw였다. rootfs 전체를 쓰기 가능하게 바꾼 것이 아니라 /tmp에 다른 파일시스템을 연결한 결과다. --tmpfs 방식으로도 같은 쓰기 성공·실패 차이를 확인했다.
필요한 임시 경로만 tmpfs로 제공한다
앱이 실제로 /tmp를 임시 작업 공간으로 쓰고 그 파일을 중지 뒤에 보존할 필요가 없다면, 컨테이너 생성 설정에서 --read-only를 유지하고 --mount type=tmpfs,dst=/tmp,tmpfs-size=1048576,tmpfs-mode=1777을 추가한다. 실행 중인 컨테이너에 chmod로 이 설정을 덧붙이는 방식이 아니라, 변경한 생성 설정으로 컨테이너를 재생성해야 한다.
다음 명령은 앱 대신 짧은 파일 쓰기로 그 설정을 확인한다. PowerShell과 Linux Docker 엔진, 로컬에 준비된 alpine:3.22 이미지가 필요하다. --pull never이므로 이미지가 없으면 실행되지 않는다. 기존 컨테이너를 변경하지 않고 새 컨테이너를 실행한 뒤 종료하면 제거한다.
docker run --rm --pull never --network none --user 0:0 --read-only `
--mount type=tmpfs,dst=/tmp,tmpfs-size=1048576,tmpfs-mode=1777 `
alpine:3.22 sh -c 'printf temporary > /tmp/rootfs-probe.txt && cat /tmp/rootfs-probe.txt'
실제 출력은 temporary였고 종료 코드는 0이었다. 같은 명령에서 --mount 인자를 빼면 앞의 읽기 전용 오류가 나며, --read-only까지 빼면 일반 컨테이너처럼 쓸 수 있었다. 위 옵션은 /tmp에만 적용된다. 앱이 /var/tmp나 다른 작업 경로를 사용한다면 /tmp를 열어도 그 쓰기는 해결되지 않으므로 실제 임시 경로를 맞춰야 한다.
여기서 1 MiB는 작은 실험 파일을 위한 상한이다. 실제 앱에는 작업 파일 크기와 컨테이너 메모리 한도를 고려해 크기를 정한다. 1777은 공유 임시 디렉터리의 모드로 사용한 값이며, 앱 전용 경로의 사용자·그룹·모드는 실제 실행 사용자에 맞춘다. 이 실험은 root로 비교했으므로 비 root 앱의 권한 문제까지 검증한 것은 아니다.
Docker tmpfs 문서 (새 탭에서 열림)에 따르면 tmpfs 내용은 컨테이너가 중지되면 보존되지 않고, 이미 파일이 있는 디렉터리에 연결하면 원래 파일이 가려진다. 따라서 재시작 뒤에도 필요한 업로드 원본이나 처리 기록을 이 경로에만 저장해서는 안 된다. tmpfs를 “절대 디스크에 기록되지 않는 비밀 저장소”로 해석해서도 안 된다. Linux의 swap에 기록될 수 있다.
확인한 범위는 위 Linux 엔진의 작은 텍스트 파일 쓰기와 마운트 상태다. 실제 앱의 임시 경로 선택, 용량 부족·메모리 고갈, Windows 컨테이너와 다른 엔진 버전은 시험하지 않았다. 자신의 컨테이너를 재생성한 뒤에는 앞의 조회로 rootfs가 여전히 읽기 전용인지, 필요한 경로에만 쓰기 가능한 마운트가 생겼는지 다시 확인한다.