はじめに
RUNTEQ Advent Calendar 2023の3日目を担当させていただきます、清水と申します。
現在は、事業会社でコーダーをしながら、プログラミングスクールRUNTEQにて、Web開発の勉強をしています。
今回は、「初めた学んだ技術」というテーマということで、個人開発で使用するためにキャッチアップした技術構成での環境構築とデプロイ方法についてまとめてみようと思います。
具体的には、Dockerを使用して、Rails(APIモード)/ Next.jsの環境構築を行い、Fly.ioとVercelにデプロイを行う方法についてまとめています。
これから、この技術構成で環境構築などを行う方に向けて、少しでも参考になればと思います。
また、もしこの記事通りに環境構築・デプロイを行ってみたがエラーが発生してしまった場合や、解説のミスなどありましたらコメントいただけると幸いです。
使用技術
- Ruby 3.2.2
- Rails(APIモード) 7.0.8
- Next.js 14.0.3
- Postgresql 15.5
- Docker 24.0.6
- Docker Compose v2.23.0
- Node.js 19.4.0
- Fly.io
- Vercel
- Github Actions
全体の流れ
今回は、Webアプリ開発を想定しているので、Githubでリポジトリを作成するところから解説をしていこうと思います。
- メインリポジトリ作成、フロントエンド・バックエンドディレクトリのサブモジュール化
- Docker設定
- フロントエンド側の環境構築(Next.js)
- バックエンド側の環境構築(Rails API)
- API作成
- Next.jsでAPIリクエスト
- Vercelへデプロイ
- Fly.ioへデプロイ
- Github Actions
- CORS設定
ソースコード
1.リポジトリ作成とサブモジュール化
今回は、フロントエンドとバックエンドを別々のリポジトリでサブモジュール化して、メインリポジトリで読み込む方法を行います。
submoduleについて
今回のディレクトリ構成を例にすると、frontディレクトリとbackディレクトリをサブモジュール化することにより、メインリポジトリからリンクはされているが、それぞれ独立したリポジトリとして扱われます。
なので、フロントエンドとバックエンドの開発が分離されて、それぞれのリポジトリで開発を進めることができます。
ディレクトリ構成
├── rails-api-nextjs-verification-app
├── front
└── back
- 任意のディレクトリで、
$ mkdir [任意のディレクトリ名]
を実行します。今回は、rails-api-nextjs-verification-app
で進めていきます。
$ mkdir rails-api-nextjs-verification-app
-
$ cd rails-api-nextjs-verification-app
で移動します。 - github上で、
rails-api-nextjs-verification-app
用のリポジトリを作成して、git init
をします。
rails-api-nextjs-verification-app $ git init
rails-api-nextjs-verification-app $ git add README.md
rails-api-nextjs-verification-app $ git commit -m "first commit"
rails-api-nextjs-verification-app $ git branch -M main
rails-api-nextjs-verification-app $ git remote add origin git@github.com:[ユーザーid]/[リポジトリ名].git
rails-api-nextjs-verification-app $ git push -u origin main
これで、リポジトリと連携ができたかと思います。
-
front
ディレクトとback
ディレクトリ用のリポジトリを作成します。リポジトリ作成時に、Add a README file
にチェックを入れておき、リポジトリ作成時にコミットがされている状態にしておきます。 -
rails-api-nextjs-verification-app
ディレクトリ内に、front
とback
のサブモジュールを追加します。
rails-api-nextjs-verification-app $ git submodule add [フロントエンドリポジトリのSSH] front
rails-api-nextjs-verification-app $ git submodule add [バックエンドリポジトリのSSH] back
- ルートディレクトリに
.gitmodules
が作成されているかと思います。もし、作成されてなかったら、以下のコマンドで作成してください。
rails-api-nextjs-verification-app $ touch .gitmodules
[submodule "front"]
path = front
url = [フロントエンドリポジトリのSSH]
[submodule "back"]
path = back
url = [バックエンドリポジトリのSSH]
- メインリポジトリ変更のcommitとpushを行います。
rails-api-nextjs-verification-app $ git add .
rails-api-nextjs-verification-app $ git commit -m "Add: submolues"
rails-api-nextjs-verification-app $ git push
以下の画面のようになれば完了です。
2.Docker設定
次に、Dockerの設定としてdocker-compose.yml
の作成とfront
back
ディレクトリにDockerfile
を作成していきます。
ディレクト構成
├── rails-api-nextjs-verification-app
├── front/
├── Dockerfile
└── back/
├── Dockerfile
├── docker-compose.yml
docker-compose.yml
version: "3"
services:
db:
image: postgres:15.5
environment:
POSTGRES_DB: app_development
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
back:
build:
context: ./back
dockerfile: Dockerfile
command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b '0.0.0.0'"
volumes:
- ./back:/app
ports:
- "3000:3000"
depends_on:
- db
tty: true
stdin_open: true
environment:
- RAILS_ENV=development
front:
build:
context: ./front/
dockerfile: Dockerfile
volumes:
- ./front:/app
command: yarn dev -p 4000
ports:
- "8000:4000"
volumes:
postgres_data:
docker-compose.ymlの各項目について
- db
- postgres:15.5を使用しています。
- environmentで環境変数を設定しています。
- portsでポートマッピングを行っています。
- volumesでデータの永続化を設定しています。
- 通常、Dockerのコンテナ内のデータはコンテナが停止・削除されると消失します。しかし、データベースのデータは永続的に保存する必要があります。
- back
- build → Dockerイメージのビルド方法を定義しています。
- context → Dockerfileがあるディレクトリパスを指定してます。
- dockerfile → Dockerfileの名前を指定します。
- command → コンテナが起動するときに実行されるコマンドを指定します。
- bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b '0.0.0.0'" → まず、tmp/pids/server.pidを削除して、Railsサーバーを起動しています。
- volumes → コンテナ内のデータをホストマシンと共有するために使用されます。
- depends_on → backが他のサービスに依存していることを指定します。
- build → Dockerイメージのビルド方法を定義しています。
- front
- command
- yarn dev -p 4000 → コンテナ起動時に、ポート4000でフロントの開発サーバーを起動します。
- command
front/Dockerfile
front
ディレクトリにDockerfile
を作成します。
Dockerfile
はイメージの設計図として機能します。必要な依存関係のインストールや、アプリケーションのコードのコピーなど、イメージを構築するために必要な情報を記載しています。
FROM node:19.4.0
WORKDIR /app
back/Dockerfile
back
ディレクトリにDockerfile
とentrypoint.sh
を作成します。
entrypoint.sh
は、コンテナが開始された時に実行されるスクリプトになります。
FROM ruby:3.2.2
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN gem install bundler
RUN bundle install
COPY . /app
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3002
CMD ["rails", "server", "-b", "0.0.0.0"]
#!/bin/bash
set -e
rm -f /app/tmp/pids/server.pid
exec "$@"
- #!/bin/bash → Bashスクリプトであることを宣言しています。
- set -e → スクリプトが失敗したら、直ちに停止します。
- rm -f /app/tmp/pids/server.pid → Railsが生成する
server.pid
ファイルが前回のプロセスで残っているとサーバーが起動しないので、それを防ぎます。
続いて、$ docker-compose build
を通すために、back
ディレクトリにGemfile
とGemfile.lock
を作成します。
Gemfile.lock
は空のままで大丈夫です。
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.2.2"
gem "rails", "~> 7.0.5"
現在のディレクトリ構造
├── rails-api-nextjs-verification-app
├── front/
├── Dockerfile
├── README.md
└── back/
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── README.md
├── docker-compose.yml
ルートディレクトリでdocker-compose build
を実行して、Dockerイメージをビルドします。
このコマンドは、docker-compose.yml
ファイルに記載された設定をもとに、Dockerイメージを作成します。
rails-api-nextjs-verification-app $ docker-compose build
これで、docker-compose build
が成功すれば、DockerにImageが作成されていると思います。
docker images
を実行して、Imageが作成されているか確認してみてください。
3. フロントエンド側の環境構築(Next.js)
次に、フロントエンド側で使用するNext.jsアプリケーションを作成していきます。
まずは、front
ディレクトリへ移動してください。
$ cd front
続いて、$ docker-compose run --rm front yarn create next-app .
を実行します。
front $ docker-compose run --rm front yarn create next-app .
すると、以下のエラーが発生するかと思います。
[##] 2/2The directory app contains files that could conflict:
Dockerfile
README.md
Either try using a new directory name, or remove the files listed above.
error Command failed.
Exit code: 1
Command: /usr/local/bin/create-next-app
Arguments: .
Directory: /app
Output:
info Visit https://yarnpkg.com/en/docs/cli/create for documentation about this command.
このエラーは、create-next-app
コマンドが新しいNext.jsアプリケーションをセットアップする際に、実行されたディレクトリが空でないということを表しています。
現在、frontディレクトリでは、Dockerfile
とREADME.me
が存在しているため、エラーが発生してしまいました。
なので、一度Dockerfile
をルートディレクトリへ移動してから、再度$ docker-compose run --rm front yarn create next-app .
を実行します。
README.me
はNext.jsアプリ作成時に新規に作成されるので、ここでは削除してしまって大丈夫です。
一時的なディレクトリ構造
├── rails-api-nextjs-verification-app
├── front/
└── back/
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── README.md
├── docker-compose.yml
├── Dockerfile // ここに移動
以下のようにNext.jsアプリ作成時の設定を色々聞かれると思いますが、今回はTypeScriptとESLintとTailwindCSSとAppRouterを使用したアプリケーションを作成します。
作成が成功するとfrontディレクトが以下のようになります。
そうしたら、先ほど一時的に移動したDockerfile
をfrontディレクトリ直下に戻します。
Dockerfile
を戻したら、frontディレクトリで$ docker-compose up front
を実行して、Next.jsアプリケーションを起動します。
docker-compose up front について
docker-compose.yml
ファイル内に定義されたfront
サービスに関連する、コンテナなどを作成します。
そして、http://localhost:8000/
にアクセスして、以下のようなNext.jsの初期画面が表示されたら成功です。
4. バックエンド側の環境構築(Rails API)
フロントエンド側のアプリケーションが作成できたら、次はバックエンド側のアプリケーションをRails APIモードで作成していきます。
まず、back
ディレクトリに移動します。
front $ cd ..
$ cd back
back
ディレクトリで、$ docker-compose run --rm --no-deps back bundle exec rails new . --api --database=postgresql
を実行します。
これは、docker-composeを使用して、Railsアプリケーションを作成するためのコマンドです。
docker-compose run --rm --no-deps back bundle exec rails new . --api --database=postgresql について
- docker-compose run
-
docker-compose.yml
ファイル内のサービスを起動するために使用されます。
-
- --rm
- コマンドの実行が完了した後にコンテナを自動的に削除するようにします。
- --no-deps
- backに依存しているデータベースも一緒に起動されないようにします。
- back
-
docker-compose.yml
ファイル内で指定されたサービス名です。
-
- bundle exec rails new . --api --database=postgresql
- 新しいRailsアプリケーションをAPI専用で作成し、データベースにはPostgreSQLを使用します。
コマンドを実行すると、README.md
とGemfile
が競合を起こしてしまうので、以下の画像のようにY
と入力して、上書き保存をします。
Railsアプリケーションの作成が完了すると、back
ディレクトリ内は以下の画像のようになります。
それでは、Railsアプリケーションを起動する前に設定を行います。
国際化とタイムゾーンの設定
config/application.rb
に以下のコードを追加します。
module App
class Application < Rails::Application
config.api_only = true
config.time_zone = 'Tokyo'
config.active_record.default_timezone = :local
config.i18n.default_locale = :ja
end
end
ホスト機能設定
config/environments/development.rb
にconfig.hosts << "api"
を追加します。
Rails.application.configure do
config.hosts << "api"
end
データベースのセットアップ
/config/database.yml
に以下のコードを追加します。
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: user
password: password
host: db
次に、back
ディレクトリで$ docker-compose run --rm back rails db:create
を実行します。
そうすると、以下のエラーが発生します。このエラーは、必要なRubyのgemがインストールされていないことを示します。
Could not find pg-1.5.4, puma-5.6.7, bootsnap-1.17.0, debug-1.8.0, msgpack-1.7.2, irb-1.9.1, reline-0.4.0, rdoc-6.6.0, psych-5.1.1.1, stringio-3.0.9 in locally installed gems
Run `bundle install --gemfile /app/Gemfile` to install missing gems.
なので、$ docker-compose run --rm back bundle install
を実行し、$ docker-compose build back
を実行します。
そしたら、再度$ docker-compose run --rm back rails db:create
を実行します。
データベースのセットアップが完了したはずなので、$ docker-compose up
を実行してアプリケーションを起動します。
http://0.0.0.0:3000/
にアクセスして、Railsの初期画面が表示されたらRailsアプリケーションの作成は完了です。
ここまでが、DockerでRailsAPIとNext.jsの環境構築が完了になります。
この後は、実際にRailsで簡単なAPIを作成してみて、Next.jsで非同期処理を行いたいと思います。
5. API作成
次に、Railsアプリケーションでscaffold
を使用して、簡単なAPIを作成してみたいと思います。
また、Next.js側で作成したAPIと簡単なやり取りができるところまで実装してみます。
scaffold追加
back
ディレクトリで、docker-compose run --rm back bundle exec rails g scaffold post title:string
を実行します。
back $ docker-compose run --rm back bundle exec rails g scaffold post title:string
次に、docker-compose run --rm back bundle exec rails db:migrate
を実行します。
back $ docker-compose run --rm back bundle exec rails db:migrate
次に、seeds.rb
でテストデータを作成します。
Post.create!(
[
{ title: '野球のルール基礎知識' },
{ title: 'プロ野球選手のトレーニング方法' },
{ title: '野球の歴史とは' },
{ title: 'メジャーリーグと日本プロ野球の違い' },
{ title: '野球用具の選び方' },
{ title: '野球のポジション紹介' },
{ title: '野球の戦術入門' },
{ title: '子供向け野球教室の選び方' },
{ title: '高校野球の魅力' },
{ title: '野球観戦の楽しみ方' },
{ title: '野球のスコアブックのつけ方' },
{ title: '野球の審判の役割' },
{ title: '野球におけるピッチングの技術' },
{ title: 'バッティングの基本' },
{ title: '野球の名言集' },
{ title: '野球のトレーニング用品紹介' },
{ title: '野球選手の食事管理' },
{ title: '野球の怪我の予防と対処法' },
{ title: '野球の上達法' },
{ title: '野球の国際大会について' }
]
)
back $ docker-compose run --rm back bundle exec rails db:seed
を実行して、テストデータを作成します。
この後に、http://0.0.0.0:3000/posts
にアクセスすると、JSON形式で先ほど作成したテストデータが表示されているかと思います。
rack-cors追加
次に、gem "rack-cors"
を追加して、CORSを管理する設定を行います。
CORSとは、セキュリティの観点から、ブラウザから異なるオリジン(ドメイン・プロトコル・ポート)からのスクリプトによるリソースの読み込みを制限するものです。
なので、RailsAPIをフロントからAPIリクエストを行った時に、Rails側でCORS設定を行っていないオリジンだった場合、エラーになってしまいます。
Gemfile
にgem "rack-cors"
がコメントアウトされていると思うので、コメントアウトして、config/initializers/cors.rb
を以下のように変更します。
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'localhost:8000', '127.0.0.1:8000'
resource "*",
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
こうすることで、localhost:8000
と127.0.0.1:8000
からのAPIリクエストを許可することができます。
そしたら、back
ディレクトリで$ docker-compose run --rm back bundle install
を実行します。
back $ docker-compose run --rm back bundle install
そして、再ビルドします。
back $ docker-compose build back
6. Next.jsでAPIリクエスト
次に、Next.js側で「記事のタイトルのみ投稿するフォーム」と「記事一覧を取得」する実装を行ってみたいと思います。
以下がTypeScriptを使用したコードになります。(不要なcssは削除しました)
page.tsx
"use client";
import React, { useEffect, useState } from "react";
type Post = {
id: number;
title: string;
};
export default function Home() {
const [posts, setPosts] = useState<Post[]>([]);
const [newTitle, setNewTitle] = useState("");
const fetchPosts = async () => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/posts`);
if (!response.ok) {
throw new Error("データの取得に失敗しました");
}
const data = await response.json();
setPosts(data);
} catch (error) {
console.error(error);
}
};
useEffect(() => {
fetchPosts();
}, []);
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title: newTitle }),
});
if (!response.ok) {
throw new Error("投稿に失敗しました");
}
setNewTitle("");
fetchPosts();
} catch (error) {
console.error(error);
}
};
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<h2 className="text-3xl mb-4">記事の一覧</h2>
<form onSubmit={handleSubmit} className="mt-4 mb-4">
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="新しい投稿のタイトル"
className="mr-2 p-2 border"
/>
<button type="submit" className="p-2 bg-blue-500 text-white">
投稿する
</button>
</form>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
);
}
7. Vercelへデプロイ
先ほど作成した、front
ディレクトリのNext.jsアプリケーションをVercelへデプロイします。
すごく簡単にデプロイを行うことができます。
- https://vercel.com/dashboard にアクセスします。まだ、Vercelへ登録していない方は、登録をお願いします。
- 「Dashboard」の「Add New」の「Project」をクリックします。
- 「Import Git Repository」で、
front
ディレクトリと紐づいているリポジトリを選択(Import)します。
- 「Configure Project」の「Deploy」をクリックします。
- 「Congratulations!」という画面が表示されたら、デプロイ成功です。
これで、front
ディレクトリのリポジトリのmainブランチ
にpushされたら、自動でデプロイが行われます。
8. Fly.ioへデプロイ
次に、RailsAPIアプリケーションをFly.ioにデプロイします。
back
ディレクトリでfly launch
を実行します。
Fly.ioの無料枠で使用できる内容はこちらです。
Up to 3 shared-cpu-1x 256mb VMs
3GB persistent volume storage (total)
160GB outbound data transfer
詳しくは公式ページをご確認ください。
https://fly.io/docs/about/pricing/
back $ fly launch
fly launch
を実行して、? Do you want to tweak these settings before proceeding
にy
と答えると、詳細を設定するページが立ち上がります。
今回は無料枠での運用を想定しているため、以下の設定にします。
- Memory & CPU
- VM Sizes → shared-cpu-1x
- VM Memory → 256MB
- Database
- Configuration → Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
設定が完了したら、以下のファイルが作成されているかと思います。
新規にファイルが作成されていることを確認できたら、fly deploy
でデプロイを行います。
エラーなく、無事にデプロイが完了したら、ダッシュボード(https://fly.io/dashboard)を確認して、
back $ fly deploy
fly open
でデプロイしたアプリケーションを確認することができます。
今回のRailsAPIアプリケーションは、scaffold
を使用しているので、https://[設定で指定した名前.fly.dev]/posts
にアクセスすると、JSON形式のページが表示されるかと思います。
9. Github Actions
次に、Fly.ioへのデプロイをGithub Actionsを使用して、backリポジトリのmainブランチ
にpushされたら自動でデプロイが行われるようにしたいと思います。
公式ドキュメントはこちら
https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
トークン作成
まず、デプロイの設定に必要なトークンを作成します。
fly.ioのダッシュボードにログインして、Account
のAccess Tokens
をクリックします。
Create token
にトークン名を入力して、Create
をクリックします。
作成されたトークンは、Githubに登録するので、コピーをして控えておきます。
GitHubにトークン設定
次に、作成したトークンをGitHubに設定します。
back
用のリポジトリページを開いて、Settings
→Secrets and variables
→Actions
を選択します。
次に、New repository secret
を選択します。
そしたら、Name
にFLY_API_TOKEN
と入力し、Secret
に先ほど作成したトークンを入力して、Add Secret
をクリックします。
ワークフロー作成
トークン設定が完了したら、次はGithub Actionsのワークフローを作成します。
back
ディレクトリ直下に、.github/workflows/fly.yml
を作成します。
name: Fly Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
branches
にmain
と設定することで、back
リポジトリのmainブランチ
にpushされた時に、GithubActionsにより自動でFly.ioへのデプロイが実行されます。
実際に、back
ディレクトリに少し修正を加えてみて、mainブランチ
にpushしてみます。
back
ディレクトリ用のリモートリポジトリのActions
を確認して、デプロイが成功することを確認してみましょう。
以下の画像のように、緑色のチェックマークになればGithub Actionsによるデプロイは成功です。
以下は、私がGithub Actionsでのデプロイ時に発生したエラーになります。もし、エラーが発生した時に参考になれば幸いです。
デプロイ時に発生したエラー
マシンの上限に達している
Error: failed to update VM 4d891224b70168: You have reached the maximum number of machines for this app.
アプリケーションに割り当てられているマシンの最大数が無料枠で使用できる数に達していると発生するエラーになります。
10. CORS設定
最後に、back
ディレクトリのconfig/initializers/cors.rb
のorigins
にフロントエンドのURL(先ほどVercelにデプロイした際に生成されたURL)を追加することで、デプロイ環境でのCORSエラーを回避することができます。
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'localhost:8000', '127.0.0.1:8000', '[フロントエンドをデプロイしたURL]'
resource "*",
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
参考情報