treeru.com
Network

NVR 없이 CCTV 녹화 서버 만들기 — RTSP + ffmpeg + systemd로 180일 보존

2026-06-12
Treeru

저희 서버실에는 CCTV가 2대 있습니다. 보안을 생각해 카메라는 업무망·서버망과 분리된 별도 망에서만 동작하고, 인터넷에는 노출하지 않은 채 운영 중입니다. 보통 이런 카메라는 NVR(녹화 전용 장비)을 사거나 제조사 클라우드 구독으로 녹화하는데, 둘 다 쓰지 않습니다. 이미 돌아가고 있는 리눅스 백업 서버가 있기 때문입니다. 카메라가 내보내는 RTSP 스트림을 ffmpeg로 받아서 디스크에 그대로 저장하면, NVR이 하는 일의 핵심은 끝납니다.

이 글은 그 구성을 처음부터 끝까지 정리한 것입니다. 결과부터 말하면 재인코딩 없이(-c copy) CPU 부담 거의 0%로 15분 단위 세그먼트를 하루 96개씩 빠짐없이 저장하고 있고, 서버를 재부팅해도 녹화가 자동으로 복구되며, 180일이 지난 영상은 매일 새벽 자동으로 정리됩니다.

0%

재인코딩 CPU 부담 (-c copy)

96개

하루 녹화 세그먼트 (15분 단위)

180일

보존 기간 (약 0.9TB)

2K

2880×1620 HEVC 원본 그대로

왜 NVR을 안 샀나

가정용·소호용 CCTV의 저장 방식은 보통 셋 중 하나입니다. 카메라에 꽂는 SD카드, 제조사 클라우드 구독, 그리고 NVR 장비입니다. 각각 한계가 분명합니다.

방식한계
SD카드카메라가 도난·파손되면 증거도 같이 사라짐. 용량 한계로 보존 기간이 짧음. 카드 수명 문제
클라우드 구독카메라 대수 × 월 구독료가 계속 나감. 영상이 외부 서버에 올라가는 것 자체가 부담일 수 있음
NVR 장비별도 하드웨어 구매 비용. 관리 포인트(펌웨어·디스크·UI)가 하나 더 늘어남

그런데 대부분의 IP 카메라는 RTSP(Real Time Streaming Protocol)라는 표준 프로토콜로 영상 스트림을 내보낼 수 있습니다. RTSP를 받을 수 있다면 녹화 장비는 무엇이든 됩니다. 24시간 돌아가는 리눅스 서버가 이미 있다면, 그 서버가 곧 NVR입니다.

저희 구성: 서버실 카메라 2대 중 1대는 RTSP로 서버에 녹화(이 글의 주제), 나머지 1대는 SD카드 단독으로 운영합니다. RTSP를 지원하는 카메라라면 제조사와 무관하게 같은 방법이 통합니다.

전체 구성

구성은 단순합니다. 카메라는 IoT 기기 전용 VLAN(무선)에 붙어 있고, 녹화 서버가 카메라의 RTSP 포트(554/tcp)로 접속해서 스트림을 끌어옵니다(pull 방식). 받은 스트림은 재인코딩 없이 15분 단위 파일로 잘라 백업용 디스크에 저장합니다.

데이터 흐름

[CCTV 카메라]          [녹화 서버]                [디스크]
IoT 전용 VLAN    ←──   ffmpeg가 RTSP pull   ──→   15분 단위 .mkv
554/tcp (RTSP)         -c copy (재인코딩 X)        180일 보존 후 자동 삭제

포인트는 카메라가 서버로 보내는 게 아니라, 서버가 카메라에서 가져온다는 것입니다. 덕분에 카메라에는 어떤 설정도 추가할 필요가 없고, 방화벽에서 “녹화 서버 → 카메라 554번” 단 한 방향만 열어주면 됩니다. 카메라가 외부로 나가는 길은 전부 막아도 녹화에 지장이 없습니다.

네트워크 준비 — IoT 망분리와 고정 IP

녹화를 시작하기 전에 두 가지를 먼저 처리했습니다. 망분리와 IP 고정입니다.

① IoT 전용 VLAN. CCTV·스마트 플러그 같은 IoT 기기는 펌웨어 업데이트가 불규칙하고 보안 이력이 좋지 않은 경우가 많습니다. 그래서 서버망·업무망과 분리된 IoT 전용 VLAN에만 붙입니다. IoT 기기가 해킹당해도 서버망으로 넘어올 수 없게 하는 게 목적입니다.

② DHCP 고정 임대(예약). 녹화 서버는 카메라 IP로 접속하므로, 카메라가 재부팅할 때마다 IP가 바뀌면 녹화가 끊깁니다. 공유기나 방화벽의 DHCP 설정에서 카메라 MAC 주소에 IP를 예약해서 항상 같은 주소를 받게 했습니다. 예약 후에는 카메라 RTSP 포트가 열려 있는지 확인합니다.

# 녹화 서버에서 — 카메라 RTSP 포트 연결 확인

nc -vz <카메라IP> 554
# Connection to <카메라IP> 554 port [tcp/rtsp] succeeded!

RTSP 접속 주소는 카메라 제조사마다 다릅니다. 보통 카메라 관리 화면이나 매뉴얼에rtsp://계정:비밀번호@카메라IP/stream_ch00_0같은 형식으로 안내되어 있습니다.

녹화의 핵심 — ffmpeg 스트림 복사

녹화 스크립트의 전부가 아래 ffmpeg 명령 하나입니다. 인증정보가 들어간 RTSP 주소는 스크립트에 하드코딩하지 않고 환경변수로 받습니다(뒤의 systemd 섹션에서 분리 보관).

/usr/local/sbin/cctv-record — 녹화 스크립트

#!/usr/bin/env bash
set -euo pipefail
: "${RTSP_URL:?missing RTSP_URL}"
: "${OUTPUT_DIR:?missing OUTPUT_DIR}"
SEGMENT_SECONDS="${SEGMENT_SECONDS:-900}"
umask 027
mkdir -p "$OUTPUT_DIR"
exec /usr/bin/ffmpeg \
  -hide_banner \
  -loglevel warning \
  -rtsp_transport tcp \
  -fflags +genpts \
  -use_wallclock_as_timestamps 1 \
  -i "$RTSP_URL" \
  -map 0:v:0 \
  -an \
  -c copy \
  -f segment \
  -segment_time "$SEGMENT_SECONDS" \
  -reset_timestamps 1 \
  -strftime 1 \
  "$OUTPUT_DIR/%Y%m%d_%H%M%S.mkv"

옵션 하나하나가 운영 안정성과 직결됩니다.

옵션왜 필요한가
-c copy이 구성의 핵심. 카메라가 이미 HEVC로 인코딩한 스트림을 그대로 파일에 담습니다. 재인코딩이 없으니 CPU를 거의 쓰지 않아, 백업 서버에 얹어도 본래 업무에 영향이 없습니다
-rtsp_transport tcpRTSP 기본값인 UDP는 무선 구간에서 패킷이 깨지기 쉽습니다. TCP로 받으면 깨진 프레임이 크게 줄어듭니다
-f segment + -segment_time 90015분(900초) 단위로 파일을 끊습니다. 파일 하나가 통째로 크면 중간에 끊겼을 때 전체가 깨질 수 있고, 특정 시간대를 찾아보기도 어렵습니다
-strftime 1파일명이 20260612_044217.mkv처럼 녹화 시작 시각이 됩니다. “어제 새벽 4시”를 파일명만으로 바로 찾을 수 있습니다
-use_wallclock_as_timestamps 1저가 카메라는 타임스탬프가 자주 튑니다(지터). 서버의 실제 시계를 기준으로 타임스탬프를 다시 찍어 재생 시 시간이 어긋나는 문제를 줄입니다
-an오디오 제외. 필요 없는 데이터를 빼서 용량을 아낍니다

재인코딩하고 싶어질 때

화질을 낮춰 용량을 더 줄이고 싶다면 -c copy 대신 libx264/libx265 인코딩을 쓸 수 있지만, 24시간 인코딩은 CPU를 계속 점유합니다. 카메라 설정에서 비트레이트를 낮추는 쪽이 먼저입니다. 서버는 받아서 저장만 하는 게 가장 안정적입니다.

systemd 서비스 — 재부팅에도 살아남기

녹화는 한 번 켜고 끝나는 게 아니라 끊기면 안 되는 상시 프로세스입니다. nohup이나 screen이 아니라 systemd 서비스로 등록해야 서버 재부팅, 네트워크 순단, 프로세스 비정상 종료를 모두 자동으로 복구할 수 있습니다.

/etc/systemd/system/cctv-record.service

[Unit]
Description=Record CCTV RTSP stream
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=cctv
Group=cctv
EnvironmentFile=/etc/cctv/camera.env
ExecStart=/usr/local/sbin/cctv-record
Restart=always
RestartSec=10
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/mnt/storage/cctv

[Install]
WantedBy=multi-user.target

여기서 챙긴 것들:

  • 인증정보 분리 — RTSP 계정·비밀번호가 들어간 URL은/etc/cctv/camera.env에만 두고 root 전용 권한(0600)으로 보호합니다. 스크립트·서비스 파일·git 어디에도 비밀번호가 남지 않습니다.
  • Restart=always + RestartSec=10 — 카메라 재부팅이나 와이파이 순단으로 ffmpeg가 죽어도 10초 뒤 자동 재접속합니다.
  • 권한 최소화 — 전용 계정으로 실행하고, NoNewPrivileges·ProtectSystem·ReadWritePaths로 녹화 폴더 외에는 쓰기 자체가 불가능하게 묶었습니다. 녹화 프로세스가 뚫려도 피해 범위가 폴더 하나로 제한됩니다.
  • network-online.target 대기 — 부팅 직후 네트워크가 올라오기 전에 ffmpeg가 먼저 떠서 실패하는 것을 방지합니다.

# 등록 및 동작 확인

sudo systemctl daemon-reload
sudo systemctl enable --now cctv-record.service

systemctl status cctv-record.service     # active (running)
systemctl show cctv-record -p NRestarts  # 재시작 횟수 확인
ls -lh /mnt/storage/cctv/ | tail         # 파일이 계속 생기는지

실제로 운영 중에 서버를 한 번 재부팅했는데(원격 WoL), 부팅 직후부터 녹화가 자동 재개되어 이후 하루 96개 세그먼트가 빠짐없이 쌓이고 있고, ffmpeg 비정상 재시작 횟수(NRestarts)는 0입니다.

보존 정책 — 180일 자동 정리

디스크는 무한하지 않으니 오래된 녹화는 자동으로 지워야 합니다. 정리 스크립트는 find 한 줄이면 충분하고, cron 대신 systemd timer로 매일 새벽에 실행합니다.

/usr/local/sbin/cctv-retention — 보존 기간 초과 파일 삭제

#!/usr/bin/env bash
set -euo pipefail
OUTPUT_DIR=/mnt/storage/cctv
RETENTION_DAYS=180
find "$OUTPUT_DIR" -type f \( -name "*.mkv" -o -name "*.mp4" \) \
  -mtime +"$RETENTION_DAYS" -print -delete
find "$OUTPUT_DIR" -type d -empty -print -delete

/etc/systemd/system/cctv-retention.timer — 매일 03:20 실행

[Timer]
OnCalendar=*-*-* 03:20:00
Persistent=true

[Install]
WantedBy=timers.target

보존 기간은 용량을 보고 정하면 됩니다. 처음에는 30일로 시작했다가, 실측 용량을 보니 여유가 커서 180일로 늘렸습니다. 실제 측정값 기준 용량 계산은 이렇습니다.

항목실측값
해상도 / 코덱2880×1620, HEVC(H.265) — 카메라 인코딩 그대로 저장
세그먼트 1개 (15분)평균 약 50MB
하루 (96개)약 4.8GB
30일약 145GB
180일약 0.9TB

2K 해상도를 HEVC 원본 그대로 저장해도 6개월에 1TB가 안 됩니다.요즘 디스크 가격을 생각하면, 클라우드 구독 몇 달치로 수년치 저장 공간이 나옵니다.

보안 — 카메라는 인터넷과 격리

IP 카메라는 인터넷에 노출되는 순간 공격 대상이 됩니다. 검색엔진에 노출된 카메라가 그대로 들여다보이는 사례는 지금도 흔합니다. 로컬 녹화 구성의 진짜 장점은 비용보다 카메라를 인터넷에서 완전히 떼어낼 수 있다는 데 있습니다.

방화벽 규칙은 이렇게 정리했습니다.

방향정책이유
녹화 서버 → 카메라 554/tcp허용RTSP pull에 필요한 유일한 경로
서버망 → IoT VLAN (나머지)차단IoT 기기 침해 시 서버망 횡적 이동 차단
카메라 → 인터넷차단 방향로컬 녹화 전용이면 외부 통신이 필요 없음

실제로 겪은 함정 — 방화벽 룰은 만든 뒤에 검증해야 한다

운영 중 보안 점검에서 “서버망 → IoT 차단” 룰이 인터페이스 지정 실수로 처음부터 한 번도 동작한 적이 없었다는 걸 발견했습니다. 더 미묘한 건, 그 구멍 덕분에 녹화 트래픽이 우연히 통과하고 있었다는 점입니다. 룰만 바로잡으면 녹화가 끊기는 상황이었습니다.

그래서 수정 순서를 바꿨습니다. ① 녹화 경로(서버→카메라 554)를 명시적으로 허용하는 룰을 먼저 추가하고 ② 그 다음 차단 룰을 교정했습니다. 적용 후 핑 차단·RTSP 연결·녹화 파일 생성을 각각 확인했습니다. 차단 룰은 만들었다는 사실이 아니라, 실제로 막히는지 테스트한 결과로만 신뢰할 수 있습니다.

그 외에 챙긴 것: RTSP 비밀번호는 환경변수 파일(0600)에만 존재하고 어떤 문서·저장소에도 평문으로 남기지 않습니다. 카메라 관리 화면 기본 비밀번호는 당연히 변경했습니다.

운영 결과와 체크리스트

이 구성으로 추가 하드웨어 0원, 월 구독료 0원에 2K 화질 6개월 보존 CCTV 녹화 시스템이 완성됐습니다. 운영 몇 주간 세그먼트 공백 없이 기록되고 있고, 서버 재부팅 후에도 자동 복구를 확인했습니다.

따라 할 때 체크리스트

  • • 카메라가 RTSP를 지원하는지, 접속 URL 형식이 무엇인지 확인한다 (제조사 매뉴얼)
  • • 카메라 IP를 DHCP 예약으로 고정한다
  • • 가능하면 IoT 전용 VLAN으로 분리하고, 녹화 서버 → 카메라 554/tcp만 허용한다
  • • ffmpeg는 -c copy + -rtsp_transport tcp + 세그먼트 분할로 구성한다
  • • RTSP 인증정보는 EnvironmentFile(0600)로 분리하고 어디에도 평문으로 남기지 않는다
  • • systemd 서비스(Restart=always)로 등록하고, 재부팅 테스트까지 해본다
  • • 보존 스크립트 + systemd timer로 디스크가 차기 전에 자동 정리한다
  • • 차단 룰은 만들고 끝내지 말고, 실제로 막히는지·녹화가 살아있는지 검증한다

서버 초기 구성은 리눅스 서버 초기 세팅 체크리스트에서, 사무실 네트워크 대역 실측은 iperf3 사무실 네트워크 속도 테스트에서 이어서 볼 수 있습니다.

T

Treeru

Sharing practical insights on web development, IT infrastructure, and AI solutions. Treeru — your partner in digital transformation.

Share

Related Posts

© 2026 TreeRU. All rights reserved.

All content is copyrighted by TreeRU. Unauthorized reproduction without attribution is prohibited.