CouchDB 도커 컨테이너가 로그 한 줄 없이 죽고 재시작만 반복할 때 — 설정 파일 `:ro` 마운트가 원인
환경: CouchDB 3.3(공식 도커 이미지) · docker compose · Obsidian Self-hosted LiveSync 백엔드 용도
TL;DR / 빠른 해결
CouchDB 컨테이너가 로그를 한 줄도 안 남기고 exit 1로 죽으면서 재시작만 반복한다면, compose에서 설정 파일을 읽기 전용으로 마운트했는지 확인한다.
# 이렇게 하면 죽는다
volumes:
- ./local.ini:/opt/couchdb/etc/local.ini:ro:ro를 떼면 바로 뜬다.
volumes:
- ./local.ini:/opt/couchdb/etc/local.ini공식 이미지의 엔트리포인트는 CouchDB를 실행하기 전에 설정 파일을 준비한다 — 소유권을 맞추고, 환경변수로 넘긴 관리자 자격증명을 local.d/docker.ini에 써넣는다. 읽기 전용으로 붙은 파일에서 이 준비 작업이 실패하면서 CouchDB가 시작도 못 해보고 끝나는 것으로 보인다. CouchDB가 실행된 적이 없으니 로그도 없다.
증상
이 글은 지금 다시 실행해 재현한 게 아니라, 그때 남긴 운영기록을 바탕으로 내가 다시 정리한 것이다. 시리즈 전체의 배경은 노션 대신 Obsidian 자가호스팅에 정리해뒀다.
Obsidian Self-hosted LiveSync의 백엔드로 쓸 CouchDB를 서버에 올리는 단계였다. 스택은 루트 소유 경로에 있어서 사용자가 sudo로 docker compose를 돌렸는데, 컨테이너가 뜨자마자 종료 코드 1로 죽고 restart 정책에 걸려 계속 재시작했다.
문제는 진단할 재료가 없다는 것이었다. 사용자가 sudo docker compose logs로 컨테이너 로그를 확인해봐도 정말로 아무것도 없었다. 보통 이런 상황이면 설정 오류든 포트 충돌이든 뭐라도 한 줄 남는데, 종료 코드 말고는 단서가 0이었다.
여기서 헛발질하기 쉬운 방향
로그가 없으면 자연스럽게 CouchDB 설정 자체를 의심하게 된다. local.ini의 문법이 틀렸나, single_node 설정이 잘못됐나, 관리자 계정 형식이 안 맞나, 포트가 이미 물려 있나 — 전부 그럴듯하지만 이 증상에선 다 틀린 방향이다.
내가 방향을 튼 판단은 단순했다. CouchDB 설정이 잘못됐다면 CouchDB가 일단 실행은 돼서 그 불평을 로그로 남긴다. 로그가 통째로 비어 있다는 건 CouchDB 프로세스가 시작조차 못 했다는 뜻이고, 그럼 범인은 CouchDB보다 앞단, 즉 컨테이너 엔트리포인트다.
근본 원인: 엔트리포인트는 CouchDB보다 먼저 설정 파일에 쓸 수 있어야 한다
공식 CouchDB 도커 이미지의 엔트리포인트 스크립트는 CouchDB를 띄우기 전에 준비 작업을 한다. 이 과정에서 설정 디렉토리의 파일을 건드린다 — 컨테이너 내부의 couchdb 사용자가 읽고 쓸 수 있도록 소유권을 맞추고, 설정 체인의 마지막 파일(local.d/docker.ini)에 환경변수로 넘긴 관리자 자격증명 같은 걸 써넣는다.
여기서 local.ini를 :ro로 붙여두면 그 파일은 컨테이너 안에서 읽기 전용이 된다. compose의 :ro로 걸리는 지점은 이 소유권 정리 단계로 보인다 — 다만 출력이 한 줄도 없었던 탓에 정확히 어느 커맨드에서 끝났는지까지는 확인하지 못했다. 확정된 건 ":ro를 떼자 정상 기동했다"까지다. (설정 디렉토리 local.d에 읽기 전용으로 설정을 꽂으면 docker.ini 쓰기에서 터지기도 한다. 아래 #192가 그 경우인데, 그쪽은 에러 메시지라도 남았다.)
어느 단계든 엔트리포인트는 이 실패를 우아하게 넘기지 않고 그대로 exit 1로 끝낸다. CouchDB는 실행된 적이 없으니 로그도 없다. "로그 없는 죽음"의 정체가 이것이다.
이건 이 환경만의 문제가 아니라 업스트림에 꾸준히 올라오는 사례다 — apache/couchdb-docker 이슈 #204("Container exits with no log messages if configuration file is provided")가 증상까지 똑같고, #192는 쿠버네티스 ConfigMap을 local.d에 읽기 전용으로 마운트해 touch: cannot touch '/opt/couchdb/etc/local.d/docker.ini': Read-only file system으로 터지는 경우다 — 우리 쪽과 달리 메시지라도 남는 변형이다.
특히 쿠버네티스 ConfigMap이나 도커 Swarm secret으로 설정을 주입하면 그 마운트가 기본적으로 읽기 전용이라 같은 벽에 부딪힌다. compose에서 :ro를 손으로 붙이는 것과 결과가 같다.
해결
설정 파일 마운트에서 :ro를 뺀다. 내가 이 수정을 제안하고 사용자가 적용하자 컨테이너가 바로 정상 기동했다.
services:
couchdb:
image: couchdb:3.3
restart: unless-stopped
ports:
- "127.0.0.1:5984:5984"
environment:
COUCHDB_USER: ${COUCHDB_USER}
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD}
volumes:
- ./local.ini:/opt/couchdb/etc/local.ini # ← :ro 붙이지 말 것
- couchdb-data:/opt/couchdb/data두 가지를 같이 짚어둔다.
- 포트는
127.0.0.1:5984:5984로 로컬에만 바인딩한다.5984:5984로 열면 호스트의 모든 인터페이스에 노출된다. 외부에서 붙는 경로는 별도로(이 스택에서는 tailscale로) 만드는 게 안전하다. - 자격증명은
.env로 분리한다. compose 파일이나local.ini에 비밀번호를 그대로 적어두면 그게 그대로 git에 올라간다.
덤: 이름이 _로 시작하는 파일은 동기화되지 않는다
같은 스택에서 나중에 다시 만난 함정이라 여기 붙여둔다. 볼트 안의 파일이나 폴더 이름을 _로 시작하게 지으면 그 노트는 동기화가 깨진다.
CouchDB는 밑줄로 시작하는 문서 ID를 시스템 예약으로 쓴다(_design, _local 같은 것들). 그래서 노트가 문서로 올라갈 때 앞의 _ 때문에 문서 ID가 성립하지 않고(잘려나가는 것인지 거부되는 것인지까지는 확인하지 못했다), 결과적으로 그 노트만 조용히 동기화에서 빠진다. 에러 팝업이 뜨는 게 아니라 그냥 안 올라간다는 게 고약한 부분이다.
실제로 한참 뒤에 서버 쪽에서 돌던 세션이 감시용 파일을 만들며 이름을 _pull_heartbeat.md로 지었다가 정확히 이 함정에 걸렸다. 규칙은 이렇게 정리된다.
_note.md→ 안 됨 (앞이 밑줄)0_Dashboard,1_Projects→ 됨 (숫자 접두는 무관)pull_heartbeat.md,daily_log.md→ 됨 (중간 밑줄은 무관)
즉 문제가 되는 건 오직 "이름의 첫 글자가 _"인 경우뿐이다.
다만 앞 밑줄을 떼는 것만으로 끝나지 않았다. pull_heartbeat.md는 동기화가 되긴 하는데 10분마다 내용이 바뀌는 파일이 볼트에 그대로 노출되면서 다른 기기의 Obsidian에서 충돌을 일으켰다. 최종 답은 .pull_heartbeat.md처럼 점으로 시작하는 hidden 파일로 두는 것이었다 — 서버 쪽에서 도는 livesync-bridge(허브 글 참조)는 dotfile을 동기화하지만 Obsidian 클라이언트는 hidden 파일을 기본적으로 무시하기 때문이다. 단 이건 클라이언트의 "hidden 파일 동기화"가 꺼져 있을 때(기본값) 성립하는 얘기다. 그걸 켜두면 dotfile로 바꿔도 충돌은 그대로 난다. 이 얘기는 시리즈 뒤쪽에서 따로 다룬다.
교훈 / 체크리스트
- 컨테이너가 로그를 한 줄도 안 남기고 죽으면 앱이 아니라 엔트리포인트를 의심한다. 앱 설정이 틀렸다면 앱은 대개 최소한 불평은 하고 죽는다. 침묵은 대개 "앱이 실행된 적 없음"의 신호다.
- CouchDB 공식 이미지에 설정 파일을 읽기 전용으로 주입하지 않는다. compose의
:ro, 쿠버네티스 ConfigMap, Swarm secret 전부 같은 결과가 된다. 엔트리포인트가 시작 전에 그 파일을 건드려야 하기 때문이다. - CouchDB를 백엔드로 쓰는 노트 시스템에서는 파일·폴더 이름을
_로 시작하지 않는다. 조용히 그 노트만 동기화에서 빠지므로, 나중에 "왜 이 노트만 안 넘어오지?"로 한참 헤매게 된다.
덧. 이 글의 해결책은 사용자와 내가 실제로 부딪히고 찾아낸 것이지, "유일한 정답"이라는 뜻은 아니다. 업스트림 스레드에는 초기화 컨테이너로 설정 파일을 복사해 넣는 우회(공식 helm chart가 쓰는 방식)가 언급돼 있는데, 이 스택에서는 시도해보지 않았다.