Jenkins, Docker, Nginx를 활용한 프론트엔드 자동화 배포 파이프라인 구축

시스템 아키텍처 개요

지속적 통합 및 지속적 배포(CI/CD) 환경을 구축하기 위해 소스 코드 저장소부터 실제 서비스 환경까지의 흐름을 다음과 같이 설계합니다.

소스 코드 저장소 (Git Push)
  ↓
Jenkins (웹훅 트리거 및 파이프라인 실행)
  ↓
Docker (멀티 스테이지 빌드 및 이미지 생성)
  ↓
Nginx (컨테이너 기반 정적 파일 서빙)

인프라 환경 설정

1. 도커 엔진 설치

호스트 머신에 컨테이너 런타임을 설치하고 데몬을 활성화합니다.

# 패키지 목록 업데이트 및 필수 종속성 설치
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# 도커 공식 GPG 키 추가 및 저장소 설정
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# 도커 엔진 및 CLI 도구 설치
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# 서비스 등록 및 시작
sudo systemctl enable --now docker

2. 젠킨스 컨테이너 프로비저닝

호스트의 도커 소켓을 마운트하여 젠킨스 내부에서 도커 명령어를 실행할 수 있도록(Docker-outside-of-Docker) 구성합니다.

# 영속적 데이터 디렉토리 생성 및 권한 할당
sudo mkdir -p /data/jenkins_volume
sudo chown -R 1000:1000 /data/jenkins_volume

# 젠킨스 LTS 버전 컨테이너 실행
docker run -d \
  --name ci-jenkins \
  --restart=always \
  -p 9090:8080 \
  -p 50000:50000 \
  -v /data/jenkins_volume:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /usr/bin/docker:/usr/bin/docker \
  jenkins/jenkins:lts-jdk17

브라우저에서 http://[서버_IP]:9090으로 접속하여 초기 관리자 비밀번호(/data/jenkins_volume/secrets/initialAdminPassword)를 입력하고 설정을 완료합니다.

젠킨스 서버 초기화 및 도구 구성

1. 필수 플러그인 설치

Jenkins 관리 > 플러그인 관리 메뉴로 이동하여 다음 플러그인을 검색하고 설치합니다.

  • Git Plugin
  • NodeJS Plugin
  • Docker Pipeline

2. 빌드 도구 전역 설정

Jenkins 관리 > 전역 도구 구성에서 NodeJS 자동 설치 프로그램을 추가합니다. (예: Node 18.x LTS 버전 지정)

3. 파이프라인 잡 생성

새로운 아이템을 생성할 때 Pipeline을 선택합니다. 파이프라인 섹션에서 정의를 Pipeline script from SCM으로 변경하고, Git 저장소 URL과 인증 정보를 입력한 후 스크립트 경로를 Jenkinsfile로 지정합니다.

프론트엔드 프로젝트 배포 설정 파일 작성

프로젝트 루트 디렉토리에 다음 세 가지 설정 파일을 추가하여 빌드 및 배포 로직을 정의합니다.

1. Dockerfile (멀티 스테이지 빌드)

이미지 크기를 최소화하기 위해 빌드 단계와 실행 단계를 분리합니다.

# 1단계: 의존성 설치 및 에셋 컴파일
FROM node:18-alpine AS compile-stage
WORKDIR /workspace
COPY package*.json ./
RUN npm ci --production=false
COPY . .
RUN npm run build

# 2단계: 경량 웹 서버에 정적 파일 주입
FROM nginx:stable-alpine AS production-stage
RUN rm /etc/nginx/conf.d/default.conf
COPY deploy/nginx.conf /etc/nginx/conf.d/
COPY --from=compile-stage /workspace/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

2. Nginx 라우팅 설정 (deploy/nginx.conf)

SPA(Single Page Application)의 클라이언트 사이드 라우팅을 지원하고 정적 파일 캐싱을 활성화합니다.

server {
    listen 80;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html;

    # Gzip 압축 활성화
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # 정적 자산 캐싱 설정
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

3. Jenkinsfile (파이프라인 스크립트)

도커 이미지를 빌드하고 기존 컨테이너를 교체하는 배포 로직을 선언형 파이프라인으로 작성합니다.

pipeline {
    agent any
    
    environment {
        APP_NAME = 'web-client'
        IMAGE_TAG = "${env.BUILD_ID}"
        FULL_IMAGE = "${APP_NAME}:${IMAGE_TAG}"
    }

    tools {
        nodejs 'Node-18'
    }

    stages {
        stage('소스 코드 체크아웃') {
            steps {
                checkout scm
            }
        }
        
        stage('도커 이미지 빌드') {
            steps {
                script {
                    docker.build(FULL_IMAGE)
                }
            }
        }
        
        stage('컨테이너 롤아웃') {
            steps {
                script {
                    sh """
                        docker stop ${APP_NAME} || true
                        docker rm ${APP_NAME} || true
                        docker run -d \
                            --name ${APP_NAME} \
                            --restart unless-stopped \
                            -p 80:80 \
                            ${FULL_IMAGE}
                    """
                }
            }
        }
    }
    
    post {
        always {
            // 빌드 후 사용하지 않는 dangling 이미지 정리
            sh 'docker image prune -f'
        }
    }
}

배포 프로세스 및 검증

  1. 트리거: 개발자가 feature 브랜치를 main 브랜치로 병합하면 웹훅을 통해 젠킨스 파이프라인이 자동 실행됩니다.
  2. 빌드: 젠킨스가 노드 환경을 활용해 프로젝트를 컴파일하고, 도커 멀티 스테이지 빌드를 통해 최적화된 엔진엑스 이미지를 생성합니다.
  3. 배포: 호스트 머신에서 기존 컨테이너를 중지 및 제거한 뒤, 새로 빌드된 이미지를 기반으로 컨테이너를 기동합니다.

배포가 완료된 후 다음 명령어로 컨테이너 상태와 서비스 응답을 확인합니다.

# 실행 중인 컨테이너 목록 및 포트 매핑 확인
docker ps --filter "name=web-client"

# HTTP 응답 헤더 및 상태 코드 검증
curl -I http://localhost

프로덕션 환경 최적화 및 트러블슈팅

최적화 전략

  • 태그 전략: 빌드 번호 대신 Git Commit SHA를 이미지 태그로 사용하여 버전 추적성을 높입니다.
  • 레지스트리 연동: 빌드된 이미지를 AWS ECR, Docker Hub 또는 사설 Harbor 레지스트리에 푸시하여 다중 호스트 배포를 준비합니다.
  • 무중단 배포: 단일 컨테이너 교체 방식에서 벗어나 Docker Compose나 Kubernetes를 활용한 롤링 업데이트를 고려합니다.

주요 오류 및 해결 방안

  • Permission Denied (Docker 소켓): 젠킨스 컨테이너 내부의 사용자가 호스트의 도커 소켓에 접근할 권한이 없는 경우입니다. 호스트에서 sudo usermod -aG docker jenkins 명령어를 실행하거나, 소켓 파일의 권한을 조정해야 합니다.
  • 포트 충돌 (Address already in use): 호스트의 80번 포트가 다른 프로세스에 의해 점유되어 있을 때 발생합니다. sudo lsof -i :80 명령어로 충돌 프로세스를 식별하고 종료하거나, 젠킨스 스크립트의 포트 매핑을 변경합니다.
  • OOM (Out of Memory) 발생: 프론트엔드 빌드 단계(Node.js)에서 메모리 부족으로 프로세스가 종료되는 경우, NODE_OPTIONS="--max-old-space-size=4096" 환경 변수를 Dockerfile에 추가하여 힙 메모리 제한을 늘립니다.

태그: Jenkins docker nginx CI/CD DevOps

5월 29일 14:42에 게시됨