【クレジットカード不要】無料の最強構成でWebアプリを作ってデプロイする【Vercel + Heroku + MongoDB Atlas】

2021年10月時点でクレジットカード不要で利用できるサービスの中で最適だと思う組み合わせで、Webアプリ開発の環境構築からデプロイまで行います。

# 利用するホスティングサービス

## Vercel

- ReactのフレームワークNext.jsで作ったアプリをデプロイできる。
- 日本のCDNを利用できるのでNetlifyに比べて応答が早いらしい。
- 無料のHobbyプランは個人利用で非営利目的のみ(広告はOKらしい)。

## Heroku

- 今回はRuby on Railsで作ったアプリをデプロイしてAPIとして利用する。
- アプリを稼働できるのは1つのアカウントにつき550時間/月。
- PostgreSQLのアドオンはレコード数10000行の制限がある。

## MongoDB Atlas

- 無料プランでは容量512MBまで利用できる。


# 開発環境

OS: Windows 10 20H2

WindowsではRuby on Railsの動作が安定しないので、WSL2という機能を使ってWindows上でLinuxを動かして開発します。

## 開発環境の構築

1. [Visual Studio Code](https://code.visualstudio.com/)をインストールする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/8344a007-a8b0-fb68-df0d-fdc0e5c0d8c7.png)
1. 「Windowsの機能の有効化または無効化」で、「Linux 用Windows サブシステム」と、「仮想マシンプラットフォーム」にチェックを入れる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/b37ba248-40e7-4fc9-657d-9740270ccc4a.png)
1. Linux カーネル更新プログラム パッケージをインストールする(ARM64 マシンの場合はARM64 パッケージを選ぶ)(https://docs.microsoft.com/ja-jp/windows/wsl/wsl2-kernel)
1. PowerShellまたはコマンドプロンプトでコマンド`wsl --set-default-version 2`を実行し、WSLのバージョンを2にする(事前にBIOSで仮想化の設定を変える必要があるかも)
1. Microsoft Storeから、Ubuntu 20.04 LTSをインストールする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/419a2186-645e-32a4-87b3-055a5c5ef944.png)
1. PCを再起動する
1. Ubuntu 20.04 LTSを起動し、ユーザー名とパスワードを設定する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/a9c8c5ae-7253-f30f-1226-a0a0894673ec.png)
1. パッケージのリポジトリを日本国内に変更する
```cd /etc/apt```
```sudo sed -i.bak -e "s/http:\/\/archive\.ubuntu\.com/http:\/\/jp\.archive\.ubuntu\.com/g" sources.list```
1. インストールされているソフトを更新する
```sudo apt update```
```sudo apt upgrade -y```
1. 日本語の言語パックをインストールし、日本語化する
```sudo apt install language-pack-ja```
```sudo update-locale LANG=ja_JP.UTF-8```
1. Ubuntu 20.04 LTSを再起動する
1. マニュアルを日本語化する
```sudo apt install manpages-ja manpages-ja-dev```
1. Rubyに必要なソフトをインストールする
```sudo apt install -y libssl-dev libreadline-dev zlib1g-dev```
```sudo apt install -y gcc g++ make```
1. rbenvをダウンロードする```cd```
```git clone https://github.com/sstephenson/rbenv.git .rbenv```
```git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build```
1. シェルの設定ファイルを編集し、変更を反映させる
```echo "# For rbenv" >> ~/.bash_profile```
```echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile```
```echo 'eval "$(~/.rbenv/bin/rbenv init -)"' >> ~/.bash_profile```
```source ~/.bash_profile```
1. インストールできるRubyのバージョンを確認し、バージョンを指定してインストールする
```rbenv install -l```
```rbenv install 3.0.2```
```rbenv rehash```
```rbenv global 3.0.2```
1. gemをアップデートする。そして、gemでライブラリをインストールしたときにドキュメントを生成しない設定にする
```gem update --system```
```touch ~/.gemrc```
```echo "install: --no-document" >> ~/.gemrc```
```echo "update: --no-document" >> ~/.gemrc```
1. yarnをインストールする
```curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -```
```echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list```
```sudo apt update```
```sudo apt install -y yarn```
1. npmをインストールする
```sudo apt install -y npm```
```sudo npm install n -g```
```sudo n stable```
```sudo apt purge nodejs npm```
```sudo apt autoremove```
1. Railsをインストールする
```gem install rails```

# Ruby on Railsアプリ制作

今回は本のデータを管理する簡単なアプリを作ります。
作業するディレクトリを作成し、作成したディレクトリに移動します。

```
mkdir -p ~/projects/book-manager
cd ~/projects/book-manager
```

Railsプロジェクトを作成し、作成されたディレクトリに移動します。

```
rails new rails-api --api --skip-bundle --skip-active-record --skip-test --skip-system-test
cd rails-api
```

WindowsにインストールしたVisual Studio コードを起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/585889ec-7265-89d5-3d1f-6bb6b184dae9.png)


Visual Studio Codeに、以下の拡張機能をインストールします。

- Japanese Language Pack for Visual Studio Code
- Remote - WSL

Visual Studio Codeを閉じます。

Ubuntu上で以下のコマンドを入力し、Visual Studio Codeで現在のディレクトリを開きます。

```
code .
```

Visual Studio Codeのウィンドウ左下に「WSL: Ubuntu-20.04」と表示されていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/89546065-7b0b-f528-0226-08d03057db1b.png)

追加で以下の拡張機能をインストールするのがおすすめです。

- Ruby
- VSCode Ruby
- Rails
- Ruby on Rails
- endwise
- Rinbow End
- Material Icon Theme

Gemfileを開き、```gem 'webpacker', '~> 5.x'```を追記します。


Ubuntu上で以下のコマンドを実行し、Gemライブラリをインストールします。

```
bundle install
```

WebPackerの初期設定をします。

```
rails webpacker:install
```

Railsサーバーを起動します。
```
rails s
```

ブラウザでhttp://localhost:3000 にアクセスし、Railsのページが表示されることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/c1c197e6-93ee-a511-9f5e-08c953302f41.png)

Railsサーバーを起動しているUbuntuのコンソールとは別に、もう一つUbuntuのコンソールを開いてください。

## MongoDBのインストール

UbuntuにMongoDBをインストールします。

```
wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org
```

## サービスとして MongoDB を開始する init スクリプトを追加する

1. MongoDB 用の init.d スクリプトをダウンロードします。 ```curl https://raw.githubusercontent.com/mongodb/mongo/master/debian/init.d | sudo tee /etc/init.d/mongodb >/dev/null```
1. スクリプト実行可能ファイルのアクセス許可を割り当てます。 ```sudo chmod +x /etc/init.d/mongodb```
1. MongoDBのサービスを起動します。 ```sudo service mongodb start```

## MongoDBのデータベース作成

MongoDBにログインします。

```
mongo
```

開発用とテスト用のデータベースとコレクションをそれぞれ作成します。

```
use book_manager_development;
db.createCollection('books');
use book_manager_test;
db.createCollection('books');
```

Ctrl + Cを押してログアウトします。

RailsでMongoDBを使うためのライブラリをインストールします。
Gemfileにmongoidを記述します。

```plaintext:Gemfile
gem 'mongoid', '~> 7.0.5'
```

Ubuntu上で以下のコマンドを実行し、Gemライブラリをインストールします。

```
cd ~/projects/book-manager/rails-api
bundle install
```

mongoidの設定ファイルを作成します。

```
rails g mongoid:config
```

mongoidの設定ファイルに接続先のデータベース名などを記述します。
config/mongoid.ymlを以下のように編集します。

```yml:mongoid.yml
development:
  clients:
    default:
      database: book_manager_development
      hosts:
        - localhost:27017
      options:
        server_selection_timeout: 1
test:
  clients:
    default:
      database: book_manager_test
      hosts:
        - localhost:27017
      options:
        read:
          mode: :primary
        max_pool_size: 1
```

Ubuntu上で以下のコマンドを実行し、Bookモデルと関連ファイルを作成します。

```
rails g scaffold Book title:string price:integer
```

# Next.jsアプリ制作

Ubuntu上で以下のコマンドを実行して、Next.jsプロジェクトを作成します。
```
cd ~/projects/book-manager
npx create-next-app@latest --ts
```

プロジェクト名を聞かれるので、```nextjs```と入力します。

作成されたディレクトリに移動し、現在のディレクトリをVisual Studio Codeで開きます。

```
cd nextjs
code .
```

Railsアプリとポートが被らないように、package.jsonの"scripts"の"dev"のコードを修正し、3000以外のポートを指定します。

```package.json(一部)
  "scripts": {
    "dev": "next dev -p 3001",
```

Ubuntu上で以下のコマンドを実行し、Next.jsアプリを起動します。

```
yarn dev
```

ブラウザでhttp://localhost:3001 にアクセスし、Next.jsのページが表示されることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/55c9e955-6fbb-05e5-4c67-63d5660ff88c.png)

Next.jsサーバーを起動しているUbuntuのコンソールとは別に、もう一つUbuntuのコンソールを開くのがおすすめです。

APIの通信に使うライブラリaxiosをインストールします。

```
cd ~/projects/book-manager/nextjs
npm i axios
```

nextjsディレクトリに型定義ファイルbook.d.tsを作成します。
_idはMongoDBでデータを作成したときに自動付与されるIDです。

```ts:book.d.ts
export type Book = {
    title:string;
    price:number;
    _id:{
        $oid: string;
    };
};
```

nextjsディレクトリに、API通信の共通処理を記述したhttp-common.tsを作成します。

```ts:http-common.ts
import axios from "axios";

export default axios.create({
    baseURL:"http://localhost:3000",
    headers:{
        "Content-type":"application/json",
    },
});
```

nextjsディレクトリに、API通信処理を記述したBook.service.tsを作成します。
rails-api/app/controllers/books_controller.rbに記述されているメソッドに対応しています。

```ts:Book.service.ts
import http from "./http-common"
import { Book } from  "./Book";

class BookService {
    index() {
        return http.get<Book[]>("/books");
    }
    create(book: Book) {
        return http.post("/books", book);
    }
    update(book: Book) {
        return http.put("/books/" + book._id.$oid, book);
    }
    delete(id: string) {
        return http.delete("/books/" + id);
    }
}

export default new BookService();
```

nextjs/pages/index.tsxを以下のように編集します。

```tsx:index.tsx
import { useEffect, useState } from 'react';
import type { NextPage } from 'next';
import { Book } from '../Book';
import BookService from '../Book.service';

const Home: NextPage = () => {
  const [books, setBooks] = useState<Book[]>([]);
  const [titleFormValue, setTitleFormValue] = useState("");
  const [priceFormValue, setPriceFormValue] = useState(0);

  useEffect(() => {
    getIndex();
  }, []);

  const getIndex = () => {
    BookService.index().then((res) => {
      if(res.status !== 200) return;
      setBooks(res.data);
    }).catch((reason) => {
      console.error(reason);
    });
  };

  const onCreateButtonClick = () => {
    BookService.create({title:titleFormValue, price:priceFormValue, _id:{$oid:""}}).then((res) => {
      if(res.status !== 201) return;
      setTitleFormValue("");
      setPriceFormValue(0);
      getIndex();
    }).catch((reason) => {
      console.error(reason);
    });
  };

  const onDeleteButtonClick = (id: string) => {
    BookService.delete(id).then((res) => {
      if(res.status !== 204) return;
      getIndex();
    }).catch((reason) => {
      console.error(reason);
    });
  };

  return (
    <>
    <h2>新しい本を登録</h2>
    タイトル
    <input type="text" value={titleFormValue} onChange={(e) => {setTitleFormValue(e.target.value)}} />
    価格
    <input type="number" value={priceFormValue} onChange={(e) => {setPriceFormValue(e.target.valueAsNumber)}}/>
    <button onClick={onCreateButtonClick}>登録</button>
    <h2>本リスト</h2>
    <table>
      <thead>
        <tr>
          <td>ID</td>
          <td>タイトル</td>
          <td>価格</td>
          <td>操作</td>
        </tr>
      </thead>
      <tbody>
        {books.map((book) => {
          return(
            <tr key={book._id.$oid}>
              <td>{book._id.$oid}</td>
              <td>{book.title}</td>
              <td>{book.price}</td>
              <td><button onClick={() => {onDeleteButtonClick(book._id.$oid)}}>削除</button></td>
            </tr>
          );
        })}
      </tbody>
    </table>
    </>
  );
};

export default Home;
```

# CORSの設定

rails-api/Gemfileの```gem 'rack-cors'```のコメントアウトを外します。

```bundle install```します。

rails-api/config/initializers/cors.rbを以下のように編集します。

```ruby:cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3001'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

Railsサーバーを再起動します。

ブラウザでhttp://localhost:3001 にアクセスし、本の登録と一覧表示、削除ができることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/b129ca60-fe08-1302-f405-4c7e53c1e4d7.png)


# デプロイ前のコード修正

nextjs/http-commons.tsを以下のように編集します。
本番環境の時はHerokuにデプロイしたRailsアプリのURLにリクエストを送るようにします。

```ts:http-common.ts
import axios from "axios";

export default axios.create({
    baseURL:
    process.env.NODE_ENV === "production"
    ? process.env.NEXT_PUBLIC_RAILS_API_URL
    : "http://localhost:3000",
    headers: {
        "Content-type":"application/json",
    },
});
```

rails-api/config/mongoid.ymlに以下のコードを追記します。
本番環境でのMongoDB Atlasの接続用の文字列を指定します。

```yml:mongoid.yml(末尾)
production:
  clients:
    default:
      uri: <%= ENV['MONGODB_URL'] %>
      options:
        server_selection_timeout: 5
```

rails-api/config/initializers/cors.rbを以下のように編集します。
本番環境で、VercelにデプロイしたNext.jsアプリからのリクエストを許可するようにします。

```rb:cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3001', ENV['FRONTEND_URL'] || ''

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

# デプロイ

## MongoDB Atlas

[MongoDB Atlas](https://www.mongodb.com/atlas)のアカウントを作成します。

ログイン後、Atlasでデータベースを作成します。
「Build a Database」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/39d007c9-8589-4f93-23af-7f04bc8f65fe.png)

無料プランであるSharedを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/1db8e053-8786-5ed9-4684-6622a4245311.png)

プロバイダとリージョンを選びます。
リージョンはTokyoにした方が良いと思ったのですが、Herokuの無料プランのリージョンはUnited StatesかEuropeしか選べないので、それに合わせてus-east-1リージョンを選んだ方が良い気がします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/5b4fd58e-95e5-ca6f-32ca-11e296619b80.png)

作成して数分経つと利用できるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/8ea483c1-fcd4-c252-7057-04750c931201.png)

「Connect」をクリックし、接続できるIPアドレスとユーザー名、パスワードを設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/50a77d85-742a-44e4-4c0f-5d77e3cc12d0.png)

Choose a connection methodでは「Connect with the MongoDB Shell」を選びます。
「Select your mongo shell version」では4.4(できればUbuntuにインストールしたMongoDBのバージョン)を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/8e0368ec-5ce7-43e1-c54f-087522423604.png)

表示された文字列をUbuntu上で実行します。

```
mongo "mongodb+srv://cluster0.uhkba.mongodb.net/myFirstDatabase" --username username
```

ログインできたら、データベースとコレクションを作成します。

```
use book_manager_db;
db.createCollection('books');
```

もう一度MongoDB Atlasのページ上で「Connect」をクリックし、Choose a connection methodでは「Connect your application」を選びます。
DRIVERに「Ruby」、VERSIONに「2.5 or later」を選ぶと表示される接続用の文字列をメモしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/bd25508e-7ace-53ff-da2b-5240c25ba785.png)

## Herokuへのデプロイ

[Heroku](https://jp.heroku.com)のアカウントを作成します。

ログインが完了したら、「Create new app」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/28d616dd-f8f8-1769-73ce-1932be34a842.png)

アプリ名を入力して「Create app」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/3dc09ba8-0b44-2619-6053-ec7bc7912238.png)

アプリが作成できたら、Ubuntu上で以下のコマンドを実行し、Heroku CLIをインストールします。

```
curl https://cli-assets.heroku.com/install-ubuntu.sh | sh
```

gitのユーザー名とメールアドレスを設定します。

```
git config --global user.name "設定したい名前"
git config --global user.email "設定したいメールアドレス"
```

RailsプロジェクトでGitのコミットをします。

```
cd ~/projects/book-manager/rails-api
git add .
git commit -m "Initial commit"
```

Herokuにログインし、プッシュします。

```
heroku login
heroku git:remote -a アプリ名
git push heroku master
```

## Vercelへのデプロイ

[Vercel](https://vercel.com)のアカウントを作成します。

Ubuntu上で以下のコマンドを実行して、Vercel CLIをインストールし、デプロイします。

```
cd ~/projects/book-manager/nextjs
sudo npm i -g vercel
vercel
```

## 環境変数の設定

## Vercelでの環境変数の設定

[Vercelのダッシュボード](https://vercel.com/dashboard)を開き、先ほどデプロイしたプロジェクトを選びます。

Settings > Environment Variablesを開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/8ab12b4d-faec-58d4-a945-bf37db2e9c5b.png)

以下の環境変数を設定します。

環境変数名(キー)|説明
---|---
NEXT_PUBLIC_RAILS_API_URL|HerokuにデプロイしたRailsアプリのURL

環境変数を反映させるために、再度デプロイを行ってください。

## Herokuでの環境変数の設定

[Herokuのダッシュボード](https://dashboard.heroku.com/apps)でプロジェクトを選び、settings > Config Varsより以下の環境変数を設定します。

環境変数名(キー)|説明
---|---
MONGODB_URL|MongoDB Atlasの接続用の文字列
FRONTEND_URL|Vercelでデプロイしたアプリのドメイン


```plaintext:接続用の文字列の例
mongodb+srv://ユーザー名:パスワード@cluster0.uhkba.mongodb.net/book_manager_db?retryWrites=true&w=majority
```

環境変数を反映させるため、「Restart all dynos」を選んでアプリを再起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183281/4f2d4e3d-638e-fa4a-ea03-4bc0bb9c4255.png)

これでデプロイが完了しました！
Vercelにデプロイしたアプリが正常に動作しているか確認しましょう。

# Herokuで稼働しているアプリをスリープさせないようにする方法

Herokuの無料プランでは、アプリに30分以上アクセスがないとスリープしてしまい、次にアクセスされてからアプリが起動するので応答が遅くなってしまいます。
対策として、[cron-job.org](https://cron-job.org)などのサービスを利用して、定期的にアプリにアクセスさせる方法があります。
ただし、クレジットカード認証をしていない場合、月に550時間しか稼働させられません(認証をすると月1000時間)。深夜～早朝などの利用者が少ない時間帯は自動アクセスしないようにして、稼働時間を温存すると良いでしょう。

# 参考記事

https://www.aise.ics.saitama-u.ac.jp/~gotoh/UbuntuOnWSL2.html

https://docs.microsoft.com/ja-jp/windows/wsl/tutorials/wsl-database#install-mongodb

# 今回作成したアプリのソースコード

GitLabに上げています。

Ruby on Railsアプリ

https://gitlab.com/ToshihiroWatanabe/book-manager-rails-api

Next.jsアプリ

https://gitlab.com/ToshihiroWatanabe/book-manager-nextjs