StrapiをHerokuで動かすまで

# はじめに

※本記事は[Strapi Advent Calender 2020](https://qiita.com/advent-calendar/2020/strapi)の17日目の投稿になります

[Strapi](https://strapi.io/documentation/v3.x/getting-started/introduction.html)はHeadlessCMSとして結構長く開発されていて、なかなか使い勝手が良いということで、弊社でもSIの中で実際に利用させて頂いております。

弊社は、主にSalesforce+Herokuの組合せでシステムを提供することが多く、StrapiをHeroku上で稼働しています。
そんなわけで、Herokuで動かすあたりの話をしてみたいと思います。

※ちなみに、公式ドキュメント : [Deployment > Heroku](https://strapi.io/documentation/v3.x/deployment/heroku.html) も充実してますね。

### 環境について

(手元のバージョン情報など)

* Node.js: 12.18.2
* Postgres: 13.0
* Strapi: 3.3.4
* heroku cli: 7.4.73

ちなみに、Herokuでのサポート状況は、現状こんな感じです。

* Node.js: `10.x`, `12.x`, `14,x`, `15.x`
    * [参考 Heroku Node.js Support](https://devcenter.heroku.com/articles/nodejs-support)
* Postgres: `12`(default), `11`, `10`, `9.6`
    * [参考 Heroku Postgres Version Support](https://devcenter.heroku.com/articles/heroku-postgresql#version-support)

## 1. Strapiの新規プロジェクトを作成

### 1-1. Strapi Appの作成

まずはローカルでStrapiアプリケーションを作成します。

```bash

$ strapi new try-strapi-334
Creating a new Strapi application at xxx

? Choose your installation type Custom (manual settings)
? Choose your default database client postgres
? Database name: try_strapi_334
? Host: 127.0.0.1
? Port: 5432
? Username: 
? Password:
? Enable SSL connection: No
```

DBにはPostgresを指定して作成します。
後々Herokuのデプロイすることを考えると、Postgresにしておくのがベターだと思います。

### 1-2. Heroku向けに修正

#### Nodeバージョンの設定 (任意)

`package.json`で、Node.jsのバージョンをローカルに合わせておきます。
デフォルトでは以下のようになっています。herokuにデプロイすると、`14.15.x`が使われるようになります。
別に14系が使われても問題なく稼働できますが、ローカルと合わせておきたい場合には変更しておきます。

```json

"engines": {
  "node": ">=10.16.0 <=14.x.x",
  "npm": ">=6.0.0"
}
```

↓

```json

"engines": {
  "node": "12.18.x",
  "npm": ">=6.0.0"
}
```

#### pg のインストール

Strapiのドキュメントにも記載されていますが、`pg`をインストールしていないと、herokuにデプロイした場合に起動でエラーになることがあるようです。

```bash

$ npm install pg --save
```

#### DB接続設定 (と、pg-connection-string のインストール)

まず、`pg-connection-string`をインストールしておきます。これはpostgresへの接続情報の文字列をパースして、`host`, `port`, `database`, `user`, `password`を抽出してくれるものです。

```bash

$ npm install pg-connection-string --save
```

Heroku Postgresを使う場合、接続情報は`DATABASE_URL`というHeroku環境変数にセットされます。
この環境変数にセットされている文字列を取得して、Postgresへの接続に利用します。

Herokuでは、Postgresへの接続情報が変更されることがあります。
[参考: Connecting to Heroku Postgres Databases from Outside of Heroku > Credentials](https://devcenter.heroku.com/articles/connecting-to-heroku-postgres-databases-from-outside-of-heroku#credentials)
そのため、`DATABASE_URL`に指定されてる文字列をパースして取得した情報で接続するよう`/config/databasejs`を修正します。

```javascript

const parse = require('pg-connection-string').parse;
const config = parse(process.env.DATABASE_URL);

module.exports = ({ env }) => ({
  defaultConnection: 'default',
  connections: {
    default: {
      connector: 'bookshelf',
      settings: {
        client: 'postgres',
        host: config.host,
        port: config.port,
        database: config.database,
        username: config.user,
        password: config.password,
      },
      options: {
        ssl: false,
        debug: env.bool('DATABASE_DEBUG', false),
      },
    },
  },
});
```

※ `DATABASE_DEBUG` はついでです。デバッグモードの切替のために自分で追加します。

#### Procfileの追加

Herokuアプリにはプロセスタイプ(Web/Worker)を指定するために`Procfile`をトップディレクトリ内に設置します。
内容はこんな感じです。 `<process type>: <command>` という具合にプロセスの種類と、起動時のコマンドを指定します。

```
web: npm run start
```

[参考: The Procfile](https://devcenter.heroku.com/articles/procfile)

## 2. Herokuの新規プロジェクトを作成

※Herokuのアカウント作成やcliの設定は割愛します。(すみません)

### 2-1. Heroku Appを作成

```bash

$ heroku apps:create try-strapi-334
Creating ⬢ try-strapi-334... done
https://try-strapi-334.herokuapp.com/ | https://git.heroku.com/try-strapi-334.git
```

### 2-2. Heroku Postgresの設定

先ほど作成したアプリ名を指定して、Postgresをプロビジョニングします。
ここではhobby-devプランを利用します。(無償プラン)

```bash

$ heroku addons:create heroku-postgresql:hobby-dev -a try-strapi-334
Creating heroku-postgresql:hobby-dev on ⬢ try-strapi-334... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-octagonal-79618 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation
```

ここまでで、Herokuダッシュボードを見ると、このようになります。（きっと

![heroku_dashboad_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840/17be4693-832b-a601-e590-8963b769cfc2.png)

### 2-3. Herokuへのデプロイ

herokuのリポジトリにgit pushできるよう設定します。

```bash

$ heroku git:remote -a try-strapi-334
set git remote heroku to https://git.heroku.com/try-strapi-334.git

$ git remote -v
heroku  https://git.heroku.com/try-strapi-334.git (fetch)
heroku  https://git.heroku.com/try-strapi-334.git (push)
```

(Herokuでビルドしている間は、数分程度お待ちください...)

```bash
$ git push heroku master

Enumerating objects: 27, done.
Counting objects: 100% (27/27), done.
Delta compression using up to 8 threads
Compressing objects: 100% (20/20), done.
Writing objects: 100% (27/27), 151.22 KiB | 7.96 MiB/s, done.
Total 27 (delta 0), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote: 
remote: -----> Node.js app detected
remote:        
remote: -----> Creating runtime environment
remote:        
remote:        NPM_CONFIG_LOGLEVEL=error
remote:        NODE_ENV=production
remote:        NODE_MODULES_CACHE=true
remote:        NODE_VERBOSE=false
remote:        
remote: -----> Installing binaries
remote:        engines.node (package.json):  >=10.16.0 <=14.x.x
remote:        engines.npm (package.json):   >=6.0.0
remote:        
remote:        Resolving node version >=10.16.0 <=14.x.x...
remote:        Downloading and installing node 14.15.2...
remote:        Bootstrapping npm >=6.0.0 (replacing 6.14.9)...
remote:        npm >=6.0.0 installed
remote:        
remote: -----> Installing dependencies
remote:        Installing node modules
remote:        
remote:        > @fortawesome/fontawesome-common-types@0.2.32 postinstall /tmp/build_68766d92/node_modules/@fortawesome/fontawesome-common-types
remote:        > node attribution.js
remote:        
remote:        Font Awesome Free 0.2.32 by @fontawesome - https://fontawesome.com
remote:        License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
remote:        
remote:        
remote:        > @fortawesome/free-solid-svg-icons@5.15.1 postinstall /tmp/build_68766d92/node_modules/@fortawesome/free-solid-svg-icons
remote:        > node attribution.js
remote:        
remote:        Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
remote:        License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
remote:        
remote:        
remote:        > @fortawesome/free-regular-svg-icons@5.15.1 postinstall /tmp/build_68766d92/node_modules/@fortawesome/free-regular-svg-icons
remote:        > node attribution.js
remote:        
remote:        Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
remote:        License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
remote:        
remote:        
remote:        > @fortawesome/fontawesome-svg-core@1.2.32 postinstall /tmp/build_68766d92/node_modules/@fortawesome/fontawesome-svg-core
remote:        > node attribution.js
remote:        
remote:        Font Awesome Free 1.2.32 by @fontawesome - https://fontawesome.com
remote:        License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
remote:        
remote:        
remote:        > @fortawesome/fontawesome-free@5.15.1 postinstall /tmp/build_68766d92/node_modules/@fortawesome/fontawesome-free
remote:        > node attribution.js
remote:        
remote:        Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
remote:        License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
remote:        
remote:        
remote:        > sharp@0.26.2 install /tmp/build_68766d92/node_modules/sharp
remote:        > (node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)
remote:        
remote:        info sharp Downloading https://github.com/lovell/sharp-libvips/releases/download/v8.10.0/libvips-8.10.0-linux-x64.tar.br
remote:        
remote:        > core-js@3.8.1 postinstall /tmp/build_68766d92/node_modules/fbjs/node_modules/core-js
remote:        > node -e "try{require('./postinstall')}catch(e){}"
remote:        
remote:        
remote:        > fsevents@1.2.13 install /tmp/build_68766d92/node_modules/webpack-dev-server/node_modules/fsevents
remote:        > node install.js
remote:        
remote:        
remote:        Skipping 'fsevents' build as platform linux is not supported
remote:        
remote:        > fsevents@1.2.13 install /tmp/build_68766d92/node_modules/watchpack-chokidar2/node_modules/fsevents
remote:        > node install.js
remote:        
remote:        
remote:        Skipping 'fsevents' build as platform linux is not supported
remote:        
remote:        > @fortawesome/free-brands-svg-icons@5.15.1 postinstall /tmp/build_68766d92/node_modules/@fortawesome/free-brands-svg-icons
remote:        > node attribution.js
remote:        
remote:        Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
remote:        License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
remote:        
remote:        
remote:        > core-js@2.6.12 postinstall /tmp/build_68766d92/node_modules/core-js
remote:        > node -e "try{require('./postinstall')}catch(e){}"
remote:        
remote:        
remote:        > strapi@3.3.4 postinstall /tmp/build_68766d92/node_modules/strapi
remote:        > node lib/utils/success.js
remote:        
remote:        added 1653 packages in 42.239s
remote:        
remote: -----> Build
remote:        Running build
remote:        
remote:        > try-strapi-334@0.1.0 build /tmp/build_68766d92
remote:        > strapi build
remote:        
remote:        Building your admin UI with production configuration ...
remote:        ℹ Compiling Webpack
remote:        ✔ Webpack: Compiled successfully in 1.75m
remote:        
remote: -----> Caching build
remote:        - node_modules
remote:        
remote: -----> Pruning devDependencies
remote:        removed 14 packages and audited 1640 packages in 12.674s
remote:        
remote:        95 packages are looking for funding
remote:          run `npm fund` for details
remote:        
remote:        found 0 vulnerabilities
remote:        
remote:        
remote: -----> Build succeeded!
remote: -----> Discovering process types
remote:        Procfile declares types     -> (none)
remote:        Default types for buildpack -> web
remote: 
remote: -----> Compressing...
remote:        Done: 142.1M
remote: -----> Launching...
remote:        Released v5
remote:        https://try-strapi-334.herokuapp.com/ deployed to Heroku
remote: 
```

ここまで来るとダッシュボードにも情報が追加されます。

![heroku_dashboad_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840/1ff13ec5-3047-8c0d-a10e-931798b5bf41.png)

ここで、`Open app`ボタンをクリックすると...いつもの画面が表示されます！（きっと

![strapi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840/272584a4-859b-b9e3-1716-401a460d20ab.png)

お疲れ様でした！

## 3. その他

### 3-1. 画像ファイルのアップロードについて

Herokuアプリが動いているDynoでは、静的ファイルを後から永続化することはできません。
※一時的に追加することはできますが、運が悪いと即消失します。
[参考: Dyno > Ephemeral filesystem](https://devcenter.heroku.com/articles/dynos#ephemeral-filesystem)

Dynoは定期的に廃棄/新規作成するようになっております。その際には、Herokuのリポジトリにあるファイルもしくはgithubと繋がっていれば、そのリポジトリから新たに作成するような仕組みなので、ビルドした後に追加したファイルは、漏れなく廃棄されます。

そのため、画像ファイルなどはCloudinaryもしくはS3に格納することが推奨されています。

幸い、Strapiのupload pluginにはCloudinary/S3を使うためのプラグインがありますので、それを使いましょう。

* [strapi-provider-upload-cloudinary](https://www.npmjs.com/package/strapi-provider-upload-cloudinary)
* [strapi-provider-upload-aws-s3](https://www.npmjs.com/package/strapi-provider-upload-aws-s3)

（さらに）Herokuでは、cloudinaryのaddonもあります（無料プランで）。

[heroku addon: cloudinary](https://elements.heroku.com/addons/cloudinary)

### 3-2. Content Typeの編集について

3-1.でも触れましたが、Herokuではビルド後に静的ファイルを永続化することはできません。従って、ContentTypeを定義する際には

1. ローカルでContentTypeを定義する
2. 生成したファイルをgitリポジトリにコミットする
3. Herokuにデプロイする

という流れになります。

### 3-3. Postgresのプランをstandard-0以上にした場合

Standardプラン以上にすると、Postgresの接続設定でSSLをONにしないと以下のようなエラーが出て接続できません。
※hobby-devプランやhobby-basicプランでは適用されません。

```
[2020-12-17T06:39:02.647Z] error error: no pg_hba.conf entry for host "xx.xx.xx.xx", user "xxxx", database "xxxx", SSL off
```

従って、Postgresのプランをstandard-xや、premium-x以上にした場合には`config/database.js`の設定もこんな感じに変わるかと思います。
`DATABASE_SSL`はHeroku環境変数として定義します。開発用などではhobby-devプラン or hobby-basicプランを使うことも多いでしょうから、そのアプリでは`DATABASE_SSL`は`false`にしておいて、本番環境用のアプリなど、SSL ONが必須な場合には`DATABASE_SSL`を`true`に設定しておけば良いと思います。

```javascript

const parse = require('pg-connection-string').parse;
const config = parse(process.env.DATABASE_URL);

module.exports = ({ env }) => {

  return {
    defaultConnection: 'default',
    connections: {
      default: {
        connector: 'bookshelf',
        settings: {
          client: 'postgres',
          host: config.host,
          port: config.port,
          database: config.database,
          username: config.user,
          password: config.password,
          ...env.bool('DATABASE_SSL', false) && {
            ssl: { rejectUnauthorized: false },
          },
        },
        options: {
          debug: env.bool('DATABASE_DEBUG', false),
        },
      },
    }
  };
};
```

[参考: Heroku Postgres > connecting in Node.js](https://devcenter.heroku.com/articles/heroku-postgresql#connecting-in-node-js)

# おしまい

Herokuも良いものですw