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 같은 것들). 실제로 _foo라는 ID로 문서를 만들어보면 Only reserved document ids may start with underscore.로 거부한다.
그런데 볼트 파일이 그 거부에 직접 걸리는 일은 없다. 대신 이렇게 된다. Obsidian 플러그인은 루트의 _note.md를 /_note.md라는 ID로 올린다 — 예약을 피하는 형태다. 그리고 서버 쪽 브리지가 그 문서를 파일로 내려쓸 때 앞의 밑줄을 떼어 note.md로 저장하고, 자기가 만든 그 파일을 다시 새 문서로 올린다. 결과적으로 파일 하나가 문서 두 개로 갈라지고 이름이 달라진다. 안 올라가는 게 아니라 다른 이름으로 올라가는 게 이 함정의 실제 모습이다.
한 가지 덧붙이면, 이 글을 처음 쓸 때 나는 이 항목을 "우리가 실제로 걸렸던 함정"으로 적었다. 그건 사실이 아니었다. 그때 문제로 지목했던 파일(_pull_heartbeat.md)은 3_Resources/system/ 아래에 있었다. 그 경로는 3으로 시작하니 문서 ID도 밑줄로 시작하지 않고, 그래서 애초에 이 문제와 무관했다. 나중에 서버를 직접 열어보고서야 그 파일이 멀쩡히 올라가 있었다는 걸 확인했다. 규칙은 이렇게 정리된다.
- 루트의
_note.md→ 안 됨 (이름이 갈라진다) - 루트의
_폴더/메모.md→ 안 됨 (폴더 이름의 밑줄이 떨어져 나간다) 0_Dashboard,1_Projects→ 됨 (숫자 접두는 무관)pull_heartbeat.md,daily_log.md→ 됨 (중간 밑줄은 무관)notes/_draft.md→ 됨 (경로가n으로 시작한다)
즉 판정하는 건 파일 이름이 아니라 볼트 루트 기준 경로의 첫 글자다. 헷갈릴 바에는 어디서든 안 쓰는 게 편하다.
_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가 쓰는 방식)가 언급돼 있는데, 이 스택에서는 시도해보지 않았다.