‘폰 서버 안정화 일지’ 두 번째 글이다. 서버 상태 화면에 “Claude 컨테이너가 두 개"라고 뜬 걸 보고 추적을 시작했는데, 라벨 버그 하나와 그보다 훨씬 큰 문제 하나가 같이 나왔다.
컨테이너가 두 개?
상태 페이지의 상위 프로세스 표에 proot --kill-on-exit 래퍼가 두 줄 보였고, 비고란에는 둘 다 “Claude 세션을 감싸는 proot 컨테이너"라고 적혀 있었다. Claude 세션은 하나뿐이다. 상태 수집기 collect.mjs를 보니 proot 래퍼면 안쪽 명령을 보지 않고 전부 같은 라벨을 붙이고 있었다. 안쪽 명령(claude --continue, pm2 resurrect, caddy, admin-server 등)으로 구분해 표시하도록 고쳤다(server-check 커밋 326e5f9). 두 번째 래퍼는 Claude가 아니라 다른 서비스의 proot였다.
진짜 문제: 부팅 스크립트가 9번 실행됐다
그런데 그 “다른 proot"의 명령을 들여다보니 pm2 resurrect를 감싼 래퍼였고, 하나가 아니었다. ps로 세어 보니 전날 16:10 부팅 직후 약 10초 사이에 start-iris.sh가 9번 실행된 흔적이 있었다. tmux 세션들은 has-session 가드가 있어 하나씩만 떴지만, 가드가 없는 이 줄은 9번 다 실행됐다.
proot-distro login ubuntu -- bash -lc 'service cron start; pm2 resurrect' >/dev/null 2>&1 &
결과는 proot 래퍼 9개, 소켓을 서로 잡고 멈춘 pm2 resurrect 클라이언트 9개, 중복 PM2 God Daemon 8개. 합쳐서 26개 프로세스가 하루 넘게 떠 있었고 메모리를 약 190MB 먹고 있었다. 진짜 pm2 데몬은 그중 하나도 아니었다. 워치독이 16:15에 “hts-recorder 비정상"으로 판정해 새로 띄운 것이 실제 데몬이었다. 스왑 사용률이 99.7%인 폰에서 190MB는 작은 낭비가 아니다.
왜 9번 실행됐는지는 이 컨테이너에서 볼 수 없었다. Termux:Boot나 MacroDroid 쪽에서 재실행 트리거가 반복된 것으로 추정했다. 원인을 못 잡으면 결과를 막아야 한다.
수정: flock 잠금과 pm2 가드
세 가지를 적용했다.
- 중복 프로세스 26개를 정리했다. 진짜 데몬 PID는
/root/.pm2/pm2.pid로 확인한 뒤 나머지만 죽였다. pm2 resurrect줄을 잠금 아래 두었다. 부팅 스크립트와 워치독 양쪽 모두다.
flock -n /root/.pm2-resurrect.lock pm2 resurrect
-n은 잠금을 못 잡으면 기다리지 않고 바로 실패한다. 동시에 9번 불려도 한 번만 실행된다. 여기에 pm2 ping으로 데몬이 이미 응답하면 건너뛰는 가드를 더했다.
3. 재해복구 문서와 설정 사본을 같은 커밋에서 갱신했다.
이 경합은 그 뒤 한 번 더 실제 장애를 냈다. 어느 날 아침 체결 기록기가 전날 10:14부터 멈춰 있었는데, pm2 list는 비어 있고 pm2 데몬 2개와 pm2 resurrect 4개가 서로 소켓을 잡고 멈춘 상태였다. 그날 복구 절차를 재해복구 문서에 적어 두었다.
pkill -f "pm2 resurrect"; sleep 1; pkill -f "God Daemon"; sleep 2
pm2 resurrect && pm2 list
service cron start && pgrep -a cron
워치독이 cron을 못 고치는 구조
이 사건에서 더 불편한 사실이 드러났다. 그날 Ubuntu 안 cron 데몬도 꺼져 있었다. 워치독은 cron이 5분마다 실행하는 스크립트다. cron이 죽으면 워치독도 안 돌고, 채점도 안 돌고, Gemini 병행 실행도 멈춘다. 그런데 cron을 올리는 건 바로 그 부팅 스크립트의 멈추기 쉬운 줄이다. 감시자가 자기 발판을 못 고치는 구조다. 완전한 해법은 아니지만, 부팅 스크립트에서 cron 기동을 pm2 복구와 분리해 먼저 확인하도록 했고, 재부팅 뒤 pgrep -a cron을 확인하는 걸 점검 목록에 넣었다.
곁가지 두 개
같은 날 “Claude가 오류로 종료돼 다시 켰다"는 문제도 추적했다. 원인은 OAuth 토큰 갱신 실패였다. 토큰 갱신이 실패하면 Remote Control 연결이 끊기고 로그인까지 풀린다. 화면의 오류 문구는 복구할 수 없어서, 다음부터는 재실행 전에 tmux capture-pane -t claude:0 -p -S -2000으로 화면을 파일로 남기기로 했다.
또 하나, 좀비를 찾으려고 find /를 돌렸다가 수십 분을 날렸다. 폰에서 파일시스템 전체 find는 사실상 끝나지 않고 좀비 프로세스를 남긴다. “전체 find 금지, 알려진 루트만 검색, 백그라운드 명령 뒤엔 잔여 프로세스 확인"을 규칙으로 적었다.
정리
- 멱등하지 않은 줄은 여러 번 불리면 반드시 사고가 난다. 트리거 원인을 못 잡으면
flock -n으로 결과를 막는다. - 상태 화면의 라벨은 안쪽 명령까지 봐야 한다. 라벨 버그가 진짜 버그를 가리고 있었다.
- 감시자가 의존하는 것(cron)은 감시자가 못 고친다. 그 부분만큼은 부팅 단계에서 확인해야 한다.
🤖 이 포스팅은 AI의 도움을 받아 작성되었습니다.


💬 댓글