카테고리 없음

[cicd] 셀프호스팅 gitlab 에서 자동 배포 파이프라인 구축하기

sian han 2026. 1. 5. 21:59

이직했다.

회사에서 배포 프로세스로 FTP 프로토콜 기반의 FileZilla를 사용해 서버에 파일을 업로드하는 방식을 사용하고 있었다. 

 

전 회사에서 초창기에 FileZila로 배포해 본 경험이 있는데 이게 여간 불편하게 아니다.
사람이 수동으로 파일을 업로드하는 거다 보니 실수할 확률이 높은데, 그래서 배포 때마다 동료들과 모여 앉아서 교차 검증하며 배포를 진행해야 한다.예를 들어 배포할 파일이 50개가 넘어선다고 가정하면, 사람이 일일이 옮기는 과정에서 누락이 없다고 100% 보장하기 어렵다.

또 배포 과정이 이러니까 자연스럽게 자주 배포하기보다는 한 번에 배포하게 된다.

그래서 배포 프로세스를 가장 먼저 개선하게 되었다. 

 

자사는 셀프 호스팅 GitLab을 사용하고 있다. GitLab Container RegistryGitLab Runner를 연동해, 특정 브랜치에 코드가 push될 경우 자동으로 배포가 수행되도록 CI/CD 환경을 구성했다.

 

본 글에서는 이 CI/CD 파이프라인을 구축한 과정을 단계별로 설명한다.

 

주요 환경은 아래와 같다

- `GitLab 서버` : 셀프 호스팅 (Docker 사용)

- `Container Registry` : GitLab 내장 Registry 활용

- `Runner` : Docker executor 기반 Runner 사용

- `대상 서버` : 운영서버(PROD) 및 테스트 서버(TEST)

 

1. GitLab Container Registry 활성화 및 설정

Container Registry 란 ?

GitLab Container Registry 는 GitLab 프로젝트 내부에서 Docker 이미지를 직접 저장하고 관리하는 전용 저장소. Docker Hub 와 같은 외부 서비스 없이도 소스코드와 배포용 이미지를 GitLab 내에서 통합 관리할 수 있음. 

 

GitLab 컨테이너 내부의 gitlab.rb 파일을 수정하여 이 기능을 활성화 해야함

 

1.1 GitLab 설정 수정

- 설정 파일 경로 : /etc/gitlab/gitlab.rb

- 추가 내역:

  registry_external_url "{Registry 도메인 주소 입력}"
  registry['enable'] = true
  registry_nginx['listen_https'] = false
  registry_nginx['listen_port']  = 5000
  gitlab_rails['registry_api_url'] = "http://127.0.0.1:5000"
  gitlab_rails['gitlab_default_projects_features_container_registry'] = true

 

- 재설정 적용

gitlab-ctl reconfigure

 

 

1.2 네트워크 및 도메인 연결 구성

내부 Registry 서비스는 Docker Registry 표준에 따라 기본적으로 5000 포트를 사용해 통신함

따라서 외부에서 이미지 push/pull 요청을 보낼 때 해당 포트로 접근할 수 있도록 네트워크 및 프록시 설정이 필요함

 

▶ NAT 규칙 설정

- iptime 에서 외부 5000 포트 요청을 GitLab 서버의 5000 포트로 전달하는 포트 포워딩 설정

 

 

▶ Nginx Proxy Manager 설정

- Registry 도메인에 대해 SSL 인증서 적용

 

- `/v2/` 경로는 Docker Registry API 의 표준 엔드포인트로, 일반적인 루트/ 블록보다 먼저 위치하도록 설정한다. 

Nginx conf 파일 예시

  # 1) Docker Registry API용 location 블록 (우선 처리)
  location /v2/ {
      add_header            X-Served-By $host;
      proxy_set_header      Host $host;
      proxy_set_header      X-Forwarded-Scheme $scheme;
      proxy_set_header      X-Forwarded-Proto $scheme;
      proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header      X-Real-IP $remote_addr;

      # Docker Registry의 큰 이미지 push를 위해 client_max_body_size 조정 권장
      client_max_body_size  0;
      chunked_transfer_encoding on;

      # 내부 서버의 5000 포트로 프록시 패스
      proxy_pass            http://{도메인명}.iptime.org:5000;
  }

  # 2) 일반 요청용 location 블록
  location / {
      include conf.d/include/proxy.conf;
  }
}

 

  DNS 설정

- Cafe24 등 도메인 관리 서비스에서 도메인명을 CNAME 으로 연결한다. 

 

2. GitLab Runner 설치 및 상세 설정

2.1 Runner 등록 및 컨테이너 가동

GitLab UI 에서 Runner 를 생성하고, 해당 토큰을 사용하여 Docker 컨테이너를 연동함

 

  GitLab 에서 Runner 생성하기

  • 프로젝트의 `Settings > CICD > Runners` 메뉴 접속\
  • `New Project Runner` 버튼 클릭
  • Tags 입력 후 `Lock to current project` 체크 여부 결정
  • 생성 후 발급되는 `Registration Token` 을 별도로 기록해두기

 

  Runner 등록 실행

    docker run --rm -it \
      -v /srv/gitlab-runner/config:/etc/gitlab-runner \
      gitlab/gitlab-runner:latest register \
      --non-interactive \
      --url "{깃랩 URL}" \
      --token "{RUNNER_REGISTRATION_TOKEN}" \
      --executor "docker" \
      --docker-image "docker:25" \
      --description "project CI/CD Runner"

 

2.2 Privileged 모드 활성화

Docker 이미지를 빌드하기 위해 Runner 설정에서 특권 모드 활성화가 필요함

  • gitlab-runner 컨테이너 접속
  • 수정파일 : `/etc/gitlab-runner/config.toml`
  • 수정내역 : 
  [[runners]]
    ...
    [runners.docker]
      privileged = true
      volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]

 

3. 인증 및 보안 설정

3.1 Deploy Token 발급

CI/CD 과정에서 Registry 에 이미지를 push/pull 하기 위한 전용 토큰을 생성함

  • GitLab : Settings > Repository > Deploy tokens
  • 권한 부여 : `read_registry`, `write_registry` 체크
  • 발급된 Username 및 Password 는 파이프라인 설정 시 사용된다. 

 

3.2 SSH 키 발급 및 서버 등록

원격 서버 배포를 위해 Runner 컨테이너 내부에서 SSH 키를 생성하고, 배포 대상 서버(TEST, PROD) 에 등록해야 함

 

▶ Runner 컨테이너 내부에서 SSH 키 생성

    # gitlab-runner 컨테이너 접속 후 실행
    # Ed25519 알고리즘 사용 권장
    ssh-keygen -t ed25519 -C "gitlab-runner"

 

공개 키(Public Key) 확인 및 서버 등록

  • 생성된 공개 키(`id_ed25519.pub`)의 내용을 복사함
  • 배포 대상 서버의 `~/.ssh/authorized_keys` 파일에 붙여넣기
    # 대상 서버에서 수행
    mkdir -p ~/.ssh
    chmod 700 ~/.ssh
    echo "[복사한_공개키_내용]" >> ~/.ssh/authorized_keys
    chmod 600 ~/.ssh/authorized_keys

 

3.3 CI/CD 환경 변수 등록

보안 민감 정보를 소스 코드에 노출하기 않기 위해 변수를 사용

  • GitLab : Settings > CI/CD > Variables
  • 필수 변수 : Private Key
    • 발급받은 Private Key(`id_ed25519`) 의 전체 내용을 복사하여 등록함
    • ⭐⭐ 중요 : 반드시 아래의 내용을 포함한 전체 텍스트를 붙여 넣어야 함
    • 시작 : `-----BEGIN OPENSSH PRIVATE KEY-----`
    • 종료 : `-----END OPENSSH PRIVATE KEY-----`\
    • 위 라인을 하나라도 누락할 경우 Load key: invalid format 에러 발생

 

4. 파이프라인 구성 (.gitlab-ci.yml 파일 작성)

도메인, 토큰, IP 주소 등의 민감 정보는 제거하고 {변수명} 형태로 치환하여 정리했음.

전체적인 파이프라인이 어떤 흐름으로 동작하는지 단계별로 요약하면 다음과 같음

image: docker:25

stages: [develop]

before_script:
  - docker -v
  - echo "CI_REGISTRY=$CI_REGISTRY"
  - echo "CI_REGISTRY_IMAGE=$CI_REGISTRY_IMAGE"
  - echo "{DEPLOY_TOKEN_PASSWORD}" | docker login {REGISTRY_URL} -u "{DEPLOY_TOKEN_USER}" --password-stdin

# develop job
develop-master:
  stage: develop
  only: [develop]
  script:
    # SSH 준비
    - echo "$SSH_KEY" > id_rsa_test
    - chmod 600 id_rsa_test
    - eval $(ssh-agent -s)
    - ssh-add id_rsa_test
    - mkdir -p ~/.ssh
    - echo -e "Host *\n StrictHostKeyChecking no\n" > ~/.ssh/config

    # 1) Build & push
    - docker build -t {REGISTRY_URL}/{PROJECT_PATH}:dev0.9.$CI_PIPELINE_IID .
    - docker tag {REGISTRY_URL}/{PROJECT_PATH}:dev0.9.$CI_PIPELINE_IID {REGISTRY_URL}/{PROJECT_PATH}:develop
    - docker push {REGISTRY_URL}/{PROJECT_PATH}:dev0.9.$CI_PIPELINE_IID
    - docker push {REGISTRY_URL}/{PROJECT_PATH}:develop

    # 2) 서버 SSH 접속 + 배포
    - ssh root@{DEV_SERVER_IP} "
        echo '{DEPLOY_TOKEN_PASSWORD}' | docker login {REGISTRY_URL} -u '{DEPLOY_TOKEN_USER}' --password-stdin &&
        docker rm -f {CONTAINER_NAME} || true &&
        docker pull $CI_REGISTRY_IMAGE:develop &&
        docker run -d --restart unless-stopped --env-file {DEV_ENV_PATH}/.env -p {DEV_PORT}:8000 -v {DEV_LOG_PATH}:/app/logs --name {CONTAINER_NAME} $CI_REGISTRY_IMAGE:develop
      "
  tags: [{RUNNER_TAG}]

 

 

1. Registry Login: 보안을 위해 생성한 {Deploy Token}을 사용하여 사내 도커 Registry 에 로그인

2. SSH Agent 설정: 배포 대상 서버에 원격으로 보안 접속하기 위해 CI 환경 내에서 SSH Agent를 구동하고 키를 등록

3. Image Build & Push: 현재 소스 코드를 바탕으로 도커 이미지를 빌드한 뒤, 고유 버전 태그를 붙여 Registry 에 저장(Push)함

4. Remote Server Access: SSH 프로토콜을 이용해 실제 서비스가 구동될 원격 서버에 접속

5. Cleanup & Pull: 서버에서 구동 중인 기존 컨테이너를 안전하게 삭제하고, Registry 로부터 방금 빌드한 최신 이미지를 가져옴

6. Container Start: 환경 변수 파일과 로그 볼륨을 매핑하여 새 컨테이너를 실행(Run)

 

5. 트러블슈팅

셀프호스팅 GitLab 에서 파이프라인 구축 도중 발생할 수 있는 주요 에러

 

5.1 Docker Registry 인증 및 로그인 에러

  • 현상 : `unauthorized: HTTP Basic: Access denied` 에러 발생
  • 원인 : 
    • GitLab Deploy Token의 패스워드가 `CI/CD Variables`에 잘못 등록됨.
    • GitLab 서버의 Nginx 설정에서 `/v2/` 경로 프록시가 정상적으로 작동하지 않음.
  • 해결
    • `Settings > Repository > Deploy tokens`에서 토큰 상태 재확인.
    • Nginx Proxy Manager 설정에서 `/v2/` 블록이 루트(`/`) 블록보다 위에 있는지, 포트 포워딩(5000)이 맞는지 점검.

5.2 SSH 키 포맷 및 서버 접속 에러

  • 현상 : `Load key: invalid format` 또는 `Permission denied` 에러 발생
Permission denied (publickey,password)
  • 원인 :
    • `CI/CD Variables`에 Private Key를 등록할 때 시작(`-----BEGIN ...`)과 끝(`-----END ...`) 태그를 누락함.
    • 배포 대상 서버의 `~/.ssh/authorized_keys` 파일 권한이 `600`이 아님
  • 해결
    • Private Key 전체 내용을 다시 복사하여 등록
    • 대상 서버에서 `chmod 700 ~/.ssh` 및 `chmod 600 ~/.ssh/authorized_keys` 명령으로 권한 재설정.

5.3 배포 스크립트 실행 및 문법 에러

나 정말 이거 때문에 너무 힘들었음

  • 현상: `docker: invalid reference format` 에러 (Exit Code 125)
  • 원인: `ssh` 명령어로 전달되는 문자열 내부에 줄 바꿈용 역슬래시(`\`)가 포함되어 Docker가 이를 파일명이나 옵션의 일부로 오인함.
  • 해결: docker run의 모든 옵션을 한 줄에 몰아넣고, 명령어 사이는 &&로만 구분함으로써 그 원인을 원천 차단
    • 명령어 간 && 결합 및 역슬래시 제거
    • YAML의 큰따옴표(" ") 활용

5.4 docker: command not found

  • 현상 : `docker: command not found` 또는 `Cannot connect to the Docker daemon`
  • 원인
    • GitLab Runner 설정 파일(`config.toml`)에서 `privileged = true` 설정이 누락됨.
    • 2.2 순서 확인하여 진행

 


GitLab CI/CD 파이프라인 구축을 통해 기존의 FileZilla를 사용해 서버에 파일을 업로드하여 배포하는 프로세스를 개선했다. 

 

진행하면서 삽질을 꽤 많이했는데, 진행하면서 셀프 호스팅 GitLab 환경에서 CI/CD를 구축하는 전체 프로세스가 한눈에 정리되어 있는걸 한번만 봤으면 좋겠다 싶었다. 


그래서 최대한 모든 과정을 담아보려 노력했는데, 일부 누락된 단계가 있을 수 있다. 


사실 에러도 훨씬 많았는데, 발생했던 모든 에러를 다 적기엔 무리가 있어서 주요 트러블슈팅 사례들만 추렸다. 
누군가의 삽질에 (또는 미래의 나에게) 도움이 되길 바라며 ! 

 

 

  • Before: 로컬 개발 환경에서 FTP(FileZilla)를 이용해 코드를 전송하고, 서버에 접속하여 수동으로 컨테이너를 재시작함.
  • After: 개발자가 코드를 수정하고 브랜치(`main`, `develop`)에 push/merge 하는 즉시, Runner가 소스 체크아웃부터 빌드 및 배포까지 전 과정을 자동으로 수행함.