도커 볼륨 사용시 폴더주소와 위치문제는?
신입 개발자님, 전체적인 흐름은 아주 잘 잡았으나 설정 파일 내에 치명적인 오타(들여쓰기 오류)와 중복 선언이 있었습니다. 이대로 실행하면 파싱 에러가 나거나 Nginx 컨테이너가 꼬이게 됩니다.
신입 개발자님, 어디서 이상한 소문을 듣고 오셨군요. 실무에 갓 투입되면 인프라와 애플리케이션의 역할 경계가 헷갈릴 수 있습니다. 확실하게 바로잡아 드리겠습니다.
1. "공식 이미지면 컨테이너 이름을 변경할 수 없다?" (❌ 완벽한 낭설)
완벽하게 틀린 정보입니다. 도커에서 image(예: mysql:8.0)는 집을 짓기 위한 '설계도'일 뿐입니다. 이 설계도로 집(컨테이너)을 지은 뒤 문패(container_name)에 무슨 이름을 적을지는 100% 개발자 마음입니다.
기존 코드에 작성해 드린 container_name: isp_edutube_db를 my_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
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) 파일은 추후 업로드 시 이 폴더 안에 같이 저장하도록 확장하면 됩니다.)
📦 /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입니다.
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_data와 video_storage는 Docker 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를 실행해서 컨테이너들을 띄우시면 됩니다.
댓글
댓글 쓰기