Computer Science/Setup

[Docker, AWS, Jenkins, Spring Boot, Nginx, React, MySQL, Redis, RTMP/HLS] 프로젝트 인프라 세팅하기 (1) - nginx.conf, Dockerfile, Docker compose 작성

_혀니 2025. 2. 19. 17:14
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
반응형