도커 볼륨 사용시 폴더주소와 위치문제는?

 신입 개발자님, 전체적인 흐름은 아주 잘 잡았으나 설정 파일 내에 치명적인 오타(들여쓰기 오류)와 중복 선언이 있었습니다. 이대로 실행하면 파싱 에러가 나거나 Nginx 컨테이너가 꼬이게 됩니다.




신입 개발자님, 어디서 이상한 소문을 듣고 오셨군요. 실무에 갓 투입되면 인프라와 애플리케이션의 역할 경계가 헷갈릴 수 있습니다. 확실하게 바로잡아 드리겠습니다.

1. "공식 이미지면 컨테이너 이름을 변경할 수 없다?" (❌ 완벽한 낭설)

완벽하게 틀린 정보입니다. 도커에서 image(예: mysql:8.0)는 집을 짓기 위한 '설계도'일 뿐입니다. 이 설계도로 집(컨테이너)을 지은 뒤 문패(container_name)에 무슨 이름을 적을지는 100% 개발자 마음입니다.

기존 코드에 작성해 드린 container_name: isp_edutube_dbmy_awesome_db로 바꾸셔도 아무 문제 없이 동작합니다. 단, 이름을 바꾸면 백엔드의 DATABASE_URL 접속 주소에 적힌 호스트명도 똑같이 바꿔주셔야 한다는 점만 기억하세요.

2. "도커가 화질별 폴더나 자막 폴더를 자동으로 만들어주나요?" (❌ 아님)

아닙니다. 도커 볼륨(video_storage)은 그저 텅 빈 거대한 '물류 창고' 하나를 대여해 준 것과 같습니다. 그 창고 안에서 비디오별로 폴더를 파고, 1080p/720p/360p 화질을 나누고, 자막 파일을 정리하는 것은 전적으로 우리가 짜는 백엔드 인코딩 서버(FastAPI + FFmpeg)의 책임입니다.

이전에 작성했던 인코딩 로직은 720p 단일 화질용이었습니다. 질문하신 김에 실무 넷플릭스/유튜브처럼 다중 해상도(Adaptive Bitrate) 스트리밍 HLS 폴더 구조를 자동으로 생성하는 코드로 업그레이드해 드리겠습니다.

이 코드를 기존 backend_player/services/encoding_service.py에 덮어씌우면 됩니다.

backend_player/services/encoding_service.py

Python
import asyncio
import os
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

async def encode_to_hls(input_path: str, output_dir: str):
    """
    원본 MP4를 1080p, 720p, 360p 3가지 화질의 HLS(.m3u8, .ts)로 인코딩하고,
    이를 하나로 묶는 master.m3u8 플레이리스트를 자동 생성합니다.
    """
    os.makedirs(output_dir, exist_ok=True)
    
    # 1. 지원할 해상도와 비트레이트 설정
    resolutions = [
        {"name": "1080p", "scale": "1920x1080", "bitrate": "5000k", "maxrate": "5500k", "bufsize": "7500k"},
        {"name": "720p",  "scale": "1280x720",  "bitrate": "2800k", "maxrate": "3000k", "bufsize": "4200k"},
        {"name": "360p",  "scale": "640x360",   "bitrate": "800k",  "maxrate": "850k",  "bufsize": "1200k"},
    ]

    master_playlist_content = "#EXTM3U\n#EXT-X-VERSION:3\n"

    try:
        # 2. 각 해상도별로 폴더를 만들고 FFmpeg 인코딩 실행
        for res in resolutions:
            res_dir = os.path.join(output_dir, res["name"])
            os.makedirs(res_dir, exist_ok=True)
            
            res_m3u8 = os.path.join(res_dir, "playlist.m3u8")
            
            command = [
                "ffmpeg", "-y", "-i", input_path,
                "-vf", f"scale={res['scale']}", # 해상도 스케일링
                "-c:v", "libx264",              # H.264 코덱 사용
                "-b:v", res["bitrate"],
                "-maxrate", res["maxrate"],
                "-bufsize", res["bufsize"],
                "-c:a", "aac",                  # 오디오 코덱
                "-b:a", "128k",
                "-profile:v", "main",
                "-start_number", "0",
                "-hls_time", "10",              # 10초 단위로 청크 분할
                "-hls_list_size", "0",          # 전체 VOD 유지
                "-f", "hls",
                res_m3u8
            ]

            logger.info(f"[{res['name']}] 인코딩 시작...")
            process = await asyncio.create_subprocess_exec(
                *command,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            stdout, stderr = await process.communicate()

            if process.returncode != 0:
                raise Exception(f"{res['name']} Encoding failed: {stderr.decode()}")

            # 3. 마스터 플레이리스트에 현재 해상도 정보 기록
            # 프론트엔드의 hls.js는 이 마스터 파일을 읽고 사용자의 인터넷 속도에 맞춰 화질을 자동 전환합니다.
            bandwidth = int(res["bitrate"].replace("k", "000"))
            master_playlist_content += f"#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},RESOLUTION={res['scale']}\n"
            master_playlist_content += f"{res['name']}/playlist.m3u8\n"

        # 4. master.m3u8 파일 생성
        master_path = os.path.join(output_dir, "master.m3u8")
        with open(master_path, "w") as f:
            f.write(master_playlist_content)
            
        logger.info(f"모든 인코딩 완료. Master Playlist 생성됨: {master_path}")

    except Exception as e:
        logger.error(f"인코딩 파이프라인 에러: {str(e)}")
    finally:
        # 5. 디스크 용량 확보를 위해 원본 임시 파일 삭제
        if os.path.exists(input_path):
            os.remove(input_path)
            logger.info("원본 임시 MP4 파일 삭제 완료")

동작 결과 (저장소 구조)

프론트엔드에서 파일을 업로드하면 백엔드가 이 코드를 실행하여 도커 볼륨(video_storage) 내부에 다음과 같은 완벽한 스트리밍 폴더 구조를 자동으로 구축합니다. (자막(.vtt) 파일은 추후 업로드 시 이 폴더 안에 같이 저장하도록 확장하면 됩니다.)

Plaintext
📦 /app/videos (video_storage 볼륨 내부)
 ┗ 📂 {비디오_UUID}
    ┣ 📜 master.m3u8        <-- 프론트엔드가 호출할 유일한 파일
    ┣ 📂 1080p
    ┃  ┣ 📜 playlist.m3u8
    ┃  ┣ 📜 0.ts
    ┃  ┗ 📜 1.ts ...
    ┣ 📂 720p
    ┃  ┣ 📜 playlist.m3u8
    ┃  ┣ 📜 0.ts
    ┃  ┗ 📜 1.ts ...
    ┗ 📂 360p
       ┣ 📜 playlist.m3u8
       ┣ 📜 0.ts
       ┗ 📜 1.ts ...




바로 복사해서 덮어씌울 수 있도록 교정한 최종 docker-compose.yml입니다.

YAML
version: '3.8'

services:
  # 1. MySQL Database (Port: 14003)
  isp_edutube_db:
    image: mysql:8.0
    container_name: isp_edutube_db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    ports:
      - "14003:3306"
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - edutube_net

  # 2. Main Backend - FastAPI (Port: 14001)
  isp_edutube_backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: isp_edutube_backend
    restart: always
    ports:
      - "14001:14001"
    environment:
      - DATABASE_URL=mysql+pymysql://root:${MYSQL_ROOT_PASSWORD}@isp_edutube_db:3306/${MYSQL_DATABASE}
    depends_on:
      - isp_edutube_db
    volumes:
      - ./backend:/app
    networks:
      - edutube_net

  # 3. Media Player/Encoding Backend - FastAPI + FFmpeg (Port: 14002)
  isp_edutube_backend_player:
    build:
      context: ./backend_player
      dockerfile: Dockerfile
    container_name: isp_edutube_backend_player
    restart: always
    ports:
      - "14002:14002"
    volumes:
      - ./backend_player:/app
      - video_storage:/app/videos               # 인코딩된 영상 및 원본이 저장될 공유 볼륨
    networks:
      - edutube_net

  # 4. Frontend - React/Vite (Port: 14000)
  isp_edutube_frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: isp_edutube_frontend
    restart: always
    ports:
      - "14000:14000"
    volumes:
      - ./frontend:/app
      - /app/node_modules
    networks:
      - edutube_net

  # 5. Media Server (Nginx) - 정적 HLS 파일 서빙 전용
  isp_edutube_media_server:
    image: nginx:alpine
    container_name: isp_edutube_media_server
    restart: always
    ports:
      - "14004:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - video_storage:/usr/share/nginx/html/videos:ro
    networks:
      - edutube_net

volumes:
  db_data:
  video_storage:

networks:
  edutube_net:
    driver: bridge

질문하신 '데이터 저장 위치'와 '삭제해야 할 폴더'에 대해 실무 개념으로 명확히 짚어드립니다.

1. 데이터(DB, 영상)는 어디에 저장되나요?

현재 YAML 하단에 선언된 db_datavideo_storageDocker Named Volume(도커 이름 지정 볼륨) 방식입니다.

이 데이터들은 우리가 작업 중인 현재 프로젝트 폴더 내부에 생기는 것이 아니라, 호스트 PC의 도커 엔진이 직접 관리하는 시스템 깊숙한 경로에 저장됩니다. (예: Linux 환경 기준 /var/lib/docker/volumes/isp_edutube_db_data/_data)

  • 실무에서 이렇게 쓰는 이유: 개발자가 실수로 프로젝트 폴더(isp_edutube)를 통째로 삭제하거나 git reset을 잘못 날려도, 도커 볼륨에 있는 DB와 영상 데이터는 독립적으로 안전하게 생존하기 때문입니다. I/O 속도 면에서도 가장 우수합니다.

2. 프로젝트 구조에서 삭제해야 할 폴더 (과감히 지우세요)

데이터가 안전한 도커 볼륨으로 분리되었기 때문에, 이전에 보여주셨던 프로젝트 트리에 있던 database/ 폴더 전체는 이제 완벽한 쓰레기 데이터(Dead Code)입니다. 전부 삭제하십시오.

  • database/mysql/ ❌ -> 삭제 (데이터는 db_data에 저장됨)

  • database/videos/ ❌ -> 삭제 (영상은 video_storage에 저장됨)

  • database/Dockerfile ❌ -> 삭제 (docker-compose.yml에서 공식 mysql:8.0 이미지를 바로 끌어오기 때문에 커스텀 빌드가 필요 없습니다)

이렇게 정리하면 Git 저장소도 훨씬 가벼워지고 구조가 명확해집니다. 폴더 삭제 후 바로 docker-compose up -d --build를 실행해서 컨테이너들을 띄우시면 됩니다.



댓글

가장 많이 본 글