はじめに
TP-Linkのネットワークカメラ「Tapo C120」の映像を、Raspberry Pi 3B+を使って外付けHDDに常時録画する仕組みを構築しました。
録画にはFFmpegを使用し、Docker Composeで管理します。さらに、systemdによる起動管理とHDDの監視を組み合わせ、HDDが使えなくなった場合には録画を停止する構成にしました。
実際に約20時間以上の連続録画を行い、以下を確認できました。
- Dockerコンテナの再起動回数:0回
- 録画ファイル:123個
- 10分ごとの録画ファイル分割:正常
- ファイル開始時刻の間隔が11分を超えた箇所:0件
- HDD監視:正常
- 監視プロセスの異常終了に連動した録画停止:成功
この記事では、再現できるようにコマンドや設定ファイルを含めて紹介します。
注意
本記事は家庭内での録画用途を想定しています。カメラのRTSP機能や認証方法は、機種・ファームウェアによって異なる場合があります。
また、HDD障害や突然の電源断によるデータ破損を完全に防ぐものではありません。
1. 使用した機材と構成
ハードウェア
| 機材 | 内容 |
|---|---|
| Raspberry Pi | Raspberry Pi 3 Model B+ |
| カメラ | TP-Link Tapo C120 |
| 保存先 | BUFFALO製 外付けHDD 3TB |
| ネットワーク | Raspberry Piとカメラが同一LAN内に存在 |
ソフトウェア
- Raspberry Pi OS
- Docker / Docker Compose
- FFmpeg
- systemd
- NTFS3ドライバー
- Samba(録画ファイルの共有)
- Python 3(確認用スクリプト)
構成は次のとおりです。
Tapo C120
│
│ RTSP
▼
Raspberry Pi 3B+
│
├─ Docker Compose
│ └─ FFmpeg
│ └─ 10分単位でMKV保存
│
├─ systemd
│ ├─ tapo-recorder.service
│ ├─ tapo-hdd-monitor.service
│ └─ tapo-recorder-stop.service
│
└─ USB接続
└─ 外付けHDD(NTFS)
└─ tapo-recordings/
└─ tapo/
├─ 2026-10-15_23-50-35.mkv
├─ 2026-10-16_00-00-35.mkv
└─ ...
ポイントは、録画処理とHDD監視を分離していることです。
FFmpegが録画を担当し、別の監視プロセスがHDDのマウント状態と書き込み可否を確認します。
2. Tapo C120のRTSPを有効にする
Tapoアプリでカメラアカウントを設定し、RTSP接続用のユーザー名とパスワードを用意します。
Tapoのカメラアカウントは、Tapoアプリへのログインアカウントとは別に設定するものです。
一般的なRTSP URLの形式は次のとおりです。
rtsp://ユーザー名:パスワード@カメラのIPアドレス:554/stream1
stream1 は高画質側のストリームです。
たとえばカメラのIPアドレスが 192.168.1.100 の場合、URLは次のようになります。
rtsp://camerauser:YOUR_PASSWORD@192.168.1.100:554/stream1
IPアドレスが変わると録画できなくなるため、ルーター側でDHCP予約を設定しておくと管理しやすくなります。
3. 外付けHDDをマウントする
今回は既存データが入った3TBのNTFS HDDを使用しました。
既存データを残したまま使うため、フォーマットはしていません。
まず接続されているディスクを確認します。
lsblk -f
今回の環境では、HDDのパーティションは /dev/sda1 でした。
マウントポイントを作成します。
sudo mkdir -p /mnt/tapo-hdd
UUIDを確認します。
sudo blkid /dev/sda1
実機では次のUUIDでした。
4294E89494E88BAB
UUIDは環境によって異なるため、自分のHDDの値に置き換えてください。
/etc/fstab を編集します。
sudo nano /etc/fstab
以下を追加します。
UUID=4294E89494E88BAB /mnt/tapo-hdd ntfs3 rw,noatime,nofail,x-systemd.device-timeout=10s 0 0
マウントします。
sudo systemctl daemon-reload
sudo mount /mnt/tapo-hdd
確認します。
findmnt /mnt/tapo-hdd
今回の環境では次のように表示されました。
TARGET SOURCE FSTYPE OPTIONS
/mnt/tapo-hdd /dev/sda1 ntfs3 rw,noatime,uid=0,gid=0,iocharset=utf8
録画用ディレクトリを作成する
sudo mkdir -p /mnt/tapo-hdd/tapo-recordings/tapo
さらに、正しいHDD上の録画領域であることを確認するため、目印となるファイルを作成します。
sudo touch /mnt/tapo-hdd/tapo-recordings/.hdd-ready
このファイルは、後述する起動スクリプトでチェックします。
HDDをマウントしていない状態での誤書き込みを防ぐ
外付けHDDを使用する際に注意したいのは、HDDがマウントされていなくても、マウントポイント自体はmicroSD上のディレクトリとして存在することです。
そのまま録画すると、意図せずmicroSD側にファイルが保存される危険があります。
今回は次の対策を組み合わせました。
- 起動時にマウント状態を確認する
- マウントされたHDDのUUIDを確認する
-
.hdd-readyファイルを確認する - Dockerのbind mountでホスト側パスを自動作成させない
- 録画中もHDDを監視する
単一の対策に頼らず、複数の条件を確認する構成です。
4. Docker ComposeでFFmpeg録画を構築する
作業用ディレクトリを作成します。
mkdir -p /home/pi/tapo-recorder
cd /home/pi/tapo-recorder
ディレクトリ構成は次のようにします。
/home/pi/tapo-recorder/
├── compose.yaml
├── Dockerfile
├── record.sh
├── .env
├── start-recording.sh
└── stop-recording.sh
以下のDockerfileと録画スクリプトは、実際の構成と同じ方針で動作する再現用の実装例です。
Dockerfile
FROM alpine:3.20
RUN apk add --no-cache ffmpeg tzdata
WORKDIR /app
COPY record.sh /app/record.sh
RUN chmod +x /app/record.sh
ENTRYPOINT ["/app/record.sh"]
record.sh
FFmpegでRTSP映像を受信し、10分単位のMKVファイルとして保存します。
#!/bin/sh
set -eu
: "${TAPO_RTSP_URL:?TAPO_RTSP_URL is required}"
mkdir -p /recordings/tapo
exec ffmpeg \
-hide_banner \
-loglevel warning \
-rtsp_transport tcp \
-i "$TAPO_RTSP_URL" \
-map 0:v:0 \
-c:v copy \
-an \
-f segment \
-segment_time 600 \
-reset_timestamps 1 \
-strftime 1 \
"/recordings/tapo/%Y-%m-%d_%H-%M-%S.mkv"
重要なのは以下のオプションです。
| オプション | 意味 |
|---|---|
-rtsp_transport tcp |
RTSP映像をTCPで受信 |
-map 0:v:0 |
最初の映像ストリームを選択 |
-c:v copy |
映像を再エンコードせずコピー |
-an |
音声を保存しない |
-f segment |
ファイルを分割して保存 |
-segment_time 600 |
600秒(10分)ごとに分割 |
-strftime 1 |
ファイル名に日時を使用 |
特に -c:v copy は重要です。
Raspberry Pi 3B+は高性能な動画エンコード向けの機材ではありませんが、映像を再エンコードせず保存することで、CPU負荷を抑えられます。
なお、FFmpegのセグメント分割は映像のキーフレーム位置に左右されるため、常に秒単位で正確に600秒になるとは限りません。
.env
TAPO_RTSP_URL=rtsp://camerauser:YOUR_PASSWORD@192.168.1.100:554/stream1
TZ=Asia/Tokyo
RECORD_MODE=production
認証情報が含まれるため、.env はGitHubなどに公開しないようにします。
chmod 600 .env
.gitignore にも追加しておきます。
.env
compose.yaml
services:
tapo-recorder:
build: .
container_name: tapo-recorder
restart: "no"
stop_grace_period: 60s
mem_limit: 256m
env_file:
- .env
environment:
TZ: Asia/Tokyo
RECORD_MODE: production
volumes:
- type: bind
source: /mnt/tapo-hdd/tapo-recordings
target: /recordings
bind:
create_host_path: false
ここで特に重要なのが次の設定です。
bind:
create_host_path: false
ホスト側の録画ディレクトリが存在しない場合に、Dockerが自動作成しないようにしています。
また、restart: "no" とすることで、監視サービスが録画停止を要求した後にDockerが独自に再起動しないようにしています。
Dockerの起動確認
docker compose up -d --build
状態を確認します。
docker compose ps
録画ファイルが作成されているか確認します。
ls -lht /mnt/tapo-hdd/tapo-recordings/tapo/ | head
動作確認後は、次のsystemd管理に移行します。
docker compose stop
5. HDDの確認後に録画を起動する
HDDが正しくマウントされている場合だけDockerを起動するスクリプトを用意します。
start-recording.sh
#!/bin/bash
set -euo pipefail
HDD=/mnt/tapo-hdd
EXPECTED_UUID=4294E89494E88BAB
RECORD_DIR="$HDD/tapo-recordings"
if ! mountpoint -q "$HDD"; then
echo "ERROR: HDD is not mounted" >&2
exit 1
fi
ACTUAL_UUID=$(findmnt -rn -o UUID --mountpoint "$HDD")
if [ "$ACTUAL_UUID" != "$EXPECTED_UUID" ]; then
echo "ERROR: Unexpected HDD UUID" >&2
exit 1
fi
if [ ! -f "$RECORD_DIR/.hdd-ready" ]; then
echo "ERROR: HDD marker file not found" >&2
exit 1
fi
cd /home/pi/tapo-recorder
exec docker compose up -d --build
UUIDは自分の環境に合わせて変更してください。
実行権限を付けます。
chmod +x start-recording.sh
このスクリプトは、HDDが未マウントの場合や別のHDDがマウントされている場合に、録画を開始しないためのものです。
6. systemdで録画を管理する
Dockerコンテナの起動と停止をsystemdから制御します。
tapo-recorder.service
sudo nano /etc/systemd/system/tapo-recorder.service
内容は次のとおりです。
[Unit]
Description=Tapo C120 Docker Recorder
Requires=docker.service
After=docker.service local-fs.target
RequiresMountsFor=/mnt/tapo-hdd
[Service]
Type=oneshot
RemainAfterExit=yes
User=pi
WorkingDirectory=/home/pi/tapo-recorder
ExecStart=/home/pi/tapo-recorder/start-recording.sh
ExecStop=/usr/bin/docker compose stop
TimeoutStartSec=180
TimeoutStopSec=90
[Install]
WantedBy=multi-user.target
Type=oneshot と RemainAfterExit=yes を組み合わせています。
起動スクリプトはDockerコンテナをバックグラウンド起動して終了しますが、systemd上ではサービスを有効状態として維持します。
ExecStop ではDocker Compose経由で録画コンテナを停止します。
7. HDDを監視する
今回の構成で特に重視した部分です。
HDDが突然使えなくなった場合、録画を続けようとするとエラーが発生したり、保存先を誤ったりする可能性があります。
そこで、独立した監視サービスを用意しました。
監視内容は次のとおりです。
- 5秒ごとにHDDのマウント状態を確認
- 5秒ごとにUUIDに対応するブロックデバイスの存在を確認
- 約60秒ごとにテストファイルの書き込み・同期・読み取り・削除を実行
- 異常があれば監視サービスを異常終了させる
監視スクリプトの配置
root権限で実行する監視スクリプトを、一般ユーザーが書き換えられるディレクトリに置かないようにします。
sudo mkdir -p /usr/local/libexec/tapo-recorder
sudo chown root:root /usr/local/libexec/tapo-recorder
sudo chmod 755 /usr/local/libexec/tapo-recorder
次のファイルを作成します。
sudo nano /usr/local/libexec/tapo-recorder/monitor-hdd.sh
内容は次のとおりです。
#!/bin/sh
set -eu
HDD=/mnt/tapo-hdd
EXPECTED_UUID=4294E89494E88BAB
DEVICE_LINK="/dev/disk/by-uuid/$EXPECTED_UUID"
RECORD_DIR="$HDD/tapo-recordings"
TEST_FILE="$RECORD_DIR/.hdd-write-check"
COUNT=0
fail() {
echo "ERROR: HDD monitoring failed: $1" >&2
exit 1
}
while true; do
ACTUAL_UUID=$(findmnt -rn -o UUID --mountpoint "$HDD" 2>/dev/null || true)
if [ "$ACTUAL_UUID" != "$EXPECTED_UUID" ] ||
[ ! -b "$DEVICE_LINK" ]; then
fail "HDD mount or device is missing"
fi
if [ "$COUNT" -eq 0 ]; then
if ! sh -c '
set -e
test_file="$1"
printf "hdd-write-check\n" > "$test_file" || exit 1
sync "$test_file" || exit 1
content=$(cat "$test_file") || exit 1
[ "$content" = "hdd-write-check" ] || exit 1
rm -f "$test_file" || exit 1
' sh "$TEST_FILE"; then
fail "HDD write/read check failed"
fi
echo "OK: HDD write/read check passed"
fi
COUNT=$(( (COUNT + 1) % 12 ))
sleep 5
done
権限を設定します。
sudo chown root:root /usr/local/libexec/tapo-recorder/monitor-hdd.sh
sudo chmod 755 /usr/local/libexec/tapo-recorder/monitor-hdd.sh
なぜ監視スクリプトをrootで実行するのか
今回のNTFS HDDはroot所有でマウントされており、録画ディレクトリもroot所有でした。
そのため、通常ユーザーでは監視用ファイルの書き込みに失敗しました。
監視サービスはrootで動作させ、スクリプト本体をroot所有の保護された場所に配置しています。
なお、録画ディレクトリの所有権・アクセス権は環境に応じて設計してください。
tapo-hdd-monitor.service
sudo nano /etc/systemd/system/tapo-hdd-monitor.service
[Unit]
Description=Tapo HDD Mount Monitor
Requires=docker.service
After=docker.service
PartOf=tapo-recorder.service
OnFailure=tapo-recorder-stop.service
[Service]
Type=simple
User=root
WorkingDirectory=/usr/local/libexec/tapo-recorder
ExecStart=/usr/local/libexec/tapo-recorder/monitor-hdd.sh
Restart=no
[Install]
WantedBy=tapo-recorder.service
OnFailure= が重要です。
監視サービスが異常終了した場合、録画停止用のサービスを起動します。
また、監視スクリプトから直接Docker Composeを実行しないようにしています。
rootで動作する監視スクリプトが、一般ユーザーの編集可能なCompose設定ファイルを読み込む構成を避けるためです。
tapo-recorder-stop.service
sudo nano /etc/systemd/system/tapo-recorder-stop.service
[Unit]
Description=Stop Tapo Recorder After HDD Monitor Failure
[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl stop tapo-recorder.service
このサービスは、監視サービスの異常終了時に録画サービスを停止します。
録画サービスに監視サービスへの依存を追加する
ドロップイン設定を作成します。
sudo mkdir -p /etc/systemd/system/tapo-recorder.service.d
sudo nano /etc/systemd/system/tapo-recorder.service.d/override.conf
[Unit]
Requires=tapo-hdd-monitor.service
After=tapo-hdd-monitor.service
これにより、録画サービスの起動時に監視サービスも起動する構成になります。
systemd設定を反映する
sudo systemctl daemon-reload
sudo systemctl enable tapo-recorder.service
sudo systemctl start tapo-recorder.service
確認します。
systemctl is-active tapo-recorder.service tapo-hdd-monitor.service
正常時は次のように表示されます。
active
active
Dockerコンテナも確認します。
docker compose -f /home/pi/tapo-recorder/compose.yaml ps
8. HDD監視が正常に動いているか確認する
監視ログを表示します。
journalctl -u tapo-hdd-monitor.service -n 10 --no-pager
正常時には次のようなログが表示されます。
OK: HDD write/read check passed
実際の環境では、おおむね1分ごとにこのログが記録されていました。
なお、このテストはHDDへの書き込み・読み取りができることを確認するものであり、HDD全体の健全性やすべてのセクターの正常性を保証するものではありません。
9. HDDを抜かずに異常停止をテストする
HDDのUSBケーブルを突然抜くテストは、ファイルシステム破損の危険があるため避けます。
今回は、監視プロセスを意図的に異常終了させ、systemdが録画を停止するか検証しました。
このテストでは録画が一時停止します。
監視プロセスに SIGKILL を送ります。
sudo systemctl kill --kill-whom=main --signal=SIGKILL tapo-hdd-monitor.service
状態を確認します。
systemctl is-active tapo-hdd-monitor.service tapo-recorder.service
docker compose -f /home/pi/tapo-recorder/compose.yaml ps -a
実際の結果は次のとおりでした。
failed
inactive
Dockerコンテナは以下の状態になりました。
Exited (0)
つまり、監視プロセスの異常終了を契機として、録画サービスとDockerコンテナが停止することを確認できました。
これは監視プロセス異常時の連動テストです。実際のHDD切断やI/Oエラーを完全に再現するものではありません。
テスト後の復旧
sudo systemctl reset-failed tapo-hdd-monitor.service
sudo systemctl start tapo-recorder.service
状態を確認します。
systemctl is-active tapo-recorder.service tapo-hdd-monitor.service
docker compose -f /home/pi/tapo-recorder/compose.yaml ps
両サービスが active、Dockerコンテナが Up になれば録画再開です。
10. 録画ファイルを確認する
録画ファイルは次の場所に保存されます。
/mnt/tapo-hdd/tapo-recordings/tapo/
確認コマンドは次のとおりです。
ls -lht /mnt/tapo-hdd/tapo-recordings/tapo/ | head
実際には次のようなファイルが生成されました。
2026-10-16_19-20-34.mkv
2026-10-16_19-30-34.mkv
2026-10-16_19-40-34.mkv
2026-10-16_19-50-34.mkv
2026-10-16_20-00-34.mkv
おおむね10分ごとに新しいMKVファイルが作成されています。
Dockerコンテナの再起動回数を確認する
docker inspect tapo-recorder \
--format 'StartedAt={{.State.StartedAt}} RestartCount={{.RestartCount}}'
今回の検証では次の結果でした。
StartedAt=2026-10-15T14:50:31.734509827Z RestartCount=0
表示はUTCなので、日本時間では2026年10月15日23:50:31です。
約20時間の連続録画中、Dockerコンテナの再起動は発生していませんでした。
11. 録画ファイルの時刻に大きな空白がないか確認する
単にファイルが存在するだけでは、途中で録画が止まっていないか分かりません。
そこで、ファイル名に含まれる日時を比較し、11分を超える空白がないか確認しました。
from pathlib import Path
from datetime import datetime, timedelta
directory = Path("/mnt/tapo-hdd/tapo-recordings/tapo")
files = sorted(directory.glob("*.mkv"))
start = datetime(2026, 10, 15, 23, 50)
times = []
for f in files:
try:
t = datetime.strptime(f.stem, "%Y-%m-%d_%H-%M-%S")
if t >= start:
times.append(t)
except ValueError:
pass
print(f"対象ファイル数: {len(times)}")
print(f"最初: {times[0] if times else 'なし'}")
print(f"最後: {times[-1] if times else 'なし'}")
for a, b in zip(times, times[1:]):
gap = b - a
if gap > timedelta(minutes=11):
print(f"要確認: {a} → {b}({gap})")
print("確認完了")
実際の結果は次のとおりです。
対象ファイル数: 123
最初: 2026-10-15 23:50:35
最後: 2026-10-16 20:10:34
確認完了
「要確認」は表示されませんでした。
したがって、約20時間以上の録画期間において、ファイル開始時刻の間隔が11分を超える箇所はありませんでした。
ただし、これはファイル名に基づくチェックです。各MKVファイルの内部に映像欠落がないことや、全ファイルが最後まで正常再生できることまでは保証しません。
12. 古い録画の自動削除と容量監視
長期間録画を続ける場合、ディスク容量の管理も必要です。
今回の環境では、次の運用を設定しました。
| 処理 | 設定 |
|---|---|
| 古い録画の削除 | 90日を超えたMKVファイルを対象 |
| 削除処理の実行時刻 | 毎日03:00 |
| HDD使用率の確認 | 毎日03:10 |
| 使用率の警告しきい値 | 80% |
これらはsystemd timerで管理しています。
削除処理には、録画サービスと同様に、HDDのマウント状態・UUID・目印ファイルの確認を組み込むことが重要です。
特に、削除処理では対象ディレクトリを厳密に限定し、写真や他の動画が入っている領域を巻き込まないようにする必要があります。
今回、タイマーの有効化と容量監視の動作は確認しましたが、90日経過したファイルの実削除までは検証していません。
そのため、自動削除機能は長期運用中に引き続き確認する項目としています。
13. Sambaで録画ファイルを共有する
録画ファイルをWindowsやスマートフォンから確認するため、Sambaで共有しています。
共有設定の例は次のとおりです。
[TapoRecordings]
path = /mnt/tapo-hdd/tapo-recordings
browseable = yes
read only = yes
guest ok = no
valid users = pi
読み取り専用にすることで、共有先から誤って録画ファイルを削除するリスクを減らせます。
Windowsからは、たとえば次の形式でアクセスできます。
\\docker-pi\TapoRecordings
Sambaユーザーの設定やファイアウォールは、各自のネットワーク環境に合わせてください。
14. 運用中に遭遇した問題
14-1. NTFS HDDの不正な取り外し
外付けHDDを正常にアンマウントせず電源を切った際、NTFSの状態が不整合になり、I/Oエラーが発生したことがありました。
Windows側でディスクチェックと修復を実施し、復旧しました。
この経験から、HDDを取り外す前に録画を停止し、アンマウントすることが重要だと分かりました。
14-2. HDD監視の書き込み権限
監視スクリプトを一般ユーザーで実行したところ、root所有の録画ディレクトリにテストファイルを書き込めませんでした。
今回は監視サービスをrootで動作させて解決しました。
ただし、root権限で実行するスクリプトは、一般ユーザーが書き換えられない場所に置く必要があります。
14-3. 監視スクリプトからDockerを直接停止しない
当初は、監視スクリプトの異常処理から直接次のようなコマンドを実行する構成を検討しました。
docker compose -f /home/pi/tapo-recorder/compose.yaml stop
しかし、root権限の監視プロセスが一般ユーザーの編集可能なCompose設定を読み込む構成は避けたいところです。
最終的には、監視スクリプトは異常時に exit 1 するだけにし、録画停止はsystemdの OnFailure= で実行するように変更しました。
この構成で、異常終了から録画停止までの連動テストにも成功しました。
14-4. docker compose ps が失敗する
ホームディレクトリで次のコマンドを実行すると、
docker compose ps
以下のエラーが表示されました。
no configuration file provided: not found
これはDockerの録画障害ではなく、Composeファイルが存在しないディレクトリで実行したためです。
次のように -f で設定ファイルを指定すれば確認できます。
docker compose -f /home/pi/tapo-recorder/compose.yaml ps
15. 20時間連続録画の検証結果
最後に、実測結果をまとめます。
| 項目 | 実測結果 |
|---|---|
| 検証開始 | 2026年10月15日 23:50頃 |
| 最終確認 | 2026年10月16日 20:10頃 |
| 検証時間 | 約20時間20分 |
| 録画ファイル数 | 123個 |
| ファイル分割 | おおむね10分ごと |
| 11分超の開始時刻の空白 | 0件 |
| Docker再起動回数 | 0回 |
| 録画サービス | active |
| HDD監視サービス | active |
| HDD書き込み・読み取り監視 | 正常 |
| 監視異常終了時の録画停止 | 別途テストで成功 |
20時間以上の連続運転で、少なくともコンテナの稼働・HDD監視・録画ファイルの継続生成に問題は確認されませんでした。
もちろん、20時間動作したからといって、今後の無停止運用が保証されるわけではありません。
まとめ
Raspberry Pi 3B+とTapo C120を組み合わせ、外付けHDDに録画する仕組みを構築できました。
今回の構成で重要だった点は、以下のとおりです。
- FFmpegの
-c:v copyで再エンコードを避け、Raspberry Piの負荷を抑える - 10分単位のMKVファイルに分割して保存する
- HDDのUUID・マウント状態・目印ファイルを確認してから録画を開始する
- Dockerのbind mountで保存先ディレクトリの自動作成を無効化する
- 録画とは別のプロセスでHDDの状態を監視する
- 監視サービスの異常終了時にはsystemd経由で録画を停止する
- 長時間の連続録画を実測して動作を確認する
今回の検証では、約20時間20分の録画で123ファイルが生成され、Dockerの再起動は0回、ファイル開始時刻の大きな空白も0件でした。
安価なRaspberry Pi 3B+でも、映像を再エンコードしない構成であれば、ネットワークカメラの録画サーバーとして活用できることを確認できました。
今後は、長期間の運用を続けながら、古い録画の自動削除や容量警告の動作も検証していく予定です。