무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
Gitea 운영 삽질기 — generative 키 비주얼
홈랩 삽질기
2026. 2. 5.

Gitea 운영 삽질기: webhook 차단, LevelDB 락, 1.25 업그레이드

GiteaKubernetesDevOps홈랩

K8s 위에서 Gitea를 돌리고 있다. GitHub 쓰면 편하지만, 홈랩에서 CI/CD 파이프라인을 직접 구성하고 싶었고, Gitea Actions가 GitHub Actions와 거의 동일한 문법을 지원하기 때문에 선택했다.

운영하면서 두 가지 문제를 만났다. 하나는 ArgoCD webhook이 먹통이 된 것, 다른 하나는 배포할 때마다 pod이 CrashLoop에 빠지는 것. 둘 다 원인을 찾고 나면 "아, 이거였어?" 싶은 설정 문제였는데, 모르면 한참을 헤맨다.

webhook이 K8s 내부로 안 나간다

Gitea에서 코드를 push하면 ArgoCD webhook으로 즉시 동기화를 트리거하는 구조다. 그런데 어느 날부터 webhook delivery 내역을 보니 전부 실패. 에러 메시지는 이랬다:

Webhook can only call allowed HTTP servers

ArgoCD 서버 주소는 argocd-server.argocd.svc.cluster.local이고, K8s 내부에서 이 도메인은 10.x 대역 ClusterIP로 resolve된다. 문제는 Gitea 1.16부터 webhook.ALLOWED_HOST_LIST 기본값이 external로 바뀌었다는 것. 보안 강화 목적인데, 이게 K8s 내부 IP 대역으로의 webhook 전달을 전부 차단한다.

해결은 ConfigMap에 한 줄 추가하면 끝이다:

data:
  GITEA__webhook__ALLOWED_HOST_LIST: "private"

private으로 설정하면 RFC 1918 사설 IP 대역(10.x, 172.16.x, 192.168.x)을 전부 허용한다. K8s 내부 서비스끼리 통신해야 하는 환경에서는 이게 맞다.

Gitea 릴리스 노트에 분명히 적혀 있었을 텐데, 업그레이드할 때 꼼꼼히 안 읽으면 이런 식으로 당한다. 보안 강화를 위한 기본값 변경이 K8s 내부 통신을 끊어버리는 케이스.

RollingUpdate에서 pod이 CrashLoop에 빠지는 이유

Gitea Deployment를 업데이트할 때마다 새 pod이 뜨자마자 CrashLoop에 빠졌다. 로그를 보면:

resource temporarily unavailable

원인은 LevelDB 파일 락이다. Gitea는 내부적으로 LevelDB를 사용하는데, LevelDB는 데이터 디렉토리에 LOCK 파일을 만들어서 배타적 접근을 보장한다. RollingUpdate 전략에서는 새 pod이 기존 pod이 종료되기 전에 먼저 뜨는데, 두 pod이 동시에 같은 PVC의 LOCK 파일에 접근하려다 충돌이 발생한다.

해결은 strategy를 Recreate로 바꾸는 거다:

spec:
  replicas: 1
  strategy:
    type: Recreate  # RollingUpdate → Recreate

Recreate는 기존 pod을 완전히 내린 뒤 새 pod을 띄운다. 짧은 다운타임이 생기지만, 홈랩 Gitea는 사용자가 나 혼자라 문제없다. 오히려 데이터 정합성이 더 중요하다.

이건 Gitea만의 문제가 아니다. SQLite, LevelDB 등 파일 기반 DB를 사용하는 모든 앱은 K8s에서 반드시 Recreate strategy를 써야 한다. 두 프로세스가 동시에 같은 파일에 쓰면 락 충돌은 가벼운 거고, 데이터 손상까지 갈 수 있다.

1.23에서 1.25로 업그레이드

위 두 이슈를 해결한 김에 Gitea 버전도 올렸다. 1.23에서 1.25로, 2 minor 버전을 한 번에 건너뛰었다.

containers:
  - name: gitea
    image: gitea/gitea:1.25  # 1.23 → 1.25

업그레이드 이유는 Actions API 개선 때문이다. 1.25에서 job 로그 조회 API가 추가되었고, CI/CD 파이프라인 디버깅이 훨씬 편해졌다.

Gitea는 DB 마이그레이션을 자동으로 처리하기 때문에 버전을 올리면 pod 시작 시 알아서 스키마 변경이 적용된다. 1.23 → 1.25도 로그에 마이그레이션 쿼리가 줄줄 뜨더니 정상 부팅되었다. strategy를 Recreate로 바꿔놓은 덕에 마이그레이션 중 다른 pod이 간섭할 걱정도 없었다.

정리: K8s에서 Gitea 운영할 때 체크리스트

  • webhook.ALLOWED_HOST_LIST: K8s 내부 서비스로 webhook을 보내야 한다면 private으로 설정. 기본값 external은 사설 IP를 차단한다.
  • Deployment strategy: Recreate 필수. LevelDB 파일 락 때문에 RollingUpdate는 CrashLoop을 유발한다.
  • 버전 업그레이드 시 릴리스 노트 필독. 보안 강화 목적의 기본값 변경이 기존 환경을 깨뜨릴 수 있다.
  • DB 마이그레이션은 자동. minor 버전 2단계 점프도 무중단으로 성공했다.

작은 설정 하나가 CI/CD 파이프라인 전체를 멈출 수 있다. 셀프호스팅은 자유도가 높은 만큼, 이런 운영 디테일을 직접 챙겨야 한다. 릴리스 노트를 읽는 게 귀찮지만, 안 읽으면 더 귀찮은 일이 생긴다.

자주 묻는 질문

Gitea webhook이 K8s 내부 서비스로 전달되지 않는 이유는?
Gitea 1.16부터 webhook.ALLOWED_HOST_LIST 기본값이 external로 변경되어 K8s 내부 ClusterIP(10.x 대역)로의 요청이 차단된다. private으로 설정하면 내부 IP 대역도 허용된다.
Gitea를 K8s에서 RollingUpdate로 배포하면 안 되는 이유는?
Gitea는 내부적으로 LevelDB를 사용하는데, LevelDB는 파일 레벨 배타 락을 건다. RollingUpdate 시 신/구 pod이 동시에 같은 PVC에 접근하면 LOCK 파일 충돌로 CrashLoop에 빠진다. Recreate strategy를 써야 한다.
Gitea 버전을 2단계 이상 올려도 괜찮은가?
Gitea는 버전 업그레이드 시 DB 마이그레이션을 자동으로 처리한다. 1.23에서 1.25로 2 minor 버전을 건너뛴 업그레이드도 문제 없이 성공했다.
홈랩 삽질기(18/19)
Prev

ArgoCD 배포 알림 고도화: oncePer 중복 방지부터 resync 튜닝까지

Next

Next.js K8s 배포 강화: startupProbe, graceful shutdown, Server Actions 암호화