Skip to content

VercelでRailsアプリケーションをデプロイする2025年版ガイド

はじめに

Vercelは、Next.jsを開発したチームが提供するフロントエンド特化型のクラウドプラットフォームとして広く知られていますが、実はRailsアプリケーションのホスティングにも対応しています。特に、APIモードのRailsアプリケーションや、フロントエンドとバックエンドを分離したモダンなWebアプリケーションアーキテクチャにおいて、Vercelは非常に強力な選択肢となります。

この記事では、2025年現在のVercelの機能を活用して、RailsアプリケーションをVercelにデプロイする方法について詳しく解説します。特に、Rails APIモード + フロントエンドフレームワーク(React、Vue.js等)の組み合わせでの活用を中心に説明します。

Vercelを選ぶ理由

メリット

  • エッジコンピューティング: 世界中のエッジロケーションでの高速レスポンス
  • ゼロ設定デプロイ: GitHubと連携するだけで自動デプロイが可能
  • Serverless Functions: サーバーレス関数による柔軟なバックエンド処理
  • プレビューデプロイ: プルリクエストごとに一意のプレビューURLを自動生成
  • 優れたDX: 開発者体験に特化した直感的なダッシュボードとCLI

デメリット

  • 実行時間制限: Serverless Functionsには実行時間の制限がある(10秒~900秒)
  • ステートレス: 永続的なファイルストレージやWebSocketの制限
  • データベース: 専用のデータベースサービスはなく、外部サービスが必要

Vercelでのデプロイ戦略

VercelでRailsアプリケーションをデプロイする主な方法は以下の通りです:

1. Rails APIモード + フロントエンドSPA

  • RailsをAPIサーバーとして使用
  • フロントエンドは別途React/Vue.js等で構築
  • Vercelでフロントエンドをホスト、APIは別サービス(Heroku、Railway等)

2. Serverless Functions化

  • RailsのコントローラーをServerless Functionsとして実行
  • 軽量なAPIエンドポイントに適している

3. Static Site Generation (SSG)

  • Railsで静的ページを生成してVercelでホスト
  • JekyllやMiddlemanの代替として

方法1: Rails API + フロントエンドSPA構成

Rails APIサーバーの準備

1. Rails APIモードプロジェクトの作成

bash
# APIモードでRailsプロジェクトを作成
rails new my_api --api
cd my_api

2. CORS設定

ruby
# Gemfile
gem 'rack-cors'
ruby
# config/application.rb
module MyApi
  class Application < Rails::Application
    config.api_only = true
    
    # CORS設定
    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins 'localhost:3000', 'your-vercel-domain.vercel.app'
        
        resource '*',
          headers: :any,
          methods: [:get, :post, :put, :patch, :delete, :options, :head],
          credentials: true
      end
    end
  end
end

3. APIコントローラーの実装

ruby
# app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < ApplicationController
  def index
    posts = Post.all
    render json: posts
  end

  def show
    post = Post.find(params[:id])
    render json: post
  end

  def create
    post = Post.new(post_params)
    if post.save
      render json: post, status: :created
    else
      render json: { errors: post.errors }, status: :unprocessable_entity
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :content)
  end
end

4. ルーティング設定

ruby
# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :posts
    end
  end
end

フロントエンド(Next.js)の作成

1. Next.jsプロジェクトの作成

bash
npx create-next-app@latest my-frontend --typescript --tailwind --eslint
cd my-frontend

2. API通信の実装

typescript
// lib/api.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'

export async function fetchPosts() {
  const response = await fetch(`${API_BASE_URL}/api/v1/posts`)
  if (!response.ok) {
    throw new Error('Failed to fetch posts')
  }
  return response.json()
}

export async function createPost(postData: { title: string; content: string }) {
  const response = await fetch(`${API_BASE_URL}/api/v1/posts`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ post: postData }),
  })
  
  if (!response.ok) {
    throw new Error('Failed to create post')
  }
  return response.json()
}

3. Reactコンポーネントの実装

tsx
// components/PostList.tsx
'use client'

import { useState, useEffect } from 'react'
import { fetchPosts } from '@/lib/api'

interface Post {
  id: number
  title: string
  content: string
}

export default function PostList() {
  const [posts, setPosts] = useState<Post[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const loadPosts = async () => {
      try {
        const postsData = await fetchPosts()
        setPosts(postsData)
      } catch (error) {
        console.error('Error loading posts:', error)
      } finally {
        setLoading(false)
      }
    }

    loadPosts()
  }, [])

  if (loading) return <div>Loading...</div>

  return (
    <div className="space-y-4">
      {posts.map((post) => (
        <div key={post.id} className="border p-4 rounded">
          <h2 className="text-xl font-bold">{post.title}</h2>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  )
}

Vercelへのデプロイ

1. 環境変数の設定

bash
# .env.local
NEXT_PUBLIC_API_URL=https://your-rails-api.herokuapp.com

2. vercel.jsonの設定

json
{
  "rewrites": [
    {
      "source": "/api/:path*",
      "destination": "https://your-rails-api.herokuapp.com/api/:path*"
    }
  ]
}

3. デプロイ

bash
# Vercel CLIのインストール
npm i -g vercel

# ログイン
vercel login

# デプロイ
vercel --prod

方法2: Serverless Functions化

Railsアプリケーションをサーバーレス関数として実行

1. vercel.jsonの設定

json
{
  "functions": {
    "api/index.rb": {
      "runtime": "ruby2.7"
    }
  },
  "routes": [
    {
      "src": "/api/(.*)",
      "dest": "/api/index.rb"
    }
  ]
}

2. サーバーレス関数の実装

ruby
# api/index.rb
require 'json'
require 'rack'

# 軽量なRackアプリケーション
app = Rack::Builder.new do
  map '/api/posts' do
    run lambda { |env|
      case env['REQUEST_METHOD']
      when 'GET'
        posts = [
          { id: 1, title: 'Hello Vercel', content: 'Serverless Rails!' },
          { id: 2, title: 'Ruby on Rails', content: 'On the edge!' }
        ]
        [200, { 'Content-Type' => 'application/json' }, [posts.to_json]]
      when 'POST'
        # POST処理の実装
        [201, { 'Content-Type' => 'application/json' }, ['{"status":"created"}']]
      else
        [405, {}, ['Method Not Allowed']]
      end
    }
  end
end

# Vercel関数のエントリーポイント
def handler(event:, context:)
  env = {
    'REQUEST_METHOD' => event['httpMethod'] || 'GET',
    'PATH_INFO' => event['path'] || '/',
    'QUERY_STRING' => event['queryStringParameters']&.map { |k, v| "#{k}=#{v}" }&.join('&') || '',
    'rack.input' => StringIO.new(event['body'] || ''),
    'rack.errors' => $stderr,
  }

  status, headers, body = app.call(env)
  
  {
    statusCode: status,
    headers: headers,
    body: body.join
  }
end

方法3: 静的サイト生成 (SSG)

Railsで静的ページを生成

1. 静的サイト生成のRakeタスク

ruby
# lib/tasks/static_site.rake
namespace :site do
  desc "Generate static site"
  task generate: :environment do
    require 'fileutils'
    
    output_dir = 'public/static'
    FileUtils.mkdir_p(output_dir)
    
    # ホームページの生成
    renderer = ActionController::Base.new
    html = renderer.render_to_string(
      template: 'posts/index',
      layout: 'application',
      locals: { posts: Post.all }
    )
    File.write("#{output_dir}/index.html", html)
    
    # 各投稿ページの生成
    Post.all.each do |post|
      html = renderer.render_to_string(
        template: 'posts/show',
        layout: 'application',
        locals: { post: post }
      )
      FileUtils.mkdir_p("#{output_dir}/posts")
      File.write("#{output_dir}/posts/#{post.id}.html", html)
    end
    
    puts "Static site generated in #{output_dir}"
  end
end

2. package.jsonでビルドスクリプトを設定

json
{
  "scripts": {
    "build": "bundle exec rails site:generate && cp -r public/static/* ."
  },
  "devDependencies": {
    "@vercel/ruby": "^1.3.0"
  }
}

高度な設定

環境変数の管理

bash
# Vercel CLIで環境変数を設定
vercel env add DATABASE_URL
vercel env add RAILS_MASTER_KEY
vercel env add API_KEY

カスタムドメインの設定

bash
# カスタムドメインを追加
vercel domains add example.com

キャッシュ戦略

json
{
  "headers": [
    {
      "source": "/api/posts",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "s-maxage=60, stale-while-revalidate"
        }
      ]
    }
  ]
}

パフォーマンス最適化

1. エッジキャッシング

typescript
// Next.jsでのキャッシュ戦略
export async function getStaticProps() {
  const posts = await fetchPosts()
  
  return {
    props: {
      posts,
    },
    revalidate: 60, // 60秒ごとに再生成
  }
}

2. 画像最適化

tsx
import Image from 'next/image'

export default function PostImage({ src, alt }: { src: string; alt: string }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={600}
      priority
      className="rounded-lg"
    />
  )
}

3. API Routes最適化

typescript
// pages/api/posts/[id].ts
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { id } = req.query
  
  // キャッシュヘッダーの設定
  res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate')
  
  try {
    const post = await fetchPost(id as string)
    res.status(200).json(post)
  } catch (error) {
    res.status(404).json({ error: 'Post not found' })
  }
}

セキュリティ対策

1. 環境変数による秘密情報管理

typescript
// 環境変数の検証
const requiredEnvVars = ['NEXT_PUBLIC_API_URL', 'API_SECRET'] as const

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`)
  }
}

2. CSRFトークン

ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include ActionController::RequestForgeryProtection
  
  protect_from_forgery with: :null_session
  before_action :authenticate_request
  
  private
  
  def authenticate_request
    token = request.headers['Authorization']&.sub('Bearer ', '')
    # JWT等での認証ロジック
  end
end

3. CORS設定の強化

ruby
# config/application.rb
config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins lambda { |source, env|
      # 本番環境では特定のドメインのみ許可
      if Rails.env.production?
        source == 'https://your-app.vercel.app'
      else
        source&.match?(/\Ahttp:\/\/localhost:\d+\z/)
      end
    }
    
    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      credentials: true
  end
end

監視とログ

Vercel Analytics

tsx
// pages/_app.tsx
import { Analytics } from '@vercel/analytics/react'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <Analytics />
    </>
  )
}

エラー追跡

typescript
// lib/error-tracking.ts
export function trackError(error: Error, context?: Record<string, any>) {
  console.error('Application error:', error, context)
  
  // Sentry等のエラー追跡サービスに送信
  if (process.env.NODE_ENV === 'production') {
    // Sentry.captureException(error, { extra: context })
  }
}

Vercel vs 他のプラットフォーム比較

機能VercelNetlifyAWS Amplify
料金$20/月~$19/月~$0.01/分~
エッジロケーション世界中世界中AWS リージョン
関数実行時間10秒~900秒10秒15分
カスタムドメイン
プレビューデプロイ
データベース外部必要外部必要DynamoDB

まとめ

VercelでのRailsアプリケーションデプロイは、従来の方法とは異なるアプローチが必要ですが、適切に設計すれば以下のメリットを享受できます:

推奨される使用ケース

  • JAMstack アーキテクチャ: 静的サイト + APIの組み合わせ
  • マイクロサービス: 軽量なAPIサービス
  • プロトタイプ開発: 素早いMVP構築
  • グローバル配信: 世界中のユーザーへの高速配信

避けるべきケース

  • 長時間実行処理: バッチ処理や重い計算処理
  • ステートフルアプリケーション: WebSocketやファイルアップロード中心の機能
  • 従来のMonolithicアプリケーション: 大規模な既存Railsアプリケーション

Vercelの強みを活かしつつ、適切なアーキテクチャ設計を行うことで、高性能でスケーラブルなWebアプリケーションを構築できるでしょう。

AI が自動生成した技術記事をまとめたテックブログ