Rails x_sendfileで実現する高速・安全なファイル配信【Rails 7.x/8.x対応・Nginx/Apache設定例付き】

2011年11月18日


RailsアプリケーションでPDFや画像などのファイルをダウンロード提供する場合、Railsプロセスが直接ファイルを読み込んで返す素朴な実装では本番環境でボトルネックになる。大容量ファイルを配信するとRailsのワーカープロセスがそのファイル転送の間ずっと占有されてしまうからだ。

x_sendfile は、この問題をWebサーバー側にファイル転送を委譲することで解決するしくみだ。Railsがファイルのパスだけを応答ヘッダーに乗せて返し、NginxやApacheが実際のファイル転送を担当する。Railsプロセスは次のリクエストをすぐに処理できる。

本記事では、Rails 7.x/8.x環境でx_sendfileを正しく設定し、本番環境で安全かつ高速なファイル配信を実現する方法を解説する。Nginx向けの X-Accel-Redirect とApache向けの X-Sendfile の両方をカバーする。

Railsのx_sendfileによるファイル配信フローの概念図


x_sendfileとは何か

通常のファイル配信の問題点

Railsで send_file を使った素朴な実装は以下のようになる:

# コントローラー
def download
  file_path = Rails.root.join('private', 'files', params[:filename])
  send_file file_path, type: 'application/pdf', disposition: 'attachment'
end

この実装では、Railsプロセスがファイルを読み込んでクライアントに転送する間、そのプロセスは他のリクエストを処理できない。100MBのPDFを配信するなら、その転送が完了するまで(回線速度によっては数十秒)そのプロセスがブロックされる。

x_sendfileによる解決

x_sendfile を使うと、処理の流れが変わる:

  1. クライアントがダウンロードリクエストを送信
  2. Railsが認証・認可チェックを実施(ここが重要)
  3. Railsは実際のファイルを返さず、ファイルパスを含む特殊なヘッダーだけを返す
  4. NginxまたはApacheがそのヘッダーを読んで直接ファイルを配信
  5. Railsプロセスはすぐに解放され、次のリクエストへ

この方式の利点:
– Railsプロセスの占有時間を最小化できる
– Webサーバーのゼロコピー最適化(sendfileシステムコール)を活用できる
– 認証・認可ロジックはRails側で管理できる(セキュリティ確保)

x_sendfileと通常のsend_fileの処理フロー比較図


前提条件

このガイドで扱う環境:

  • Rails: 7.x または 8.x(Rails 5.x以降であれば同様に動作)
  • Webサーバー: Nginx(推奨)または Apache
  • Ruby: 3.1以上
  • 前提知識: Rails基本操作、Nginxまたはhttpdの設定ファイル編集

バージョン確認:

rails --version
# Rails 7.2.1 など

ruby --version
# ruby 3.3.0 など

nginx -v
# nginx version: nginx/1.24.0 など

全体の流れ

このガイドは以下の5ステップで構成される。

  1. Rails側でx_sendfileを有効化する — 5分
  2. Nginxの設定(X-Accel-Redirect) — 10分
  3. Apacheの設定(X-Sendfile) — 10分
  4. Railsコントローラーの実装 — 15分
  5. セキュリティ考慮事項 — 10分

ステップ1: Rails側でx_sendfileを有効化する

production.rb の設定

config/environments/production.rb に以下を追加する:

# config/environments/production.rb

Rails.application.configure do
  # Nginx を使う場合
  config.action_dispatch.x_sendfile_header = "X-Accel-Redirect"

  # Apache を使う場合(どちらか一方のみ有効にする)
  # config.action_dispatch.x_sendfile_header = "X-Sendfile"
end

注意: x_sendfile_header を設定するだけで、send_file 呼び出し時に自動的にそのヘッダーが使われるようになる。既存のコントローラーコードを変更する必要はない。

開発環境では設定しない

開発環境(config/environments/development.rb)にはこの設定を入れないこと。ローカルのWEBrickやPumaには X-Sendfile を処理する機能がないため、ファイルが配信されない。

# development.rb には設定不要
# config.action_dispatch.x_sendfile_header = ... # 入れない

ステップ1完了の確認: config/environments/production.rbx_sendfile_header の設定が追加されていること。


ステップ2: Nginx の設定(X-Accel-Redirect)

NginxでX-Sendfileに相当する機能は X-Accel-Redirect ヘッダーで実現する。Nginxはこのヘッダーを受け取ると、Railsの応答を破棄し、指定されたURIのファイルを直接配信する。

Nginx設定の基本構造

# /etc/nginx/sites-available/myapp(または nginx.conf の server ブロック)

server {
    listen 80;
    server_name example.com;

    root /var/www/myapp/current/public;

    # Rails アプリへのプロキシ設定
    location / {
        try_files $uri @rails;
    }

    location @rails {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # x_sendfile 用: 非公開ファイルの配信
    # この location は直接アクセス不可(internal ディレクティブ)
    location /private/ {
        internal;
        alias /var/www/myapp/private/;
    }
}

internal ディレクティブが重要

internal; ディレクティブにより、この location は外部からの直接アクセスを拒否し、内部リダイレクト(X-Accel-Redirect ヘッダー経由)のみを受け付ける。これにより、認証を通過していないユーザーがURLを直打ちしてファイルを取得しようとしても拒否される。

# 直接アクセスしようとすると 404 が返る
curl https://example.com/private/secret.pdf
# HTTP 404 Not Found

# Rails 経由(認証後)は正常に配信
curl -H "Authorization: Bearer TOKEN" https://example.com/downloads/secret.pdf
# ファイルが正常にダウンロードされる

Unicorn/Pumaをsocketで使う場合

本番環境でUnicornやPumaをUnixソケット経由で使う場合の設定:

upstream rails_app {
    server unix:/var/www/myapp/shared/tmp/sockets/puma.sock fail_timeout=0;
}

server {
    listen 80;
    server_name example.com;

    root /var/www/myapp/current/public;

    location / {
        try_files $uri @rails;
    }

    location @rails {
        proxy_pass http://rails_app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # プライベートファイル配信用(internal アクセスのみ)
    location /private-files/ {
        internal;
        alias /var/www/myapp/private_uploads/;
    }
}

ステップ2完了の確認: nginx -t でシンタックスエラーがないことを確認し、sudo nginx -s reload で設定を反映する。


ステップ3: Apache の設定(X-Sendfile)

Apache を使う場合は mod_xsendfile モジュールが必要だ。

mod_xsendfile のインストール

# Ubuntu/Debian
sudo apt-get install libapache2-mod-xsendfile
sudo a2enmod xsendfile
sudo systemctl reload apache2

# CentOS/RHEL
sudo yum install mod_xsendfile
sudo systemctl reload httpd

Apache VirtualHost の設定

# /etc/apache2/sites-available/myapp.conf

<VirtualHost *:80>
    ServerName example.com
    DocumentRoot /var/www/myapp/current/public

    # mod_xsendfile の有効化
    XSendFile On

    # X-Sendfile ヘッダーで使用可能なパス(ホワイトリスト)
    XSendFilePath /var/www/myapp/private/

    # Rails への ProxyPass
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    <Directory /var/www/myapp/current/public>
        Options FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>
</VirtualHost>

XSendFilePath の重要性

XSendFilePath には、X-Sendfile ヘッダーで指定できるパスをホワイトリスト形式で設定する。これを設定しないと任意のパスのファイルが配信できてしまうため、必ず適切なパスに制限すること

# 複数のパスを許可する場合
XSendFilePath /var/www/myapp/private/
XSendFilePath /var/www/myapp/uploads/secure/

ステップ3完了の確認: apache2ctl configtest でシンタックスエラーがないことを確認する。


ステップ4: Railsコントローラーの実装

基本的な実装

# app/controllers/downloads_controller.rb

class DownloadsController < ApplicationController
  before_action :authenticate_user!

  def show
    @document = Document.find(params[:id])

    # 認可チェック: 現在のユーザーがこのファイルにアクセス可能か確認
    unless current_user.can_access?(@document)
      return head :forbidden
    end

    # ファイルの存在確認
    file_path = @document.file_path
    unless File.exist?(file_path)
      return head :not_found
    end

    # x_sendfile が有効な場合、このヘッダーが自動的に設定される
    send_file file_path,
              filename: @document.original_filename,
              type: @document.content_type,
              disposition: 'attachment'
  end
end

Nginx 用のパス変換

Nginx の X-Accel-Redirect はファイルシステムのパスではなく、Nginx の location に対応する URI を指定する必要がある。

send_file はデフォルトでファイルの絶対パスをそのまま X-Accel-Redirect ヘッダーに設定するが、Nginx の alias 設定と組み合わせる場合はパスの変換が必要になることがある。

# Nginx 設定
location /private-files/ {
    internal;
    alias /var/www/myapp/private/;
    # /private-files/foo.pdf → /var/www/myapp/private/foo.pdf に変換
}
# Rails コントローラー
def show
  file_path = Rails.root.join('..', 'private', params[:filename]).to_s

  # send_file を使う(x_sendfile が有効なら自動的にヘッダーを設定)
  send_file file_path,
            type: 'application/pdf',
            disposition: 'attachment'
end

ポイント: Rails の send_fileconfig.action_dispatch.x_sendfile_header が設定されていれば、ファイルを実際に読まずにヘッダーだけをセットして返す。Nginx が X-Accel-Redirect: /private-files/foo.pdf ヘッダーを受け取り、aliasに従って /var/www/myapp/private/foo.pdf を配信する。

ダウンロードログを記録する例

# app/controllers/downloads_controller.rb

class DownloadsController < ApplicationController
  before_action :authenticate_user!

  def show
    @document = Document.find(params[:id])

    unless current_user.can_access?(@document)
      Rails.logger.warn "Unauthorized download attempt: user=#{current_user.id} document=#{@document.id}"
      return head :forbidden
    end

    # ダウンロードログの記録(非同期推奨)
    DownloadLog.create!(
      user: current_user,
      document: @document,
      ip_address: request.remote_ip,
      user_agent: request.user_agent
    )

    send_file @document.file_path,
              filename: @document.original_filename,
              type: @document.content_type,
              disposition: 'attachment'
  end
end

Railsコントローラーから始まるx_sendfileの処理フロー詳細図

ステップ4完了の確認: 開発環境で send_file が正常に動作すること(x_sendfileなし)を確認し、Staging/本番環境でX-Accel-RedirectまたはX-Sendfileヘッダーが返ることをcurlで確認する。

# レスポンスヘッダーの確認(本番環境)
curl -I -H "Cookie: _session=..." https://example.com/downloads/1
# X-Accel-Redirect: /private-files/document.pdf が含まれていれば OK
# (実際には Nginx がこのヘッダーを処理してクライアントには見えない)

セキュリティ考慮事項

x_sendfile を使う際にとくに注意すべきセキュリティリスクと対策をまとめる。

1. ディレクトリトラバーサル攻撃の防止

ファイルパスにユーザー入力を使う場合、必ずサニタイズすること:

# 危険: ユーザー入力を直接パスに使う
file_path = Rails.root.join('private', params[:filename])
send_file file_path  # /private/../../../etc/passwd のような攻撃が可能

# 安全: basename で実際のファイル名のみを使う
safe_filename = File.basename(params[:filename])
file_path = Rails.root.join('private', 'files', safe_filename)

# さらに安全: DBからファイルパスを取得し、ユーザー入力を直接使わない
document = Document.find(params[:id])
send_file document.file_path  # DB に保存された絶対パスを使う

2. 認可チェックの徹底

x_sendfile は「ファイル転送の高速化」の仕組みであり、「アクセス制御」はRails側で必ず実装すること:

def show
  document = Document.find(params[:id])

  # 認可チェックは必須
  authorize! :download, document  # CanCanCan の例

  # または明示的なチェック
  unless document.accessible_by?(current_user)
    return head :forbidden
  end

  send_file document.absolute_path
end

3. private ディレクトリを public の外に置く

配信するファイルは public/ ディレクトリの外に置くこと。public/ 内のファイルはNginxが直接配信してしまい、Railsの認可チェックを通らない:

# 推奨ディレクトリ構造
/var/www/myapp/
├── current/
│   ├── public/          ← Nginx が直接配信(認証不要のファイル)
│   │   └── assets/
│   └── ...
└── private/             ← Rails(x_sendfile)経由でのみ配信
    └── documents/
        └── secret.pdf

4. Content-Disposition の適切な設定

# ブラウザ内表示(PDF等)
send_file file_path, disposition: 'inline', type: 'application/pdf'

# 強制ダウンロード
send_file file_path, disposition: 'attachment', filename: 'document.pdf'

filename には信頼できる値(DB から取得したものなど)を使い、ユーザー入力をそのまま使わないこと。

5. 有効期限付きトークンによるURLの保護

定期的にアクセスが必要なユーザーへのリンクには、有効期限付きトークンを発行することを検討する:

# app/models/download_token.rb
class DownloadToken < ApplicationRecord
  belongs_to :document
  belongs_to :user

  before_create :generate_token

  def self.valid_for(document, user, expires_in: 10.minutes)
    create!(
      document: document,
      user: user,
      expires_at: expires_in.from_now
    )
  end

  def expired?
    expires_at < Time.current
  end

  private

  def generate_token
    self.token = SecureRandom.urlsafe_base64(32)
  end
end
# ダウンロードURLの生成
token = DownloadToken.valid_for(document, current_user)
download_url = download_url(token: token.token)

Rails 7.x / 8.x での変更点

Rails 7.x

Rails 7.0 以降では config.action_dispatch.x_sendfile_header の動作は変わっていないが、Zeitwerkによる自動読み込みの変更や、デフォルトのミドルウェアスタックが変わっているため、設定が正しく反映されているか確認する。

# config/environments/production.rb
Rails.application.configure do
  config.action_dispatch.x_sendfile_header = "X-Accel-Redirect"
  # ... その他の設定
end

Rails 8.x

Rails 8.0 では Rack::Sendfile ミドルウェアが引き続き使われており、x_sendfile の仕組みは変わっていない。Solid Cache/Solid Queue などの新機能との組み合わせも問題なく動作する。

現在のミドルウェアスタックで Rack::Sendfile が含まれていることを確認:

rails middleware
# ...
# use Rack::Sendfile
# ...

Rack::Sendfile ミドルウェアが X-Sendfile-TypeX-Accel-Mapping などの環境変数も参照する点を覚えておくと、デバッグ時に役立つ。


パフォーマンス比較

実際の違い

方式 Railsプロセスの占有時間 ファイル転送の担当
通常の send_file(x_sendfileなし) ファイル転送完了まで(数秒〜数十秒) Railsプロセス
x_sendfile(Nginx/Apache) ヘッダーを返すだけ(数ms) Nginx/Apache

Railsのワーカープロセス数が限られている場合(Pumaのスレッド数など)、x_sendfileを使わないと大きなファイルの配信リクエストが増えた際にスループットが大幅に低下する。


トラブルシュート

症状 原因 解決策
ファイルが配信されず空のレスポンスが返る Nginx の internal ロケーションのパス設定ミス X-Accel-Redirect ヘッダーのURIと Nginx の location が一致しているか確認
500 Internal Server Error ファイルパスが存在しない File.exist? で事前チェックする
開発環境でファイルが見つからない 開発環境に x_sendfile_header を設定してしまっている development.rb には設定しない
本番環境でヘッダーが設定されない Rack::Sendfile ミドルウェアが無効化されている rails middleware でスタックを確認
Apache で X-Sendfile が無視される mod_xsendfile が有効化されていない a2enmod xsendfile で有効化後、再起動
ファイル名が文字化けする マルチバイト文字を含む filename の扱い filename に ASCII 文字を使うか、filename* ヘッダーを使う

まとめ

Railsの x_sendfile は、本番環境でのファイル配信パフォーマンスを大きく改善する設定だ。

  • Rails側: config.action_dispatch.x_sendfile_header を設定するだけで既存の send_file コードがそのまま動く
  • Nginx: X-Accel-Redirect + internal ディレクティブで安全に実装できる
  • Apache: mod_xsendfile + XSendFilePath でホワイトリスト制限付きで実装できる
  • セキュリティ: 認可チェックはRails側で必ず実施し、ディレクトリトラバーサル対策を徹底する

Rails 7.x/8.x でも設定方法は変わらないため、既存のRailsアプリに追加コストなく導入できる。ファイルダウンロード機能を持つRailsアプリには必須の最適化と言える。


関連記事