계기

오랫동안 집에 물리 서버를 두고 Docker Compose + nginx로 블로그(Quartz), 개인 위키, code-server, Grafana 모니터링, n8n, portainer 등을 돌리고 있었습니다. 그런데 이사를 하게 되면서 한동안 인터넷은 물론 전기 연결도 못 하는 기간이 생겨버렸고, 집에 서버를 두는 방식 자체가 더 이상 안정적이지 않다는 걸 체감했습니다. 그래서 서버를 오라클 클라우드(Oracle Cloud)로 옮기기로 했습니다.

마침 최근 이직을 하면서 실무에서 Kubernetes, Helm Chart, Terraform을 쓰게 된 참이라, 어차피 서버를 새로 올리는 김에 예전처럼 Docker Compose + nginx를 그대로 재현하지 않고 k3s + Traefik + Helm Chart로 새로 구성해보기로 했습니다.

기존 구조

기존엔 서비스마다 docker-compose.yml을 따로 두고, 전부 nginx_my_network라는 외부(external) 네트워크에 조인시킨 다음, nginx 컨테이너 하나가 리버스 프록시로 모든 트래픽을 받아 각 서비스로 흘려보내는 구조였습니다.

# 서비스마다 반복되던 패턴
services:
  my-service:
    image: ...
    networks:
      - my_network
 
networks:
  my_network:
    external:
      name: nginx_my_network

동작은 잘 했지만 몇 가지 불편함이 있었습니다.

  1. 서비스를 하나 추가할 때마다 nginx 설정 파일을 직접 열어서 location 블록을 추가해야 했습니다.
  2. 인증서 갱신, 네트워크 조인 여부 등이 전부 암묵적인 규칙으로만 존재해서, 시간이 지나면 “이게 왜 이렇게 돼있더라” 싶은 부분이 늘어났습니다.
  3. 인프라 설정이 버전 관리가 전혀 안 되고 있었습니다. 서버에 SSH로 들어가서 파일을 직접 고치는 식이었어서, 언제 뭘 바꿨는지 기록이 남지 않았습니다.

새 구조

nginx가 하던 역할을 두 가지로 쪼갰습니다.

  • 리버스 프록시 / 라우팅: Traefik (k3s 내장 IngressController)
  • 정적 파일 서빙: static-web-server 컨테이너 (nginx 대신, 블로그 정적 빌드 결과물만 서빙)

그리고 이 모든 리소스를 Helm Chart 하나로 묶어서, values.yaml 파일 하나만 고치면 서비스 추가/라우팅 변경이 끝나도록 만들었습니다.

# values.yaml - 서비스 추가는 이렇게 선언만 하면 끝
apps:
  myApp:
    enabled: true
    name: my-app
    image: my-app:latest
    port: 3000
    volumes:
      - name: app
        hostPath: /service/my-app
        hostPathType: Directory
        mountPath: /app

라우팅은 Traefik의 IngressRoute CRD로 관리합니다. path prefix 기반으로 여러 서비스를 한 도메인 아래 묶었습니다.

ingressRoutes:
  https:
    routes:
      - match: "PathPrefix(`/monit`)"
        middlewares: [strip-monit]
        service: grafana
        port: 3000

인증서는 cert-manager + Let’s Encrypt HTTP01 챌린지로 자동 발급/갱신되도록 구성해서, 인증서 만료를 신경 쓸 일이 없어졌습니다.

마이그레이션하며 겪은 것들

1. 서브패스 호스팅이 안 되는 서비스들

관찰성 도구를 몇 개 추가로 붙이면서(ntfy, Uptime Kuma) 알게 된 건데, SPA 형태로 만들어진 셀프호스팅 도구들 중 일부는 /service-name 같은 서브패스 뒤에서 도는 걸 아예 지원하지 않습니다. ntfy는 아예 서버 기동 시점에 base-url에 경로가 있으면 에러를 냅니다.

if set, base-url must not have a path (/ntfy), as hosting ntfy on a sub-path is not supported

이런 서비스들은 깔끔하게 서브도메인(ntfy.yongspark.com, status.yongspark.com)으로 분리하는 쪽이 마음 편합니다. Traefik의 IngressRoute에서 Host 매치 규칙만 추가하고, cert-manager CertificatednsNames에 서브도메인을 추가해주면 인증서까지 자동으로 같이 발급됩니다.

2. 인프라도 결국 코드다

이번에 정리하면서 가장 크게 느낀 건, k8s 매니페스트/Helm Chart로 옮겨놓고도 정작 그 파일들이 Git으로 관리되고 있지 않았다는 점이었습니다. values.yaml을 손으로 고치고 바로 helm upgrade를 때리는 식으로 작업하고 있었는데, 변경 이력이 전혀 안 남으니 롤백도 안 되고 “언제부터 이렇게 됐지”를 알 방법이 없었습니다.

결국 k8s/ 디렉토리만 따로 떼서 private GitHub 저장소로 옮겼습니다. 시크릿 값(토큰, 비밀번호)이 들어간 파일은 제외하고, Helm Chart와 매니페스트만 커밋했습니다.

마무리

Docker Compose + nginx 조합도 개인 서버 규모에서는 충분히 잘 동작하지만, 서비스 개수가 늘어날수록 “새 서비스 = nginx 설정 직접 수정”이라는 패턴이 부담스러워졌습니다. Helm Chart로 옮기고 나니 서비스 추가/라우팅 변경이 선언적으로 관리되고, 회사에서 매일 쓰게 된 도구를 처음부터 끝까지 다 써볼 수 있는 좋은 기회였습니다.

다음 단계로는 지금 수동으로 실행하고 있는 helm upgrade 배포 과정을 GitHub Actions와 연결해서, values.yaml이 바뀌면 자동으로 반영되도록 CI/CD까지 붙여볼 계획입니다.