😀

MERNフルスタックアプリをHerokuにデプロイする方法

2023/04/22に公開

はじめに

先日、MERNフルスタックのアプリケーションをローカルで開発したので、Herokuにデプロイしようと思ったら、意外と参照する記事が少なかったので、備忘録的に記事にしようかと思います。

Herokuを選択した理由

HerokuはPaasの1つで無料でデプロイできるので選択しました。
AWSでもできますが、無料期間を過ぎたため、コストがかかるので、それは避けたいと思ったためです。

デプロイ

まずはMERNプロジェクトを編集します。
私の場合、プロジェクトの階層が以下のようになっています。

app
 |
 |_ _ client
 |       |_ src
 |
 |_ _ server
 |       |_ src
 |
 |_ _ package.json
 |_ _ Procfile

後ほど、説明しますが、package.jsonProcfileはHerokuのデプロイで必須となるので、作成しておきます。

サーバー側の編集

:::note info
npm run buildしたものをサーバーで読み込む
:::
デプロイではクライアント側で生成するbuildフォルダをサーバーにインポートする必要があるので、そのための記述を加えます。

index.ts
const path = require("path");

const app: express.Express = express();

app.use(express.static(path.resolve(__dirname, "../client/build")));

app.get("*", (request, response) => {
	response.sendFile(path.resolve(__dirname, "../client/build", "index.html"));
});

上記を記述することで、アプリケーションがデプロイされた後、React Routerで定義されたルーティングにリクエストを送ることができます。

:::note info
ポートの設定
:::

次にポートの設定を行います。

index.ts
const app: express.Express = express();

// Express FWによるサーバーの立ち上げ
const PORT = process.env.PORT || 4000;

app.listen(PORT, () => {
	console.log(`Server is running on port ${PORT}`);
});

上記を追加します。
Herokuの場合、process.env.PORTとすることでHerokuのアプリケーション立ち上げ時のポート番号を取得することができます。

ちなみに、ローカルでは4000番を使用していたので、このような記述になっています。

全体の記述としては以下のようになります。

index.ts
import express from "express";
import mongoose from "mongoose";
import "dotenv/config";
const cors = require("cors");
const path = require("path");

const app: express.Express = express();

// CORS対応
app.use(
	cors({
		origin: "*",
		exposedHeaders: [
			"Content-Length",
			"Authorization",
			"Access-Control-Allow-Origin",
			"Access-Control-Allow-Headers",
		],
	})
);

// Express FWによるローカルサーバーの立ち上げ
const PORT = process.env.PORT || 4000;

const url = process.env.MONGODB_URL as string;

// jsonオブジェクトを扱うため
app.use(express.json());

// エンドポイントからAPIを呼び出す
app.use("/api/v1", require("./src/v1/routes"));

// DB接続
try {
	mongoose.set("strictQuery", true);
	mongoose.connect(url);
	console.log("DB接続");
} catch (e) {
	console.log(e);
}

app.use(express.static(path.resolve(__dirname, "../client/build")));

app.get("*", (request, response) => {
	response.sendFile(path.resolve(__dirname, "../client/build", "index.html"));
});

app.listen(PORT, () => {
	console.log(`Server is running on port ${PORT}`);
});

Herokuではクロスオリジンの推奨としてワイルドカードを指定しますが、本来であれば、環境変数などで定義する必要があります。

以上が、サーバー側の設定です。

クライアント側の編集

こちらは記述を1行変更するだけで完了します。

:::note info
BaseURLの変更
:::

REACT_APP_API_BASE_URL='https://<app-name>.herokuapp.com/<自身のAPIエンドポイント>'
axiosClient.ts
import axios from "axios";

const BASE_URL = process.env.REACT_APP_API_BASE_URL as string;

const axiosClient = axios.create({
	// エンドポイントとなるURLのベース
	baseURL: BASE_URL,
});

axiosClient.interceptors.response.use(
	(response) => {
		return response;
	},
	(err) => {
		throw err.response;
	}
);

export default axiosClient;

APIのエンドポイントとなるBASE_URLをHerokuのものにするだけで、クライアント側の修正は終了です。

package.json

次に、先ほど少し登場したプロジェクトのルートディレクトリに作成したpackage.jsonの中身を作成していきます。

内容としては、サーバー側のpackage.jsonとほとんど同じ大丈夫です。

package.json
{
   // scriptsより前の記述はサーバー側のpackage.jsonをコピー
  "scripts": {
    "start": "node_modules/.bin/ts-node server/index.ts",
    "start:dev": "nodemon --exec node_modules/.bin/ts-node server/index.ts",
    "heroku-postbuild": "npm install --only=dev --no-shrinkwrap && npm install --prefix client && npm run build --prefix client"
  }
  // devDependenciesやdependencies等もサーバー側のpackage.jsonをコピー
}

scriptsの各内容を説明すると、以下のようになります。

  • start・・・node_modulesのts-nodeを使用しindex.tsを起動(devDependenciesでts-nodeを記載しているため)
  • start:dev・・・開発環境用の起動コマンド。Herokuでnodemonの起動をするとエラーが出力され、起動しない場合があるため(npm run start:dev で実行)
  • heroku-postbuild・・・Herokuのデプロイで使用するスクリプト。

heroku-postbuildの細かい内容は以下になります。

heroku-postbuildのコマンド内容

  • npm install --only=dev --no-shrinkwrap・・・開発環境(devDependencies)のみの依存関係をインストールする。--no-shrinkwrapオプションはnpm-shrinkwrap.jsonを無視して、package.jsonから直接依存関係を読み取るように指示する
  • npm install --prefix client・・・clientディレクトリにある依存関係をインストールする。--prefixオプションは、特定のディレクトリに対して、操作を実行するために使用する。
  • nnpm run build --prefix client・・・clientディレクトrのbuildスクリプト(npm run build)を実行する

Procfile

Procfileというのは、Herokuアプリを起動時に実行したいコマンドやタスク、Webサーバー等を定義するためのファイルです。
詳しくは以下、公式を参照ください。

Procfile

内容ですが、1行のみで、アプリを起動するコマンドを記載します。

Procfile
web: npm run start

上記を記述することでHerokuのデプロイプロセスが完了した際に、アプリケーションが起動します。

以上で、プログラムの設定は終了です。

Heroku CLI

次にターミナルでHerokuのコマンドをポチポチしていきます。
※Heroku CLIをインストール済という前提で進めます。

zsh
% heroku login

このコマンドでHerokuのログイン画面が立ち上がるので、ユーザー情報を入力して、ログインしてください。

次にHerokuのアプリケーションを作成していきます。

zsh
% cd your-project
% heroku create app-name
Creating ⬢ app-name... done
https://app-name.herokuapp.com/ | https://git.heroku.com/app-name.git

Creating・・・doneと表示されれば成功です。
表示されたURLにアプリケーションがデプロイされます。

:::note alert
環境変数を設定
※サーバーサイドプロジェクトで環境変数を設定している場合は必須
:::
私の場合、MongoDBにアクセスするURL等を環境変数としているため、以下のように設定しました。

zsh
% heroku config:set MONGODB_URL='mongodb+srv://<username>:<password>@clusterX.xxxxx.mongodb.net/?retryWrites=true&w=majority'
% heroku config:set SECRET_KEY='xxxxxxxxxxxxxxxx'
% heroku config:set TOKEN_SECRET_KEY='xxxxxxxxxxxxxxxx'

このようにして設定できます。
以下のようにして、値が出力されれば、正しく設定できています。

zsh
% heroku config:get MONGODB_URL
mongodb+srv://<username>:<password>@clusterX.xxxxx.mongodb.net/?retryWrites=true&w=majority

続いてここまでの変更をgitにプッシュします。
(HerokuのアプリケーションもHeroku上のmaster(main)ブランチにプッシュする形でデプロイするため)
そして、herokuのmaster(main)ブランチにもプッシュ(デプロイ)していきます。

zsh
% git add .
% git commit -m "Prepare for deployment to Heroku"
% git push heroku master

※自身のgitでmainブランチを使用している場合は以下のようにします。

zsh
% git push heroku main

以下のようにターミナルに出力されればデプロイ成功です。

zsh
emote: Verifying deploy... done.
To https://git.heroku.com/your-app.git
   xxxxxxx..xxxxxxx  main -> main

以下、コマンドでアプリケーションのWebページを開いてみましょう。

zsh
% heroku open

:::note warn
Application Errorとなった場合
:::
上記でデプロイ自体は完了ですが、APIのエンドポイントが違ったり、ポート周辺の記述が違うなど、デプロイ後に画面表示できずApplication Errorとなる場合が多々あります。

その際は、以下のコマンドでログを出力してください。

zsh
% heroku logs --tail

これで、エラーの詳細を終えると思います。

以上で、MERNフルスタックアプリケーションのHerokuへのデプロイ方法についての解説を終了です。

参考になれば幸いです。

参考文献

Heroku を使用したフルスタック MERN アプリのデプロイ

Discussion