728x90
반응형
2년 반 만의 인프라 세팅이다.
2년 전에는 그냥 api테스트하려고 docker썼는데 이번엔 좀 다르다.
0. 폴더 구조
├── aws (키 보관, gitignore 설정으로 레포에 올라가지 않음)
├── client
│ ├── Client project
│ ├── Dockerfile
├── db
│ ├── init.sql (DDL 정의 - schema)
├── hls (폴더 이하 내용은 방송 시작 시 생성, 종료 시 삭제됨)
│ ├── 스트리밍key-스트림번호.ts
│ ├── 스트리밍key.m3u8
├── media
├── nginx
│ ├── default.conf # client 요청 처리할 웹서버
│ ├── rtmp.conf # nginx-rtmp 전용
├── server
│ ├── Server project
├── .gitignore
├── deploy.sh
├── docker-compose.yml
├── Jenkinsfile
일단 대충은 이런 모양이다...
1. db/init.sql
테이블은 JPA가 생성해주기 때문에 스키마만 만들어준다.
CREATE DATABASE IF NOT EXISTS {schema};
USE {schema};
2. Nginx 설정
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types; # mime.type 지정이유: 프론트엔드 svg 등 다양한 파일 형식 지원을 위해
default_type application/octet-stream;
limit_req_zone $binary_remote_addr zone=ddos_req:10m rate=20r/s;
limit_req_status 503;
limit_conn_zone $binary_remote_addr zone=ddos_conn:10m;
# header의 'object-src 'none'; media-src 'self' blob:;' 부분은 클라이언트에서의 영상 재생을 위한 부분
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self'; object-src 'none'; media-src 'self' blob:;";
# 보안설정
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
add_header Referrer-Policy "no-referrer";
add_header X-XSS-Protection "1; mode=block" always;
upstream api {
server server:8080; # 백엔드 서버 컨테이너 이름이 server임
}
server {
listen 80;
server_name {your_domain}; # 서버이름은 호스트 도메인 이름으로, 프로토콜은 제외하기
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name {your_domain}; # 서버이름은 호스트 도메인 이름으로, 프로토콜은 제외하기
# https ssl 인증 부분
ssl_certificate /etc/letsencrypt/live/{your_domain}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{your_domain}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 프론트엔드 캐시 무시
location ~* (service-worker\.js)$ {
add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
expires off;
proxy_no_cache 1;
}
location / { # 프론트엔드 정적파일 서빙
root /opt/app; # 빌드파일 저장한 위치에 root
try_files $uri $uri/ /index.html;
}
location /uploads/ { # 프론트엔드 업로드 파일 서빙
alias /app/uploads/; # 프론트 컨테이너 내에 파일이 저장된 위치 (백엔드와 compose로 마운트한 상태)
autoindex on;
expires max;
access_log off;
# try_files $uri $uri/ =404;
}
location /api/ { # 백엔드 모든 api요청은 서버컨테이너로 reverse proxy
proxy_pass http://api$request_uri;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
location /oauth2 { # 카카오 로그인 관련 역시 서버로
proxy_pass http://api$request_uri;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 3600s;
}
location /login/oauth2 { # 카카오 로그인 관련 역시 서버로
proxy_pass http://api$request_uri;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 3600s;
}
location /ws { # 웹소켓 설정도 웹소켓 서버로 (백엔드 서버와 동일)
proxy_pass http://api$request_uri;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 3600s;
}
location /stream { # 웹소켓 설정도 웹소켓 서버로 (백엔드 서버와 동일)
proxy_pass http://api$request_uri;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 3600s;
}
location /hls/ { # /hls/ 로 들어온 요청은 nginx-rtmp 컨테이너에 저장된 hls 파일 가져오기
proxy_hide_header Access-Control-Allow-Origin;
add_header 'Access-Control-Allow-Origin' '*' always; # hls 관련 CORS 설정 허용
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range';
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://nginx-rtmp:8050/hls/;
proxy_set_header Host $host;
proxy_buffering off;
}
client_max_body_size 10M; # 클라이언트가 업로드 가능한 최대 파일 사이즈
}
}
각 라인별 자세한 설정은 주석을 달았다.
80번 포트로 들어온 http요청을 301 리다이렉트로 https 설정,
443 listen 하고 ssl certificate key는 let's encrypt로 만들었다.
3. docker-compose.yml과 Dockerfile 작성
docker-compose.yml
services:
client: # 프론트엔드
build:
context: ./client
dockerfile: Dockerfile # 빌드 시 실행할 dockerfile
container_name: client
ports:
- "80:80"
- "443:443"
volumes:
- ./client:/client # 소스코드
- /etc/letsencrypt:/etc/letsencrypt:ro # ssl 인증서
- ./nginx/default.conf:/etc/nginx/nginx.conf:ro # nginx 설정파일
- ./media/uploads:/app/uploads # 파일 위치 백엔드와 공유
server: # 백엔드
image: openjdk:17-alpine
build:
context: ./server
container_name: server
working_dir: /server
environment: # 백엔드 datasource 연결 및 설정 application.yml 파일 프로필 지정
SPRING_DATASOURCE_URL: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?serverTimezone=UTC&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: ${DB_USER}
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
command: sh -c "
apk add --no-cache tzdata &&
ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime &&
export TZ=Asia/Seoul &&
date &&
apk add --no-cache ffmpeg &&
chmod +x /server/gradlew &&
/server/gradlew clean build --stacktrace &&
cp /server/build/libs/server-0.0.1-SNAPSHOT.jar /server/app.jar &&
java -jar /server/app.jar"
# 첫 4줄은 server timezone KST로 변경, 이후 ffmpeg 라이브러리 설치
# 6번째부터 끝줄까지 spring boot 빌드 및 실행
ports:
- "8000:8080"
volumes:
- ./server/:/server # 소스코드
- ./media/uploads:/app/uploads # 파일 저장 경로 (프론트엔드와 공유)
depends_on: # spring boot 실행 시 db(mysql)과 redis의 연결 상태를 체크하므로 이 두개 컨테이너가 실행중이어야 함
db:
condition: service_healthy
redis:
condition: service_healthy
nginx-rtmp: # rtmp/hls 스트리밍용 컨테이너
image: tiangolo/nginx-rtmp
restart: always
container_name: nginx-rtmp
ports:
- "1935:1935" # RTMP
- "8050:8050" # HLS
- "1936:80" # RTMP console
environment:
TZ: "Asia/Seoul"
volumes:
- ./nginx/rtmp.conf:/etc/nginx/nginx.conf:ro # nginx 설정파일
- ./hls:/opt/data/hls # 생성된 파일들 위치 공유
db: # MySQL 컨테이너
image: mysql:8.0.30
restart: always
container_name: db
command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci --default-authentication-plugin=mysql_native_password
ports:
- "3307:3306"
volumes:
- ./db/:/docker-entrypoint-initdb.d/ # 스키마 생성 init.sql 마운트
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
platform: linux/x86_64
healthcheck: # mysql 실행확인용 healthcheck
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
interval: 1m30s
timeout: 30s
retries: 10
start_period: 30s
redis:
image: redis:alpine
restart: always
container_name: redis
ports:
- "6379:6379"
environment:
TZ: "Asia/Seoul"
command: redis-server --appendonly yes # 레디스 서버 run
healthcheck: # redis 실행확인용 healthcheck
test: ["CMD", "redis-cli", "ping"]
interval: 1m30s
timeout: 30s
retries: 10
start_period: 30s
jenkins: # Jenkins 컨테이너
image: jenkins/jenkins:lts
container_name: jenkins
ports:
- "9000:8080" # Jenkins 콘솔 접속용
- "50000:50000"
volumes:
- ./jenkins:/var/jenkins_home # Jenkins 관련 파일들 저장
- ./jenkins/home:/var/jenkins_home/data
user: root # Jenkins에 접속할 유저 root 로 설정
이후 client에서 작성한 Dockerfile이다
server의 경우 이미지 1개만 사용하므로 command에서 전부 수행했고,
client의 경우 이미지 2개를 사용하므로 Dockerfile을 사용하여 빌드했다.
./client/Dockerfile
FROM node:20.18.1-alpine AS build # 프엔 소스코드 빌드를 위해 node 이미지 사용
WORKDIR /app
# 소스 복사 및 빌드 (/app 디렉토리에 있기 때문에 context내에 있는 소스코드를 현재 디렉토리로 복사해주어야 한다.)
COPY . .
# .env파일 생성 (중요한 건 아니라서 그냥 이렇게 만들었다 ^^;)
RUN echo "VITE_PUBLIC_API_URL=https://{your_domain}/api" > .env
RUN yarn install
RUN yarn build
# Nginx 웹서버 실행을 위해 이미지 변경
FROM nginx:alpine
COPY --from=build /app/dist /opt/app # 빌드가 완료된 파일은 옮겨줘야 한다. 그러지 않으면 사라짐! /opt/app은 추후 nginx에서 location / 에서 서빙할 위치와 동일
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] # nginx run!
CI/CD는 이어서...
728x90
반응형