왜 이 글을 쓰게 됐나
GPU 워크스테이션(RTX A5000 x2, Ubuntu 24.04)에 NVIDIA DeepStream 9.0 개발 환경을 새로 세팅하게 됐다. DeepStream, CUDA, TensorRT를 호스트에 직접 깔면 버전 궁합 맞추기가 은근히 손이 많이 가고, 나중에 다른 장비에 옮길 때 또 삽질을 반복해야 한다. 그래서 이번에는 호스트에는 GPU 드라이버만 설치하고, 나머지는 전부 Docker 컨테이너 안에 넣는 방식으로 접근했다.
전체 구조
u24.04-deepstream9.0/
├── docker-compose.yml # 컨테이너 실행 정의
├── Makefile # docker compose 명령어 단축
├── docker/
│ └── Dockerfile # DeepStream 9.0 이미지 커스터마이징
├── scripts/
│ ├── install_host.sh # 호스트: 드라이버 + Docker + container-toolkit 설치
│ └── smoke_test.sh # 컨테이너 안: 정상 동작 확인 테스트
├── configs/ # DeepStream 파이프라인 설정 (git 관리)
├── models/ # AI 모델 파일 (git 제외, 용량 큼)
└── videos/ # 테스트 영상 (git 제외, 용량 큼)
호스트에는 GPU 드라이버만 있으면 되고, CUDA/TensorRT/DeepStream처럼 복잡한 건 전부 컨테이너 안에서 해결한다.
1. Dockerfile — DeepStream 9.0 이미지
베이스는 NVIDIA가 공식으로 배포하는 이미지를 그대로 쓴다.
# docker/Dockerfile
ARG BASE_IMAGE_TAG=9.0-triton-multiarch
FROM nvcr.io/nvidia/deepstream:${BASE_IMAGE_TAG}
WORKDIR /workspace
RUN apt-get update && apt-get install -y --no-install-recommends \
vim git curl wget gdb python3-pip \
&& rm -rf /var/lib/apt/lists/*
# 이미지에 pyds(파이썬 바인딩) wheel이 있으면 설치
RUN wheel=$(find /opt/nvidia/deepstream/deepstream/lib -maxdepth 1 -name 'pyds*.whl' 2>/dev/null | head -n1); \
if [ -n "$wheel" ]; then pip3 install --no-cache-dir "$wheel"; fi
ARG BASE_IMAGE_TAG로 빼둔 이유는, 나중에 triton 대신 더 가벼운 samples
태그로 바꾸고 싶을 때 이 한 줄만 고치면 되게 하기 위해서다.
2. docker-compose.yml
services:
deepstream:
build:
context: .
dockerfile: docker/Dockerfile
image: u24-deepstream9.0:latest
container_name: deepstream9
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all # 2GPU 환경에서 모든 GPU를 사용하겠다
capabilities: [gpu]
environment:
- NVIDIA_VISIBLE_DEVICES=all # 2GPU 환경에서 모든 GPU를 사용하겠다
- NVIDIA_DRIVER_CAPABILITIES=all
volumes:
- ./models:/workspace/models
- ./videos:/workspace/videos
- ./configs:/workspace/configs
network_mode: host
stdin_open: true
tty: true
3. 호스트 설치 — 드라이버 + Docker + Container Toolkit
컨테이너가 GPU를 쓰려면 호스트에 최소 세 가지가 필요하다.
- NVIDIA 드라이버 (GPU 인식)
- Docker
- nvidia-container-toolkit (컨테이너가 호스트 GPU에 접근할 수 있게 연결)
이 세 가지를 한 번에 설치하는 스크립트를 만들어뒀다.
sudo bash scripts/install_host.sh
# scripts/install_host.sh
#!/usr/bin/env bash
# 이 컴퓨터(호스트)에 "GPU를 쓸 수 있는 최소한의 것"만 설치하는 스크립트.
# DeepStream, CUDA, TensorRT는 여기서 설치하지 않는다. 전부 도커 컨테이너 안에 들어있기 때문.
#
# 이 스크립트가 하는 일:
# 1) NVIDIA 그래픽카드 드라이버 설치
# 2) make 설치 (Makefile의 make build/up/shell 명령을 쓰기 위해 필요)
# 3) Docker 설치 (이미 있으면 건너뜀)
# 4) 컨테이너가 GPU를 쓸 수 있게 해주는 NVIDIA Container Toolkit 설치
#
# 실행 후에는 재부팅이 필요할 수 있다.
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then
echo "root 권한이 필요합니다. 다음과 같이 실행하세요: sudo bash scripts/install_host.sh" >&2
exit 1
fi
echo "==> 1/4 NVIDIA 드라이버 설치"
if command -v nvidia-smi >/dev/null 2>&1; then
echo " 이미 드라이버가 설치되어 있습니다. 건너뜁니다."
nvidia-smi --query-gpu=name,driver_version --format=csv,noheader
else
apt-get update
apt-get install -y ubuntu-drivers-common
ubuntu-drivers autoinstall
echo " 드라이버 설치 완료. 적용하려면 컴퓨터를 재부팅해야 합니다."
fi
echo "==> 2/4 make 설치"
if command -v make >/dev/null 2>&1; then
echo " 이미 설치되어 있습니다. 건너뜁니다."
else
apt-get update
apt-get install -y make
fi
echo "==> 3/4 Docker 설치"
if command -v docker >/dev/null 2>&1; then
echo " 이미 Docker가 설치되어 있습니다. 건너뜁니다."
else
curl -fsSL https://get.docker.com | sh
usermod -aG docker "${SUDO_USER:-$USER}"
echo " Docker 설치 완료. 방금 그룹 권한이 바뀌었으니 로그아웃 후 다시 로그인하세요."
fi
echo "==> 4/4 NVIDIA Container Toolkit 설치 (컨테이너가 GPU를 쓰게 해주는 다리 역할)"
if dpkg -l | grep -q nvidia-container-toolkit; then
echo " 이미 설치되어 있습니다. 건너뜁니다."
else
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
| sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
| tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
apt-get update
apt-get install -y nvidia-container-toolkit
fi
nvidia-ctk runtime configure --runtime=docker # Docker에 nvidia 런타임을 연결해준다.
systemctl restart docker
echo ""
echo "==> 전부 끝났습니다."
echo ""
echo " [1] 먼저 그래픽카드가 잡히는지 확인하세요:"
echo " nvidia-smi"
echo " 아무것도 안 나오면 재부팅이 필요합니다: sudo reboot"
echo ""
echo " [2] 그 다음 아래 순서대로 실행하면 됩니다"
echo " make build # 도커 이미지 빌드"
echo " make up # 컨테이너 실행"
echo " make smoke-test # DeepStream이 GPU를 잘 쓰는지 확인"
echo " make setup-yolov8 # YOLOv8 모델 준비 (처음 한 번, 몇 분 소요)"
echo " make analyze VIDEO=내영상.mp4 # videos/ 에 넣은 영상 분석"
echo ""
echo " 컨테이너 안으로 직접 들어가려면: make shell"
4. 빌드 & 실행
Makefile은 긴 docker compose ... 명령에 짧은 별명을 붙여둔 것뿐이다. 특별한 문법은 없고 평소 치던 명령을 그대로 적으면 되는데, 맨 위 .PHONY는 "이 이름들은 파일이 아니라 명령"이라고 알려주는 관례적 선언이다.
.PHONY: build up down shell smoke-test setup-yolov8 analyze logs
build:
docker compose build
up:
docker compose up -d
down:
docker compose down
shell:
docker compose exec deepstream bash
smoke-test:
docker compose exec deepstream bash scripts/smoke_test.sh
# 최초 1회만: YOLOv8s(COCO 80클래스)를 ONNX로 변환해서 models/YOLOv8/ 에 채워 넣음
setup-yolov8:
docker compose exec deepstream bash scripts/setup_yolov8.sh
# 영상 처리 관련 값은 바로 인자로 넘겨서 바꿀 수 있다
DROP_FRAME_INTERVAL ?= 1
GIE_INTERVAL ?= 0
CONF_THRESHOLD ?= 0.25
# 사용법: make analyze VIDEO=내영상.mp4 (처음엔 make setup-yolov8 먼저 실행해둘 것)
# make analyze VIDEO=내영상.mp4 DROP_FRAME_INTERVAL=4 GIE_INTERVAL=4 CONF_THRESHOLD=0.3
analyze:
docker compose exec \
-e DROP_FRAME_INTERVAL=$(DROP_FRAME_INTERVAL) \
-e GIE_INTERVAL=$(GIE_INTERVAL) \
-e CONF_THRESHOLD=$(CONF_THRESHOLD) \
deepstream bash scripts/analyze_video.sh $(VIDEO)
logs:
docker compose logs -f
5. 스모크 테스트
SDK에 원래 들어있는 샘플 영상과 설정 파일로 딱 한 번 돌려보는 make smoke-test 스크립트를 따로 준비했다.
#!/usr/bin/env bash
set -euo pipefail
CONFIG_DIR=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app
# 화면(X11) 없는 서버라, 결과를 파일로 저장하는(sink type=3) 설정을 쓴다.
# source1 계열은 EglSink(화면 출력)라 headless에서 "Could not open X Display"로 죽는다.
PREFERRED_CONFIG="source2_1080p_dec_infer-resnet_demux.txt"
CONFIG_FILE="$PREFERRED_CONFIG"
if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then
# 없으면 sink0 type=3(파일 출력)인 설정을 자동으로 찾는다
for f in "$CONFIG_DIR"/source*.txt; do
if awk '/^\[sink0\]/{f=1} f&&/^type=/{print;exit}' "$f" | grep -q 'type=3'; then
CONFIG_FILE=$(basename "$f"); break
fi
done
fi
nvidia-smi --query-gpu=name,memory.used,memory.total --format=csv,noheader
mkdir -p /workspace/videos/smoke_test_output && cd /workspace/videos/smoke_test_output
deepstream-app -c "$CONFIG_DIR/$CONFIG_FILE"
6. YOLOv8 모델 준비 (make setup-yolov8)
DeepStream은 파이프라인일 뿐 스스로 물체를 찾지는 못한다. 눈에 해당하는 모델을 꽂아줘야 하는데, 그냥 .pt 파일을 던져준다고 되는 게 아니다. DeepStream이 읽을 수 있는 형태로 3단계를 거쳐야 한다.
① 모델 학습
PyTorch / YOLO
↓
best.pt / yolov8s.pt
② ONNX 변환
best.pt / yolov8s.pt
↓
model.onnx / yolov8s.onnx
③ TensorRT Engine 생성
model.onnx / yolov8s.onnx
↓
TensorRT
↓
model.engine
④ 출력 해석
TensorRT Output Tensor
↓
YOLO Output Parser
↓
Bounding Box
Confidence
Class ID
⑤ DeepStream
Bounding Box + Metadata
↓
Tracking / OSD / Event / 저장
- ① ONNX 변환: PyTorch/Ultralytics에서 학습한 모델을 ONNX라는 프레임워크 독립적인 모델 표현 형식으로 변환하는 과정
- ② TensorRT 엔진: ONNX를 읽고 실제 사용할 GPU와 설정에 맞춰 최적화된 실행 엔진을 만든다
- 이 과정에서 FP16, INT8 등의 정밀도와 GPU에 맞는 최적화가 적용될 수 있다
- 그래서 .engine 은 단순히 ONNX를 다른 확장자로 바꾼 게 아니라 TensorRT가 실제 추론에 사용할 수 있도록 빌드된 엔진
- ③ 커스텀 파서: TensorRT가 추론한 YOLO의 출력 Tensor를 DeepStream이 사용할 수 있는 Bounding box, Confidence, Class ID 등의 객체 정보로 변환하는 역할을 한다. YOLO 모델과 출력 형식에 따라 DeepStream에서 기본적으로 처리하지 못하는 경우 Custom Parser가 필요하며, 이번 구성에서는 DeepStream-Yolo 에서 제공하는 YOLO용 Custom Parser를 사용한다.
- [DeepStream-Yolo](https://github.com/marcoslucianops/DeepStream-Yolo) 오픈소스를 쓴다. 즉 YOLO의 출력 결과를 DeepStream이 이해할 수 있도록 연결해 주는 부분을 이미 오픈소스에서 구현했다.
# scripts/setup_yolov8.sh
# 1) DeepStream-Yolo 저장소 받기 (파서 소스 + 변환 스크립트)
git clone --depth 1 https://github.com/marcoslucianops/DeepStream-Yolo.git
# 2) 변환에만 필요한 파이썬 패키지
pip3 install ultralytics onnx onnxslim onnxruntime onnxscript
# 3) 가중치 받아서 ONNX로 변환
wget https://github.com/ultralytics/assets/releases/download/v8.4.0/yolov8s.pt
python3 export_yoloV8.py -w yolov8s.pt --dynamic --simplify
# 4) 커스텀 bbox 파서 컴파일 (CUDA 버전을 맞춰줘야 함)
export CUDA_VER=13.1
make -C DeepStream-Yolo/nvdsinfer_custom_impl_Yolo
# 5) 결과물 3개가 models/YOLOv8/에 들어간다.
yolov8s.onnx (모델 본체)
labels.txt (클래스 이름 80개)
libnvdsinfer_custom_impl_Yolo.so (출력 해석용 파서)
7. 추론 결과
앞에서 구성한 환경에서 DeepStream 9.0 + TensorRT + YOLOv8s를 연결해 실제 영상 추론까지 테스트 했다. 이번 테스트에서는 다음과 같은 흐름으로 동작한다.
MP4 영상
↓
DeepStream
↓
NVDEC 영상 디코딩
↓
YOLOv8 TensorRT
↓
Custom YOLO Parser
↓
Bounding Box / Confidence / Class
↓
DeepStream OSD
↓
추론 결과 영상
태스트에 사용한 모델은 COCO 80개 클래스를 학습한 YOLOv8s 이며, 별도로 YOLO 모델을 학습 시키지 않고 DeepStream-Yolo에서 제공하는 변환 스크립트와 Custom Parser를 사용해 DeepStream에 연결했다.
실제 추론 결과는 다음과 같다.
영상에서 확인할 수 있듯이 컨테이너 내부의 DeepStream 9.0 환경에서 영상 디코딩부터 TensorRT 기반 YOLO 추론, Bounding Box 생성 및 화면 출력까지 정상적으로 동작하는 것을 확인했다.
다음 게시글에서는 Secondary Stream 기반 추론(Multi-Stream), Smart Record를 이용한 이벤트 영상 저장, 이벤트 발생 시 고화질 썸네일 생성을 할 예정이다.
