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

무색

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

연락처

[email protected]

사업자 정보

상호: 무색

대표: 배성재

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

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

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관
INCHEON, KR
GitHub 대신 Gitea: 셀프호스팅 Git + CI/CD를 2주간 운영해보니 — bauhaus 스타일 키 비주얼
크로스커팅
2026. 1. 25.

GitHub 대신 Gitea: 셀프호스팅 Git + CI/CD를 2주간 운영해보니

GiteaCI/CDGitOpsArgoCDDocker

GitHub Actions에서 워크플로우 파일 하나 올리면 CI/CD가 알아서 돌아간다. 편하고 빠르다. 그런데 "알아서 돌아간다"가 정확히 뭘 하는 건지 물어보면 대답하기 어려운 사람이 꽤 많다.

나도 그랬다. 홈랩 쿠버네티스 클러스터에서 kubectl apply를 반복하다가, "이거 Git push 한 번으로 배포까지 끝나면 좋겠다"는 생각이 들었고, Gitea를 올렸다. GitHub 대신 셀프호스팅 Git 서버를 택한 이유, act_runner DinD로 CI/CD를 구축하면서 깨진 것들, 그리고 ArgoCD까지 연결한 GitOps 파이프라인 이야기를 풀어보겠다.

왜 GitHub 대신 Gitea인가

GitHub은 훌륭한 서비스다. 솔직히 대부분의 프로젝트에서는 GitHub을 쓰는 게 맞다. 그런데 내 상황은 좀 달랐다.

  • 이미 K8s 클러스터가 있었다. 컨테이너 레지스트리도 클러스터 안에 두면 네트워크 비용이 0이다.
  • CI/CD 러너도 같은 클러스터에서 돌리면 이미지 push/pull이 내부 네트워크로 끝난다.
  • Gitea는 Git 서버와 OCI 컨테이너 레지스트리를 하나의 서비스로 제공한다. GitHub + Docker Hub 두 개를 관리할 필요가 없다.
  • 무엇보다 추상화 아래에서 실제로 뭐가 돌아가는지 직접 보고 싶었다.

Gitea는 Go로 작성된 경량 Git 서버로 SQLite 하나면 돌아간다. K8s에 PVC 하나 붙여서 배포하면 끝이다. GitHub Actions와 호환되는 Gitea Actions도 지원하니까 워크플로우 문법을 새로 배울 필요도 없다.

전체 파이프라인 구조

두 주 동안 구축한 파이프라인 전체 흐름이다.

git push 한 번이면 Gitea가 webhook으로 act_runner를 깨우고, DinD 사이드카 안에서 Docker 빌드가 돌아간다. 빌드된 이미지는 Gitea의 OCI 레지스트리에 올라가고, 워크플로우가 homelab 레포의 Kustomization 태그를 업데이트하면 ArgoCD가 자동으로 K8s 클러스터에 배포한다. 여기까지 전부 셀프호스팅이다.

act_runner + DinD: 여기서부터 삽질이 시작된다

Gitea Actions의 러너인 act_runner는 GitHub의 self-hosted runner와 비슷하다. 문제는 우리 클러스터가 containerd 기반이라 Docker 소켓이 없다는 거다.

CI에서 Docker 이미지를 빌드하려면 Docker 데몬이 필요한데 containerd에는 그런 거 없다. 해결책은 Docker-in-Docker(DinD) 사이드카 패턴이다. Pod 안에 docker:dind 컨테이너를 하나 더 띄우고 TCP로 통신한다.

containers:
  - name: runner
    image: gitea/act_runner:latest
    env:
      - name: DOCKER_HOST
        value: tcp://localhost:2375
  - name: dind
    image: docker:dind
    securityContext:
      privileged: true
    env:
      - name: DOCKER_TLS_CERTDIR
        value: ""  # 같은 Pod 내부 통신이라 TLS 불필요

핵심은 DOCKER_HOST=tcp://localhost:2375다. Unix 소켓 공유 방식은 containerd 환경에서 안 된다. 같은 Pod 안이라 localhost TCP 통신이 가능하고 TLS도 필요 없다.

actions/checkout이 안 돼요

DinD 환경의 두 번째 함정. GitHub Actions에서 당연하게 쓰던 actions/checkout이 동작하지 않는다. DinD 안에서 실행되는 워크플로우는 일반적인 러너 환경과 달라서 표준 액션이 깨지는 경우가 많다. 결국 git clone을 직접 쓰는 수밖에 없었다.

steps:
  - name: Install dependencies
    run: |
      apt-get update && apt-get install -y --no-install-recommends \
        docker-ce-cli docker-buildx-plugin

  - name: Checkout
    run: git clone --depth 1 --branch master $REPO_URL .

  - name: Build and Push
    run: |
      docker build -t gitea.xssh.org/admin/museck:$TAG .
      docker push gitea.xssh.org/admin/museck:$TAG

Docker CLI도 수동으로 설치해야 한다. DinD 사이드카의 Docker API 버전과 맞는 최신 docker-ce-cli를 Docker 공식 리포에서 받아 설치했다. API 버전 불일치로 빌드가 깨진 적이 두 번이나 있었다.

빌드 최적화: 느린 건 못 참는다

파이프라인은 돌아가기 시작했는데 빌드가 느렸다. 매번 의존성을 처음부터 설치하고 Docker 이미지도 캐시 없이 빌드하니까 당연한 결과였다. 4개 커밋에 걸쳐 최적화를 진행했다.

가장 효과가 컸던 건 다층 캐시 전략이다.

  1. Docker 레이어 캐시: 이전 빌드 이미지를 --cache-from으로 끌어와서 변경된 레이어만 재빌드
  2. BuildKit 마운트 캐시: pnpm store와 .next/cache를 빌드 간에 재사용
  3. 병렬 push: 3개 태그를 & + wait로 동시에 레지스트리로 전송
# 캐시 이미지 당겨오기
- name: Pull cache image
  run: docker pull gitea.xssh.org/admin/museck:buildcache || true

# 캐시 활용 빌드
- name: Build
  run: |
    docker build \
      --cache-from gitea.xssh.org/admin/museck:buildcache \
      --build-arg BUILDKIT_INLINE_CACHE=1 \
      -t gitea.xssh.org/admin/museck:$TAG .

# 3개 태그 병렬 푸시
- name: Push images
  run: |
    docker push gitea.xssh.org/admin/museck:$TAG &
    docker push gitea.xssh.org/admin/museck:latest &
    docker push gitea.xssh.org/admin/museck:buildcache &
    wait

Dockerfile 쪽도 손봤다. BuildKit의 --mount=type=cache로 pnpm store를 빌드 간에 유지하고 .dockerignore를 확장해서 .husky, .claude, scripts 같은 빌드에 불필요한 파일을 제외했다.

ArgoCD와 App of Apps 패턴

CI는 이미지를 만들고 끝이다. 실제 배포는 ArgoCD가 담당한다. ArgoCD는 Git 레포를 감시하다가 매니페스트가 바뀌면 K8s 클러스터에 자동으로 적용해주는 GitOps 컨트롤러다.

서비스가 하나일 때는 ArgoCD Application 하나면 된다. 그런데 홈랩에 서비스가 하나둘 늘어나면? 매번 ArgoCD UI에서 새 앱을 등록하는 건 귀찮다. 그래서 App of Apps 패턴을 쓴다.

# root Application: apps/ 디렉토리를 감시
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  source:
    repoURL: https://gitea.xssh.org/homelab/homelab.git
    path: k8s-platform/services/argocd/apps
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

root Application이 apps/ 디렉토리를 감시한다. 새 서비스를 배포하고 싶으면 apps/ 안에 Application YAML 파일 하나만 커밋하면 ArgoCD가 알아서 등록하고 배포한다. 20개가 넘는 서비스를 이 방식으로 관리하고 있는데 한 번도 ArgoCD UI를 열 필요가 없었다.

배포 알림: ntfy로 모바일 push

파이프라인이 잘 도는지 확인하려고 매번 ArgoCD 대시보드를 열어보는 건 바보 같은 짓이다. ntfy라는 셀프호스팅 push notification 서버를 올려서 ArgoCD 배포 알림을 모바일로 받게 만들었다.

처음에 글로벌 구독을 설정했더니 알림이 폭주했다. homelab 레포는 mono-repo라서 push 한 번에 20개 넘는 앱 전부의 sync 상태가 바뀌고 각각 알림이 왔다. 하루에 알림 수백 개. 해결책은 per-app annotation이다.

# 필요한 앱에만 알림 annotation 추가
metadata:
  annotations:
    notifications.argoproj.io/subscribe.on-deployed.ntfy: ""

# 트리거: 성공 + Healthy + root 앱 제외
trigger.on-deployed: |
  - when: >-
      app.status.operationState.phase in ['Succeeded']
      and app.status.health.status == 'Healthy'
      and app.metadata.name != 'root'
    oncePer: app.status.sync.revision

root 앱을 제외하는 것도 중요하다. App of Apps 패턴에서 root Application은 자식 앱이 sync될 때마다 같이 상태가 바뀌니까 빼지 않으면 중복 알림이 쏟아진다. oncePer로 같은 revision에 대한 중복 알림도 막았다.

GitHub Actions vs Gitea Actions: 솔직한 비교

2주간 Gitea Actions를 쓰면서 느낀 점을 솔직하게 정리해봤다.

Gitea가 나은 점. Git 서버와 컨테이너 레지스트리가 하나다. 네트워크 비용이 0이다. CI 러너가 같은 클러스터에 있어서 이미지 push가 빠르다. 모든 데이터가 내 서버에 있다.

GitHub이 나은 점. actions/checkout이 그냥 된다. marketplace에서 액션 가져다 쓸 수 있다. 러너 환경을 신경 쓸 필요가 없다. 당연히 가용성도 비교가 안 된다.

결론은? 프로덕션 서비스라면 GitHub을 쓰겠다. 하지만 "CI/CD가 실제로 어떻게 돌아가는지 이해하고 싶다"면 Gitea + act_runner를 직접 구축해보는 걸 추천한다. actions/checkout이 안 될 때 비로소 checkout이 뭘 하는 건지 알게 된다.

삽질 모음: 이건 미리 알았으면 좋았을 텐데

  • Docker CLI 버전 불일치: DinD 사이드카의 Docker API 버전과 러너에 설치된 docker-cli 버전이 안 맞으면 빌드가 실패한다. Docker 공식 리포에서 최신 버전을 설치해야 한다.
  • docker-buildx-plugin 누락: DOCKER_BUILDKIT=1을 켰는데 buildx 플러그인이 없으면 --mount=type=cache 같은 BuildKit 기능을 못 쓴다. DinD 환경에서는 별도 설치 필수.
  • ArgoCD repo-server copyutil 크래시: Helm 차트 업그레이드 시 init 컨테이너 이미지 버전이 안 맞아 반복 크래시가 일어났다. Helm values에서 initContainers를 직접 재정의해서 해결.
  • Gitea webhook → ArgoCD: Gitea webhook 타입을 gitea로 설정하면 ArgoCD가 X-Gitea-Event 헤더를 인식 못 한다. gogs 타입으로 설정해야 ArgoCD가 처리할 수 있다.

배운 것

관리형 서비스는 추상화를 제공한다. 그 추상화 덕분에 빠르게 움직일 수 있다. 하지만 추상화 아래에서 무슨 일이 벌어지는지 모르면 문제가 생겼을 때 손을 못 쓴다.

Gitea + act_runner + ArgoCD를 직접 구축하면서 배운 것은 CI/CD 파이프라인의 각 단계가 실제로 무엇을 하는지에 대한 이해다. checkout이 뭘 하는지, Docker 빌드 캐시가 어떻게 작동하는지, GitOps에서 빌드와 배포가 왜 분리되어야 하는지. 이런 것들은 GitHub Actions만 쓰면 알 수 없는 것들이다.

셀프호스팅이 정답은 아니다. 그런데 한 번쯤 직접 해보면 관리형 서비스를 쓸 때도 더 나은 판단을 내릴 수 있게 된다. git clone 한 번이면 전체 인프라를 복원할 수 있는 GitOps의 가치는 직접 삽질해봐야 체감된다.

자주 묻는 질문

Gitea는 GitHub Actions 워크플로우와 호환되나요?
네. Gitea의 act_runner는 GitHub Actions 워크플로우 YAML을 거의 그대로 실행합니다. 다만 일부 GitHub 전용 액션이나 마켓플레이스 의존성은 수정이 필요할 수 있습니다.
홈랩에서 Gitea로 CI/CD 파이프라인을 구축하면 어떤 장점이 있나요?
Git 서버, OCI 컨테이너 레지스트리, CI 러너를 단일 서비스에서 운영할 수 있어 외부 의존 없이 완전한 GitOps 파이프라인을 구축할 수 있습니다. 로컬 네트워크에서 빌드와 배포가 완료되므로 속도도 빠릅니다.
GitHub 대신 Gitea를 쓰면 어떤 점이 불편한가요?
GitHub의 풍부한 에코시스템(Actions 마켓플레이스, Copilot 통합, 커뮤니티)을 포기해야 합니다. 반면 인프라를 직접 제어할 수 있어 CI/CD가 실제로 어떻게 동작하는지 깊이 이해하게 됩니다.
크로스커팅(9/18)
Prev

서버 진행상황을 클라이언트에서 보는 세 가지 패턴

Next

2주간의 Vibe Coding 회고: Claude Code로 10개 프로젝트 만들기