<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>VPS on Off-Grid</title><link>https://offgrid.taikiito.com/tags/vps/</link><description>Recent content in VPS on Off-Grid</description><generator>Hugo</generator><language>ja-JP</language><lastBuildDate>Mon, 05 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://offgrid.taikiito.com/tags/vps/index.xml" rel="self" type="application/rss+xml"/><item><title>自宅サーバーが5.5日止まったので海外VPSへ移った</title><link>https://offgrid.taikiito.com/guides/home-server-to-vps/</link><pubDate>Mon, 05 Oct 2026 00:00:00 +0000</pubDate><guid>https://offgrid.taikiito.com/guides/home-server-to-vps/</guid><description>自宅のWindowsノートPCが原因不明のまま5.5日間フリーズし、写真・パスワード・ファイルのサーバーが全部止まった。壊れた機械から転送するのではなく、毎晩取っていたクラウドバックアップからVPS側へ引っ張って復元する方式で、26分の停止で移行した記録。公開IPになって初めて露出した穴とその塞ぎ方も書いた。</description><content:encoded><![CDATA[<p>写真・パスワード・ファイルの置き場を全部自宅のPC1台に集めていた。そのPCが<strong>5.5日間フリーズしたまま</strong>動かなくなり、全部止まった。家を離れていた期間だったので、帰るまで何もできなかった。原因は結局特定できていない。</p>
<p>その障害の記録と、そこから海外VPS(レンタルサーバー)へ移った記録。<strong>壊れた機械からデータを転送するのではなく、毎晩取っていたクラウドバックアップからVPS側へ引っ張って復元した</strong>のが一番再利用できる部分だと思う。実際の停止時間は26分だった。そして移行後、<strong>自宅のルーターの内側にいたから気づかずに済んでいた穴</strong>が一気に表に出た。</p>
<h2 id="前提">前提</h2>
<ul>
<li>自宅のWindows機〈ノート型〉(WSL〈Windowsの中でLinuxを動かす仕組み〉上でサーバーを動かしていた)で、Immich(写真)・Nextcloud(ファイル)・Vaultwarden(パスワード)・Authentik(ログインの入口)など<strong>常時14コンテナ</strong>を動かしている状態</li>
<li>外部公開はCloudflare Tunnel(ルーターに穴を開けずにサーバーを公開する仕組み)経由で、ルーターのポート開放は一切していない</li>
<li>毎晩<code>rclone</code>(いろいろなクラウドストレージへファイルを同期するコマンド)でBackblaze B2(安価なオブジェクトストレージ)へ日次バックアップを取っている</li>
<li>構築の土台は<a href="/guides/self-hosting-basics/">自分の「サーバー」を持つとはどういうことか</a>、バックアップの設計は<a href="/guides/backup-ransomware-resistant/">バックアップを「消されても戻せる」形にする</a></li>
</ul>
<h2 id="何が死んだのか">何が死んだのか</h2>
<p>最初の症状は、自分のドメインを開くと<strong>Cloudflare Error 1033</strong>(トンネルの出口であるサーバー側にCloudflareが接続できない時のエラー)が出たこと。Cloudflareの障害情報ページには何も出ていない。別経路のリモートデスクトップで見ると「最後のオンライン 12:07」でオフライン表示。<strong>トンネルが切れたのではなく、サーバーの入っているPC自体が落ちている。</strong></p>
<p>遠隔からできることは何もなかった。SSHも入れない。Cloudflare Tunnelは「サーバーが生きていれば外から入れる」仕組みなので、サーバーが死ぬと一緒に死ぬ。</p>
<p>帰ってから見たPCの状態がこれ。<strong>電源は入っていて画面も出ているのに、マウスカーソルが一切動かない。</strong> キーボードも効かない。ブルースクリーンも出ていない。電源ボタンの長押しで強制終了して再起動したら、何事もなかったように復旧した。</p>
<p>後日イベントログを調べて停止時間が確定した。</p>
<pre tabindex="0"><code>EventLog ID 6008
&#34;The previous system shutdown at 11:11:13 AM on 9/21/2026 was unexpected.&#34;
</code></pre><p>このログが記録されたのは<strong>9/26 23:45</strong>(自分が強制再起動した時刻)。つまり<strong>9/21 11:11:13にハングして、そこから5日半固まったまま</strong>だった。</p>
<ul>
<li>9/21〜9/26の間、Systemログに<strong>イベントが1件も記録されていない</strong>。完全な空白。瞬断の繰り返しではなく、OSが本当に何も処理できない「サイレントハング」だった裏付けになる。</li>
<li><strong>ブルースクリーンのダンプファイルが無い</strong>(残っていたのは1年半前の古いものだけ)。<strong>ディスプレイドライバのクラッシュログも0件</strong>。写真の顔認識にGPU(画像処理用の高性能な部品)を使っていたのでGPUドライバのハングが一番疑わしい候補だったが、<strong>ログが書かれる前に固まった可能性もあり確定できなかった。</strong></li>
</ul>
<p><strong>根本原因は今も分かっていない。</strong> BSODもエラーログも残さず固まる種類の故障は、原因究明が原理的に難しい。</p>
<p>🔴 <strong>ここで一度、原因を誤診した。</strong> 強制再起動後に別のソフトの認証セッションが切れていたので、最初はそれが5.5日の原因だと分析した。実際は「PCが固まっていた」が唯一の原因で、セッション切れは再起動後に浮上した<strong>二次的な問題</strong>だった。<strong>復旧直後に見つかった不具合は、障害の原因ではなく結果であることがある。</strong> 時系列のログで裏を取るまで原因を名指ししない。</p>
<h2 id="なぜvpsに移ることにしたか">なぜVPSに移ることにしたか</h2>
<p>最初に考えたのは、もっと安い対策ばかりだった。ウォッチドッグタイマー(固まったら自動でリセットする機能)、GPUドライバの更新、旅行時はSSHできる端末を必ず携帯する運用ルール。</p>
<p>🔴 <strong>「スマートプラグで遠隔から電源を入れ直す」案は、そもそも使えなかった。</strong> サーバーにしていたのはノート型のWindows機で、<strong>内蔵バッテリーがあるのでコンセントを切っても電源は落ちない</strong>。ノートPCを常時稼働のサーバーに流用するのは「静かで省電力」という利点があって自分もそれで選んだのだが、<strong>固まったときに遠隔で殴る手段を一つ失う</strong>という代償がついてくる。これは置き場所を決める段階で知っておきたかった。「WindowsをやめてこのPCを素のLinux専用機にする」案も検討したが<strong>否定的</strong>な結論になった。マウスも効かないOS全体の停止なのでLinux側だけの不調ではなく、<strong>原因がGPUドライバなら素のLinuxでも再発しうるので、OS再インストール＋全サービス移行という大工事に見合う保証がない</strong>。</p>
<p>それでも移行を決めた理由は、<strong>数日後に同じサイレントフリーズが再発した</strong>こと。原因が分からない以上「次はいつ、どれだけ止まるか」が見積もれない。<strong>データセンターの機械なら、少なくとも電源とネットワークと物理的な復旧は他人の仕事になる。</strong></p>
<h2 id="vpsの比較で実際に効いた軸">VPSの比較で実際に効いた軸</h2>
<p><strong>2026年9月時点</strong>の調査値。今は変わっているはず。</p>
<table>
	<thead>
			<tr>
					<th>候補</th>
					<th>スペックと価格の感覚</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Contabo</td>
					<td>6vCPU / 12GB RAM / 200GB SSD が月$9前後。容量あたりで圧倒的に安い</td>
			</tr>
			<tr>
					<td>Vultr</td>
					<td>2vCPU / 8GB で月$24〜48</td>
			</tr>
			<tr>
					<td>DigitalOcean</td>
					<td>2vCPU / 4GB で月$24</td>
			</tr>
			<tr>
					<td>Hetzner</td>
					<td>単価は安いが月間転送量の上限が低く、写真用途では超過料金のリスク</td>
			</tr>
			<tr>
					<td>Oracle Cloud Free Tier</td>
					<td>2 OCPU / 12GB ARM が無料枠</td>
			</tr>
			<tr>
					<td>国内VPS</td>
					<td>2コア / 2GB / SSD 200GB が月770円〜。ただし週1回程度の数秒〜数分の瞬断報告あり</td>
			</tr>
			<tr>
					<td>AWS Lightsail</td>
					<td>以前使っていたが、この用途では割高だったので自宅運用に移った経緯がある</td>
			</tr>
	</tbody>
</table>
<p>効いた軸は4つ。</p>
<p><strong>① ディスク容量が最優先。</strong> 写真のライブラリが実測115GB、ファイル同期が16.6GB。OS・Docker・データベースを含めて170〜180GBと見積もり、200GBのプランにした。「写真が増えるたびに月額が雪だるま式に増えるのが嫌だ」という条件があったので、<strong>同じ値段でどれだけディスクが付いてくるか</strong>が一番差の出る軸になった。</p>
<p><strong>② メモリは写真の量ではなくコンテナの数で決まる。</strong> ここは一度自分で間違えかけた。「古い写真をクラウドへ逃がすならメモリも小さくていいのでは」と考えたが違う。常時14コンテナが動いていて、以前メモリ不足で全サービスが繰り返し落ちた実績がある。しかも<strong>VPSにはGPUが無いので、顔認識や画像検索の処理をCPUとメモリでやることになり、必要メモリはむしろ増える</strong>。8GBではなく12GBから始め、安定を確認してから下のプランへ落とす順序にした。</p>
<p><strong>③ リージョン(どの国のデータセンターか)は在庫で決まった。</strong> 第一候補にしていたリージョンは<strong>月+$4.90の追加料金</strong>がつく上に、申し込み画面で<strong>在庫切れ</strong>だった。追加料金ゼロで常に在庫があるリージョンに変えたら、結果として<strong>バックアップ先のB2との経路が短くなり、復元も移行後の日次バックアップも速くなる</strong>という副産物があった。「近いほうが良いに決まっている」と思い込んでいた軸が、実際には料金・在庫・バックアップ先との距離で裏返った。</p>
<p><strong>④ 遅延は実測すると論点が変わる。</strong> 申し込み画面の「近いリージョン4ms、遠いリージョン158ms」は<strong>生のネットワーク遅延で、Cloudflare Tunnel経由の実構成には当てはまらない</strong>。自宅のPCで動かしていた時点ですでに写真サーバーの初期応答は73〜87msだった。DNSがCloudflareを指しているので、自宅のWiFiからでも一度エッジへ出て戻る経路になり、「目の前にあるから速い」状態には元々なっていなかった。移行後は<strong>0.55〜0.7秒</strong>(自宅PC時代は0.3秒前後)。比較のためにGoogle Photosのアプリ画面を測ると248〜255msで、<strong>「すでに受け入れている遅さ」と同程度</strong>だと分かったので許容できる論点に落ちた。</p>
<ul>
<li>🔴 サムネイルをCloudflare側にキャッシュさせれば速くなるが、<strong>それは自分の写真をCloudflareに置かせることになるのでやらない</strong>。速度のために主権を手放すなら移行の意味がない。</li>
</ul>
<p>契約は6vCPU / 12GB RAM / 200GB SSD / 転送量無制限、Ubuntu 24.04、<strong>1ヶ月契約で月$9.00、初期費用ゼロ</strong>。年契約の割引は見送った。原因不明の障害から逃げるための移行で、移行先が気に入らなかった時にすぐ降りられないのは本末転倒だから。</p>
<h2 id="移行方式-壊れた機械から運ぶのをやめる">移行方式: 壊れた機械から運ぶのをやめる</h2>
<p>ここが今回の中心。</p>
<p>普通に考えると、移行とは<strong>旧サーバー → 新サーバー</strong>へデータを直接転送することだ。ランブックの最初の案もそうなっていた。150GBを<code>rsync</code>(差分だけコピーするコマンド)で送る、数時間〜半日かかる、という計画。</p>
<p>これを見て思ったのが、**「毎晩クラウドにバックアップを取っているのだから、新サーバーがそこからダウンロードすればいいのでは?」**だった。採用した。</p>
<pre tabindex="0"><code>これまでの案:  自宅PC ──(細い家庭回線)──&gt; 新VPS
採用した案:    自宅PC ──&gt; B2(クラウド) ──(データセンター間)──&gt; 新VPS
</code></pre><ol>
<li><strong>自宅の細い上り回線に依存しない</strong>。VPS↔B2はデータセンター間なので桁違いに速く、実測で約30MB/sだった。</li>
<li><strong>旧サーバーが移行作業中ずっと安定している必要がない</strong>。これが決定的だった。5.5日固まる機械を、半日の転送が終わるまで無事でいてくれと祈りながら使うのは設計として間違っている。</li>
<li><strong>既にそこにあるので準備作業がゼロ</strong>。写真の本体もデータベースのダンプ(中身を1つのファイルに書き出したもの)も毎晩同期済み。</li>
<li><strong>ついでに災害復旧の予行演習になる</strong>。「クラウドのコピーしか残っていない状態から戻せるか」を本番で一度やることになるので、バックアップが本当に使えるのかが確定する。</li>
</ol>
<h3 id="鶏と卵の問題は先に解いてあった">鶏と卵の問題は先に解いてあった</h3>
<p>前提がある。<strong>B2から引っ張るにはB2へのアクセス鍵が必要</strong>で、その鍵が旧サーバーの中にしか無ければ、サーバーを失った時点で何も取り出せない。</p>
<p>ここは1ヶ月前に手を打ってあった。<strong>B2アクセス用の<code>rclone</code>設定、Cloudflare Tunnelの設定、<code>docker-compose.yml</code>一式、環境変数ファイルの内容を、Vaultwarden(自分のパスワード管理サーバー)にSecure Note 4件として複製保存していた。</strong> Vaultwardenのモバイルアプリはオフラインキャッシュを持つので、サーバーに一切アクセスできなくてもスマートフォンから取り出せる。後日<strong>このメモに書かれた値だけで</strong>B2上の全データが見えることを実測で確認した。🔴 <strong>この検証は鍵を入れ替えるたびに毎回やる。</strong> 「保存したつもり」で中身が古い、という事故が一番ありそうな場所だから。</p>
<h2 id="ランブックの形">ランブックの形</h2>
<p>手順はPhase A〜Hに分けて1つの文書にした。<strong>実行中にその場で考える余地を削るのが目的</strong>で、特にDNSの切り替えは後戻りの判断が数秒で要る。</p>
<pre tabindex="0"><code>Phase A  旧サーバーを止めて最終バックアップ
Phase B  Vaultwardenから鍵4件を取り出す
Phase C  新VPSの初期設定(ユーザー・SSH鍵・Docker)
Phase D  B2から rclone copy でデータを引っ張る
Phase E  データベースのダンプをリストア
Phase F  docker-compose を復元してコンテナ起動
Phase G  新しいトンネルを作ってDNSを1本ずつ切替
Phase H  移行後(バックアップの再構築・監視・検証)
</code></pre><p>書き上げた最初の版には穴が4つあり、実行前のレビューで直した。</p>
<ul>
<li><strong>稼働中のままコピーしたパスワードデータは壊れている可能性がある。</strong> Vaultwardenのデータベースファイルは書き込み中にコピーすると不整合になりうる。当日は<strong>コンテナを止めてから</strong>バックアップを手動実行する手順に変えた。これで「日次バックアップから最大24時間分が欠ける」問題も同時に消える。</li>
<li>🔴 <strong>イメージのバージョンを固定せずに復元すると壊れる。</strong> データベースのダンプは、取得した時点のソフトのバージョンの構造に紐づいている。<code>docker compose pull</code>で最新版を取ると構造が合わなくなる。</li>
<li><strong>環境変数ファイルの復元元を間違って書いていた。</strong> 以前使っていたパスワード管理サービスは解約済みで、正しくはVaultwardenだった。<strong>手順書は、書いた時点では正しくても構成変更で腐る。</strong></li>
<li>🔴 <strong>同じトンネルを2台のホストで同時に動かしてはいけない。</strong> リクエストが2台にランダムに振り分けられて、「たまに繋がらない」という一番デバッグしづらい障害になる。<strong>新しいトンネルを別に作って、DNSを1本ずつ向け替える</strong>形にした。</li>
</ul>
<p>他に決めておいたこと。</p>
<ul>
<li><strong><code>rclone copy</code>を使い、<code>sync</code>は使わない</strong>。<code>sync</code>は転送元に無いファイルを転送先から消す。過去にこれで必要なディレクトリを消して障害にした実績がある。</li>
<li><strong>切替順序</strong>: 一番軽いパスワードサーバー → <strong>ログインの入口(最重要。落ちると他のサービスにログインできなくなる)</strong> → ファイル → 写真 → 残り。<strong>旧サーバーへのSSH経路は一番最後。</strong> 先に向け替えると旧サーバーに入れなくなって確認作業ができない。</li>
<li><strong>DNSのTTL(切替が反映されるまでの待ち時間)を事前に短くする手順は不要だった。</strong> 対象レコードは全部Cloudflareのプロキシを通る設定で、外に返るアドレスは切替前後で変わらない。振り分けはCloudflare内部で起きるので<strong>書き換えの数秒後に反映され、戻すのも数秒</strong>。TTLを下げる必要があるのは、プロキシを通さずサーバーのアドレスを直接返している場合だけ。</li>
</ul>
<h2 id="実際の停止時間は26分">実際の停止時間は26分</h2>
<p><strong>15:19にサービスを止めて、15:45に全部復帰した。</strong> 写真150GBを運ぶ移行としては想定よりはるかに短い。クラウド経由にしたことで、一番重い写真の転送が「停止中にやる作業」から外れたのが効いている。</p>
<p>復元後の検証値は、写真<strong>59,104ファイル</strong>(<code>rclone check</code>でB2との差分ゼロ)、写真サーバーの登録件数<strong>29,720件</strong>、ファイル同期サーバー144テーブル・<strong>16.6GB</strong>、ログイン入口のユーザー3件。</p>
<h3 id="ランブックに書いていなかった落とし穴">ランブックに書いていなかった落とし穴</h3>
<ul>
<li>🔴 <strong>浮動タグが他にもあった。</strong> バージョン固定は写真サーバーだけ警告していたが、ファイル同期・データベース・パスワード管理も<code>:latest</code>(常に最新を取るタグ)のままだった。<strong>旧サーバーで実際に動いているイメージのダイジェスト(内容に対する一意の識別子)を全部調べて固定した</strong>。<code>docker inspect</code>の<code>.Image</code>はそのマシンの中だけのIDでダウンロードには使えないので<code>RepoDigests</code>を使う。</li>
<li>🔴 <strong>写真サーバーが起動ループした。</strong> <code>Failed to read /data/encoded-video/.immich</code>。サムネイルと変換済み動画のフォルダをバックアップ対象から外していたため、<strong>そのフォルダにある整合性チェック用の目印ファイルまで一緒に欠けていた</strong>。空のフォルダではなく「目印だけがある」状態が必要だった。</li>
<li><strong>ファイル同期サーバーが<code>Cannot write into &quot;config&quot; directory!</code>で503。</strong> <code>rclone</code>はファイルの所有者情報を保持しないので、復元したファイルが全部自分のユーザーの所有になっていた。<strong>クラウド経由の復元では所有者が落ちることを前提に、復元後のチェック項目に入れておく。</strong></li>
<li>🔴 <strong>ユーザーのIDが揃っていなかった。</strong> 新VPSに旧サーバーと<strong>同じ名前</strong>のユーザーを作ってパスを揃えたので、設定ファイルを無改造で使い回せた。これは正解だった。ただし名前は同じでも<strong>内部の数値のID</strong>が1つずれていて、一部のコンテナ内で<code>No user exists for uid 1001</code>で落ちた。<strong>名前だけでなく数値IDまで揃える必要がある。</strong></li>
<li><strong>「Dockerのデータとワークスペースを移せば終わり」ではなかった。</strong> 動画変換用のコマンドや日本語フォントといった<strong>ホスト側のOSパッケージ</strong>が新VPSに無く、移行の夜に機能が動かないことで発覚した。さらに<code>cron</code>(定時実行の仕組み)自体が未インストールで、登録コマンドが<strong>黙って失敗</strong>していた。バックアップの自動実行が最初から動いていないところだった。</li>
<li>🔴 <strong>写真46件が旧サーバーにしか存在しなかった。</strong> 復元結果をファイル単位で突き合わせて翌日見つけた。<strong>データベースには46件の記録があるのに、写真の実体がVPSにもB2にも無い</strong>。原因は、移行当日の最終バックアップで<strong>データベースのダンプは成功し、写真の同期ステップが完走しないまま終わっていた</strong>こと。しかも旧スクリプトはステップごとの成否をログに出していなかったので<strong>無言で失敗した</strong>。旧サーバーがまだ生きていたので手元から押し出して復旧できたが、運が良かっただけ。
<ul>
<li><strong>教訓1: 復元の検証を「ファイル数の一致」でやってはいけない。</strong> 60,525件と60,619件で0.15%差。見逃す水準だった。<strong>データベースに記録されているパスが1件ずつ実在するか</strong>を全件突き合わせる必要がある。</li>
<li><strong>教訓2: バックアップスクリプトはステップごとにOK/FAILをログへ出す。</strong> まさにこの欠陥を直すつもりだった移行当日に、その欠陥に刺された。</li>
</ul>
</li>
</ul>
<h2 id="公開ipになって初めて出た問題">公開IPになって初めて出た問題</h2>
<p>ここが<strong>一番多くの人が外すところ</strong>だと思う。移行そのものより危ない。</p>
<p><strong>自宅のPCはルーターの内側にいた。</strong> だから、どのサービスが「外から直接届く形」で動いていようが、外からは届かなかった。Cloudflare Tunnelという1本の出口だけが外部との接点だった。<strong>VPSは公開IPを持っている。</strong> 全部のポート(通信の入口の番号)が、世界中から直接叩ける状態で始まる。ランブックにこの観点は1行も無かった。</p>
<h3 id="-中身が剥き出しで公開されていた">① 中身が剥き出しで公開されていた</h3>
<p><strong>写真サーバーのログイン画面が生で公開</strong>されていた。Cloudflare側のログイン要求を通らない素の入口。<strong>ログイン入口サーバーのHTTPSポートも公開</strong>で、本来Cloudflare側の防御を通ってから届くはずのものが迂回できる状態。小さな静的サイトのポートも開いていた。</p>
<p>原因は<code>docker-compose.yml</code>の<code>ports:</code>の書き方。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="s1">&#39;2283:2283&#39;</span><span class="w">            </span><span class="c"># 🔴 これは全てのネットワークに向けて開く</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="s1">&#39;127.0.0.1:2283:2283&#39;</span><span class="w">  </span><span class="c"># ✅ 自分自身からしか届かない</span><span class="w">
</span></span></span></code></pre></div><p><strong>自宅では両方とも同じ結果になる</strong>ので、違いに気づく機会がなかった。トンネル経由で公開する構成なら、外に向けて開く必要は最初から無い。</p>
<p>応急処置としてファイアウォールで遮断し、<strong>後日、設定ファイル側を<code>127.0.0.1</code>に直して「ファイアウォールで塞いでいる」状態をやめた</strong>。この順序は意図的で、ファイアウォールなら数秒で塞げるがコンテナの作り直しは止まる時間が出るから。ただし<strong>応急処置で終わらせない</strong>。ファイアウォールは1行消えれば無防備に戻るが、そもそも誰も外向きに待ち受けていなければ消しようがない。</p>
<h3 id="-sshのパスワードログインが無効にしたのに有効だった">② SSHのパスワードログインが「無効にしたのに有効」だった</h3>
<pre tabindex="0"><code>/etc/ssh/sshd_config.d/50-cloud-init.conf   ← PasswordAuthentication yes
/etc/ssh/sshd_config.d/99-hardening.conf    ← PasswordAuthentication no(自分が書いた)
</code></pre><p><strong>SSHのサーバーは「最初に見つけた値」を採用する。</strong> 番号が小さいファイルが先に読まれるので、99番に書いた自分の設定は永遠に負ける。VPSの初期設定が置いていった50番のファイルを無効化して解決した。🔴 <strong>設定を書いたら、書いたことを確認するのではなく、効いているかを確認する。</strong> 鍵でログインできることと、パスワードが本当に拒否されることの両方を実測する。</p>
<h3 id="-ipv6が完全に丸腰だった">③ IPv6が完全に丸腰だった</h3>
<p>ここが一番ひやりとした。遮断はしたが、<strong>IPv4のファイアウォールだけ</strong>だった。</p>
<p>VPSにはIPv6のアドレスも付いている。確認すると、<strong>IPv6側のファイアウォールのルールは空、ポリシーは全通過</strong>。コンテナはIPv4とIPv6の両方で待ち受けていたので、<strong>塞いだつもりの写真サーバーのログイン画面は、IPv6経由では公開されたまま</strong>だった。🔴 <strong>IPv4を塞いだら必ずIPv6も確認する。</strong> 設定は完全に別物で、片方だけやっても「塞いだ」とは言えない。</p>
<h3 id="-認証なしのポート19本が素通しだった">④ 認証なしのポート19本が素通しだった</h3>
<p>自分のアシスタントが外部サービスと喋るための中継プロセスが、<strong>ポート19本で認証なしで待ち受けていた</strong>。ファイアウォールのルールも無かった。<strong>この中の1本は自分のメールの受信箱に繋がっていた。</strong> 写真のログイン画面(少なくともパスワードは要求する)より危険度は上だった。</p>
<p>思い込みが穴を作った。<strong>「外に出ているポートは<code>docker-compose</code>の<code>ports:</code>に書いてあるものだけ」<strong>と思っていた。これらはコンテナではなく</strong>ホスト上で直接動いているプロセス</strong>なので、Docker向けのファイアウォール設定では止まらない。棚卸しの書き方にも罠がある。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 🔴 これでは漏れる</span>
</span></span><span class="line"><span class="cl">ss -tlnp <span class="p">|</span> grep 0.0.0.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ✅ 「自分自身以外」を除外方式で全部出す</span>
</span></span><span class="line"><span class="cl">ss -tlnp <span class="p">|</span> grep -vE <span class="s2">&#34;127\.0\.0\.|\[::1\]&#34;</span>
</span></span></code></pre></div><p>待ち受けアドレスの表記は<code>0.0.0.0:PORT</code>だけではない。<strong><code>*:PORT</code>という表記(IPv4とIPv6の両方で待ち受け)もあり、<code>0.0.0.0</code>では引っかからない。</strong> 19本はこの形で見落とされていた。**「危ないものを探す」のではなく「安全なものを除外して残り全部を見る」**書き方にしないと漏れる。</p>
<p>後日これも応急処置をやめて根本を直した。使っていた中継ソフトには<strong>待ち受けアドレスを指定するオプションが存在しなかった</strong>ので、ソフト側のコードを1行書き換える形になった。直した後の確認が気持ちよかった。<strong>公開IPへは「接続拒否」が返る</strong> — ファイアウォールで弾いているのではなく、そもそも誰も待ち受けていない状態になった。これが一番強い証明。</p>
<h3 id="-ファイアウォールの確認はそのサーバー自身からでは絶対にできない">⑤ ファイアウォールの確認は、そのサーバー自身からでは絶対にできない</h3>
<p>遮断ルールには「外部から来た通信に対して」という条件が付く。<strong>VPSの中から自分の公開アドレスに繋ごうとすると内部の経路を通るので、必ず「繋がる」と出る。</strong> 塞がっているのに塞がっていないように見える。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 別のパソコンから実行する(VPS内からでは意味がない)</span>
</span></span><span class="line"><span class="cl">nc -vz &lt;サーバーのアドレス&gt; <span class="m">2283</span>   <span class="c1"># → Operation timed out  が正しい</span>
</span></span><span class="line"><span class="cl">nc -vz &lt;サーバーのアドレス&gt; <span class="m">22</span>     <span class="c1"># → succeeded!            が正しい</span>
</span></span><span class="line"><span class="cl">nc -vz -6 &lt;IPv6アドレス&gt; <span class="m">2283</span>      <span class="c1"># IPv6も必ず測る</span>
</span></span></code></pre></div><p>この外部からの実測を、<strong>VPSを再起動した後・ファイアウォールを触った後・<code>docker-compose</code>を触った後に回す定型チェック</strong>として手順書に書き出した。</p>
<h3 id="-監視が自分の死を通報できない">⑥ 監視が自分の死を通報できない</h3>
<p>死活監視にUptime Kumaを使っているが、<strong>それが監視対象と同じVPSの上で動いている</strong>。VPSごと落ちたら監視も一緒に落ちて通知が飛ばない。<strong>一番知りたい障害だけ通報されない。</strong> <a href="/guides/backup-ransomware-resistant/">バックアップの記事</a>で書いたデッドマンスイッチ(生存信号が途絶えたら警報を出す仕掛け)と同じ構造なので、B2の複製先を見張らせていたGitHub Actions(GitHubが決めた時刻にスクリプトを自動実行する仕組み)に死活監視を1本足して解決した。<strong>サーバーの外で動くので、サーバーが丸ごと死んでも黙らない。</strong></p>
<h2 id="再起動して自分で戻ってくるかを確かめる">再起動して自分で戻ってくるかを確かめる</h2>
<p><strong>移行が完了した状態は、一度も再起動していない状態</strong>でもある。「今動いている」と「電源が落ちても自分で戻ってくる」は別の話で、自宅サーバー時代は再起動のたびに何かが立ち上がらない問題に何度も遭遇していた。</p>
<p>事前に、systemdのユニット7件がすべて<code>enabled</code>(起動時に自動で立ち上がる設定)か、コンテナ14個の再起動ポリシーが<code>unless-stopped</code>か<code>always</code>かを確認した。<strong>ファイアウォールのルールを永続化する仕組みも必ず含める</strong> — ここが抜けると再起動で遮断ルールが消え、さっき塞いだ穴が全部開く。厄介な事情が1つあって、<strong>自分のアシスタント自身がこのVPSの上で動いているので、再起動すると自分との会話も切れる</strong>。1分の猶予を置いて再起動し、<strong>復旧手順をチャットの外に控えておく</strong>段取りにした。自分を再起動する作業は、自分以外の場所に手順を置いてから実行する。</p>
<p>結果は成功。<strong>22秒で起動</strong>し、systemd 7件・コンテナ14個すべて稼働、全9サービスがHTTP 200/302、データも再起動前と完全一致。そして<strong>外部のパソコンからの<code>nc</code>実測で、遮断したポートは<code>Operation timed out</code>、SSHは<code>succeeded!</code></strong> — 永続化が再起動をまたいで効いていることを外部視点で立証できた。</p>
<p>後日ディスクを増設した時に<strong>こちらの意図しない強制リセットが起きた</strong>(公式ヘルプには「再起動不要」と書かれていたが実態は違った)。その時も全部がデータ欠損ゼロで自動復帰したので、この検証が本物だったことは意図せず二重に確認できた。<strong>増設系の作業は、公式が無停止と書いていてもダウンタイムを見込む。</strong></p>
<h2 id="結果">結果</h2>
<ul>
<li>原因不明の5.5日間停止から、<strong>26分の停止で海外VPSへ移行</strong>した。月額は$9.00から始まり、後にディスク増設で$12.60</li>
<li>応答は0.3秒前後から0.55〜0.7秒に落ちた。<strong>遅くなったのは事実</strong>で、これは引き受けた</li>
<li>公開IPになって初めて出た露出を6種類見つけて塞いだ。<strong>うち2種類(IPv6と、認証なしの19本)は最初の対処では見落としていた</strong></li>
</ul>
<p><strong>旧サーバーはすぐ捨てずに2週間温存した</strong>。これは正解で、移行翌日に「写真46件が旧サーバーにしか無い」と判明した時に助かった。復元の検証が全部終わるまでは、古い機械を消してはいけない。</p>
<p>そして<strong>ハングの原因は今も分かっていない</strong>。分からないまま、分からないものに依存する構成をやめた、というのが今回やったことの全部だ。</p>
<p><strong>更新履歴</strong> — 2026-10-05: 初版</p>
]]></content:encoded></item><item><title>写真サーバーの容量が足りない問題に、遠回りして決着をつけた</title><link>https://offgrid.taikiito.com/guides/photo-storage-capacity/</link><pubDate>Mon, 05 Oct 2026 00:00:00 +0000</pubDate><guid>https://offgrid.taikiito.com/guides/photo-storage-capacity/</guid><description>自前の写真サーバーは写真が増えるほど容量が足りなくなる。古い写真を安いクラウドストレージへ追い出す仕掛けを一晩かけて作り、10枚で実証まで通したあとに、VPS業者が月数ドルでSSDを増設できることに気づいて全部捨てた記録。測った数字と踏んだ罠を書いた。</description><content:encoded><![CDATA[<p>写真を自分のサーバーに置くようにしてから、ずっと気になっていたことがある。<strong>写真は毎年増えるのに、サーバーのディスクは契約した大きさのまま</strong>だということ。</p>
<p>結論を先に書くと、自分は<strong>古い写真を安いオブジェクトストレージへ追い出す仕掛け</strong>を一晩かけて作り、動くところまで確認してから、<strong>全部捨てた</strong>。業者が月$3.60でディスクを足せると分かったからだ。その遠回りの中身を書く。</p>
<h2 id="前提">前提</h2>
<ul>
<li>写真は<a href="/guides/google-photos-to-immich/">Google PhotosからImmichに移した</a>状態（Immich = 自前サーバーで動かす写真管理ソフト）</li>
<li>サーバーは<a href="/guides/home-server-to-vps/">自宅PCからVPSへ移した</a>直後（VPS = 仮想専用サーバー。業者のデータセンターにある自分専用のLinuxマシンを借りる形）。Contaboの6vCPU / 12GB RAM / <strong>SSD 200GB</strong> のプランで月$9.00（2026年9月時点）</li>
<li>バックアップ先はBackblaze B2（安価なオブジェクトストレージ。2026年9月時点で1TBあたり月$6.95）。構成は<a href="/guides/backup-ransomware-resistant/">バックアップを「消されても戻せる」形にする</a></li>
<li>土台の考え方は<a href="/guides/self-hosting-basics/">自分の「サーバー」を持つとはどういうことか</a></li>
</ul>
<h2 id="まず現実を測った">まず現実を測った</h2>
<p>移行直後のディスクはこうだった。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">df -h /
</span></span><span class="line"><span class="cl"><span class="c1"># 使用 164GB / 193GB、空き 29GB</span>
</span></span></code></pre></div><p>しかもこの時点でサムネイル（一覧表示用の縮小画像）がまだ生成されていなかった。以前のサーバーでは9.6GBあったので、それが戻ると<strong>空きは約19GB</strong>。200GBのプランを選んだ数日後にもう先が見えた。</p>
<p>次に増える速さを測った。Immichのデータベースに撮影年が入っているので、年ごとに集計できる。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="k">extract</span><span class="p">(</span><span class="k">year</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">a</span><span class="p">.</span><span class="s2">&#34;fileCreatedAt&#34;</span><span class="p">),</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">),</span><span class="w"> </span><span class="n">pg_size_pretty</span><span class="p">(</span><span class="k">sum</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="s2">&#34;fileSizeInByte&#34;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">from</span><span class="w"> </span><span class="n">asset</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="k">join</span><span class="w"> </span><span class="n">asset_exif</span><span class="w"> </span><span class="n">e</span><span class="w"> </span><span class="k">on</span><span class="w"> </span><span class="n">e</span><span class="p">.</span><span class="s2">&#34;assetId&#34;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">a</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">where</span><span class="w"> </span><span class="n">a</span><span class="p">.</span><span class="s2">&#34;deletedAt&#34;</span><span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="k">null</span><span class="w"> </span><span class="k">group</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>実測（写真の実体は合計115GB）。</p>
<ul>
<li><strong>直近3年分の合計で約22GB</strong>。年あたり3〜10GBのペース</li>
<li><strong>それより古い分が約19,000件・約93GB</strong></li>
</ul>
<p>つまり<strong>今の撮り方で年10GB弱</strong>。29GBの空きは3年もたない。いずれRAW（カメラが記録した無加工のデータ。1枚25〜50MB）で撮り始めたら桁が変わる。「毎年ちょっとずつ月額が増えていく」のが一番嫌だったので、<strong>容量と月額を切り離す</strong>方法を探し始めた。</p>
<h2 id="最初の案-古い写真を安いストレージへ追い出す">最初の案: 古い写真を安いストレージへ追い出す</h2>
<p><strong>サーバーのSSDには直近1〜2年の「よく見る」写真だけ置き、古い写真はB2に預けて見たいときだけ取り出す。</strong> SSDの使用量が年によらず一定になるので月額が増えない。B2には毎晩のバックアップで全写真の実体がすでにあるから、<strong>アップロードすらいらない</strong>のが魅力だった。</p>
<h3 id="方式をいくつか捨てた">方式をいくつか捨てた</h3>
<ul>
<li>🔴 <strong>Immichの「External Library」（外部ライブラリ）機能は使えない</strong>。サーバーの外のフォルダを読み込む機能だが、<strong>すでにImmichの中にある写真を引き取れず、新規の写真として再登録される</strong>。アルバム・顔認識の結果・お気に入りが失われる恐れがある。</li>
<li>🔴 <strong>「年ごとのフォルダを切り出す」はそもそも不可能</strong>。ディスク上の写真は<code>library/upload/&lt;ユーザーID&gt;/&lt;16進2桁&gt;/&lt;16進2桁&gt;/&lt;ランダムな名前&gt;.拡張子</code>という形で、<strong>ランダムな名前のハッシュで256×256のフォルダに均等にばらまかれている</strong>。年ごとのフォルダは存在しない。</li>
</ul>
<p>残ったのは、<strong>写真ファイル1個ずつをシンボリックリンク（ファイルの位置を指すだけのショートカット）に置き換え、リンク先をB2をフォルダとして見せた場所に向ける</strong>方式。データベースに一切触らないので、Immichから見れば何も変わらない。B2をフォルダとして見せる部分は<code>rclone</code>（いろいろなクラウドストレージへファイルを同期するコマンド）のマウント機能で、読み取り専用で常駐させた。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/rclone-immich-cold.service の要点</span>
</span></span><span class="line"><span class="cl"><span class="nv">ExecStart</span><span class="o">=</span>/usr/local/bin/rclone mount &lt;リモート名&gt;:&lt;バケット&gt;/immich-cold <span class="se">\
</span></span></span><span class="line"><span class="cl">  /home/&lt;user&gt;/immich/library/cold <span class="se">\
</span></span></span><span class="line"><span class="cl">  --read-only --allow-other --uid <span class="m">1001</span> --gid <span class="m">1001</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --vfs-cache-mode full --vfs-cache-max-size 3G --dir-cache-time 24h
</span></span></code></pre></div><p>ここで踏んだもの。</p>
<ul>
<li><strong><code>rclone</code>の場所は<code>/usr/local/bin/rclone</code>で、<code>/usr/bin/rclone</code>ではなかった</strong>。systemd（Linuxの常駐サービス管理）はそれだけの理由で<code>203/EXEC</code>と言って起動に失敗する。<code>which rclone</code>で確かめる。<code>--allow-other</code>には<code>/etc/fuse.conf</code>への<code>user_allow_other</code>の追記も必要。</li>
<li><strong><code>--dir-cache-time</code>は必ず延ばす</strong>。既定の5分だと期限切れごとにフォルダ一覧の問い合わせが全件走る。24時間にした。</li>
<li>🔴 <strong>Immichのコンテナ設定で写真フォルダのマウント指定に<code>:rslave</code>を付けないと見えない</strong>。<code>docker-compose.yml</code>の<code>${UPLOAD_LOCATION}:/data</code>を<code>${UPLOAD_LOCATION}:/data:rslave</code>にする。コンテナ（ソフトを隔離した箱に入れて動かす仕組み）へのフォルダ受け渡しは既定で「あとから生えたマウントを伝えない」ので、<strong>ホスト側で後から作ったマウントはコンテナの中からは空に見える</strong>。ここで30分溶かした。なお<code>docker compose up -d immich_server</code>は<code>no such service</code>になる——<strong>設定上のサービス名はハイフンの<code>immich-server</code>、コンテナ名がアンダースコアの<code>immich_server</code></strong>。</li>
</ul>
<h3 id="一番危なかったのはバックアップスクリプトだった">一番危なかったのはバックアップスクリプトだった</h3>
<p>実験で確証を取った、本当の地雷。毎晩のバックアップは<code>rclone sync</code>（送り元に無いものは送り先からも消す片方向ミラー）で動いている。<strong>ローカルの写真をシンボリックリンクに置き換えると、<code>rclone</code>は<code>Can't follow symlink without -L/--copy-links</code>と言ってそれをスキップし、「送り元に無い」と判断してB2側の実体を削除する。</strong></p>
<ul>
<li>🔴 <strong>行き先が存在しない壊れたリンクでも同じ挙動で、しかも終了コードは0</strong>（＝成功として記録される）。失敗として検知できない。つまり<strong>最も危険な順序は「B2へコピーする前にシンボリックリンクを作ってしまう」こと</strong>で、やると、その夜のバックアップがB2にある唯一の実体を無言で消す。</li>
<li><code>--copy-links</code>を付ける解決策は不可。B2に二重保存になって容量が倍になる。</li>
</ul>
<p>正解は<code>--exclude &quot;cold/**&quot;</code>で追い出し先を同期対象から外し、<strong>かつ順序を守ること</strong>。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. B2の中で複製（同一バケット内のサーバーサイドコピー＝転送ゼロ・一瞬）</span>
</span></span><span class="line"><span class="cl">rclone copyto REMOTE/immich/library/&lt;rel&gt; REMOTE/immich-cold/&lt;rel&gt;
</span></span><span class="line"><span class="cl"><span class="c1"># 2. 宛先のサイズを照合</span>
</span></span><span class="line"><span class="cl">rclone lsl REMOTE/immich-cold/&lt;rel&gt;
</span></span><span class="line"><span class="cl"><span class="c1"># 3. ローカルの実体は「削除」ではなく退避（ロールバックできるように）</span>
</span></span><span class="line"><span class="cl">mv library/upload/&lt;rel&gt; /root/poc-staging/&lt;rel&gt;
</span></span><span class="line"><span class="cl"><span class="c1"># 4. 相対パスのリンクを張る（library まで ちょうど4階層）</span>
</span></span><span class="line"><span class="cl">ln -s <span class="s2">&#34;../../../../cold/&lt;rel&gt;&#34;</span> library/upload/&lt;rel&gt;
</span></span><span class="line"><span class="cl">chown -h &lt;user&gt;:&lt;user&gt; library/upload/&lt;rel&gt;
</span></span><span class="line"><span class="cl"><span class="c1"># 5. リンク越しに md5 を照合。違ったら即ロールバック</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 6. 全件の検証が終わってから、はじめて旧パスを削除</span>
</span></span><span class="line"><span class="cl">rclone deletefile REMOTE/immich/library/&lt;rel&gt;
</span></span></code></pre></div><ul>
<li><strong>存在確認に<code>rclone lsjson</code>を使ってはいけない</strong>。存在しないファイルを渡すとフォルダとして扱われ、空の一覧と<strong>終了コード0</strong>を返す。これで一度「B2にある」と誤判断した。<strong><code>rclone lsl</code>の出力が空でないことを見る。</strong></li>
</ul>
<h3 id="10枚で通して確認したこと">10枚で通して、確認したこと</h3>
<p>PoC（Proof of Concept = 動くことを示すための小さな実装）として写真10枚・46MBで最後まで通した。ホストからもコンテナの中からも同じmd5で読め、ImmichのAPI経由でオリジナル画像が10/10でHTTP 200かつmd5一致、サムネイルは10/10がローカルから配信（<strong>一覧の表示速度は一切変わらない</strong>）、エラーログなし。B2からの初回読み込みは<strong>0.39秒</strong>（636KB）、キャッシュ後は<strong>0.017秒</strong>。</p>
<p>そして<strong>最も確認したかったこと: マウントが切れたらどうなるか</strong>。<code>systemctl stop</code>で接続を落として測った。</p>
<ul>
<li>オリジナル画像は<strong>404</strong>、サムネイルは<strong>200のまま</strong></li>
<li>写真のレコード件数は<strong>前・中・後で29,716件のまま不変</strong>、該当写真の削除日時もnullのまま</li>
<li>マウントを戻すと即座に200に復帰</li>
</ul>
<p>つまり<strong>Immichは、ファイルが読めなくなっても勝手にレコードを消したりゴミ箱に落としたりしない</strong>。これが成立しなければ全部やめるつもりだった。</p>
<p>安全網が既にあったことも実測した。<strong>B2のバケットは「隠してから30日で削除」の設定にしてあり、<code>rclone deletefile</code>は既定でハード削除ではなく論理削除</strong>。だから消したファイルは30日間復元できる。15分前に消した2.1MBのHEICを<code>rclone lsf --b2-versions</code>で版名（<code>&lt;名前&gt;-v&lt;日時&gt;.&lt;拡張子&gt;</code>）を調べて取り出し、md5一致まで確認した（<strong><code>--b2-versions</code>を付けても元のファイル名を渡すと失敗する</strong>ので、必ず版名で指定する）。これで「スクリプトのミスで写真が消える」は<strong>永久喪失ではなく30日以内なら戻せるミス</strong>に格下げされた。残る課題は復元手段ではなく<strong>30日以内に気づくこと</strong>になる。</p>
<h2 id="途中で前提そのものを疑った">途中で前提そのものを疑った</h2>
<p>10枚が通った時点でB2からの読み出し速度を測り直して、気が変わった。197MBのデータベースダンプが6.58秒＝<strong>約30MB/s</strong>、17MBの写真が単一ストリームで0.52秒＝<strong>約33MB/s（265Mbps）</strong>。一方で手元の動画は535本・平均11.6MB、ビットレートは平均7.3Mbps・上位5%で17.9Mbps・最大67.5Mbps。<strong>最も重い動画でも4倍近い余裕がある。</strong></p>
<p>それなら「古い写真を追い出す」のではなく、<strong>写真の置き場そのものをB2にすればいい</strong>のではないか。SSDの天井が消える。コストも、写真をB2に置けば月$10前後、1TBまで増えても月$16で、大きいディスクのプランへ引っ越す月$19より安い。<strong>安いほうが天井も無い</strong>という綺麗な結論になりかけた。</p>
<p>そこで手を止めて2つ調べた。</p>
<h3 id="自分以外に誰がやっているのか">自分以外に誰がやっているのか</h3>
<ul>
<li><strong>主流は「オリジナルはローカル、クラウドはバックアップ専用」</strong>。<a href="https://docs.immich.app/administration/backup-and-restore/">Immich公式のバックアップ手順</a>もローカル本番を前提に書かれている。クラウドを本番の置き場にしている例は少数派・実験的で、数少ない実践者は「わずかなレイテンシのゆらぎでマウントが壊れる」「ローカルキャッシュがすぐ溢れる」と報告している。</li>
<li><strong>「クラウドが唯一のコピー」でやっている人は、調べた範囲で1人も見つからなかった。</strong> 定番は3コピー（本番＋ローカルの2本目＋クラウド）。</li>
<li>🔴 <strong>Immichに未解決の不具合がある</strong>: <a href="https://github.com/immich-app/immich/issues/10480">issue #10480</a> — サムネイル生成が同じファイルを2回読むため、リモートストレージでは<strong>ダウンロード量が実データの8〜10倍</strong>に膨らむ。16,000ファイル・110GBで900GBという報告がある。B2の無料ダウンロード枠は保存量の3倍/月なので、<strong>サムネイルの全件再生成を一度回すと枠を超えて課金が出る</strong>。</li>
<li><strong>Immich本体のS3対応（オブジェクトストレージへの公式対応）は期待できない</strong>。2026年時点で未実装、ロードマップにも無い。**「待つ」は非現実的で、マウント方式が唯一の道（公式サポート外）**という整理になった。</li>
<li>追い風もあった。<strong>B2のAPI呼び出し課金が2026-05-01に全廃された</strong>（代わりに保存料が$6→$6.95/TB/月に値上げ）ので、「マウントが大量の一覧取得を発行して請求が爆発する」という有名な事故モード（過去に約$598の実例がある）は構造的に消えていた。</li>
</ul>
<h3 id="暗号化のコストを測った">暗号化のコストを測った</h3>
<p>クラウドに本番を置くなら預け先に中身を読まれない形にしたい。界隈の多数派も「上げる前に自分で暗号化する」だった。そして改めて気づいたのだが、<strong>自分の既存のバックアップは暗号化されていなかった</strong>。毎晩のバックアップを始めた時点で、預け先は写真59,000枚を読める状態だった。</p>
<p>17MBの写真で測った結果は、<strong>暗号化あり0.63〜0.95秒 / 平文0.46〜0.53秒</strong>。差は0.2〜0.4秒でボトルネックは回線のまま。<strong>暗号化は速度面の障害にはならない。</strong></p>
<ul>
<li>⚠️ <strong>最初の計測では「暗号化7.01秒 vs 平文0.46秒＝15倍」と誤った結論を出した</strong>。原因は、比較した2つのマウントで<code>--vfs-read-chunk-size</code>などの設定が揃っていなかったこと。<strong>ベンチマークは条件を揃える。</strong> 危うく「暗号化は無理」と誤った判断を下すところだった。</li>
<li>代償は速度ではなく別の2つ。①預け先のファイル名が不可読な文字列になり管理画面から中身を確認できない ②<strong>既存の115GBを暗号化し直すには全部アップロードし直す必要があり、「移行作業ゼロ」という最大の利点が消える</strong>。</li>
</ul>
<h2 id="本命の答えはボタンひとつだった">本命の答えは、ボタンひとつだった</h2>
<p>ここまで自分は「B2へ追い出す」「オブジェクトストレージを本番にする」「大きいプランへ引っ越す」の3つを丁寧に比較していた。そして<strong>契約しているVPSの管理画面を開いて、「ディスクだけ足す」という選択肢を一度も検討していなかったことに気づいた。</strong></p>
<p>Contaboの管理画面には**「Extend SSD Storage」ボタン**がある。<a href="https://help.contabo.com/en/support/solutions/articles/103000404620-can-i-add-more-ssd-storage-to-my-vps-or-vds-dedicated-server-">公式ヘルプ</a>にも、既存のVPSにSSDを足せる・再インストール不要・移行不要と書いてある。200GBを400GBにした。</p>
<p>月額の増分で並べるとこうなる（2026年9月時点）。</p>
<table>
	<thead>
			<tr>
					<th>選択肢</th>
					<th>月額の増分</th>
					<th>必要な作業</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>SSDを200GB増設</strong></td>
					<td><strong>+$3.60</strong></td>
					<td><strong>なし</strong></td>
			</tr>
			<tr>
					<td>B2へ追い出す（200GB相当）</td>
					<td>+$0.87</td>
					<td>マウント常駐＋リンク張り＋見張り＋孤児掃除</td>
			</tr>
			<tr>
					<td>同社のオブジェクトストレージ（最小250GB）</td>
					<td>+$2.99</td>
					<td>同じくマウントが必要</td>
			</tr>
			<tr>
					<td>大きいプランへ引っ越す</td>
					<td>+$8〜10</td>
					<td>データ移行、数分の停止</td>
			</tr>
	</tbody>
</table>
<p><strong>差額が月$2〜3しかないなら、その$2で複雑さを全部買い取れる。</strong> 一晩かけて作った仕掛けは、この表の2行目のためのものだった。</p>
<h3 id="増設の実作業">増設の実作業</h3>
<p>公式ヘルプの「activated right away（即座に有効、再起動不要）」は実態と違った。⚠️ <strong>増設は業者側の強制リセットを伴った</strong>——正常なシャットダウンの記録がなく、前のログが突然途切れている。<strong>増設するときはダウンタイムを見込むこと。</strong> 結果的に、<strong>予期しない電源断からの自動復旧が完全に機能することが実証できた</strong>（コンテナ14個・常駐サービス7件・公開している9サービスすべて復帰、写真のデータ欠損ゼロ）。棚からぼた餅の検証になった。</p>
<p><strong>増えた分は既存ディスクの拡大として現れた</strong>（別のディスクとしてではない）。ただし<strong>パーティションとファイルシステムは自動では広がらない</strong>ので、ここだけ手作業。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 作業前に、必ず手動でバックアップを一度完走させる</span>
</span></span><span class="line"><span class="cl">/root/backup-scripts/backup-to-b2.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">growpart --dry-run /dev/sda <span class="m">1</span>   <span class="c1"># まず空振りで確認</span>
</span></span><span class="line"><span class="cl">growpart /dev/sda <span class="m">1</span>             <span class="c1"># パーティションを広げる</span>
</span></span><span class="line"><span class="cl">resize2fs /dev/sda1             <span class="c1"># ファイルシステムを広げる</span>
</span></span></code></pre></div><ul>
<li>ルートのパーティションテーブルを書き換える操作なので、<strong>直前のバックアップは作法として必須</strong>。</li>
<li>対象が最後のパーティションで、直後に連続した空き領域がある理想的なケースだった。ext4に<code>resize_inode</code>があるので<strong>マウントしたまま、無停止でオンライン拡張できた</strong>。</li>
<li>結果: パーティション199GB→399GB、ファイルシステム193GB→387GB、<strong>使用率93%→46%、空き16GB→209GB</strong>。RAWを30MB換算で約7,000枚の余裕。</li>
<li><strong>料金の実績は月$9.00 → $12.60。100GBあたり月$1.80（＝$18/TB/月）。</strong></li>
</ul>
<h2 id="仕掛けは完全に撤去した">仕掛けは完全に撤去した</h2>
<p>増設で解決したので、一晩かけて作ったものは全部元に戻した。<strong>「使っていないだけ」で残すのが一番たちが悪い</strong>——あとでバックアップスクリプトを触ったときに、忘れた除外設定が事故になる。退避していた10枚の実体を元の場所に戻してB2へ再アップロードし、追い出し先のプレフィックスを削除。常駐マウントのサービスを停止・無効化・削除し、マウント用フォルダと<code>/etc/fuse.conf</code>の追記も撤去。バックアップスクリプトの<code>--exclude &quot;cold/**&quot;</code>とコンテナ設定の<code>:rslave</code>は、<strong>事前に取った<code>.bak</code>との差分で元と一致することを確認してから<code>.bak</code>も消した</strong>。撤去後の検証は10枚のmd5一致・シンボリックリンク0件・API 10/10・B2側も10/10復元・レコード件数29,716件で不変。</p>
<p>残したのは<strong>メモだけ</strong>。実際に効いた相対パスとmd5の一覧を<code>/root/notes/</code>に置いてある。将来TB級になってオブジェクトストレージが本当に必要になったとき、この一晩をゼロからやり直さずに済むように。</p>
<h2 id="単価で見るといつか判断が逆転する">単価で見ると、いつか判断が逆転する</h2>
<p>増設が常に正解ではない。1TBあたりの単価では<strong>増設が一番高い</strong>（VPSのSSD増設 <strong>$18/TB</strong> ＞ 同社オブジェクトストレージ $11.96/TB ＞ B2 <strong>$6.95/TB</strong>）。200GBでは差額が月$2程度にしかならないから複雑さを避けるのが正解だった、という話にすぎない。分岐点は2つ決めてある。<strong>追い出したい古い写真が1TBを超えたらB2方式を再検討する</strong>（差額が年$132になり、ようやくマウント方式の複雑さを引き受ける価値が出る）。<strong>1TB規模ならプランごと乗り換えたほうが安い</strong>（現プラン＋800GB増設の月$23.40に対し、1TBのSSDが付いたプランは月$15前後。ただし数分の停止を伴うし、<strong>SSDからNVMeへの変更は再インストール必須</strong>なので絶対に選ばない）。</p>
<p>残っている課題も書いておく。<strong>写真の2本目のコピーは引き続きバックアップ先の1本だけ</strong>で、以前使っていた自宅のPCに全量が残っているうちは3コピーだが、あれを片付けるときに決め直す必要がある。<strong>バックアップの暗号化も未着手</strong>——速度は問題にならないと測れたが、115GBを全部アップロードし直すコストが残っている。</p>
<h2 id="この遠回りから取ったもの">この遠回りから取ったもの</h2>
<p>一晩かけて作って、10枚で実証して、全部捨てた。<strong>月$3.60のボタンを先に押していれば丸ごと要らなかった作業</strong>だ。素直に書くと、「どう作るか」を考えるのが面白くて、「買えば済むか」を確かめる手順を飛ばしていた。</p>
<p><strong>構成を変える検討を始めるときは、まず今契約しているサービスの管理画面に、その問題をそのまま解決するボタンが無いかを確認する。</strong> 自分は3つの案を丁寧に比較表にしておいて、「ディスクだけ足す」をその表に入れていなかった。<strong>選択肢の作り方を間違えると、どれだけ丁寧に比べても正解には当たらない。</strong></p>
<p>捨てた作業から残ったものもある。マウントが切れてもImmichのレコードは壊れないこと、削除が30日間は取り返せること、<code>rclone sync</code>がリンクを見て送り先を消すこと、暗号化が速度の障害にならないこと、サムネイルの全件再生成が課金を踏むこと。これは<strong>どの方式を選んでも効く知識</strong>で、1TBを超えた日に取り出せる形でメモに残した。遠回りの元は取れていないが、ゼロでもない。</p>
<p><strong>更新履歴</strong> — 2026-10-05: 初版</p>
]]></content:encoded></item></channel></rss>