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

무색

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

연락처

[email protected]

사업자 정보

상호: 무색

대표: 배성재

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

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

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관
INCHEON, KR
Gitea Actions + DinD로 셀프 호스팅 CI/CD 삽질기
museck 만들기
2026. 1. 7.

Gitea Actions + DinD로 셀프 호스팅 CI/CD 삽질기

gitea-actionscicddocker-in-dockergitopsself-hosted

GitHub Actions 쓰듯이 하면 되겠지?

무색 홈페이지를 홈랩 Kubernetes에 배포하면서 CI/CD가 필요해졌다. Docker 이미지를 수동으로 빌드하고 푸시하는 건 딱 두 번까지만 참을 수 있는 일이니까.

이미 홈랩에 Gitea가 돌고 있었고 Gitea Actions라는 기능이 있다는 걸 알고 있었다. GitHub Actions와 문법이 거의 같다길래 워크플로우 파일 하나 만들면 끝이라 생각했다. 결론부터 말하면 7번의 커밋이 더 필요했다.

actions/checkout이 안 된다고?

첫 번째 벽은 actions/checkout이었다. GitHub Actions에서 거의 모든 워크플로우의 첫 줄에 들어가는 그 액션이 Gitea의 act_runner 환경에서는 동작하지 않았다.

원인은 DinD(Docker-in-Docker) sidecar 구성에 있었다. act_runner가 컨테이너 안에서 실행되는데, 그 안에서 Docker 명령을 쓰려면 별도의 DinD 컨테이너가 sidecar로 붙는다. 문제는 이 구조에서 워크스페이스 마운트가 일반적인 방식으로 안 된다는 것.

해결은 단순했다. actions/checkout을 버리고 git clone을 직접 쓰는 거다.

- name: Checkout
  run: git clone --branch master https://gitea.xssh.org/admin/museck-public.git .

끝에 점(.) 하나가 핵심이다. 현재 디렉토리에 바로 clone해야 이후 docker build가 정상 동작한다. 서브디렉토리에 clone하면 Dockerfile 경로가 꼬인다.

Docker CLI 버전도 문제

두 번째 벽은 Docker CLI 버전이었다. act_runner 이미지에 기본 탑재된 docker-cli가 DinD sidecar의 Docker API(v1.44+)와 호환되지 않았다. apt-get install docker.io로 설치되는 버전이 너무 낡아서 API 핸드셰이크부터 실패하는 상황.

Docker 공식 레포지토리에서 최신 docker-ce-cli를 설치하는 방식으로 해결했다.

- name: Install dependencies
  run: |
    apt-get update && apt-get install -y ca-certificates curl git
    install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/debian/gpg \
      -o /etc/apt/keyrings/docker.asc
    echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] \
      https://download.docker.com/linux/debian bookworm stable" \
      > /etc/apt/sources.list.d/docker.list
    apt-get update && apt-get install -y docker-ce-cli

매번 빌드할 때마다 docker-cli를 새로 설치하는 게 좀 아깝긴 하다. 하지만 커스텀 러너 이미지를 만드는 건 나중으로 미뤘다. 지금은 돌아가는 게 우선이었으니까.

전체 파이프라인 구조

삽질 끝에 완성된 파이프라인은 이런 흐름이다.

master 브랜치에 push하면 Gitea가 webhook으로 act_runner를 깨운다. 러너가 코드를 clone하고 Docker 이미지를 빌드해서 Gitea 내장 레지스트리(gitea.xssh.org/admin/museck)에 push한다. 마지막으로 homelab 레포의 staging kustomization.yaml 이미지 태그를 새 커밋 SHA로 업데이트하면 ArgoCD가 이를 감지해서 K8s 클러스터에 자동 배포한다.

GitOps 패턴의 장점이 여기서 드러난다. 빌드 파이프라인은 이미지를 만들어서 레지스트리에 올리는 것까지만 책임지고, 실제 배포는 ArgoCD가 매니페스트 레포의 변경을 감지해서 처리한다. 빌드와 배포가 완전히 분리돼 있어서 문제가 생기면 매니페스트 레포의 태그만 되돌리면 된다.

7번의 커밋에서 배운 것

커밋 로그를 돌아보면 삽질의 궤적이 선명하다.

  1. actions/checkout 넣고 실행 -- 실패. DinD 환경에서 워크스페이스 마운트가 안 됨.
  2. git clone으로 교체 -- 코드는 받았는데 docker build 실패.
  3. docker-cli 버전 불일치 발견. 기본 패키지 docker.io 대신 공식 리포에서 docker-ce-cli 설치.
  4. clone 경로 문제로 Dockerfile 못 찾음. 현재 디렉토리(.)로 clone하도록 수정.
  5. 러너 설정 변경 후 몇 차례 retry.

GitHub Actions에서 당연하게 쓰던 기능이 셀프 호스팅에서는 전혀 당연하지 않았다. 관리형 서비스가 추상화해 주는 부분이 얼마나 많은지 직접 겪어봐야 체감된다. 러너가 어떤 환경에서 도는지, Docker 데몬이 어디 있는지, 워크스페이스가 어떻게 마운트되는지. 이런 걸 신경 써본 적이 없으니까.

DinD 구성은 보안 면에서는 낫다. 호스트의 Docker 소켓을 직접 마운트하는 것보다 격리가 잘 되니까. 대신 이렇게 표준 액션이 안 되는 트레이드오프가 있다. 홈랩이라 보안에 크게 민감하지 않지만 그래도 DinD 쪽이 마음이 편하다.

결국 추상화 아래를 들여다보게 된다

셀프 호스팅 CI/CD를 구축하면서 느낀 건 하나다. 관리형 서비스의 "당연한" 기능을 직접 만들다 보면 그 추상화 아래에 뭐가 있었는지 비로소 보인다는 것.

actions/checkout 한 줄이 내부에서 얼마나 많은 일을 하는지 이번에 알았다. 워크스페이스 준비, git 설정, 브랜치 체크아웃, shallow clone 최적화 같은 것들. 그걸 전부 git clone --branch master ... . 한 줄로 때울 수 있었던 건 우리 프로젝트가 단순했기 때문이지 일반적인 해법은 아니다.

다음은 이 파이프라인에 production 배포를 붙일 차례다. staging은 자동 배포지만 production은 수동 트리거로 가야 한다. 그건 다음 글에서.

자주 묻는 질문

Gitea Actions에서 actions/checkout이 안 되는 이유는?
DinD sidecar 구성에서는 워크스페이스 마운트가 일반적인 방식으로 동작하지 않습니다. git clone으로 대체하면 현재 디렉토리에 직접 clone되어 이후 docker build가 정상 동작합니다.
DinD 환경에서 docker-cli 버전 불일치는 어떻게 해결하나요?
act_runner 이미지의 기본 docker.io 패키지가 DinD의 Docker API와 호환되지 않을 수 있습니다. Docker 공식 레포지토리에서 docker-ce-cli를 직접 설치하면 API 핸드셰이크 문제가 해결됩니다.
Gitea Actions로 GitOps 자동 배포 파이프라인을 구성할 수 있나요?
네. Gitea Actions가 docker build/push까지 담당하고, homelab 레포의 이미지 태그를 업데이트하면 ArgoCD가 변경을 감지해 K8s에 자동 배포합니다. 빌드와 배포가 완전히 분리됩니다.
museck 만들기(3/10)
Prev

Next.js 15 standalone 앱을 Docker와 K8s로 배포하기

Next

Docker 빌드에서 터진 7가지 호환성 지뢰: ESM + React 19 + PayloadCMS