前回のあらすじ
第1部では、LINEグループ限定のフォトアルバムシステムの構想から、MinIO+imgproxyによるローカルストレージサーバーの構築まで書きました。
しかし、大きな問題が残っていました:
自宅サーバーにインターネットからアクセスできない
ルーターのポートフォワーディング? セキュリティ的に怖い。固定IPサービス? コストがかかる。DDNS?設定の煩雑さが…。
Cloudflare Tunnelが、これらの悩みを全て解決してくれました。
Cloudflare Tunnel とは
Cloudflare Tunnel(旧称 Argo Tunnel)は、自宅サーバーからアウトバウンド接続のみでCloudflareのエッジネットワークにトンネルを張る仕組み。
ポイント
- ルーターのポート開放不要:外向き(outbound)の接続のみ
- 固定IPも不要:Cloudflare側のサブドメインで名前解決
- Cloudflare WAF が使える:DDoS防御やGeo-blockingが無料
- 無料:基本機能は無料プランで利用可能
ホームサーバーの物理マシン移行
Day 8:Orange Piへの移行
2026-01-18 ホームサーバーをOrange Pi R1+へ移行
MinIO/Imgproxy/Nginx構成
アップロード処理のストリーム化による最適化
Docker開発環境で動いていたストレージ構成を、実際の物理マシンに移行する作業。選んだのはOrange Pi R1+(ARM64)。小型・省電力で24時間稼働に向いています。
苦労した点:ARM64対応
MinIOは公式でARM64をサポートしていましたが、imgproxyは自前ビルドが必要でした。libvipsのコンパイルから始まる長い道のり…。
# homeserver/docker-compose.imgproxy.yml
services:
imgproxy:
build:
context: .
dockerfile: Dockerfile # ARM64用のカスタムビルド
environment:
IMGPROXY_BIND: ":9762"
IMGPROXY_USE_S3: "true"
# MinIOからS3プロトコルで画像を取得
AWS_ACCESS_KEY_ID: ${MINIO_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${MINIO_SECRET_ACCESS_KEY}
IMGPROXY_S3_ENDPOINT: "http://minio:9000"
Nginx Reverse Proxyの役割
MinIOの前段にNginxを置いた理由:
- SSL終端:自己署名証明書でHTTPS化(Cloudflare Tunnel内はHTTP)
-
MinIOのブラウザリダイレクトループ防止:
X-Forwarded-Proto: httpsを固定 - imgproxyのルーティング統合:1つのポートで複数サービスに振り分け
# 最小限のNginx設定例
server {
listen 9761 ssl;
location / {
proxy_pass http://localhost:9000;
proxy_set_header X-Forwarded-Proto https;
}
}
⚠️ ハマりポイント: MinIOは
MINIO_BROWSER: "off"を設定しないと、APIリクエストをコンソールUIにリダイレクトしてしまい、無限ループが発生します。これの原因特定に数時間かかりました。
Cloudflare Tunnelの構築
セットアップ
Cloudflare Zero Trustダッシュボードからトンネルを作成し、Docker Composeに追加するだけ。
# homeserver/docker-compose.yml に追加
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel run --token <トンネルトークン>
network_mode: "host" # Nginx/MinIOへのlocalhostアクセス用
3つのサブドメインを設定:
| サブドメイン | サービス | 用途 |
|---|---|---|
photoalbum.example.com |
http://localhost:80 |
アプリ本体 |
minio.example.com |
https://localhost:9761 |
ストレージAPI |
imgproxy.example.com |
http://localhost:9762 |
画像変換 |
WAFによるGeo-blocking
Cloudflareの無料WAFで、日本国外からのアクセスを一括ブロック。
ルール: 国 ≠ Japan → ブロック
これだけで、海外からの不正アクセスをほぼ完全にシャットアウトできます。実際にデプロイ後、ログにあった海外からの不審なリクエストがゼロになりました。
大容量ファイルのアップロード対策
Day 10:Multipart Upload対応
2026-01-20 大容量ファイルのMultipart Upload対応
Presigned URLによる直接アップロード機能の追加
NginxとCloudflareの設定最適化
問題:一眼レフのRAW現像後のJPEG(30MB〜)や動画ファイル(数百MB)をアップロードすると、Cloudflare Tunnelの100MBリクエストサイズ制限でエラーに。
解決策:ハイブリッドアップロード方式
// アップロード方式の自動選択
async function uploadFile(file) {
if (file.size < 80 * 1024 * 1024) { // 80MB未満
// 方式1: Presigned URL直接アップロード(高速)
const { url } = await getPresignedUploadUrl(file.name);
await fetch(url, { method: 'PUT', body: file });
} else {
// 方式2: サーバー経由チャンクアップロード(大ファイル対応)
await uploadInChunks(file, CHUNK_SIZE);
}
}
- 小ファイル(<80MB): Presigned URLでブラウザからMinIOに直接PUT
- 大ファイル(≥80MB): サーバー経由で分割アップロード(Multipart Upload)
さらに、Nginx側のバッファリングを無効化して、チャンク転送がストリーム処理されるように最適化。
# ストリーム化の設定
proxy_buffering off;
proxy_request_buffering off;
client_max_body_size 0; # サイズ制限なし
動画サムネイルの課題
2026-01-20 動画サムネイルがPresigned URLアップロード時に生成されない問題を修正
2026-01-21 動画サムネイル即時生成
Presigned URLでMinIOに直接アップロードする方式に変更したことで、サーバー側で動画データを処理するタイミングがなくなった。当然、サーバー側でサムネイルを生成できません。
解決策:ブラウザ側で動画サムネイルを生成
// ブラウザのCanvas APIで動画の1秒目をキャプチャ
async function captureVideoThumbnail(videoFile) {
const video = document.createElement('video');
video.src = URL.createObjectURL(videoFile);
video.currentTime = 1; // 1秒目
await new Promise(resolve => video.addEventListener('seeked', resolve));
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
return canvas.toBlob(null, 'image/webp', 0.8);
}
動画のアップロードと同時に、ブラウザ側でCanvasを使ってサムネイルをキャプチャし、別途サーバーに送信する仕組み。
パフォーマンスチューニング
ビューアーの段階的読み込み
2026-01-23 ビューアーの元画像表示を改善: 遅延読み込みと暗転防止
フォトビューアーで大きな画像を開くとき、読み込みに時間がかかって画面が真っ暗に…。
解決策:段階的画像読み込み(Progressive Loading)
- まずサムネイル(数KB)を即座に表示
- 0.5秒後にオリジナル画像の読み込みを開始
- オリジナル読み込み完了後、シームレスに差し替え
- 読み込み失敗時はサムネイルのまま表示
ユーザーは「表示が遅い」と感じることがなくなりました。
ズーム・パン操作の改善
2026-01-24 画像ビューアーのズーム・パン挙動とミニマップ表示の不具合を修正
ピンチズーム、パンドラッグ、ダブルタップズーム…。モバイルとPCの両方で自然な操作感を実現するのは想像以上に大変でした。
特に**「Seamless Pinch-to-Pan」**(2本指で拡大中に1本指を離してパンに移行する操作)は、タッチイベントの管理が複雑で、何度もリファクタリングしました。
この時点のアーキテクチャ
まとめ
第2部で達成したこと
- Cloudflare Tunnelによるルーター穴あけ不要のインターネット公開
- Orange Pi R1+への物理サーバー移行
- Cloudflare WAF(Geo-blocking)による海外アクセス遮断
- 大容量ファイルのMultipart Upload対応
- ブラウザ側での動画サムネイル生成
- フォトビューアーの段階的読み込みとズーム操作改善
コスト
| 項目 | 月額 |
|---|---|
| レンタルサーバー | 数百円(既存契約) |
| Cloudflare | ¥0(無料プラン) |
| MinIO | ¥0(OSS, 自宅サーバー) |
| imgproxy | ¥0(OSS) |
| Orange Pi R1+ 電気代 | 約¥100/月 |
| 合計 | 数百円/月 |
クラウドストレージを使えば月数千円はかかるところを、自宅サーバー+Cloudflare Tunnelでほぼ無料に。
ハマった箇所TOP3
-
MinIOのブラウザリダイレクトループ —
MINIO_BROWSER: "off"で解決 - Cloudflareの100MB制限 — Multipart Upload方式で回避
- ARM64でのimgproxyビルド — libvipsのコンパイルに依存関係の嵐
第3部では、実ユーザーのフィードバックに基づくUI改善と、AIによるセキュリティ脆弱性評価・対策について書きます。
📮 第1部:構想からMinIOローカルサーバー構築まで
📮 第3部:UI改善とセキュリティ強化 (近日公開)