0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【第2部】LINEグループ専用フォトアルバムを自宅サーバーで作った話 ~Cloudflare Tunnelでルーター穴あけ不要に~

0
Last updated at Posted at 2026-03-23

前回のあらすじ

第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を置いた理由:

  1. SSL終端:自己署名証明書でHTTPS化(Cloudflare Tunnel内はHTTP)
  2. MinIOのブラウザリダイレクトループ防止X-Forwarded-Proto: https を固定
  3. 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)

  1. まずサムネイル(数KB)を即座に表示
  2. 0.5秒後にオリジナル画像の読み込みを開始
  3. オリジナル読み込み完了後、シームレスに差し替え
  4. 読み込み失敗時はサムネイルのまま表示

ユーザーは「表示が遅い」と感じることがなくなりました。

ズーム・パン操作の改善

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

  1. MinIOのブラウザリダイレクトループMINIO_BROWSER: "off" で解決
  2. Cloudflareの100MB制限 — Multipart Upload方式で回避
  3. ARM64でのimgproxyビルド — libvipsのコンパイルに依存関係の嵐

第3部では、実ユーザーのフィードバックに基づくUI改善と、AIによるセキュリティ脆弱性評価・対策について書きます。

📮 第1部:構想からMinIOローカルサーバー構築まで
📮 第3部:UI改善とセキュリティ強化 (近日公開)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?