写真サーバーの容量が足りない問題に、遠回りして決着をつけた
自前の写真サーバーは写真が増えるほど容量が足りなくなる。古い写真を安いクラウドストレージへ追い出す仕掛けを一晩かけて作り、10枚で実証まで通したあとに、VPS業者が月数ドルでSSDを増設できることに気づいて全部捨てた記録。測った数字と踏んだ罠を書いた。
- 公開日:
- 2026-10-05
写真を自分のサーバーに置くようにしてから、ずっと気になっていたことがある。写真は毎年増えるのに、サーバーのディスクは契約した大きさのままだということ。
結論を先に書くと、自分は古い写真を安いオブジェクトストレージへ追い出す仕掛けを一晩かけて作り、動くところまで確認してから、全部捨てた。業者が月$3.60でディスクを足せると分かったからだ。その遠回りの中身を書く。
前提
- 写真はGoogle PhotosからImmichに移した状態(Immich = 自前サーバーで動かす写真管理ソフト)
- サーバーは自宅PCからVPSへ移した直後(VPS = 仮想専用サーバー。業者のデータセンターにある自分専用のLinuxマシンを借りる形)。Contaboの6vCPU / 12GB RAM / SSD 200GB のプランで月$9.00(2026年9月時点)
- バックアップ先はBackblaze B2(安価なオブジェクトストレージ。2026年9月時点で1TBあたり月$6.95)。構成はバックアップを「消されても戻せる」形にする
- 土台の考え方は自分の「サーバー」を持つとはどういうことか
まず現実を測った
移行直後のディスクはこうだった。
df -h /
# 使用 164GB / 193GB、空き 29GB
しかもこの時点でサムネイル(一覧表示用の縮小画像)がまだ生成されていなかった。以前のサーバーでは9.6GBあったので、それが戻ると空きは約19GB。200GBのプランを選んだ数日後にもう先が見えた。
次に増える速さを測った。Immichのデータベースに撮影年が入っているので、年ごとに集計できる。
select extract(year from a."fileCreatedAt"), count(*), pg_size_pretty(sum(e."fileSizeInByte"))
from asset a join asset_exif e on e."assetId" = a.id
where a."deletedAt" is null group by 1 order by 1;
実測(写真の実体は合計115GB)。
- 直近3年分の合計で約22GB。年あたり3〜10GBのペース
- それより古い分が約19,000件・約93GB
つまり今の撮り方で年10GB弱。29GBの空きは3年もたない。いずれRAW(カメラが記録した無加工のデータ。1枚25〜50MB)で撮り始めたら桁が変わる。「毎年ちょっとずつ月額が増えていく」のが一番嫌だったので、容量と月額を切り離す方法を探し始めた。
最初の案: 古い写真を安いストレージへ追い出す
サーバーのSSDには直近1〜2年の「よく見る」写真だけ置き、古い写真はB2に預けて見たいときだけ取り出す。 SSDの使用量が年によらず一定になるので月額が増えない。B2には毎晩のバックアップで全写真の実体がすでにあるから、アップロードすらいらないのが魅力だった。
方式をいくつか捨てた
- 🔴 Immichの「External Library」(外部ライブラリ)機能は使えない。サーバーの外のフォルダを読み込む機能だが、すでにImmichの中にある写真を引き取れず、新規の写真として再登録される。アルバム・顔認識の結果・お気に入りが失われる恐れがある。
- 🔴 「年ごとのフォルダを切り出す」はそもそも不可能。ディスク上の写真は
library/upload/<ユーザーID>/<16進2桁>/<16進2桁>/<ランダムな名前>.拡張子という形で、ランダムな名前のハッシュで256×256のフォルダに均等にばらまかれている。年ごとのフォルダは存在しない。
残ったのは、写真ファイル1個ずつをシンボリックリンク(ファイルの位置を指すだけのショートカット)に置き換え、リンク先をB2をフォルダとして見せた場所に向ける方式。データベースに一切触らないので、Immichから見れば何も変わらない。B2をフォルダとして見せる部分はrclone(いろいろなクラウドストレージへファイルを同期するコマンド)のマウント機能で、読み取り専用で常駐させた。
# /etc/systemd/system/rclone-immich-cold.service の要点
ExecStart=/usr/local/bin/rclone mount <リモート名>:<バケット>/immich-cold \
/home/<user>/immich/library/cold \
--read-only --allow-other --uid 1001 --gid 1001 \
--vfs-cache-mode full --vfs-cache-max-size 3G --dir-cache-time 24h
ここで踏んだもの。
rcloneの場所は/usr/local/bin/rcloneで、/usr/bin/rcloneではなかった。systemd(Linuxの常駐サービス管理)はそれだけの理由で203/EXECと言って起動に失敗する。which rcloneで確かめる。--allow-otherには/etc/fuse.confへのuser_allow_otherの追記も必要。--dir-cache-timeは必ず延ばす。既定の5分だと期限切れごとにフォルダ一覧の問い合わせが全件走る。24時間にした。- 🔴 Immichのコンテナ設定で写真フォルダのマウント指定に
:rslaveを付けないと見えない。docker-compose.ymlの${UPLOAD_LOCATION}:/dataを${UPLOAD_LOCATION}:/data:rslaveにする。コンテナ(ソフトを隔離した箱に入れて動かす仕組み)へのフォルダ受け渡しは既定で「あとから生えたマウントを伝えない」ので、ホスト側で後から作ったマウントはコンテナの中からは空に見える。ここで30分溶かした。なおdocker compose up -d immich_serverはno such serviceになる——設定上のサービス名はハイフンのimmich-server、コンテナ名がアンダースコアのimmich_server。
一番危なかったのはバックアップスクリプトだった
実験で確証を取った、本当の地雷。毎晩のバックアップはrclone sync(送り元に無いものは送り先からも消す片方向ミラー)で動いている。ローカルの写真をシンボリックリンクに置き換えると、rcloneはCan't follow symlink without -L/--copy-linksと言ってそれをスキップし、「送り元に無い」と判断してB2側の実体を削除する。
- 🔴 行き先が存在しない壊れたリンクでも同じ挙動で、しかも終了コードは0(=成功として記録される)。失敗として検知できない。つまり最も危険な順序は「B2へコピーする前にシンボリックリンクを作ってしまう」ことで、やると、その夜のバックアップがB2にある唯一の実体を無言で消す。
--copy-linksを付ける解決策は不可。B2に二重保存になって容量が倍になる。
正解は--exclude "cold/**"で追い出し先を同期対象から外し、かつ順序を守ること。
# 1. B2の中で複製(同一バケット内のサーバーサイドコピー=転送ゼロ・一瞬)
rclone copyto REMOTE/immich/library/<rel> REMOTE/immich-cold/<rel>
# 2. 宛先のサイズを照合
rclone lsl REMOTE/immich-cold/<rel>
# 3. ローカルの実体は「削除」ではなく退避(ロールバックできるように)
mv library/upload/<rel> /root/poc-staging/<rel>
# 4. 相対パスのリンクを張る(library まで ちょうど4階層)
ln -s "../../../../cold/<rel>" library/upload/<rel>
chown -h <user>:<user> library/upload/<rel>
# 5. リンク越しに md5 を照合。違ったら即ロールバック
# 6. 全件の検証が終わってから、はじめて旧パスを削除
rclone deletefile REMOTE/immich/library/<rel>
- 存在確認に
rclone lsjsonを使ってはいけない。存在しないファイルを渡すとフォルダとして扱われ、空の一覧と終了コード0を返す。これで一度「B2にある」と誤判断した。rclone lslの出力が空でないことを見る。
10枚で通して、確認したこと
PoC(Proof of Concept = 動くことを示すための小さな実装)として写真10枚・46MBで最後まで通した。ホストからもコンテナの中からも同じmd5で読め、ImmichのAPI経由でオリジナル画像が10/10でHTTP 200かつmd5一致、サムネイルは10/10がローカルから配信(一覧の表示速度は一切変わらない)、エラーログなし。B2からの初回読み込みは0.39秒(636KB)、キャッシュ後は0.017秒。
そして最も確認したかったこと: マウントが切れたらどうなるか。systemctl stopで接続を落として測った。
- オリジナル画像は404、サムネイルは200のまま
- 写真のレコード件数は前・中・後で29,716件のまま不変、該当写真の削除日時もnullのまま
- マウントを戻すと即座に200に復帰
つまりImmichは、ファイルが読めなくなっても勝手にレコードを消したりゴミ箱に落としたりしない。これが成立しなければ全部やめるつもりだった。
安全網が既にあったことも実測した。B2のバケットは「隠してから30日で削除」の設定にしてあり、rclone deletefileは既定でハード削除ではなく論理削除。だから消したファイルは30日間復元できる。15分前に消した2.1MBのHEICをrclone lsf --b2-versionsで版名(<名前>-v<日時>.<拡張子>)を調べて取り出し、md5一致まで確認した(--b2-versionsを付けても元のファイル名を渡すと失敗するので、必ず版名で指定する)。これで「スクリプトのミスで写真が消える」は永久喪失ではなく30日以内なら戻せるミスに格下げされた。残る課題は復元手段ではなく30日以内に気づくことになる。
途中で前提そのものを疑った
10枚が通った時点でB2からの読み出し速度を測り直して、気が変わった。197MBのデータベースダンプが6.58秒=約30MB/s、17MBの写真が単一ストリームで0.52秒=約33MB/s(265Mbps)。一方で手元の動画は535本・平均11.6MB、ビットレートは平均7.3Mbps・上位5%で17.9Mbps・最大67.5Mbps。最も重い動画でも4倍近い余裕がある。
それなら「古い写真を追い出す」のではなく、写真の置き場そのものをB2にすればいいのではないか。SSDの天井が消える。コストも、写真をB2に置けば月$10前後、1TBまで増えても月$16で、大きいディスクのプランへ引っ越す月$19より安い。安いほうが天井も無いという綺麗な結論になりかけた。
そこで手を止めて2つ調べた。
自分以外に誰がやっているのか
- 主流は「オリジナルはローカル、クラウドはバックアップ専用」。Immich公式のバックアップ手順もローカル本番を前提に書かれている。クラウドを本番の置き場にしている例は少数派・実験的で、数少ない実践者は「わずかなレイテンシのゆらぎでマウントが壊れる」「ローカルキャッシュがすぐ溢れる」と報告している。
- 「クラウドが唯一のコピー」でやっている人は、調べた範囲で1人も見つからなかった。 定番は3コピー(本番+ローカルの2本目+クラウド)。
- 🔴 Immichに未解決の不具合がある: issue #10480 — サムネイル生成が同じファイルを2回読むため、リモートストレージではダウンロード量が実データの8〜10倍に膨らむ。16,000ファイル・110GBで900GBという報告がある。B2の無料ダウンロード枠は保存量の3倍/月なので、サムネイルの全件再生成を一度回すと枠を超えて課金が出る。
- Immich本体のS3対応(オブジェクトストレージへの公式対応)は期待できない。2026年時点で未実装、ロードマップにも無い。**「待つ」は非現実的で、マウント方式が唯一の道(公式サポート外)**という整理になった。
- 追い風もあった。B2のAPI呼び出し課金が2026-05-01に全廃された(代わりに保存料が$6→$6.95/TB/月に値上げ)ので、「マウントが大量の一覧取得を発行して請求が爆発する」という有名な事故モード(過去に約$598の実例がある)は構造的に消えていた。
暗号化のコストを測った
クラウドに本番を置くなら預け先に中身を読まれない形にしたい。界隈の多数派も「上げる前に自分で暗号化する」だった。そして改めて気づいたのだが、自分の既存のバックアップは暗号化されていなかった。毎晩のバックアップを始めた時点で、預け先は写真59,000枚を読める状態だった。
17MBの写真で測った結果は、暗号化あり0.63〜0.95秒 / 平文0.46〜0.53秒。差は0.2〜0.4秒でボトルネックは回線のまま。暗号化は速度面の障害にはならない。
- ⚠️ 最初の計測では「暗号化7.01秒 vs 平文0.46秒=15倍」と誤った結論を出した。原因は、比較した2つのマウントで
--vfs-read-chunk-sizeなどの設定が揃っていなかったこと。ベンチマークは条件を揃える。 危うく「暗号化は無理」と誤った判断を下すところだった。 - 代償は速度ではなく別の2つ。①預け先のファイル名が不可読な文字列になり管理画面から中身を確認できない ②既存の115GBを暗号化し直すには全部アップロードし直す必要があり、「移行作業ゼロ」という最大の利点が消える。
本命の答えは、ボタンひとつだった
ここまで自分は「B2へ追い出す」「オブジェクトストレージを本番にする」「大きいプランへ引っ越す」の3つを丁寧に比較していた。そして契約しているVPSの管理画面を開いて、「ディスクだけ足す」という選択肢を一度も検討していなかったことに気づいた。
Contaboの管理画面には**「Extend SSD Storage」ボタン**がある。公式ヘルプにも、既存のVPSにSSDを足せる・再インストール不要・移行不要と書いてある。200GBを400GBにした。
月額の増分で並べるとこうなる(2026年9月時点)。
| 選択肢 | 月額の増分 | 必要な作業 |
|---|---|---|
| SSDを200GB増設 | +$3.60 | なし |
| B2へ追い出す(200GB相当) | +$0.87 | マウント常駐+リンク張り+見張り+孤児掃除 |
| 同社のオブジェクトストレージ(最小250GB) | +$2.99 | 同じくマウントが必要 |
| 大きいプランへ引っ越す | +$8〜10 | データ移行、数分の停止 |
差額が月$2〜3しかないなら、その$2で複雑さを全部買い取れる。 一晩かけて作った仕掛けは、この表の2行目のためのものだった。
増設の実作業
公式ヘルプの「activated right away(即座に有効、再起動不要)」は実態と違った。⚠️ 増設は業者側の強制リセットを伴った——正常なシャットダウンの記録がなく、前のログが突然途切れている。増設するときはダウンタイムを見込むこと。 結果的に、予期しない電源断からの自動復旧が完全に機能することが実証できた(コンテナ14個・常駐サービス7件・公開している9サービスすべて復帰、写真のデータ欠損ゼロ)。棚からぼた餅の検証になった。
増えた分は既存ディスクの拡大として現れた(別のディスクとしてではない)。ただしパーティションとファイルシステムは自動では広がらないので、ここだけ手作業。
# 作業前に、必ず手動でバックアップを一度完走させる
/root/backup-scripts/backup-to-b2.sh
growpart --dry-run /dev/sda 1 # まず空振りで確認
growpart /dev/sda 1 # パーティションを広げる
resize2fs /dev/sda1 # ファイルシステムを広げる
- ルートのパーティションテーブルを書き換える操作なので、直前のバックアップは作法として必須。
- 対象が最後のパーティションで、直後に連続した空き領域がある理想的なケースだった。ext4に
resize_inodeがあるのでマウントしたまま、無停止でオンライン拡張できた。 - 結果: パーティション199GB→399GB、ファイルシステム193GB→387GB、使用率93%→46%、空き16GB→209GB。RAWを30MB換算で約7,000枚の余裕。
- 料金の実績は月$9.00 → $12.60。100GBあたり月$1.80(=$18/TB/月)。
仕掛けは完全に撤去した
増設で解決したので、一晩かけて作ったものは全部元に戻した。「使っていないだけ」で残すのが一番たちが悪い——あとでバックアップスクリプトを触ったときに、忘れた除外設定が事故になる。退避していた10枚の実体を元の場所に戻してB2へ再アップロードし、追い出し先のプレフィックスを削除。常駐マウントのサービスを停止・無効化・削除し、マウント用フォルダと/etc/fuse.confの追記も撤去。バックアップスクリプトの--exclude "cold/**"とコンテナ設定の:rslaveは、事前に取った.bakとの差分で元と一致することを確認してから.bakも消した。撤去後の検証は10枚のmd5一致・シンボリックリンク0件・API 10/10・B2側も10/10復元・レコード件数29,716件で不変。
残したのはメモだけ。実際に効いた相対パスとmd5の一覧を/root/notes/に置いてある。将来TB級になってオブジェクトストレージが本当に必要になったとき、この一晩をゼロからやり直さずに済むように。
単価で見ると、いつか判断が逆転する
増設が常に正解ではない。1TBあたりの単価では増設が一番高い(VPSのSSD増設 $18/TB > 同社オブジェクトストレージ $11.96/TB > B2 $6.95/TB)。200GBでは差額が月$2程度にしかならないから複雑さを避けるのが正解だった、という話にすぎない。分岐点は2つ決めてある。追い出したい古い写真が1TBを超えたらB2方式を再検討する(差額が年$132になり、ようやくマウント方式の複雑さを引き受ける価値が出る)。1TB規模ならプランごと乗り換えたほうが安い(現プラン+800GB増設の月$23.40に対し、1TBのSSDが付いたプランは月$15前後。ただし数分の停止を伴うし、SSDからNVMeへの変更は再インストール必須なので絶対に選ばない)。
残っている課題も書いておく。写真の2本目のコピーは引き続きバックアップ先の1本だけで、以前使っていた自宅のPCに全量が残っているうちは3コピーだが、あれを片付けるときに決め直す必要がある。バックアップの暗号化も未着手——速度は問題にならないと測れたが、115GBを全部アップロードし直すコストが残っている。
この遠回りから取ったもの
一晩かけて作って、10枚で実証して、全部捨てた。月$3.60のボタンを先に押していれば丸ごと要らなかった作業だ。素直に書くと、「どう作るか」を考えるのが面白くて、「買えば済むか」を確かめる手順を飛ばしていた。
構成を変える検討を始めるときは、まず今契約しているサービスの管理画面に、その問題をそのまま解決するボタンが無いかを確認する。 自分は3つの案を丁寧に比較表にしておいて、「ディスクだけ足す」をその表に入れていなかった。選択肢の作り方を間違えると、どれだけ丁寧に比べても正解には当たらない。
捨てた作業から残ったものもある。マウントが切れてもImmichのレコードは壊れないこと、削除が30日間は取り返せること、rclone syncがリンクを見て送り先を消すこと、暗号化が速度の障害にならないこと、サムネイルの全件再生成が課金を踏むこと。これはどの方式を選んでも効く知識で、1TBを超えた日に取り出せる形でメモに残した。遠回りの元は取れていないが、ゼロでもない。
更新履歴 — 2026-10-05: 初版