본문으로 건너뛰기
← Development Log

Development Log

2편. Docker로 직접 운영해보니 — 장애를 계층별로 보기 시작했다.

Docker로 서비스를 직접 운영하며 네트워크, 인증서와 컨테이너 장애를 계층별로 확인하는 습관을 만든 과정입니다.

  • #Home Server
  • #Docker
  • #Operations

문제 사이트 장애 하나만 봐서는 DNS·공유기·Nginx·Docker·Next.js 중 어디가 원인인지 알 수 없었다. 선택 증상이 보이는 곳을 바로 수정하지 않고 curl, nslookup, openssl, docker logs로 계층별 상태부터 확인했다. 결과 HTTPS 캐시 문제와 네트워크 장애를 실제 원인 계층까지 좁혀 해결했고, 실행 중 컨테이너 대신 Git + Dockerfile에서 clean image를 다시 만드는 기준도 세웠다. 배운 점 운영에서는 수정하기 전에 측정하고, 장애를 계층별로 분리하는 습관이 기술 자체보다 중요했다.

홈서버에 ddongmy.com을 연결하고 Docker + Nginx + HTTPS까지 구성하니 처음에는 끝난 줄 알았다. 하지만 실제 운영은 그때부터 시작이었다. 이 편에서는 장애 자체보다 증상을 보고 어느 계층이 원인인지 좁혀가면서 운영 방식을 바꿔간 과정을 정리한다.

서비스를 띄우는 것과 운영하는 것은 달랐다

처음 구조는 단순했다.

ddongmy.com으로 접속되고 HTTPS도 붙었으니 처음에는 운영 환경이 완성됐다고 생각했다. 하지만 장애가 생기자 한 가지 문제가 보였다. 브라우저에서 사이트가 안 열린다는 하나의 증상만으로는 어느 계층이 문제인지 알 수 없었다. DNS가 잘못된 것인지, 공유기에서 요청이 막힌 것인지, Ubuntu가 문제인지, Nginx가 요청을 못 넘기는 것인지, Docker 컨테이너가 죽은 것인지 구분해야 했다. 이때부터 홈서버 운영은 단순한 Docker 사용법보다 장애 지점을 계층별로 좁히는 연습에 가까워졌다.

네트워크 장애 — 인터넷이 살아났다고 서비스가 살아난 것은 아니었다

실제로 LG U+ GW의 LAN 문제가 발생해 홈서버 네트워크가 끊긴 적이 있었다. GW를 재부팅하고 인터넷이 다시 되는 것을 확인했지만, 그것만으로 ddongmy.com 서비스까지 정상이라고 판단할 수는 없었다. 예전 같으면 애플리케이션이나 Docker부터 재시작했겠지만, 이번에는 경로를 나눠 확인했다.

plain
Next.js Application
        ↑
Docker Container
        ↑
Ubuntu Home Server
        ↑
내부 IP / LAN
        ↑
Port Forwarding
        ↑
Public IP
        ↑
DNS: ddongmy.com

curl ifconfig.me로 현재 외부에서 보이는 공인 IP를 확인하고, nslookup ddongmy.com 등으로 DNS가 그 주소를 제대로 가리키는지 비교했다. SSH 역시 ddongmy.com:2222 경로가 실제로 홈서버까지 이어지는지 확인했다. 여기서 중요했던 건 명령어 자체보다 확인 순서였다.

사이트가 안 된다 → Docker를 재시작한다 가 아니라, 사이트가 안 된다 → DNS는 맞는가 → 공유기까지 들어오는가 → 홈서버가 네트워크에 붙어 있는가 → Nginx는 살아 있는가 → 그 뒤의 컨테이너는 살아 있는가 로 사고방식이 바뀌었다. 장애가 보이는 위치와 실제 원인이 있는 위치는 다를 수 있다. 브라우저에서 보이는 장애가 반드시 애플리케이션 장애인 것은 아니었다.

HTTPS 장애 — 인증서를 갱신했는데 왜 브라우저는 계속 만료라고 할까

또 한 번 이 방식을 체감한 건 HTTPS 문제였다. ddongmy.comdaily.ddongmy.com에서 인증서 만료 경고가 나타났고 Certbot으로 갱신을 진행했다. 처음에는 갱신이 제대로 적용되지 않았거나 Nginx가 이전 인증서를 계속 사용한다고 의심했다. 그래서 바로 설정을 다시 바꾸기보다 서버가 실제로 어떤 인증서를 내보내고 있는지부터 확인했다.

bash
echo | openssl s_client -connect ddongmy.com:443 -servername ddongmy.com 2>/dev/null | openssl x509 -noout -dates -issuer -subject

그리고 Nginx가 어떤 인증서 파일을 바라보는지도 확인했다.

bash
sudo nginx -T | grep -E "ssl_certificate|server_name"

서버가 제공하는 인증서는 이미 정상적으로 갱신되어 있었다. Nginx reload도 정상이었다. 결국 남은 차이는 브라우저였고, 시크릿 창에서 확인하면서 브라우저가 이전 인증서 상태를 캐시하고 있었던 것을 확인했다.

plain
브라우저: 인증서 만료 표시
        ↓
Certbot 실패인가?
        ↓
openssl로 실제 서버 인증서 확인
        ↓
서버 인증서는 정상
        ↓
Nginx 인증서 경로 확인
        ↓
정상
        ↓
브라우저 캐시 문제로 범위 축소

이 경험은 꽤 중요했다. 화면에 HTTPS 오류가 보인다는 이유로 Certbot이나 Nginx 설정을 무작정 수정했다면 정상인 서버 설정을 오히려 망가뜨릴 수도 있었다. 수정하기 전에 현재 상태를 측정한다. 이 원칙이 여기서 명확해졌다.

컨테이너 문제도 "일단 들어가 보자"로 해결되지 않았다

Docker를 운영하면서 컨테이너 내부를 확인하려고 docker exec를 사용했지만, 이미지에 따라 기대했던 sh가 없어서 바로 진입할 수 없는 경우도 있었다. Docker를 사용한다고 해서 모든 컨테이너가 같은 사용자 공간과 도구를 갖는 것은 아니었다. 또한 파일 권한과 ENTRYPOINT 문제도 겪었다. 호스트의 소스 디렉터리 권한 때문에 작업이 막히면 소유권을 확인하고 정리해야 했고, 컨테이너가 어떤 명령으로 시작되는지 모르면 단순히 "Docker가 안 뜬다"고만 볼 수 없었다.

plain
Container가 이상하다
        ↓
실행 중인가?       → docker ps
로그는 무엇인가?   → docker logs
어떤 이미지인가?   → docker inspect
어떻게 시작되는가? → ENTRYPOINT / CMD
파일 접근 가능한가? → host/container 권한

여기서 Docker를 단순히 docker compose up -d로 애플리케이션을 띄우는 도구가 아니라, 이미지·실행 명령·파일시스템·네트워크가 합쳐진 실행 환경으로 보기 시작했다.

기존 컨테이너를 그대로 K3s로 옮기지 않은 이유

K3s로 넘어가기 전 기존 ddongmy-os 실행 환경을 확인하면서 또 하나의 선택이 필요했다. 기존 컨테이너에는 비정상적으로 큰 writable layer와 바로 설명하기 어려운 실행 흔적이 있었다. 여기서 선택지는 두 가지였다.

  1. 지금 실행 중인 컨테이너를 최대한 그대로 보존해서 Kubernetes로 옮긴다.
  2. GitHub의 source와 Dockerfile을 기준으로 clean image를 다시 만든다. 나는 두 번째를 선택했다. 운영 중인 컨테이너는 여러 번의 수정과 실행 흔적이 쌓인 현재 상태일 뿐, 다시 만들 수 있는 배포 원본이라고 보기 어려웠다. 반면 Git source + Dockerfile에서 새 이미지를 만들면 어떤 코드와 설정으로 실행 환경이 만들어졌는지를 다시 설명할 수 있었다.

이 판단이 이후 GHCR을 이미지 저장소로 사용하고, GitHub Actions에서 매번 이미지를 다시 빌드하는 구조로 이어졌다.

장애를 해결하는 방식이 바뀌었다

이 시기에 얻은 가장 큰 변화는 특정 기술을 하나 더 알게 된 것이 아니었다. 처음에는 문제가 생기면 증상이 보이는 곳부터 고치려고 했다.

plain
사이트 안 됨 → 앱 재시작
HTTPS 경고 → 인증서 다시 설정
컨테이너 이상 → 컨테이너부터 수정

직접 운영한 뒤에는 먼저 경계를 나누기 시작했다.

plain
1. 외부에서 어디까지 요청이 들어오는가?
2. DNS가 올바른 IP를 가리키는가?
3. 공유기와 홈서버 네트워크는 정상인가?
4. Nginx가 요청을 받고 있는가?
5. Docker container는 정상인가?
6. 마지막으로 application은 정상인가?

그리고 가능하면 각 단계에서 curl, nslookup, openssl, docker ps, docker logs처럼 현재 상태를 보여주는 값부터 확인한 뒤 변경하려고 했다.

이 시기에 얻은 가장 큰 습관은 문제가 보이는 계층을 바로 고치지 않고, 그 계층이 정말 원인인지 먼저 측정하는 것이었다.

그런데 Docker만으로 운영하니 또 다른 질문이 생겼다

Docker로 하나의 서비스를 실행하는 것까지는 충분했다. 하지만 서비스를 계속 운영한다는 관점에서는 새로운 질문들이 생겼다.

  • 컨테이너가 죽으면 누가 원하는 개수로 다시 맞춰주는가?
  • 새 버전으로 교체할 때 기존 서비스를 최대한 유지하면서 어떻게 바꾸는가?
  • 여러 인스턴스가 생기면 요청은 어디로 보내는가?
  • 현재 실행 상태를 명령이 아니라 선언으로 관리할 수는 없는가? 회사에서는 Kubernetes 위에서 Pod가 재생성되고 Deployment가 상태를 관리하는 모습을 접하고 있었지만, 개발자 권한으로 그 내부를 처음부터 끝까지 직접 다뤄볼 기회는 많지 않았다. 그래서 다음 단계에서는 단순히 Docker를 Kubernetes로 바꾸는 것이 아니라, Docker를 직접 운영하면서 사람이 하던 상태 관리가 Kubernetes에서는 어떻게 바뀌는지를 홈서버에서 직접 확인해보기로 했다.

다음 편: Docker에서 K3s로 — Kubernetes의 Desired State와 Service, Ingress, Traefik을 직접 구성하면서 왜 이런 구조가 필요한지 확인한다.