본문 바로가기
서버, 인프라

[월 1만원으로 내 서비스 올리기] GitHub push 하면 자동으로 배포된다고? Jenkins CI/CD 파이프라인 직접 구성해봤습니다 — Hetzner 실전 세팅 5편

by 요즘IT 2026. 5. 25.

Jenkins 파이프라인으로 GitHub push 시 Next.js·Spring Boot 자동 빌드·배포 구성하는 전 과정 실제 삽질 기반으로 정리했어요.

 

이전 글: [서버, 인프라] - Next.js + Spring Boot, Hetzner 서버에 직접 올려봤습니다 — Docker Compose + Nginx 세팅 4편


4편에서 Docker Compose로 프론트/백엔드 올리는 것까지 했잖아요.

근데 솔직히 말하면, 코드 바꿀 때마다 서버 들어가서 직접 빌드하고 재시작하는 게 너무 귀찮거든요. 파일 하나 고쳤는데 SSH 접속 → git pull → docker build → docker compose restart 이 과정을 매번 반복해야 해요.

이게 Jenkins CI/CD를 쓰는 이유예요. GitHub에 push 하면 알아서 빌드하고 배포까지 해주거든요.


전체 흐름 먼저 잡고 시작할게요

GitHub push
→ Webhook으로 Jenkins에 알림
→ Jenkins가 코드 pull
→ Docker 이미지 빌드
→ 기존 컨테이너 교체
→ 배포 완료

AWS 쓸 때는 ECR에 이미지 올리고 EC2에 SSH로 배포하는 복잡한 구조였는데, Hetzner에서는 Jenkins가 같은 서버에 있으니까 훨씬 단순해요.


1단계 — Jenkins 컨테이너에 Docker 소켓 마운트

여기서 잠깐. 중요한 포인트예요.

Jenkins가 컨테이너로 떠있거든요. 컨테이너 안에서 docker build 명령어를 쓰려면 호스트의 Docker 소켓을 Jenkins 컨테이너 안으로 연결해줘야 해요.

안 하면 Jenkins 파이프라인에서 docker 명령어 칠 때마다 Permission denied 나와요.

Jenkins 컨테이너 재실행이 필요해요.

docker stop jenkins
docker rm jenkins

docker run -d \
  --name jenkins \
  --restart unless-stopped \
  -p 8090:8080 \
  -p 50000:50000 \
  -v /opt/jenkins_home:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /usr/bin/docker:/usr/bin/docker \
  -v /opt/app:/opt/app \
  -e JAVA_TOOL_OPTIONS="-Duser.timezone=Asia/Seoul" \
  jenkins/jenkins:lts

추가된 게 세 가지예요.

/var/run/docker.sock — 호스트 Docker 소켓 마운트 /usr/bin/docker — docker 바이너리 마운트 /opt/app — Docker Compose 파일 접근용

Docker 소켓 권한도 열어줘야 해요

sudo chmod 666 /var/run/docker.sock

재부팅해도 유지되게 udev 규칙도 추가해요.

cat > /etc/udev/rules.d/99-docker.rules << 'EOF'
KERNEL=="docker.sock", GROUP="docker", MODE="0666"
EOF

2단계 — Jenkins 컨테이너에 SSH 키 등록

Jenkins가 GitHub Private 레포에서 코드를 pull 해와야 하거든요. SSH 키가 Jenkins 컨테이너 안에 있어야 해요.

호스트 서버의 SSH 키를 Jenkins 컨테이너 안으로 복사해줄게요.

docker exec jenkins mkdir -p /var/jenkins_home/.ssh

docker cp ~/.ssh/id_ed25519 jenkins:/var/jenkins_home/.ssh/id_ed25519
docker cp ~/.ssh/id_ed25519.pub jenkins:/var/jenkins_home/.ssh/id_ed25519.pub

chmod 600 /opt/jenkins_home/.ssh/id_ed25519
chown -R 1000:1000 /opt/jenkins_home/.ssh

chmod랑 chown은 컨테이너 안에서 하면 권한 문제가 생겨요. 호스트에서 /opt/jenkins_home 경로로 직접 해줘야 해요. 이게 포인트예요.

연결 테스트

docker exec -it jenkins ssh -T git@github.com

Hi 계정명! You've successfully authenticated 나오면 성공이에요.

처음엔 Are you sure you want to continue connecting? 나오는데 yes 입력하면 돼요.

Jenkins 파이프라인 빌드 성공 후 콘솔 출력 화면
Jenkins 파이프라인 빌드 성공 후 콘솔 출력 화면

 


3단계 — Jenkinsfile 작성

기존 Jenkinsfile이 AWS ECR + EC2 기반으로 돼있었어요. Hetzner 로컬 빌드 방식으로 완전히 새로 작성했어요.

프론트엔드 Jenkinsfile

pipeline {
    agent any

    environment {
        IMAGE_NAME = 'front-app'
        CONTAINER_NAME = 'front-app'
        APP_DIR = '/opt/app'
    }

    stages {
        stage('Pull') {
            steps {
                git branch: 'main',
                    credentialsId: 'github-credentials',
                    url: 'git@github.com:계정명/프론트레포.git'
            }
        }

        stage('Build Image') {
            steps {
                sh """
                    docker build -t ${IMAGE_NAME}:latest .
                """
            }
        }

        stage('Deploy') {
            steps {
                sh """
                    cd ${APP_DIR}
                    docker-compose stop ${CONTAINER_NAME}
                    docker-compose rm -f ${CONTAINER_NAME}
                    docker-compose up -d ${CONTAINER_NAME}
                """
            }
        }

        stage('Clean up') {
            steps {
                sh """
                    docker image prune -f
                """
            }
        }
    }

    post {
        success {
            echo '프론트엔드 배포 완료'
        }
        failure {
            echo '프론트엔드 배포 실패'
        }
    }
}

백엔드 Jenkinsfile

백엔드는 Gradle 빌드가 추가되고, 배포 후 Nginx 재시작이 필요해요.

왜냐면 백엔드 컨테이너가 교체되면 IP가 바뀌는데, Nginx가 예전 IP를 캐싱하고 있어서 502 에러가 나거든요. 배포 마지막에 Nginx 재시작을 넣어줘야 해요.

pipeline {
    agent any

    environment {
        IMAGE_NAME = 'api-app'
        CONTAINER_NAME = 'api-app'
        APP_DIR = '/opt/app'
    }

    stages {
        stage('Pull') {
            steps {
                git branch: 'master',
                    credentialsId: 'github-credentials',
                    url: 'git@github.com:계정명/백엔드레포.git'
            }
        }

        stage('Gradle Build') {
            steps {
                dir('api') {
                    sh """
                        chmod +x gradlew
                        ./gradlew clean build -x test
                    """
                }
            }
        }

        stage('Build Image') {
            steps {
                dir('api') {
                    sh """
                        docker build -t ${IMAGE_NAME}:latest .
                    """
                }
            }
        }

        stage('Deploy') {
            steps {
                sh """
                    cd ${APP_DIR}
                    docker-compose stop ${CONTAINER_NAME}
                    docker-compose rm -f ${CONTAINER_NAME}
                    docker-compose up -d ${CONTAINER_NAME}
                    docker-compose restart nginx
                """
            }
        }

        stage('Clean up') {
            steps {
                sh """
                    docker image prune -f
                """
            }
        }
    }

    post {
        success {
            echo '백엔드 배포 완료'
        }
        failure {
            echo '백엔드 배포 실패'
        }
    }
}

AWS 방식이랑 비교하면 엄청 단순해졌어요. ECR 로그인, 이미지 push, EC2 SSH 접속 이런 과정이 다 사라졌거든요. Jenkins가 같은 서버에 있으니까 바로 빌드하고 바로 배포하면 끝이에요.


4단계 — Jenkins 파이프라인 아이템 생성

Dashboard → New Item

  • Item name: front-pipeline
  • Pipeline 선택 → OK

설정 화면에서

General

  • GitHub project 체크
  • Project url: GitHub 레포 URL

Build Triggers

  • GitHub hook trigger for GITScm polling 체크

Pipeline

  • Definition: Pipeline script from SCM
  • SCM: Git
  • Repository URL: git@github.com:계정명/레포명.git
  • Credentials: github-credentials
  • Branch: */main
  • Script Path: Jenkinsfile

Save 누르면 돼요. 백엔드도 동일하게 만들면 돼요.


5단계 — GitHub Webhook 설정

Webhook이 없으면 Jenkins가 GitHub push를 감지 못해요. 수동으로 Build Now 눌러야 하거든요.

GitHub 레포 → Settings → Webhooks → Add webhook

  • Payload URL: http://서버IP:8090/github-webhook/
  • Content type: application/json
  • Which events: Just the push event
  • Active 체크

프론트/백엔드 레포 각각 등록해야 해요.

등록하고 나서 초록색 체크 뜨면 연결 성공이에요.

GitHub Webhook 등록
GitHub Webhook 등록


삽질 포인트 정리

1. Docker 소켓 권한

chmod 666 /var/run/docker.sock 안 하면 Jenkins에서 docker 명령어 칠 때마다 permission denied 나와요. 서버 재부팅하면 날아가니까 udev 규칙으로 영구 적용해두는 게 좋아요.

2. SSH 키 권한

chmod 600이랑 chown 1000:1000은 컨테이너 안에서 하면 안 돼요. 호스트에서 /opt/jenkins_home/.ssh 경로로 직접 해줘야 해요.

3. APP_DIR 경로

Jenkins 컨테이너에서 /root/app은 접근이 안 돼요. /opt/app으로 옮기고 마운트 해줘야 해요.

4. 백엔드 배포 후 Nginx 502 에러

백엔드 컨테이너가 교체되면 내부 IP가 바뀌는데 Nginx가 예전 IP를 캐싱하고 있어서 502가 나와요. Jenkinsfile Deploy 단계 마지막에 docker-compose restart nginx 넣어두면 자동으로 해결돼요.

5. docker compose vs docker-compose

Jenkins 컨테이너 안에서 docker compose(신버전 플러그인 방식)는 동작 안 할 수 있어요. docker-compose(구버전) 설치해서 쓰는 게 안정적이에요.

docker exec -u root jenkins bash -c "curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose"

최종 구성 완성

GitHub push
    ↓
Webhook (8090/github-webhook/)
    ↓
Jenkins 파이프라인 자동 트리거
    ↓
코드 pull → 빌드 → 컨테이너 교체 → Nginx 재시작
    ↓
서비스 자동 반영

결국 핵심은 Jenkins가 컨테이너로 떠있을 때 Docker 소켓이랑 SSH 키 두 가지를 제대로 연결해주는 거예요. 이게 안 되면 파이프라인이 아무리 잘 짜여있어도 소용없거든요.

다음 편에서는 도메인 연결이랑 HTTPS 설정까지 다뤄볼게요.

 

다음 글: [서버, 인프라] - 서버에 내 도메인 달기 — 도메인 구매부터 DNS 연결까지 (6편)