오늘 밤 10시 59분, 운영 서버의 백업 버튼을 눌렀다.

빨간 토스트가 떴다.

백업 실패: pg_dump: error: could not open output file "/app/scripts/db/backups/finance_20260304.sql": Permission denied

이런 에러는 항상 타이밍이 나쁘다. 뭔가 중요한 걸 하려고 할 때만 튀어나온다.


범인 찾기

에러 메시지는 명확했다. 파일을 열 수 없다. 권한 없음.

먼저 컨테이너 내부를 들여다봤다.

docker exec oikos_finance_backend ls -la /app/scripts/db/backups/
drwxr-xr-x 2 root root  157 Mar  1 06:54 .

디렉토리 소유자: root. 권한: 755. 앱이 실행되는 사용자가 누군지 확인해봤다.

docker exec oikos_finance_backend whoami
# appuser

docker exec oikos_finance_backend id
# uid=1000(appuser) gid=1000(appuser)

아, 그렇구나. 컨테이너 안의 앱은 appuser로 돌고 있는데, backups/ 디렉토리는 root 소유에 755 권한이다. 755는 "소유자(root)만 쓸 수 있고, 남은 읽고 실행만 가능하다"는 뜻이다.

appuser는 남이다.


컨테이너 안에서 고치면 되지 않나?

당연히 그 생각이 먼저 들었다.

docker exec oikos_finance_backend chmod 777 /app/scripts/db/backups
# chmod: changing permissions of '/app/scripts/db/backups': Operation not permitted

안 된다. appuser는 자기 소유가 아닌 디렉토리의 권한을 바꿀 수 없다. 이건 리눅스의 기본 원칙이다. 파일 권한 변경은 소유자이거나 root여야 한다.

그러면 어디서 고쳐야 하나. 호스트다.


진짜 원인: 볼륨 마운트의 소유권 상속

Docker 볼륨 마운트는 이렇게 생겼다.

호스트: /root/oikos_finance/backups  →  컨테이너: /app/scripts/db/backups

docker inspect로 마운트 구성을 확인하니 딱 세 가지가 마운트되어 있었다.

/root/oikos_finance/instance  →  /app/instance
/root/oikos_finance/data      →  /app/data
/root/oikos_finance/backups   →  /app/scripts/db/backups

핵심은 이거다. Docker는 볼륨을 마운트할 때 호스트 디렉토리의 소유권과 권한을 그대로 들고 온다. 컨테이너 안에서 보이는 /app/scripts/db/backups의 소유자가 root인 이유는, 호스트의 /root/oikos_finance/backupsroot 소유이기 때문이다.

서버를 처음 세팅할 때 root로 SSH 접속해서 디렉토리를 만들었을 거다. 자연스럽게 root 소유가 된다. 그 사실을 잊고 지나간다. 그러다 몇 달 후 앱이 그 디렉토리에 뭔가를 쓰려 할 때 이렇게 터진다.


해결: 호스트에서 소유권 변경

chown -R 1000:1000 /root/oikos_finance/data \
                   /root/oikos_finance/instance \
                   /root/oikos_finance/backups

1000appuser의 uid다. 컨테이너 안에서 uid=1000(appuser)라고 떴던 그 숫자.

호스트는 appuser라는 이름을 모른다. 하지만 숫자로는 소통할 수 있다. chown 1000:1000은 "uid 1000인 사용자에게 줘라"는 말이다. 컨테이너 안에서는 그게 appuser다.

그러고 나면 컨테이너 내부에서 보이는 소유자가 appuser로 바뀐다.

drwxr-xr-x 2 appuser appuser  157 Mar  1 06:54 .

백업 버튼을 다시 눌렀다. 됐다.


backups/만 터졌을까

instance/에는 recurring_config.json이라는 파일이 있는데, 이건 앱이 쓰기 때문에 예전에 이미 chmod 666으로 개별 파일 권한을 고쳐놨다. 파일 단위 응급처치였다.

data/는 비어 있어서 아직 안 터졌다. 다음 마감 때 주차 폴더를 새로 만들려 하면 거기서 터진다. 조용히 기다리는 시한폭탄.

그리고 backups/는... 오늘 처음으로 앱에서 백업을 시도했으니까 오늘 터진 거다.


배운 것

Docker 볼륨 마운트를 쓸 때, 호스트 디렉토리의 소유권은 컨테이너가 사용하는 uid와 맞춰야 한다.

구체적으로는:

  • Dockerfile에서 앱 유저를 생성할 때의 uid를 기억해두자 (보통 1000)
  • 운영 서버에서 마운트 디렉토리를 새로 만들 때 항상 chown 1000:1000 하자
  • 컨테이너 안에서 chmod로 고치려 해도 소유자가 아니면 안 된다

그리고 하나 더. 에러는 항상 "처음 그 경로를 쓰려 할 때" 터진다. 설치할 때 비어있던 디렉토리는 조용하다. 그래서 놓친다. 마운트되는 모든 디렉토리를 처음부터 chown해두는 습관이 이런 새벽 디버깅을 막아준다.


해결 시간: 약 10분. 핵심 명령어: docker inspect, chown -R 1000:1000.