
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
*# 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
*### 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
# 빌드 스테이지
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;"]
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";
}
}
}
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!'
}
}
}
spring-boot-project/
├── src/
├── pom.xml (또는 build.gradle)
├── Dockerfile
├── docker-compose.yml
└── Jenkinsfile
# 빌드 스테이지
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"]
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
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
"""
}
}
}
react-frontend-pipeline설정 페이지에서:
GitHub project 체크https://github.com/your-username/react-projectGitHub hook trigger for GITScm polling 체크Pipeline script from SCMGithttps://github.com/your-username/react-project.gitgithub-credentials 선택/main (중요!)Jenkinsfile동일한 방식으로:
spring-backend-pipelinehttps://github.com/your-username/spring-boot-project.git각 프로젝트 리포지토리에서:
http://your-server-ip:8080/github-webhook/application/jsonJust the push event 선택*# 필요한 포트 열기*
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
# 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/
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;"]
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";
}
}
}
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!'
}
}
}
sudo apt update
sudo apt install certbot
#먼저 컨테이너가 실행 중인지 확인:
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
#갱신 후 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
sudo certbot renew --dry-run
#Certbot은 자동으로 systemd 타이머를 설정합니다:
sudo systemctl status certbot.timer
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"]
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
"""
}
}
}
*# 설정 파일 디렉토리 생성
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