Jenkins CI/CD 구축 가이드 (React + Spring Boot)

danhyeon·2026년 1월 27일

Deploy

목록 보기
6/6

1. 서버 초기 설정

1.1 필수 패키지 설치

bash

*# 시스템 업데이트*
sudo apt update && sudo apt upgrade -y

*# Docker 설치*
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io

*# Docker Compose 설치*
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

*# Docker 권한 설정*
sudo usermod -aG docker $USER
newgrp docker

*# Git 설치*
sudo apt install -y git

2. Jenkins 설치 및 설정

2.1 Jenkins 설치 (Docker 방식)

*# Jenkins 데이터 저장 디렉토리 생성*
mkdir -p ~/jenkins_home
chmod 777 jenkins_home/

*# Jenkins 컨테이너 실행*
docker run -d \
  --name jenkins \
  --restart=unless-stopped \
  -p 8088:8080 \
  -p 50000:50000 \
  -v ~/jenkins_home:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $(which docker):/usr/bin/docker \
  jenkins/jenkins:lts

### 2.2 Jenkins 초기 설정

```bash
*# 초기 관리자 비밀번호 확인*
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

웹 브라우저에서 접속: http://your-server-ip:8080

  1. 초기 비밀번호 입력
  2. "Install suggested plugins" 선택
  3. 관리자 계정 생성

2.3 Jenkins 내 Docker 권한 설정

*### 2.4 필수 플러그인 설치*

Jenkins 대시보드에서:
1. **Manage Jenkins** → **Manage Plugins** → **Available** 탭
2. 다음 플러그인 검색 및 설치:
   - `GitHub Integration`
   - `Docker Pipeline`
   - `NodeJS` (React 빌드용)

3. 설치 후 Jenkins 재시작

*### 2.5 GitHub Credentials 설정*

1. **Manage Jenkins** → **Manage Credentials** → **System** → **Global credentials**
2. **Add Credentials** 클릭
3. 다음 정보 입력:
   - Kind: `Username with password`
   - Username: GitHub 계정명
   - Password: GitHub Personal Access Token (repo 권한 필요)
   - ID: `github-credentials` (기억해둘 것)

---

*## 3. 프로젝트1: React + Nginx 설정### 3.1 프로젝트 구조*

react-project/
├── src/
├── public/
├── package.json
├── vite.config.js
├── Dockerfile
├── nginx.conf
└── Jenkinsfile


```bash
*# Jenkins 컨테이너에 Docker 그룹 추가*
docker exec -u root jenkins chmod 666 /var/run/docker.sock

docker exec -u root jenkins bash -c "
	apt-get update && \
  apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release && \
  curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
  echo 'deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bullseye stable' | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
  apt-get update && \
  apt-get install -y docker-ce-cli
"

# Docker Compose 다운로드 및 설치
docker exec -u root jenkins bash -c "
curl -SL https://github.com/docker/compose/releases/download/v2.24.5/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose && \
chmod +x /usr/local/bin/docker-compose && \
ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose
"

# Jenkins 컨테이너 내부에서 Docker 확인
docker exec jenkins docker --version

# Docker Compose 확인
docker exec jenkins docker-compose --version

3.2 React Dockerfile

# 빌드 스테이지
FROM node:22-alpine AS builder

WORKDIR /app

# 패키지 파일 복사 및 의존성 설치
COPY package*.json ./
RUN npm ci

# 소스 코드 복사 및 빌드
COPY . .
RUN npm run build

# 프로덕션 스테이지
FROM nginx:alpine

# Nginx 설정 파일 복사
COPY nginx.conf /etc/nginx/nginx.conf

# 빌드된 파일 복사
COPY --from=builder /app/dist /usr/share/nginx/html

# 포트 노출
EXPOSE 80

# Nginx 실행
CMD ["nginx", "-g", "daemon off;"]

3.3 nginx.conf

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # 로그 설정
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # Gzip 압축
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript 
               application/x-javascript application/xml+rss 
               application/javascript application/json;

    server {
        listen 80;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;

        # React Router 지원 (SPA)
        location / {
            try_files $uri $uri/ /index.html;
        }

        # API 프록시 설정 (백엔드로 포워딩)
        location /api/ {
            # 프록시 버퍼 설정
            proxy_buffer_size 128k;
            proxy_buffers 4 256k;
            proxy_busy_buffers_size 256k;
            
            proxy_pass http://172.17.0.1:8080/;
            proxy_http_version 1.1;
            
            # 모든 헤더 전달
            proxy_pass_request_headers on;
            
            # Authorization 헤더 명시적으로 전달 (JWT용)
            proxy_set_header Authorization $http_authorization;
            
            # 기본 프록시 헤더
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            
            # CORS 관련 헤더 전달
            proxy_set_header Origin $http_origin;
            
            proxy_cache_bypass $http_upgrade;
            
            # 타임아웃 설정
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
            
            rewrite ^/api/(.*)$ /$1 break;
        }

        # 정적 파일 캐싱
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }
}

3.3 Jenkinsfile

pipeline {
    agent any
    
    environment {
        IMAGE_NAME = 'react-frontend'
        CONTAINER_NAME = 'react-app'
        DOCKER_NETWORK = 'app-network'
        PORT = '80'
    }
    
    stages {
        stage('Checkout') {
            steps {
                echo 'Checking out code from GitHub...'
                checkout scm
            }
        }
        
        stage('Build Docker Image') {
            steps {
                script {
                    echo 'Building Docker image...'
                    sh """
                        docker build -t ${IMAGE_NAME}:latest .
                    """
                }
            }
        }
        
        stage('Stop Old Container') {
            steps {
                script {
                    echo 'Stopping and removing old container...'
                    sh """
                        docker stop ${CONTAINER_NAME} || true
                        docker rm ${CONTAINER_NAME} || true
                    """
                }
            }
        }
        
        stage('Create Network') {
            steps {
                script {
                    echo 'Creating Docker network if not exists...'
                    sh """
                        docker network create ${DOCKER_NETWORK} || true
                    """
                }
            }
        }
        
        stage('Deploy') {
            steps {
                script {
                    echo 'Deploying new container...'
                    sh """
                        docker run -d \
                            --name ${CONTAINER_NAME} \
                            --network ${DOCKER_NETWORK} \
                            -p ${PORT}:80 \
                            --restart unless-stopped \
                            ${IMAGE_NAME}:latest
                    """
                }
            }
        }
        
        stage('Clean Up') {
            steps {
                script {
                    echo 'Cleaning up unused Docker images...'
                    sh """
                        docker image prune -f
                    """
                }
            }
        }
    }
    
    post {
        success {
            echo 'Deployment successful!'
        }
        failure {
            echo 'Deployment failed!'
        }
    }
}

4. 프로젝트2: Spring Boot + MySQL 설정

4.1 프로젝트 구조

spring-boot-project/
├── src/
├── pom.xml (또는 build.gradle)
├── Dockerfile
├── docker-compose.yml
└── Jenkinsfile

4.2 Dockerfile

# 빌드 스테이지
FROM gradle:8.5-jdk21 AS builder
# Maven 사용 시: FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /app

# Gradle 사용 시
COPY build.gradle settings.gradle ./
COPY gradle gradle
COPY gradlew ./

# gradlew 실행 권한 부여
RUN chmod +x ./gradlew

RUN ./gradlew dependencies --no-daemon

# Maven 사용 시 (위 4줄 대신 아래 2줄 사용)
# COPY pom.xml ./
# RUN mvn dependency:go-offline

# 소스 코드 복사 및 빌드
COPY src ./src

# Gradle 빌드
RUN chmod +x ./gradlew && ./gradlew bootJar --no-daemon
# Maven 빌드 시: RUN mvn clean package -DskipTests

# 런타임 스테이지
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=builder /app/build/libs/*.jar app.jar
# Maven 빌드 시: COPY --from=builder /app/target/*.jar app.jar

# 포트 노출
EXPOSE 8080

# 애플리케이션 실행
ENTRYPOINT ["sh", "-c", "java", "-Duser.timezone=Asia/Seoul", "-jar", "app.jar"]

4.3 docker-compose.yml

services:
  mysql:
    image: mysql:8.0
    container_name: mysql-db
    restart: unless-stopped
    command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_ROOT_PASSWORD: 1234
      MYSQL_DATABASE: base_db
      TZ: Asia/Seoul
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql # 초기화 SQL (선택사항)
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p1234"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 40s # 초기화 시간 증가

  spring-backend:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: spring-backend
    restart: unless-stopped
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://mysql-db:3306/base_db?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
      SPRING_DATASOURCE_USERNAME: root
      SPRING_DATASOURCE_PASSWORD: 1234
      SPRING_DATASOURCE_DRIVER_CLASS_NAME: com.mysql.cj.jdbc.Driver
      SPRING_JPA_HIBERNATE_DDL_AUTO: update
      SPRING_JACKSON_TIME_ZONE: Asia/Seoul
    ports:
      - "8080:8080"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - app-network

networks:
  app-network:
    external: true

volumes:
  mysql-data:
    driver: local

4.4 Jenkinsfile

pipeline {
    agent any
    
    environment {
        COMPOSE_PROJECT = 'spring-backend'
        DOCKER_NETWORK = 'app-network'
    }
    
    stages {
        stage('Checkout') {
            steps {
                echo 'Checking out code from GitHub...'
                checkout scm
            }
        }
        
        stage('Create Network') {
            steps {
                script {
                    echo 'Creating Docker network if not exists...'
                    sh """
                        docker network create ${DOCKER_NETWORK} || true
                    """
                }
            }
        }
        
        stage('Stop Old Containers') {
            steps {
                script {
                    echo 'Stopping old containers...'
                    sh """
                        docker compose down || true
                    """
                }
            }
        }
        
        stage('Build and Deploy') {
            steps {
                script {
                    echo 'Building and deploying with docker-compose...'
                    sh """
                        docker compose up -d --build
                    """
                }
            }
        }
        
        stage('Wait for Health Check') {
            steps {
                script {
                    echo 'Waiting for services to be healthy...'
                    sh """
                        timeout 120 sh -c 'until docker inspect --format="{{.State.Health.Status}}" mysql-db | grep -q healthy; do sleep 2; done' || true
                        sleep 10
                    """
                }
            }
        }
        
        stage('Verify Deployment') {
            steps {
                script {
                    echo 'Verifying deployment...'
                    sh """
                        docker compose ps
                        docker logs spring-backend --tail=50
                    """
                }
            }
        }
        
        stage('Clean Up') {
            steps {
                script {
                    echo 'Cleaning up unused Docker resources...'
                    sh """
                        docker image prune -f
                        docker volume prune -f
                    """
                }
            }
        }
    }
    
    post {
        success {
            echo 'Spring Boot deployment successful!'
        }
        failure {
            echo 'Spring Boot deployment failed!'
            sh """
                docker compose logs || true
            """
        }
    }
}

5. GitHub Webhook 설정

5.1 Jenkins에서 각 프로젝트 Job 생성

React 프로젝트 Job 생성

  1. Jenkins 대시보드 → New Item 클릭
  2. 이름: react-frontend-pipeline
  3. 타입: Pipeline 선택
  4. OK 클릭

설정 페이지에서:

  • General 섹션:
    • GitHub project 체크
    • Project url: https://github.com/your-username/react-project
  • Build Triggers 섹션:
    • GitHub hook trigger for GITScm polling 체크
  • Pipeline 섹션:
    • Definition: Pipeline script from SCM
    • SCM: Git
    • Repository URL: https://github.com/your-username/react-project.git
    • Credentials: 앞서 생성한 github-credentials 선택
    • Branch Specifier: /main (중요!)
    • Script Path: Jenkinsfile
  • 저장 클릭

Spring Boot 프로젝트 Job 생성

동일한 방식으로:

  1. 이름: spring-backend-pipeline
  2. Repository URL: https://github.com/your-username/spring-boot-project.git
  3. 나머지 설정 동일

5.2 GitHub Webhook 설정

각 프로젝트 리포지토리에서:

  1. SettingsWebhooksAdd webhook 클릭
  2. 다음 정보 입력:
    • Payload URL: http://your-server-ip:8080/github-webhook/
    • Content type: application/json
    • Which events: Just the push event 선택
    • Active 체크
  3. Add webhook 클릭

6. 추가 설정 및 최적화

6.1 방화벽 설정 (UFW)

*# 필요한 포트 열기*
sudo ufw allow 22/tcp      *# SSH*
sudo ufw allow 8080/tcp    *# Jenkins*
sudo ufw allow 3000/tcp    *# React App*
sudo ufw allow 80/tcp      *# Nginx (프로덕션)*
sudo ufw allow 443/tcp     *# HTTPS (프로덕션)*

sudo ufw enable
sudo ufw status

6.2 SSL 인증서 적용

# ssl 디렉토리 생성 (이미 있을 수 있음)
mkdir -p ssl
cd ssl

# fullchain.pem 생성 (도메인 인증서 + 중간 인증서)
cat *_cert.pem.txt *_chain_cert.pem.txt > fullchain.pem

# 개인 키 복사
cp *.key privkey.pem

# 권한 설정
chmod 644 fullchain.pem
chmod 600 privkey.pem

# fullchain.pem 검증
openssl x509 -in ssl/fullchain.pem -text -noout | grep -E "Subject:|Issuer:|Not After"
# 문제 발생시 합쳐진 fullchain의 end--start 부분이 줄바꿈 되어 있는 지 확인

# privkey.pem 검증
openssl rsa -in ssl/privkey.pem -check -noout

# 인증서-키 매칭 확인
echo "인증서 Modulus:"
openssl x509 -noout -modulus -in ssl/fullchain.pem | openssl md5

echo "키 Modulus:"
openssl rsa -noout -modulus -in ssl/privkey.pem | openssl md5

# 서버에 SSL 디렉토리 생성
sudo mkdir -p /opt/ssl/inufleet.com

# 인증서 복사
sudo cp ssl/fullchain.pem /opt/ssl/inufleet.com/
sudo cp ssl/privkey.pem /opt/ssl/inufleet.com/

# 권한 설정
sudo chmod 644 /opt/ssl/inufleet.com/fullchain.pem
sudo chmod 600 /opt/ssl/inufleet.com/privkey.pem

# 확인
ls -la /opt/ssl/inufleet.com/

6.2.1 Jenkinsfile, nginx.conf, Dockerfile 변경

pipeline {
    agent any
    
    environment {
        IMAGE_NAME = 'react-frontend'
        CONTAINER_NAME = 'react-app'
        DOCKER_NETWORK = 'app-network'
        HTTP_PORT = '80'
        HTTPS_PORT = '443'
    }
    
    stages {
        stage('Checkout') {
            steps {
                echo 'Checking out code from GitHub...'
                checkout scm
            }
        }
        
        stage('Build Docker Image') {
            steps {
                script {
                    echo 'Building Docker image...'
                    sh """
                        docker build \
                            --build-arg VITE_API_BASE_URL=https://${DOMAIN}/api \
                            --build-arg VITE_ENV=production \
                            -t ${IMAGE_NAME}:latest .
                    """
                }
            }
        }
        
        stage('Stop Old Container') {
            steps {
                script {
                    echo 'Stopping and removing old container...'
                    sh """
                        docker stop ${CONTAINER_NAME} || true
                        docker rm ${CONTAINER_NAME} || true
                    """
                }
            }
        }
        
        stage('Create Network') {
            steps {
                script {
                    echo 'Creating Docker network if not exists...'
                    sh """
                        docker network create ${DOCKER_NETWORK} || true
                    """
                }
            }
        }
        
        stage('Deploy') {
            steps {
                script {
                    echo 'Deploying new container...'
                    sh """
                        docker run -d \
                            --name ${CONTAINER_NAME} \
                            --network ${DOCKER_NETWORK} \
                            -p ${HTTP_PORT}:80 \
                            -p ${HTTPS_PORT}:443 \
                            -v /opt/ssl/inufleet.com/fullchain.pem:/etc/nginx/ssl/fullchain.pem:ro \
                            -v /opt/ssl/inufleet.com/privkey.pem:/etc/nginx/ssl/privkey.pem:ro \
                            --restart unless-stopped \
                            ${IMAGE_NAME}:latest
                    """
                }
            }
        }
        
        stage('Clean Up') {
            steps {
                script {
                    echo 'Cleaning up unused Docker images...'
                    sh """
                        docker image prune -f
                    """
                }
            }
        }
    }
    
    post {
        success {
            echo 'Deployment successful!'
        }
        failure {
            echo 'Deployment failed!'
        }
    }
}
events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # 로그 설정
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # Gzip 압축
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript 
               application/x-javascript application/xml+rss 
               application/javascript application/json;

    # HTTP -> HTTPS 리다이렉트
    server {
        listen 80;
        server_name www.your-domain.com your-domain.com;
        
        location / {
            return 301 https://$host$request_uri;
        }
    }

    # HTTPS 서버
    server {
        listen 443 ssl http2;
        server_name www.your-domain.com your-domain.com;
        
        root /usr/share/nginx/html;
        index index.html;

        # SSL 인증서 설정
        ssl_certificate /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;

        # SSL 보안 설정
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 10m;

        # 보안 헤더
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;

        # React Router 지원 (SPA)
        location / {
            try_files $uri $uri/ /index.html;
        }

        # API 프록시 설정 (백엔드로 포워딩)
        location /api/ {
            proxy_pass http://spring-backend:8080/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_cache_bypass $http_upgrade;
            
            rewrite ^/api/(.*)$ /$1 break;
        }

        # 정적 파일 캐싱
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }
}
# 빌드 스테이지
FROM node:22-alpine AS builder

WORKDIR /app

# 패키지 파일 복사 및 의존성 설치
COPY package*.json ./
RUN npm ci

# 소스 코드 복사 및 빌드
COPY . .
RUN npm run build

# 프로덕션 스테이지
FROM nginx:alpine

# Nginx 설정 파일 복사
COPY nginx.conf /etc/nginx/nginx.conf

# 빌드된 파일 복사
COPY --from=builder /app/dist /usr/share/nginx/html

# 포트 노출
EXPOSE 80
EXPOSE 443

# Nginx 실행
CMD ["nginx", "-g", "daemon off;"]

6.2.2 무료 SSL인증서 발급이 필요한 경우

6.2.2.1. nginx.conf 수정

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # 로그 설정
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # Gzip 압축
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript 
               application/x-javascript application/xml+rss 
               application/javascript application/json;

    # HTTP 서버 (Let's Encrypt 검증용 + HTTPS 리다이렉트)
    server {
        listen 80;
        server_name www.your-domain.com your-domain.com;
        
        # Let's Encrypt 검증 경로
        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }
        
        # 나머지는 HTTPS로 리다이렉트
        location / {
            return 301 https://$host$request_uri;
        }
    }

    # HTTPS 서버
    server {
        listen 443 ssl http2;
        server_name www.your-domain.com your-domain.com;
        
        root /usr/share/nginx/html;
        index index.html;

        # SSL 인증서 설정
        ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

        # SSL 보안 설정
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 10m;

        # 보안 헤더
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;

        # React Router 지원 (SPA)
        location / {
            try_files $uri $uri/ /index.html;
        }

        # API 프록시 설정
        location /api/ {
            proxy_pass http://spring-backend:8080/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_cache_bypass $http_upgrade;
            
            rewrite ^/api/(.*)$ /$1 break;
        }

        # 정적 파일 캐싱
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }
}

6.2.2.2. Jenkinsfile 수정

pipeline {
    agent any
    
    environment {
        IMAGE_NAME = 'react-frontend'
        CONTAINER_NAME = 'react-app'
        DOCKER_NETWORK = 'app-network'
        HTTP_PORT = '80'
        HTTPS_PORT = '443'
    }
    
    stages {
        stage('Checkout') {
            steps {
                echo 'Checking out code from GitHub...'
                checkout scm
            }
        }
        
        stage('Build Docker Image') {
            steps {
                script {
                    echo 'Building Docker image...'
                    sh """
                        docker build -t ${IMAGE_NAME}:latest .
                    """
                }
            }
        }
        
        stage('Stop Old Container') {
            steps {
                script {
                    echo 'Stopping and removing old container...'
                    sh """
                        docker stop ${CONTAINER_NAME} || true
                        docker rm ${CONTAINER_NAME} || true
                    """
                }
            }
        }
        
        stage('Create Network and Volume') {
            steps {
                script {
                    echo 'Creating Docker network and volume if not exists...'
                    sh """
                        docker network create ${DOCKER_NETWORK} || true
                        docker volume create certbot-webroot || true
                    """
                }
            }
        }
        
        stage('Deploy') {
            steps {
                script {
                    echo 'Deploying new container...'
                    sh """
                        docker run -d \
                            --name ${CONTAINER_NAME} \
                            --network ${DOCKER_NETWORK} \
                            -p ${HTTP_PORT}:80 \
                            -p ${HTTPS_PORT}:443 \
                            -v /etc/letsencrypt:/etc/letsencrypt:ro \
                            -v certbot-webroot:/var/www/certbot:ro \
                            --restart unless-stopped \
                            ${IMAGE_NAME}:latest
                    """
                }
            }
        }
        
        stage('Clean Up') {
            steps {
                script {
                    echo 'Cleaning up unused Docker images...'
                    sh """
                        docker image prune -f
                    """
                }
            }
        }
    }
    
    post {
        success {
            echo 'Deployment successful!'
        }
        failure {
            echo 'Deployment failed!'
        }
    }
}

6.2.2.3. 호스트에서 Certbot 설치

sudo apt update
sudo apt install certbot

6.2.2.4. 첫 인증서 발급

#먼저 컨테이너가 실행 중인지 확인:
docker ps | grep react-app

#certbot-webroot 볼륨의 실제 경로 확인:
docker volume inspect certbot-webroot
*# Mountpoint를 확인 (예: /var/lib/docker/volumes/certbot-webroot/_data)*

#인증서 발급:
sudo certbot certonly --webroot \
  -w /var/lib/docker/volumes/certbot-webroot/_data \
  -d daehoint-issue.com \
  -d www.daehoint-issue.com \
  --email your-email@example.com \
  --agree-tos \
  --no-eff-email

6.2.2.5. 컨테이너 재시작

6.2.2.6. 자동 갱신 설정

#갱신 후 Nginx 리로드 스크립트 생성:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

#내용:
#!/bin/bash
docker exec react-app nginx -s reload 2>/dev/null || docker restart react-app

#실행 권한 부여:
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

6.2.2.7. 갱신 테스트

sudo certbot renew --dry-run

6.2.2.8. 자동 갱신 확인

#Certbot은 자동으로 systemd 타이머를 설정합니다:

sudo systemctl status certbot.timer

6.3 운영 환경 별도 설정 파일 적용

6.3.1 Dockerfile 변경

FROM gradle:8.5-jdk21 AS builder

WORKDIR /app

COPY build.gradle settings.gradle ./
COPY gradle ./gradle
RUN gradle dependencies --no-daemon || true

# src 전체 복사 (application.properties 포함)
COPY src ./src

RUN gradle bootJar --no-daemon -x test

# JAR 내부 확인
RUN echo "=== Checking JAR contents ===" && \
    jar tf /app/build/libs/*.jar | grep application.properties

FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "-jar", "app.jar"]

6.3.2 Jenkinsfile 변경

pipeline {
    agent any
    
    environment {
        COMPOSE_PROJECT = 'spring-backend'
        DOCKER_NETWORK = 'app-network'
        // 호스트의 설정 파일 경로 (Jenkins가 접근 가능한 경로)
        HOST_CONFIG_PATH = '/var/jenkins_config/application.properties'
    }
    
    stages {
        stage('Checkout') {
            steps {
                echo 'Checking out code from GitHub...'
                checkout scm
            }
        }
        
        stage('Copy Configuration to Build') {
            steps {
                script {
                    echo "Copying application.properties for build..."
                    sh """
                        # src/main/resources 디렉토리 생성
                        mkdir -p src/main/resources

                        # 호스트의 실제 설정을 빌드용으로 복사
                        if [ -f ${HOST_CONFIG_PATH} ]; then
                            cp ${HOST_CONFIG_PATH} src/main/resources/application.properties
                            echo "✅ Configuration copied to build resources"
                            ls -lh src/main/resources/application.properties
                        else
                            echo "❌ Configuration file not found at ${HOST_CONFIG_PATH}"
                            exit 1
                        fi
                    """
                }
            }
        }
        
        stage('Create Network') {
            steps {
                script {
                    echo 'Creating Docker network if not exists...'
                    sh """
                        docker network create ${DOCKER_NETWORK} || true
                    """
                }
            }
        }
        
        stage('Stop Old Containers') {
            steps {
                script {
                    echo 'Stopping old containers...'
                    sh """
                        docker compose down || true
                    """
                }
            }
        }
        
        stage('Build and Deploy') {
            steps {
                script {
                    echo 'Building and deploying with docker-compose...'
                    sh """
                        docker compose up -d --build
                    """
                }
            }
        }
        
        stage('Wait for Health Check') {
            steps {
                script {
                    echo 'Waiting for services to be healthy...'
                    sh """
                        # MySQL health check
                        timeout 120 sh -c 'until docker inspect --format="{{.State.Health.Status}}" mysql-db | grep -q healthy; do sleep 2; done' || true
                        
                        # Milvus health check
                        timeout 120 sh -c 'until docker inspect --format="{{.State.Health.Status}}" milvus-standalone | grep -q healthy; do sleep 2; done' || true
                        
                        # Spring Boot health check
                        timeout 120 sh -c 'until docker inspect --format="{{.State.Health.Status}}" spring-backend | grep -q healthy; do sleep 2; done' || true
                        
                        echo "All services are healthy"
                    """
                }
            }
        }
        
        stage('Verify Deployment') {
            steps {
                script {
                    echo 'Verifying deployment...'
                    sh """
                        echo "=== Container Status ==="
                        docker compose ps
                        
                        echo "=== Spring Backend Logs (Last 50 lines) ==="
                        docker logs spring-backend --tail=50
                        
                        echo "=== Testing Spring Boot Health Endpoint ==="
                        curl -f http://localhost:8080/actuator/health || echo "Health check endpoint not available yet"
                    """
                }
            }
        }
        
        stage('Clean Up') {
            steps {
                script {
                    echo 'Cleaning up unused Docker resources...'
                    sh """
                        docker image prune -f
                        # volume은 데이터 유실 방지를 위해 주석 처리
                        # docker volume prune -f
                    """
                }
            }
        }
    }
    
    post {
        success {
            echo '✅ Spring Boot deployment successful!'
            sh """
                echo "=== Deployment Summary ==="
                docker compose ps
            """
        }
        failure {
            echo '❌ Spring Boot deployment failed!'
            sh """
                echo "=== Docker Compose Logs ==="
                docker compose logs --tail=100 || true
                
                echo "=== Container Status ==="
                docker ps -a || true
            """
        }
    }
}

6.3.3 서버 내 디렉토리 생성 및 볼륨 마운트

*# 설정 파일 디렉토리 생성
sudo mkdir -p /var/jenkins_config

# application.properties 파일 생성/복사
sudo vim /var/jenkins_config/application.properties

# Jenkins 사용자가 읽을 수 있도록 권한 설정
sudo chmod 644 /var/jenkins_config/application.properties

# Jenkins 컨테이너 실행
# 보안 정보가 담긴 설정파일을 복사해야 하는경우
#* -v /var/jenkins_config:/var/jenkins_config:ro \ 추가
docker run -d \
  --name jenkins \
  --restart=unless-stopped \
  -p 8088:8080 \
  -p 50000:50000 \
  -v ~/jenkins_home:/var/jenkins_home \
  -v /var/jenkins_config:/var/jenkins_config:ro \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $(which docker):/usr/bin/docker \
  jenkins/jenkins:lts
profile
문제를 반복하지 않기

0개의 댓글