
TrueNAS 25.10 + Immich + Tailscale でスマホ写真の自動バックアップサーバーを構築(ファイアウォール DMZ 分離、スナップショット、復元テストまで)
この記事は韓国語の原文を翻訳したものです。スクリーンショットは韓国語の画面です。
なぜ作ったのか
家族のスマホ2台(Android)にたまった写真・動画が1TB 近くになりました。クラウドの料金を払い続ける代わりに、余っている PC でバックアップサーバーを自作することにしました。目標は次のとおりです。
- 誰も気にしなくても、新しい写真が自動でサーバーにアップロードされること
- スマホで削除してもサーバーからは消えないこと(一方向バックアップ)
- インターネットにポートを1つも開けないこと
- サーバーを同じネットワークのほかの機器から分離すること
- ディスクの異常、サーバーダウン、バックアップ失敗を通知で知れること
- バックアップが本当に復元できるか確かめること
この記事は、構築の手順と途中でつまずいたところを順番にまとめた記録です。
環境
| 項目 | 構成 |
|---|---|
| サーバー PC | Intel 第9世代 i5(6コア、内蔵グラフィックス)、RAM 16GB |
| OS ディスク | SATA SSD 250GB |
| アプリ・DB ディスク | SATA SSD 500GB |
| 元データ用ディスク | 8TB ヘリウム HDD 1台(外付け HDD から取り出した 5400rpm クラス、使用時間 約6.7年) |
| OS | TrueNAS Community Edition 25.10.7 |
| 写真アプリ | Immich(TrueNAS アプリ、v3.2.4 で導入 → のちに v3.3.0) |
| リモート接続 | Tailscale(TrueNAS アプリ) |
| ネットワーク | 既存の内部ネットワーク、次世代ファイアウォール(UTM)の内側に設置 |
| クライアント | Android スマホ2台 |
HDD が1台だけで、しかも古いディスクであることが、この構成の最大の弱点です。そこで「2つ目のコピーを確保するまで、スマホの元データは消さない」を原則にしました。
ステップ1:ディスクの確認と TrueNAS のインストール
インストールイメージの入手
TrueNAS のダウンロードページは最初にニュースレター登録の画面が出るので、リンクを見つけにくいです。公式の配布サーバー(download.truenas.com)でバージョンのフォルダーを直接開けば、ISO とチェックサムファイルをすぐにダウンロードできます。ファイル名には旧名称の SCALE がそのまま残っています。
:: Windows で ISO のハッシュを確認
certutil -hashfile "TrueNAS-SCALE-25.10.7.iso" SHA256
type "TrueNAS-SCALE-25.10.7.iso.sha256"
2つの値が一致すれば問題ありません。

ハードウェア
- HDD は USB ドックではなく、マザーボードの SATA に直結しました。USB 接続の上で ZFS を使うと、接続が切れたときにプールが壊れるおそれがあります。
- 外付け HDD から取り出した HDD は、SATA 電源の 3.3V ピン(PWDIS)が原因で認識されないことがあります。今回は BIOS ですぐに認識されました。
- BIOS は AHCI、UEFI に設定しました。停電後に自動で電源が入る設定(AC Power Recovery)は、実際に停電を経験してから有効にしました。最初から有効にしておくのがおすすめです。
インストール先の選択
インストール画面にディスクが3台表示されたら、容量とモデル名で 250GB の SSD だけを選びます。TrueNAS はインストールしたディスク全体を OS 専用に使うので、別のディスクを選ぶと、そのディスクはデータの保存に使えなくなります。

インストール後、次の2つをすぐに変更しました。
- タイムゾーン:既定値が米国西部時間なので、System → General Settings → Localization で
Asia/Seoulに変更。このままだとスナップショットや Cron のスケジュールがすべてずれます。 - UI の言語:コミュニティ翻訳は一部のメニューが抜けているので、English のままにしておくほうがドキュメントと照らし合わせやすいです。
ステップ2:HDD のロングテスト(TrueNAS 25.10 は GUI なし)
TrueNAS 25.10 から、S.M.A.R.T. テストを実行・予約する GUI がなくなりました。そのため System → Shell で smartctl を使って実行しました。
# 対象ディスクの確認(モデル名・シリアル番号)
sudo smartctl -i /dev/sdb
# ロングテスト開始(読み取りのみ、データは変わらない)
sudo smartctl -t long /dev/sdb
# 進捗の確認
sudo smartctl -a /dev/sdb | grep -i -A1 "self-test execution"
# 結果の確認
sudo smartctl -l selftest /dev/sdb
sudo smartctl -A /dev/sdb
8TB・5400rpm のディスクでは、ロングテストに約18時間(1,088分)かかりました。結果では次の項目を確認しました。
| ID | 項目 | 正常値 |
|---|---|---|
| 5 | Reallocated_Sector_Ct | 0 |
| 197 | Current_Pending_Sector | 0 |
| 198 | Offline_Uncorrectable | 0 |
| 199 | UDMA_CRC_Error_Count | 0 |
| 22 | Helium_Level(ヘリウムディスク) | 100 |
すべて正常だったので、大量アップロードに進みました。

テスト中に再起動するとテストが中断されるので、サーバーを起動したままほかの作業を並行して進めました。
ステップ3:プールとデータセットの構成
| プール | ディスク | 用途 |
|---|---|---|
| apps | SSD 500GB | アプリ、Immich DB、システムデータセット |
| DATA | HDD 8TB | 写真・動画の元データ |
- ディスクが1台ずつなので、レイアウトは Stripe です。「Stripe は推奨されない」という警告が出ますが、構成上避けられません。
- プールの暗号化は使いませんでした。TrueNAS のドキュメントもプール単位の暗号化を推奨しておらず、鍵を失うとプール全体を復元できなくなります。
- プール名は作成後に変更できません。
- Apps → Configuration → Choose Pool で、アプリの保存先を SSD のプールにしました。
- SSD のプールは Auto TRIM を有効にしました。

ステップ4:ファイアウォールで写真サーバーを分離
最初はサーバーを内部ネットワークのルーターの下に置いていましたが、これだと同じアドレス帯のほかの機器と、ファイアウォールを通らずに直接通信します。同じサブネット内の通信は、ファイアウォールのルールでは止められません。
そこで、ファイアウォールの空いている物理ポートを1つ DMZ ゾーンに割り当て、サーバーをそのポートに直結しました。
[内部ネットワーク] ── ファイアウォール ── [DMZ ポート:写真サーバー専用、プライベート /24]
│
インターネット
- サーバーにはグローバル IP を割り当てず、プライベートアドレス帯を使いました。ルールを誤って開けても、インターネットから直接は入れないからです。
- 内部の PC がよく使うプライベートアドレス帯(例:192.168.0.x)と重ならない帯域を選びました。重なると、PC がサーバーを自分のルーターの中で探してしまい、接続に失敗します。
- DMZ ゾーンからファイアウォール本体へのアクセス(管理画面の HTTPS、SSH など)はすべてオフにしました。
ファイアウォールのルール(上から順に)
| 順序 | 送信元 | 宛先 | サービス | 動作 |
|---|---|---|---|---|
| 1 | 管理用 PC 1台 | 写真サーバー | HTTPS | 許可 |
| 2 | 写真サーバー | WAN | DNS, HTTP, HTTPS, NTP, ping, Tailscale UDP | 許可 |
| 3 | DMZ | LAN, VPN, WiFi | すべて | 遮断(Drop) |
| 4 | LAN, WAN, VPN, WiFi | DMZ | すべて | 遮断(Drop) |
- インターネットから入ってくる DNAT は作っていません。
- 遮断は Reject ではなく Drop にしました。応答を返さないほうが、内部からのスキャンに対して情報を出しにくくなります。
- 許可ルールの送信元は「すべて」ではなく、サーバーのオブジェクトを正確に指定します。
SNAT
既存のデフォルト SNAT ルールが「指定したホストのみ」変換する設定だったため、新しいアドレス帯はインターネットに出られませんでした。デフォルトのルールには手を付けず、送信元を写真サーバー1台に限定した SNAT(MASQ)ルールをデフォルトルールの上に追加しました。送信元と宛先を逆に入れるミスをしやすいので、保存前の確認が必要です。
サーバーの IP 変更(コンソールメニュー)
ケーブルを差し替えると Web UI に接続できなくなるので、サーバーのコンソールで変更しました。
1) Configure network interfaces→ipv4_dhcp: No、aliases: <サーバー IP>/24→ Save- 一覧画面で
a(適用)→ すぐにp(永続化)。約60秒以内にpを押さないと、元の設定に戻ります。 2) Configure network settings→ ゲートウェイ(ファイアウォールのポートの IP)、DNS(1.1.1.1、8.8.8.8)
遮断テスト
ping -c 3 1.1.1.1 # インターネット:成功すること
ping -c 3 google.com # DNS:成功すること
ping -c 3 -W 2 <内部ネットワークのサーバー IP> # 内部ネットワーク:100% loss になること
sudo chronyc sources # 時刻同期:^* の表示があること
管理用 PC 以外の PC から Web UI に接続しようとすると、ファイアウォールの遮断ページが表示されました。ログを確認すると DMZ の遮断ルールに引っかかっており、転送量は0バイトでした。Web 通信がファイアウォールのルールで遮断されると、ファイアウォールが代わりに遮断ページを表示するので、これは正常な動作です。
ステップ5:Tailscale のインストール
Apps → Discover Apps → Tailscale からインストールしました。
| 項目 | 値 | 理由 |
|---|---|---|
| Auth Key | 使い捨てキー(Reusable オフ、Ephemeral オフ) | サーバー1台だけを登録 |
| Advertise Exit Node | オフ | サーバー経由でのインターネット利用を禁止 |
| Advertise Routes | 空欄 | 外部から内部ネットワークへ入る経路を遮断 |
| Accept Routes / Accept DNS | オフ | |
| Host Network | オン | サーバー自身の Web UI と Immich に接続するため |
| Userspace | オフ | Host Network と組み合わせて使用 |
登録後、管理コンソールでサーバーの Disable key expiry をオンにしました。オンにしないと、一定期間後に再認証を求められてバックアップが止まります。
2人目のユーザーには同じアカウントでログインさせず、本人のアカウントで登録してもらったうえで、このサーバー1台だけを共有(Share)しました。
テスト:スマホを Wi-Fi ではなく LTE/5G にした状態で、Tailscale をオンにするとサーバーに接続でき、オフにすると接続できないことを確認しました。
ステップ6:Immich のインストール
ストレージの項目が3つしかなかった
以前のドキュメントには Immich のストレージが7つ(library、upload、thumbs など)とあったので、データセットを先に7つ作っておきました。ところが、インストールしたアプリのバージョン(1.14.x)のインストール画面には項目が3つしかありませんでした。
| 項目 | 設定 |
|---|---|
| Data Storage (Upload Location) | Host Path → HDD プールのデータセット |
| Machine Learning Cache | ixVolume |
| Postgres Data Storage | Host Path → SSD プールのデータセット |
Data Storage の中に library、upload、thumbs、encoded-video、profile、backups を Immich が自分で作ります。結果的に DB 本体は SSD に、元データと DB の自動バックアップは HDD に置かれるので、SSD が故障しても DB のバックアップは残る構成になりました。先に作っておいた空のデータセット6つは削除しました。
そのほかの設定:
- データセットは Apps プリセットで作成しました。アプリの実行ユーザー(UID 568)に書き込み権限が自動で付与されます。
- Automatic Permissions をオンにしました。DB のコンテナは別のユーザーで動くので、pgData フォルダーの所有者を合わせないと起動しません。
- GPU Passthrough で内蔵グラフィックスを割り当て、動画のトランスコードに Quick Sync を使うようにしました。
- DB と Redis のパスワードは英数字だけにしました。理由は後述の「つまずいたところと解決方法」にあります。
- Host IPs は空欄にしました。Tailscale の IP を指定すると、再起動時に Immich が Tailscale より先に起動した場合にポートを開けず、起動に失敗することがあります。アクセス制限はファイアウォールの役目です。
サーバー設定(Immich の管理画面)
| 項目 | 値 |
|---|---|
| ストレージテンプレート | {{y}}/{{y}}-{{MM}}/{{filename}} |
| ユーザーごとのストレージラベル | 英字の名前 |
| DB 自動バックアップ | 毎日 02:00、14世代保持 |
| ゴミ箱 | 30日 |
| トランスコードのハードウェア支援 | Quick Sync |
管理者アカウントはサーバー設定専用にして、スマホアプリには一般ユーザーのアカウントだけでログインしました。
スマホの設定
- Immich アプリのサーバーアドレス:
http://<Tailscale IP>:30041(https ではない) - リモート削除の同期(Sync remote deletions)はオフ、空き容量の確保(Free up space)は使わない
- バックアップ条件:Wi-Fi + 充電中
- Tailscale:常時接続 VPN をオン、VPN なしの接続をブロックはオフ(オンにすると、Tailscale が切れたときにスマホのインターネット全体が使えなくなる)
- Tailscale と Immich はどちらも電池の使用を「制限なし」にして、省電力の対象外に登録
早朝にだけアップロードされると思っていましたが、実際には充電中やスマホを使っていないときに随時アップロードされ、アプリを開くとすぐにアップロードされました。その日の朝に撮った写真もすぐにサーバーに上がっていたので、予約アップロード用のアプリに切り替える必要はありませんでした。
ステップ7:スナップショット、バックアップ、通知、ディスク監視
ZFS スナップショット
Data Protection → Periodic Snapshot Tasks
| データセット | 時刻 | 保持期間 |
|---|---|---|
| 元データのデータセット(HDD) | 毎日 07:00 | 30日 |
| DB のデータセット(SSD) | 毎日 07:00 | 14日 |
- 02:00 の DB バックアップファイルが 07:00 のスナップショットに一緒に入るよう、時刻を配置しました。
- 「Allow Taking Empty Snapshots」はオンにしました。毎日欠かさずスナップショットができていれば、一覧を見るだけでタスクが失敗したかどうかがわかります。

TrueNAS の設定ファイルのバックアップ
TrueNAS 25.10 では System → Advanced Settings → Manage Configuration → Download File にあります(以前のバージョンでは General Settings)。Export Password Secret Seed にチェックを入れておかないと、新しいブートディスクに復元したときに保存済みの認証情報を使えません。秘密情報が含まれるので、普段使いの PC や共有フォルダーには置きませんでした。
通知:Telegram
ファイアウォールでメール用のポートを追加で開けずに済むよう、HTTPS だけで動く Telegram を使いました。
- BotFather でボットを作成 → トークンを保管
- 自分のボットで「開始」(Start)
- 自分の Chat ID を確認
- System → Alert Settings → Alert Services → Add → Telegram、Level は Warning
サーバーダウンの検知:Healthchecks.io
TrueNAS の通知は、サーバーが止まると送れません。そこで外部の監視サービスに5分ごとに信号を送り、途絶えたらそちらから通知が来るようにしました。Period は5分、Grace は15分に設定しました。
System → Advanced Settings → Cron Jobs(5分ごと):
curl -fsS -m 10 -o /dev/null http://127.0.0.1:30041/api/server/ping && curl -fsS -m 10 --retry 3 -o /dev/null https://hc-ping.com/<UUID> || curl -fsS -m 10 --retry 3 -o /dev/null https://hc-ping.com/<UUID>/fail
- Immich が応答すれば正常の信号を、応答しなければ失敗の信号を送ります。
- サーバーが落ちたりインターネットが切れたりすると信号そのものが途絶え、20分後に通知が届きます。
- Immich アプリを自分で停止してから再起動し、ダウンの通知と復旧の通知が両方届くことを確認しました。

定期的なディスクチェック
再起動すると sda、sdb のようなデバイス名が変わります(実際に変わりました)。そこでシリアル番号を含む固定のパスを使いました。
lsblk -d -o NAME,SIZE,MODEL,SERIAL
ls -l /dev/disk/by-id/ | grep -v part
チェック用スクリプト(/mnt/<SSD プール>/scripts/disk-health.sh):
#!/bin/bash
# ディスク状態のチェック:基準値より増えた場合やテスト失敗時に Healthchecks.io へ fail を送信
URL="https://hc-ping.com/<UUID>"
DISKS="ata-<HDDモデル>_<シリアル番号> ata-<SSDモデル>_<シリアル番号> ata-<SSDモデル>_<シリアル番号>"
declare -A BASE
BASE[ata-<SSDモデル>_<シリアル番号>_199]=394 # 過去の累積値を基準値にしている項目
MSG=""
for d in $DISKS; do
DEV="/dev/disk/by-id/$d"
if [ ! -e "$DEV" ]; then MSG="$MSG\n$d: ディスクが認識されない"; continue; fi
H=$(smartctl -H "$DEV" | grep -iE "result|status")
echo "$H" | grep -qiE "PASSED|OK" || MSG="$MSG\n$d: 全体の状態に異常 ($H)"
for id in 5 197 198 199; do
v=$(smartctl -A "$DEV" | awk -v id="$id" '$1==id {print $10}')
b=${BASE[${d}_${id}]:-0}
if [ -n "$v" ] && [ "$v" -gt "$b" ] 2>/dev/null; then MSG="$MSG\n$d: ID $id の値 $v (基準 $b)"; fi
done
st=$(smartctl -l selftest "$DEV" | grep -m1 "^# 1")
if [ -n "$st" ] && ! echo "$st" | grep -qiE "Completed without error|in progress|Interrupted|Aborted by host"; then
MSG="$MSG\n$d: 直近のセルフテストに異常 ($st)"
fi
done
if [ -z "$MSG" ]; then
curl -fsS -m 10 --retry 3 -o /dev/null "$URL"
else
printf "%b" "$MSG" | curl -fsS -m 10 --retry 3 -o /dev/null --data-binary @- "$URL/fail"
fi
sudo chmod 700 /mnt/<SSD プール>/scripts/disk-health.sh # UUID を含むので root だけが読めるように
Cron の登録(Run As User: root):
| ジョブ | スケジュール | コマンド |
|---|---|---|
| 状態チェック+通知 | 毎日 08:00 | bash /mnt/<SSD プール>/scripts/disk-health.sh |
| ショートテスト | 毎週水曜 04:30 | for d in <by-id 3台分>; do smartctl -t short /dev/disk/by-id/$d; done |
| ロングテスト | 毎月15日 09:00 | for d in <by-id 3台分>; do smartctl -t long /dev/disk/by-id/$d; done |
テストのコマンドはテストを開始するだけで、結果は翌日のチェック用スクリプトが確認します。

ステップ8:復元テスト
バックアップがあることと、復元できることは別の話です。元データには手を付けずに、次の3つを確認しました。
A. スナップショットの中のファイルが元データと同じか
sudo ls /mnt/<元データのデータセット>/.zfs/snapshot/
sudo sha256sum /mnt/<元データのデータセット>/.zfs/snapshot/<スナップショット名>/library/<ユーザー>/<年>/<年-月>/<ファイル> \
/mnt/<元データのデータセット>/library/<ユーザー>/<年>/<年-月>/<ファイル>
2つのハッシュ値は一致しました。

B. DB のバックアップファイルが壊れていないか
sudo gzip -t /mnt/<元データのデータセット>/backups/<バックアップファイル>.sql.gz && echo "圧縮ファイルは正常"
sudo zcat /mnt/<元データのデータセット>/backups/<バックアップファイル>.sql.gz | head -n 20
PostgreSQL database dump という文字列を確認しました。
C. スナップショットのクローンで Immich をもう1つ起動する
- 元データと DB のデータセットのスナップショットを、それぞれ Clone To New Dataset でクローン
- Install Another Instance でテスト用の Immich をインストール:ポート 30042、機械学習はオフ、ストレージはクローン、Automatic Permissions はオフ
- ブラウザで接続し、一般ユーザーのアカウントでログイン → 写真が正しく表示されることを確認
- テスト用アプリとクローンを削除(クローンが残っていると、元のスナップショットが自動削除されない)

カタログには最新バージョンしかなかったので、このテストは復元と同時に新バージョンへの DB アップグレードのリハーサルも兼ねることになりました。その過程で2つの問題にぶつかったので、後でまとめています。リハーサルが成功したあと、手動でスナップショットを取ってから本番の Immich を更新しました。
つまずいたところと解決方法
1. Ventoy で作った USB から起動できない
Unable to find a medium containing a live file system というエラーで、インストール画面の前で止まりました。Debian Live 系の ISO を Ventoy の通常モードで起動したときに出る症状です。Rufus の DD イメージモードで USB を作り直して解決しました。DD モードで作った USB を挿すと、Windows のエクスプローラーで「フォーマットしてください」という画面が出ますが、ここでフォーマットを押してはいけません。
2. smartctl と grep の入力ミス
smartctl -t longのハイフンが別の文字に変わっていて、You have provided 2 device namesというエラーになりました。grep "self-test execution"は、出力の大文字のSelf-testと一致せず、何も表示されませんでした。grep -iで解決しました。- Web の Shell に長いコマンドを貼り付けると、末尾の文字が切れたり、制御文字(
^[[200~)が混ざったりすることがあります。貼り付けたら最後の行を確認する習慣が必要です。
3. Immich に接続できない → https で入力していた
Immich アプリのポートは http でしか応答しません。ブラウザに https で入力していたので接続できませんでした。Tailscale の区間は暗号化されているので、http で問題ありません。
4. Tailscale がリレー(relay)経由でしかつながらない
内部ネットワークの Wi-Fi にいるスマホから大量アップロードしようとしたところ、tailscale status に relay "tok" と表示されました。海外のリレーサーバーを経由する接続なので、大量アップロードには遅すぎます。
# Tailscale アプリのコンテナのシェルで
tailscale status
tailscale ping <スマホの Tailscale IP>
ファイアウォールのログを見ると、サーバーとスマホが UDP で直接つながろうとして、DMZ↔LAN の遮断ルールに止められていました。アップロード期間中だけ、スマホがつながっているルーター1台 ↔ サーバー、UDP の全ポートを許可する一時ルール2つを遮断ルールの上に追加したところ、direct に変わりました。その後の受信速度は約 340Mbps でした。大量アップロードが終わったあと、このルールは無効にしました。
自宅やモバイル回線ではリレー経由になりますが、写真1枚あたり数秒程度なので、日々のバックアップには問題ありませんでした。
5. 元データが UUID のフォルダーと Other フォルダーに保存された
アップロードを始めてから確認すると、パスが library/<ユーザー UUID>/2025/Other/07/ファイル.jpg になっていました。
- ユーザーのストレージラベルが空だったため、UUID がフォルダー名になっていました。
- テンプレートがアルバム基準のプリセットだったので、アルバムに入っていない写真は
Otherに入りました。この方式だと、写真をアルバムに入れたり外したりするとファイルの場所が変わることがあります。
ラベルを設定してテンプレートを日付基準に変えたあと、管理画面のジョブのページで Storage Template Migration を実行し、既存のファイルを移動しました。同じデータセットの中での移動なので、すぐに終わりました。
sudo ls /mnt/<元データのデータセット>/library
sudo find /mnt/<元データのデータセット>/library/<ラベル> -type f | head -n 5
6. 停電のあと、サーバーが起動しなかった
停電のあと、リモートで接続できなくなりました。同じ UPS につないだほかのサーバーは動いていました。
sudo journalctl -b -1 -n 50 --no-pager # 前回起動時の最後のログ
last -x | head -n 10 # 起動・終了の記録(crash の表示を確認)
sudo zpool status -x # all pools are healthy であることを確認
最後のログは、エラーもなく通常の記録の途中で途切れていて、終了の記録は crash でした。サーバーのプラグが UPS のバッテリーバックアップ付きコンセントに挿さっておらず、電源がすぐに落ちたのが原因でした。BIOS にも停電後に自動で電源を入れる設定がありませんでした。
- バッテリーバックアップ付きのコンセントに挿し替え、BIOS で AC Power Recovery = Power On に設定
- プールとディスクのチェック結果は異常なし(Power-Off_Retract_Count だけが増加)
- これをきっかけに、前述の Healthchecks.io によるダウン検知を追加
ちなみに、サーバーが落ちていたのに、ファイアウォールではポートが 1000Mbps 接続 と表示されていました。マザーボードの待機電源で LAN カードがリンクを保っていたためです。ポートの状態だけを見て、サーバーが動いていると判断してはいけません。
UPS の USB 連携(停電時の自動シャットダウン)は設定しませんでした。ほかのサーバーと共用の UPS なので、設定ミスで「UPS 出力停止」のコマンドが送られると、ほかの機器まで落ちるおそれがあるからです。
7. チェック用スクリプトがすぐに失敗通知を送った
OS 用 SSD の UDMA_CRC_Error_Count が 394 でした。ディスクとマザーボード間の転送エラーの累積値で、記録メディアの損傷とは別のもので、減ることはありません。現在の値を基準値として保存し、増えたときだけ通知するようにスクリプトを直しました(前述のスクリプトの BASE 配列)。値が増えたら SATA ケーブルを点検します。
8. テスト用 Immich のインストール失敗:Redis password cannot contain [#]
パスワードに # が入っていたため、インストール用テンプレートを作る段階で拒否されました。アプリのパスワードは英数字だけにするのが安全です。
9. テスト用 Immich が起動しない:password authentication failed for user "immich"
クローンした DB には、本番 Immich の DB パスワードが保存されています。テスト用アプリには別の値を入れていたので、接続に失敗しました。本番アプリの Edit 画面でパスワードを確認し(保存せずにキャンセル)、同じ値を入れて解決しました。実際の災害復旧でも、DB のパスワードがなければ DB を戻せないということなので、パスワードマネージャーに正確な値があるか改めて確認しました。

10. テスト用 Immich が起動しない:EACCES: permission denied … /data/encoded-video/.immich
DB のマイグレーションまでは成功したのに、フォルダーの確認の段階で止まりました。

sudo zfs get acltype,aclmode <元データのデータセット> <クローンのデータセット>
| データセット | acltype | aclmode | SOURCE |
|---|---|---|---|
| 元(Apps プリセット) | nfsv4 | passthrough | local |
| クローン | posix | discard | inherited |
ZFS のクローンは、この属性を元のデータセットではなく親のデータセットから継承します。そのため、ファイルに付いている NFSv4 の権限が無視されていました。クローンにだけ元と同じ値を設定して解決しました。
sudo zfs set acltype=nfsv4 aclmode=passthrough <クローンのデータセット>
スナップショットのロールバック(元のデータセット自体を戻す方法)では属性がそのまま残るので、この問題は起きません。

11. 手動スナップショットにデータが入らないところだった
手動でスナップショットを作るとき、Dataset の既定値が最上位のプールになっていました。Recursive をオフにしたまま最上位を選ぶと、子データセットにある実際のデータはスナップショットに含まれません。実際にデータがあるデータセットを直接選ぶ必要があります。

12. Telegram の偽 BotFather
検索結果の一番上が、認証バッジのない状態で「購読」や別のボットへの移動を促す、なりすましアカウントでした。https://t.me/BotFather のリンクから直接開き、青い認証バッジを確認する必要があります。Healthchecks.io の Telegram 連携も、Telegram 内で検索せず、サービスの画面にあるリンクから開きました。
アップデート手順
復元テストで整理した順番で Immich をアップデートしました。
- 元データと DB のデータセットの手動スナップショット(最上位ではなく実際のデータセットを選択)
- リリースノートを確認し、できればクローンでリハーサル
- Apps → 対象のアプリ → Update(Update All は使わない)
- 確認
curl -sS -m 5 http://127.0.0.1:30041/api/server/ping # {"res":"pong"}
- スマホの Immich アプリも最新版に更新
- 問題が起きたら:Roll Back → アプリを停止 → スナップショットの Rollback → アプリを起動
アップデート中に、ダウン検知の通知が一度届くことがあります。

まとめと注意点
最終的にできたこと
- スマホ2台 → サーバーへの自動の一方向バックアップ(Wi-Fi + 充電中、その日の写真も自動アップロード)
- インターネットに開いているインバウンドポートはなし、リモート接続は Tailscale のみ
- 写真サーバーはファイアウォールの DMZ ポートに分離し、内部ネットワークへのアクセス遮断を確認
- 毎日の ZFS スナップショットと Immich の DB バックアップ
- ディスクの定期チェックと異常時の通知、サーバーダウンの通知
- 復元テストとアップデートのリハーサルが完了
注意点
- スナップショットはディスクの故障には備えられません。HDD 1台の構成なので、2つ目のコピー(別のディスクや別の場所)を確保するまで、スマホの元データは消しません。この記事の時点では、2つ目のコピーはまだ残っている課題です。
- TrueNAS と Immich は、バージョンごとにメニューの場所やアプリのインストール画面がよく変わります。古いガイドより、インストール画面と最新のドキュメントを基準に判断するほうが確実です。
- ディスクは
/dev/sdXではなく/dev/disk/by-id/のパスで指定します。 - サーバーの電源は UPS のバッテリーバックアップ付きコンセントにつなぎ、BIOS で停電後の自動起動を設定します。
- 復元に必要な情報(DB のパスワード、設定ファイル、アプリのバージョン)は、サーバーの外に別途保管します。
- Android スマホで VPN 方式の広告ブロックアプリを使うと、Tailscale が切れます。VPN は同時に1つしか使えません。
- ルーターの内側にある PC は、ファイアウォールからはルーターの1つのアドレスとして見えます。特定の PC だけを許可したい場合は、その PC がファイアウォールから直接見えるアドレスを持っている必要があります。