Docker Swarm 2026.09.12 11 min read

MeetBack에서 실제로 겪은 트러블슈팅과 최종 회고

MeetBack에서 겪은 NFS 권한, 복제 연결, 설정 반영, 로그인·실시간 오류를 정리했다. 해결 과정과 최종 검증값, 끝내 남은 문제를 함께 기록했다.

MeetBack을 구축하는 동안 비슷한 오류 메시지를 여러 번 만났다. Permission denied가 나와도 확인할 곳은 NFS 권한, Secret 파일 모드, 레지스트리 인증으로 달랐다. 접속 실패도 마찬가지였다. 라우팅과 NAT를 볼 때가 있었고, TLS나 OAuth 콜백, STOMP 상태까지 들어가야 할 때도 있었다.

그때 겪은 문제를 증상부터 정리했다. 무엇을 확인했고 어떻게 처리했는지, 해결하지 못한 부분은 무엇인지 함께 남긴다.

1. nmtui에 NIC가 보이지 않았다

NetworkManager를 설치했는데 인터페이스 목록이 비어 있었다. 확인해 보니 이 서버는 Netplan의 systemd-networkd renderer로 네트워크를 관리하고 있었다.

기존 renderer를 유지한 채 Netplan으로 정적 IP를 설정했다. 이후에는 OS 이름만 보고 관리 도구를 정하지 않았다. netplan get 결과와 renderer, 실행 중인 서비스를 먼저 확인했다.

2. Netplan 예제의 interface 이름이 서버마다 달랐다

서버마다 NIC 이름이 달랐다. 한 서버에서 사용한 ens33을 그대로 복사하면 다른 서버에서는 없는 인터페이스에 설정을 적용하게 된다. 원격 접속을 잃지 않도록 모든 서버에서 ip -br link로 이름을 확인하고 각각 설정을 작성했다.

3. Swarm IP 변경은 quorum까지 흔들 수 있었다

Manager 주소를 한꺼번에 변경하면 Raft와 오버레이 통신을 동시에 잃을 수 있었다. 한 대의 주소를 바꾼 뒤 노드가 Ready인지, Manager 상태가 정상인지 확인하고 다음 서버로 넘어갔다.

4. Private image가 Worker에서 pull되지 않았다

Manager에서 Docker 로그인이 성공해 배포할 수 있을 것으로 생각했지만, Worker의 태스크는 pull access denied와 함께 Rejected 상태가 됐다. 배포할 때 레지스트리 인증도 전달하도록 수정했다. 이후 Worker가 실제로 받은 이미지의 digest까지 확인했다.

5. 두 App replica가 한 Worker에 모였다

호스트 이름을 기준으로 분산하도록 설정해도 두 복제본이 한 Worker에 올라갔다. 이 설정은 선호 조건이었다. 노드당 최대 복제본 수를 1로 제한한 뒤 두 Worker에 하나씩 배치되는 것을 확인했다.

배치 제한을 넣자 업데이트 방식에도 제약이 생겼다. Worker가 두 대뿐이라 start-first로 새 태스크를 먼저 띄울 자리가 없었다. 이 환경에서는 stop-first와 parallelism 1을 사용했다.

6. App task가 NFS mount 단계에서 거부됐다

애플리케이션이 시작되기 전에 Docker 볼륨 경로에서 Permission denied가 발생했다. Worker의 NFS 클라이언트 패키지부터 확인하고, export의 접근 허용 대상, root_squash, 컨테이너 UID/GID를 차례로 대조했다.

App 컨테이너와 NFS 디렉터리의 UID/GID를 10001:10001로 맞추자 접근할 수 있었다. root_squash는 유지했다. no_root_squash로 바꿀 필요는 없었다.

7. Root로 backup directory를 읽지 못했다

NFS의 root_squash가 적용돼 원격 root의 접근이 거부된 경우였다. 백업을 실행하는 dbbackup 계정으로 다시 확인하니 무결성을 검사할 수 있었다. 이후 백업 검증도 실제 서비스가 사용하는 계정으로 진행했다.

8. VyOS route는 보이는데 DB 외부 통신이 되지 않았다

라우팅 경로가 있다는 이유로 NAT도 적용됐다고 생각한 것이 문제였다. 내부 서비스망은 NO-NAT, 외부 통신은 masquerade로 나누고 후보 설정을 commit/save해 반영했다.

처음 작업한 환경이 Live ISO였던 점도 확인했다. 디스크에 영구 설치한 뒤 재부팅해 설정이 유지되는지 다시 점검했다.

9. MySQL replication password가 거부됐다

복제 채널에서 허용하는 길이보다 긴 비밀번호를 넣어 설정이 거부됐다. 애플리케이션 계정과 복제 계정을 분리하고, 복제 채널의 길이 제한 안에서 별도의 강한 비밀번호를 만들었다.

10. Replica IO thread가 Connecting에 머물렀다

복제를 다시 구성하기 전에 TCP 연결, MySQL 계정의 Host 조건, TLS, Last_IO_Error, 재접속 간격을 각각 확인했다. 이후 IO/SQL 스레드가 모두 Yes로 돌아왔고 복제 지연도 0초였다.

11. Prometheus 설정을 바꿨는데 Target이 나타나지 않았다

호스트에서 수정한 파일과 컨테이너가 bind mount로 읽는 파일의 inode가 달랐다. 양쪽 해시를 비교한 뒤 컨테이너가 읽는 설정으로 문법을 검사하고 서비스를 재생성했다.

Exporter에 직접 요청이 된다는 것만으로는 수집 여부를 알 수 없었다. Prometheus Target API에서 대상이 실제로 로드됐는지, 상태가 정상인지까지 확인했다.

12. Metrics pipe에서 curl: (23)이 발생했다

grep -m1이 먼저 종료하면서 파이프가 닫혔다. Exporter 자체의 실패가 아니었다. HTTP 응답 코드와 수집 대상 상태, PromQL 조회 결과를 따로 확인해 판단했다.

13. Grafana .env 비밀번호를 바꿔도 로그인되지 않았다

관리자 초기화 환경변수는 DB를 처음 만들 때만 적용됐다. 이미 저장된 계정은 공식 관리자 비밀번호 초기화 절차로 변경해야 했다. 설정 파일을 바꿔도 기존 DB의 사용자 정보까지 바뀌지는 않았다.

14. 인증서는 발급됐는데 NPM에 Not Used로 보였다

인증서는 발급됐지만 아직 프록시 호스트에 연결하지 않은 상태였다. 발급 로그, 호스트 연결 상태, 외부 TLS 핸드셰이크를 나눠 확인했다.

15. HTTPS인데 HTTP redirect와 insecure session cookie가 나왔다

외부 TLS 연결은 정상이었다. 다만 애플리케이션이 원래 요청의 스킴을 제대로 인식하지 못했다. 전달 헤더 처리와 Spring의 리디렉션·쿠키 설정을 따로 확인할 문제로 정리했다.

전달 헤더와 쿠키 설정은 최종 점검 때까지 수정이 더 필요한 애플리케이션 항목으로 남았다.

16. WebSocket 101인데 Presence가 느렸다

Cloudflare Tunnel과 NPM에서 /ws 업그레이드는 모두 성공했다. 그런데 STOMP 로그에서 CONNECT 대비 CONNECTED 비율은 약 1~2%에 그쳤다.

프록시 차단만 의심해서는 설명되지 않는 결과였다. STOMP 인증, 클라이언트의 반복 재접속, JVM별 Presence 상태, 세션 생성·종료 처리를 우선 확인할 대상으로 정했다. 연결 업그레이드 이후에도 실시간 기능을 따로 검증해야 했다.

17. Kakao 로그인 오류를 Secret 문제로 단정했다

KOE006은 리디렉션 URI와 앱 설정을 먼저 확인해야 했다. KOE320은 인가 코드의 중복 사용·만료와 콜백 재요청을 확인할 대상이었다. Secret 유무만 보는 데서 멈추지 않고 공급자 요청과 콜백 로그를 시간순으로 맞췄다.

내가 사용한 공통 진단 순서

문제를 하나씩 처리하면서 다음 순서로 점검하게 됐다.

  1. 사용자가 본 증상과 발생 시각을 기록한다.
  2. DNS, TCP, TLS, HTTP를 확인하고 애플리케이션 상태로 좁혀 간다.
  3. 실행 중인 프로세스가 실제로 읽은 설정을 확인한다.
  4. 실제 서비스 계정의 권한으로 시험한다.
  5. 제어 영역의 상태와 실제 요청 처리 결과를 함께 확인한다.
  6. 성공해야 하는 요청과 차단해야 하는 요청, 원복 절차를 시험한다.
  7. 결과를 확인한 뒤 임시 검증 데이터를 삭제한다.

최종 검증 결과

프로젝트 종료 시점에 확인한 인프라 상태는 다음과 같았다.

Swarm nodes: 5/5 Ready
Application replicas: 2/2, Worker별 1개
Cloudflared connectors: 5/5
External readiness: 기본·예비 경로 모두 HTTP 200
MySQL replication: IO Yes / SQL Yes / lag 0s
Flyway: 2 migrations
Application schema: 18 tables
NFS shared file test: PASS
Backup gzip/SHA-256: PASS
Prometheus targets: 23/23 UP
Alert rules: 21/21 healthy
Firing alerts: 0
Linux account/firewall audit: Ubuntu 10대 PASS

로그인 리디렉션·쿠키와 Presence/STOMP에는 애플리케이션 수정이 남아 있었다. Docker Service의 healthcheck도 readiness 엔드포인트와 아직 연결되지 않았다.

남은 단일 장애점과 개선 순서

현재 한계는 다음과 같다.

현재 한계 장애 시 영향 내가 생각한 다음 개선
Worker 2대, 노드당 App 1개 한 대 장애 시 1/2 용량 Worker 1대 추가
MySQL 비동기·수동 승격 전환 지연과 최근 transaction 손실 가능 반동기 복제·proxy·fencing 검토
Redis 단일·비영속 Presence와 replica 간 event 영향 Sentinel 또는 HA, TTL 상태 모델
NFS server별 단일 node RAID가 server 장애는 보호하지 못함 별도 복제·object storage·DR
NPM 단일 replica 예비 경로 중단 상태 저장소와 진입점 이중화
VyOS 단일 router DB망 경로 중단 VRRP 기반 이중화
Monitoring 단일 server 관측 공백 remote storage 또는 이중화
Docker healthcheck 미연결 scheduler가 readiness를 판단하지 못함 /readyz 기반 healthcheck 적용
수동 배포 사람의 누락 가능 후속 CI/CD와 승인 gate

프로젝트를 마치며

설정 파일만으로 원인을 판단했던 문제는 실행 중인 상태와 비교하면서 풀렸다. 호스트와 컨테이너의 파일 해시를 맞춰 보거나, 서비스 계정으로 다시 요청해 보면서 확인할 범위를 줄일 수 있었다. 자동화하지 못한 조치는 운영 절차와 다음 작업 목록에 남겼다.

팀에서는 기능 완료 여부를 판단할 때 재현 명령과 확인할 수치를 함께 정했다. 인프라는 실제 요청, 권한, 복제, 복원, 알림 결과를 확인했고, 직접 구현한 Google 인증과 Redis 실시간 모듈은 입력 검증과 상태 전달 범위를 점검했다. 도구의 정상 표시와 사용자가 기능을 쓸 수 있는 상태가 항상 같지는 않았다.

MeetBack의 프로젝트 일정은 2026년 9월 2일에 끝났다. 남은 세션·Presence 문제는 애플리케이션 지표와 E2E 테스트로 더 좁혀 보고 싶다. readiness를 사용한 롤링 업데이트와 배포 자동화도 이후에 이어갈 작업으로 정리했다.

실제 검증 화면

  1. /readyz는 있었지만 Swarm Service의 Healthcheck는 null이었다. 배포 설정에 연결할 항목으로 남겼다.
  2. 실제 사용자 오류 화면을 보고 공급자 설정과 콜백 요청을 추적했다.
  3. 최종 점검에 사용한 통합 화면이다. 확인한 상태와 이후에 추가로 관측할 항목을 나눠 정리했다.
    Docker Secret 목록과 앱 실행 사용자 10001:10001, Healthcheck가 null인 조회 결과

MeetBack 카카오 로그인에서 앱 관리자 설정 오류 KOE006이 표시된 모바일 화면

노드 10개와 MySQL 2개가 온라인이고 발생 알림 0개, 복제 지연 0초인 Grafana 통합 대시보드

SEARCH JOURNAL

기록 검색

글 제목, 요약, 태그에서 찾습니다.

검색을 준비하고 있어요.

검색어를 입력해 주세요.

    Ctrl/⌘ K 검색 Esc 닫기 결과 이동