# 배포 매뉴얼
## 버전 등 정리
### 프론트엔드
- Vite (React + Typescript + Eslint)
- Zustand
- Axios
- Tanstack Query
- React Three Fiber(R3F)
### 백엔드
- Java
- jdk 21
- Zulu 21.0.3+9
- [<https://www.azul.com/downloads/?package=jdk#zulu>](<https://www.azul.com/downloads/?package=jdk#zulu>)
- SpringBoot
- 3.3.1
- JPA
- 3.3.1
- Gradle
- 8.9
- Redis
- Mysql
## 배포
모든 컴포넌트는 도커 컨테이너로 관리합니다.
### 젠킨스파일
gitlab의 파일을 가져와서 빌드하는 스크립트이다. Dockerfile을 참고하여 도커 이미지를 생성하고 컨테이너로 실행한다.
pipeline { agent any
environment {
BACKEND_IMAGE = 'backend'
FRONTEND_IMAGE = 'frontend'
VITE_APP_BASE_URL = '<https://j11a206.p.ssafy.io/api/>'
VITE_APP_SOCKET_URL = 'wss://j11a206.p.ssafy.io/omg'
}
stages {
stage('Backend Build') {
steps {
script {
echo '********** Backend Build Start **********'
dir('omg-back') {
sh 'docker build -t docker-image/$BACKEND_IMAGE .'
}
echo '********** Backend Build End **********'
}
}
}
stage('Frontend Build') {
steps {
script {
echo '********** Frontend Build Start **********'
dir('omg-front') {
sh 'pwd'
sh 'ls -al'
sh """
docker build --no-cache \\
--build-arg VITE_APP_BASE_URL=${VITE_APP_BASE_URL} \\
--build-arg VITE_APP_SOCKET_URL=${VITE_APP_SOCKET_URL} \\
-t docker-image/$FRONTEND_IMAGE .
"""
}
echo '********** Frontend Build End **********'
}
}
}
stage('Docker Compose Up') {
steps {
script {
echo '******** Docker Compose Start ************'
sh 'docker compose down'
sh 'docker rm -f frontend || true' // 이미 존재하는 컨테이너가 있다면 강제로 삭제
sh 'docker rm -f backend || true'
sh 'docker compose up -d'
echo '********** Docker Compose End ***********'
}
}
}
}
post {
success {
script {
def Author_ID = sh(script: "git show -s --pretty=%an", returnStdout: true).trim()
def Author_Name = sh(script: "git show -s --pretty=%ae", returnStdout: true).trim()
mattermostSend (color: 'good',
message: "빌드 성공: ${env.JOB_NAME} #${env.BUILD_NUMBER} by ${Author_ID}(${Author_Name})\\n(<${env.BUILD_URL}|Details>)",
endpoint: '<https://meeting.ssafy.com/hooks/ceutz8ibfbgm3mwbji6f7yeehy>',
channel: 'jenkins'
)
}
}
failure {
script {
def Author_ID = sh(script: "git show -s --pretty=%an", returnStdout: true).trim()
def Author_Name = sh(script: "git show -s --pretty=%ae", returnStdout: true).trim()
mattermostSend (color: 'danger',
message: "빌드 실패: ${env.JOB_NAME} #${env.BUILD_NUMBER} by ${Author_ID}(${Author_Name})\\n(<${env.BUILD_URL}|Details>)",
endpoint: '<https://meeting.ssafy.com/hooks/ceutz8ibfbgm3mwbji6f7yeehy>',
channel: 'jenkins'
)
}
}
}
}
### 프론트 관련 파일
여기에서는 프론트 Dockerfile, nginx 설정이 포함된다.
FROM node:lts-slim AS build
WORKDIR /app
COPY package.json . COPY package-lock.json .
RUN npm i
ARG VITE_APP_BASE_URL ARG VITE_APP_SOCKET_URL
ENV VITE_APP_BASE_URL=${VITE_APP_BASE_URL} ENV VITE_APP_SOCKET_URL=${VITE_APP_SOCKET_URL}
COPY . /app
RUN npm run build
FROM nginx:1.21.4-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80 CMD [ "nginx", "-g", "daemon off;" ]
nginx 설정
server {
listen 80;
server_name j11a206.p.ssafy.io;
return 301 https://$host$request_uri;
}
server { listen 443 ssl; server_name j11a206.p.ssafy.io;
ssl_certificate /etc/nginx/ssl/live/j11a206.p.ssafy.io/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/j11a206.p.ssafy.io/privkey.pem;
ssl_trusted_certificate /etc/nginx/ssl/live/j11a206.p.ssafy.io/chain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384";
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass <http://backend:8080>;
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;
}
location /omg {
proxy_pass <http://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;
}
location / {
try_files $uri $uri/ /index.html;
}
location /models/ {
autoindex on;
add_header Cache-Control "public, max-age=31536000, immutable";
}
error_page 404 /404.html;
location = /404.html {
internal;
}
}
### 백엔드 관련 파일
Dockerfile과 Redis, Mysql 등 백엔드 관련 docker-compose.yml을 포함한다.
FROM gradle:8.8-jdk-focal AS build
WORKDIR /app
COPY build.gradle settings.gradle ./
RUN apt-get update && apt-get install -y openjdk-21-jdk
COPY . /app
RUN gradle clean build --no-daemon -x test
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY --from=build /app/build/libs/*.jar /app/omg-backend.jar
EXPOSE 8080
ENTRYPOINT [ "java" ] CMD [ "-jar", "omg-backend.jar", "--spring.profiles.active=prod"]
mysql
version: '3'
services:
mysql:
image: mysql:8.0
container_name: mysql
environment:
MYSQL_ROOT_PASSWORD: ssafy
MYSQL_DATABASE: omg
MYSQL_USER: ssafy
MYSQL_PASSWORD: ssafy
ports:
- "3306:3306"
volumes:
- /home/ubuntu/mysql/mysql_data:/var/lib/mysql
- /home/ubuntu/mysql/mysql_data/my.cnf:/etc/mysql/my.cnf
networks:
- mysql_my_network
restart: always
networks: mysql_my_network: external: true
redis
version: '3'
services: redis: image: "redis:latest" container_name: "redis" ports: - "6379:6379" networks: - mysql_my_network volumes: - redis-data:/data
networks: mysql_my_network: external: true
volumes: redis-data:
### 백엔드, 프론트엔드의 docker-compose.yml
```json
version: '3'
services:
spring_app:
image: docker-image/backend:latest
container_name: backend
environment:
DB_URL_PROD: jdbc:mysql://mysql:3306/omg
DB_USERNAME_PROD: ssafy
DB_PASSWORD_PROD: ssafy
REDIS_URL_PROD: redis
REDIS_PORT_PROD: 6379
ports:
- "8080:8080"
networks:
- mysql_my_network
volumes:
- /home/ubuntu/logs:/app/logs
restart: always
frontend:
image: docker-image/frontend:latest
container_name: frontend
ports:
- "80:80"
- "443:443"
networks:
- mysql_my_network
volumes:
- /home/ubuntu/certbot/conf:/etc/nginx/ssl
- /home/ubuntu/certbot/data:/var/www/certbot
restart: always
networks:
mysql_my_network:
external: true