CLOVER🍀

That was when it all began.

Dockerコンテナ内で、PM2を使ってアプリケーションを起動する

これは、なにをしたくて書いたもの?

前に、PM2を使ってNode.jsアプリケーションのクラスター化をしてみました。

PM2を使って、Node.jsアプリケーションをクラスター化(CPUスケーリング)させてみる - CLOVER🍀

今度は、Dockerコンテナ内でPM2を使い、アプリケーションをCPUスケーリングできるようにしたいと思います。

Dockerコンテナ内でPM2を使う

PM2の方にドキュメントがあります。

PM2 - Docker Integration

簡単に言うと、PM2をグローバルインストールして、pm2-runtimeを使ってアプリケーションを起動すればOKです。

pm2-runtimeは、Node.jsバイナリの代わりに使えるCLIです。

pm2-runtime is a drop-in replacement node.js binary with some interesting production features

Docker Integration / pm2-runtime Helper

Launch PM2 in no deamon

あと、Graceful Shutdownについても見ておいた方がよいでしょう。

PM2 - Graceful Start/Shutdown

ちなみに、PM2自体のDockerイメージもあるみたいですが、今回はパスします。

DockerHub / keymetrics/pm2

環境

今回の環境は、こちら。

$ docker version
Client: Docker Engine - Community
 Version:           20.10.12
 API version:       1.41
 Go version:        go1.16.12
 Git commit:        e91ed57
 Built:             Mon Dec 13 11:45:33 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.12
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.12
  Git commit:       459d0df
  Built:            Mon Dec 13 11:43:42 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.12
  GitCommit:        7b11cfaabd73bb80907dd23182b9347b4245eb5d
 runc:
  Version:          1.0.2
  GitCommit:        v1.0.2-0-g52b36a2
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

アプリケーションを作っている時に使ったNode.js、npmのバージョンはこちらですが、最終的にはDockerイメージに
なります(Node.jsは同じバージョンを選択しますが)。

$ node --version
v16.13.1


$ npm --version
8.1.2

アプリケーションの作成

まずはアプリケーションを作成します。TypeScriptでプロジェクトをセットアップ。

$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier

設定。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

.prettierrc.json

{
  "singleQuote": true
}

アプリケーションはExpressを使って作成することにします。

$ npm i express
$ npm i -D @types/node@v16 @types/express

依存関係は、このようになりました。

  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/morgan": "^1.9.3",
    "@types/node": "^16.11.18",
    "prettier": "2.5.1",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "express": "^4.17.2"
  }

scriptsはこのような定義にして、npm run buildでTypeScriptファイルをビルドできるようにしました。

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "format": "prettier --write src"
  },

この時点では、PM2はインストールしません。Dockerイメージを作成する際に、グローバルにインストールします。

ソースコードはこちら。

src/app.ts

import express from 'express';

const app = express();

const address = process.env['LISTEN_ADDRESS']
  ? process.env['LISTEN_ADDRESS']
  : '0.0.0.0';
const port = process.env['LISTEN_PORT']
  ? parseInt(process.env['LISTEN_PORT'], 10)
  : 3000;

const logger = (fun: () => any) => {
  console.log(`[${new Date().toISOString()}] ${fun.call(null)}`);
};

app.get('/', (req, res) => {
  logger(() => `access client[${req.ip}]`);

  res.json({
    message: 'Hello World',
  });
});

const server = app.listen(port, address, () =>
  logger(() => `server[${address}:${port}] startup.`)
);

process.on('SIGINT', () => {
  logger(() => 'SIGINT signal received: closing HTTP server.');
  server.close(() => {
    logger(() => 'HTTP server closed');
  });
});

process.on('SIGTERM', () => {
  logger(() => 'SIGTERM signal received: closing HTTP server.');
  server.close(() => {
    logger(() => 'HTTP server closed');
  });
});

いくつか標準出力へのログ出力を行いつつ、Graceful Shutdownもこちらを見ながら入れておきました。
今回は2つのドキュメントに習い、SIGINTSIGTERMをトラップするようにしています。

PM2 - Graceful Start/Shutdown

Health Checks and Graceful Shutdown

PM2に限って言えば、プロセス停止時にはSIGINTが送信されます。

1度、動作確認しておきましょう。

ビルド。

$ npm run build

起動。

$ node dist/app.js
[2022-01-04T10:43:49.704Z] server[0.0.0.0:3000] startup.

確認。

$ curl localhost:3000
{"message":"Hello World"}

この時のログ。

[2022-01-04T10:43:59.667Z] access client[127.0.0.1]

Ctrl-cで停止。

[2022-01-04T10:44:28.748Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:44:28.749Z] HTTP server closed

OKですね。

$ docker image build -t kazuhira/express-pm2:latest .

この時点ではPM2はまだ出てきていません。

Dockerイメージを作成する

次は、Dockerイメージを作成しましょう。

Dockerfileは、こんな感じで作成。

Dockerfile

FROM node:16.13.1-bullseye as builder

COPY src src
COPY tsconfig.json tsconfig.json
COPY package*.json ./

RUN npm ci && \
    npm run build

FROM node:16.13.1-bullseye

RUN mkdir /app && \
    useradd -m user && \
    chown user:user /app

USER user
WORKDIR /app

ENV NPM_CONFIG_PREFIX /home/user/node_modules
ENV LISTEN_ADDRESS 0.0.0.0
ENV LISTEN_PORT 3000
ENV PATH /home/user/node_modules/bin:${PATH}

EXPOSE 3000

COPY --from=builder --chown=user:user dist dist
COPY --chown=user:user package*.json ./

RUN mkdir /home/user/node_modules

RUN npm ci --production && \
    npm i -g pm2@5.1.2

ENTRYPOINT ["pm2-runtime", "-i", "max", "dist/app.js"]

ポイントは、こんな感じでしょうか。

  • ベースイメージは、Node.jsのオフィシャルイメージを利用
  • マルチステージビルドを使い、TypeScriptファイルのビルドとステージを分離
    • 実行時のコンテナにはdevDependenciesは含めない
  • ユーザーをrootではなく、一般ユーザーを作成してこちらを利用するように変更
    • 合わせて、npmパッケージのグローバルインストール先を変更
  • PM2はグローバルインストール

Node.jsのオフィシャルイメージは、グローバルモジュールのインストール先が/usr/local/lib/node_modules
なっているので、一般ユーザーでnpm install -gをしようとすると失敗します。

npm ERR! code EACCES
npm ERR! syscall mkdir
npm ERR! path /usr/local/lib/node_modules/pm2
npm ERR! errno -13
npm ERR! Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/pm2'
npm ERR!  [Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/pm2'] {
npm ERR!   errno: -13,
npm ERR!   code: 'EACCES',
npm ERR!   syscall: 'mkdir',
npm ERR!   path: '/usr/local/lib/node_modules/pm2'
npm ERR! }
npm ERR!
npm ERR! The operation was rejected by your operating system.
npm ERR! It is likely you do not have the permissions to access this file as the current user
npm ERR!
npm ERR! If you believe this might be a permissions issue, please double-check the
npm ERR! permissions of the file and its containing directories, or try running
npm ERR! the command again as root/Administrator.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/user/.npm/_logs/2022-01-04T09_52_14_306Z-debug.log

こちらを見て、修正。今回は/home/user/node_modulesとしました。

Resolving EACCES permissions errors when installing packages globally | npm Docs

いったん、コンテナで利用可能なCPUの数だけプロセスを起動するようにしています。

ENTRYPOINT ["pm2-runtime", "-i", "max", "dist/app.js"]

では、ビルド。

$ docker image build -t kazuhira/express-pm2:latest .

起動してみます。

$ docker container run -it --rm --name app kazuhira/express-pm2:latest

特にリソース制限を入れていないので、8つのプロセスが起動しました(ホスト側のCPUが8個のため)。

2022-01-04T10:58:43: PM2 log: Launching in no daemon mode
2022-01-04T10:58:43: PM2 log: App [app:0] starting in -cluster mode-
2022-01-04T10:58:43: PM2 log: App [app:0] online
2022-01-04T10:58:43: PM2 log: App [app:1] starting in -cluster mode-
2022-01-04T10:58:43: PM2 log: App [app:1] online
2022-01-04T10:58:43: PM2 log: App [app:2] starting in -cluster mode-
2022-01-04T10:58:43: PM2 log: App [app:2] online
2022-01-04T10:58:43: PM2 log: App [app:3] starting in -cluster mode-
2022-01-04T10:58:43: PM2 log: App [app:3] online
2022-01-04T10:58:43: PM2 log: App [app:4] starting in -cluster mode-
2022-01-04T10:58:43: PM2 log: App [app:4] online
2022-01-04T10:58:43: PM2 log: App [app:5] starting in -cluster mode-
2022-01-04T10:58:43: PM2 log: App [app:5] online
2022-01-04T10:58:43: PM2 log: App [app:6] starting in -cluster mode-
[2022-01-04T10:58:43.901Z] server[0.0.0.0:3000] startup.
2022-01-04T10:58:43: PM2 log: App [app:6] online
2022-01-04T10:58:43: PM2 log: App [app:7] starting in -cluster mode-
[2022-01-04T10:58:43.971Z] server[0.0.0.0:3000] startup.
[2022-01-04T10:58:43.977Z] server[0.0.0.0:3000] startup.
2022-01-04T10:58:43: PM2 log: App [app:7] online
[2022-01-04T10:58:44.055Z] server[0.0.0.0:3000] startup.
[2022-01-04T10:58:44.083Z] server[0.0.0.0:3000] startup.
[2022-01-04T10:58:44.121Z] server[0.0.0.0:3000] startup.
[2022-01-04T10:58:44.152Z] server[0.0.0.0:3000] startup.
[2022-01-04T10:58:44.209Z] server[0.0.0.0:3000] startup.

動作確認。

$ APP_HOST=`docker container inspect app | jq -r '.[].NetworkSettings.IPAddress'`
$ curl $APP_HOST:3000
{"message":"Hello World"}

OKですね。

この時のログ。

[2022-01-04T10:59:41.393Z] access client[172.17.0.1]

Ctrl-cで停止してみます。

こんな感じでアプリケーション自身の停止処理が動作した後、

[2022-01-04T10:59:54.609Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.611Z] HTTP server closed
[2022-01-04T10:59:54.609Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.611Z] HTTP server closed
[2022-01-04T10:59:54.609Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.611Z] HTTP server closed
[2022-01-04T10:59:54.609Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.611Z] HTTP server closed
[2022-01-04T10:59:54.609Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.611Z] HTTP server closed
[2022-01-04T10:59:54.609Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.611Z] HTTP server closed
[2022-01-04T10:59:54.609Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.611Z] HTTP server closed
[2022-01-04T10:59:54.612Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.612Z] HTTP server closed
2022-01-04T10:59:54: PM2 log: Stopping app:app id:0
2022-01-04T10:59:54: PM2 log: Stopping app:app id:1
2022-01-04T10:59:54: PM2 log: Stopping app:app id:2
2022-01-04T10:59:54: PM2 log: Stopping app:app id:3
2022-01-04T10:59:54: PM2 log: Stopping app:app id:4
2022-01-04T10:59:54: PM2 log: Stopping app:app id:5
2022-01-04T10:59:54: PM2 log: Stopping app:app id:6
2022-01-04T10:59:54: PM2 log: Stopping app:app id:7
[2022-01-04T10:59:54.713Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.713Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.714Z] HTTP server closed
[2022-01-04T10:59:54.713Z] HTTP server closed
[2022-01-04T10:59:54.713Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.714Z] HTTP server closed
[2022-01-04T10:59:54.713Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.714Z] HTTP server closed
[2022-01-04T10:59:54.713Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.714Z] HTTP server closed
[2022-01-04T10:59:54.713Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.714Z] HTTP server closed
[2022-01-04T10:59:54.713Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.714Z] HTTP server closed
[2022-01-04T10:59:54.714Z] SIGTERM signal received: closing HTTP server.
[2022-01-04T10:59:54.715Z] HTTP server closed

最終的にPM2も停止します。

2022-01-04T10:59:56: PM2 log: pid=31 msg=failed to kill - retrying in 100ms
2022-01-04T10:59:56: PM2 log: pid=24 msg=failed to kill - retrying in 100ms
2022-01-04T10:59:56: PM2 log: pid=17 msg=failed to kill - retrying in 100ms
2022-01-04T10:59:56: PM2 log: pid=82 msg=failed to kill - retrying in 100ms
2022-01-04T10:59:56: PM2 log: Process with pid 71 still alive after 1600ms, sending it SIGKILL now...
2022-01-04T10:59:56: PM2 log: Process with pid 60 still alive after 1600ms, sending it SIGKILL now...
2022-01-04T10:59:56: PM2 log: Process with pid 49 still alive after 1600ms, sending it SIGKILL now...
2022-01-04T10:59:56: PM2 log: Process with pid 38 still alive after 1600ms, sending it SIGKILL now...
2022-01-04T10:59:56: PM2 log: Process with pid 31 still alive after 1600ms, sending it SIGKILL now...
2022-01-04T10:59:56: PM2 log: Process with pid 24 still alive after 1600ms, sending it SIGKILL now...
2022-01-04T10:59:56: PM2 log: Process with pid 17 still alive after 1600ms, sending it SIGKILL now...
2022-01-04T10:59:56: PM2 log: pid=82 msg=failed to kill - retrying in 100ms
2022-01-04T10:59:56: PM2 log: Process with pid 82 still alive after 1600ms, sending it SIGKILL now...
2022-01-04T10:59:56: PM2 log: App name:app id:2 disconnected
2022-01-04T10:59:56: PM2 log: App name:app id:1 disconnected
2022-01-04T10:59:56: PM2 log: App name:app id:3 disconnected
2022-01-04T10:59:56: PM2 log: App [app:1] exited with code [0] via signal [SIGKILL]
2022-01-04T10:59:56: PM2 log: App [app:2] exited with code [0] via signal [SIGKILL]
2022-01-04T10:59:56: PM2 log: App [app:3] exited with code [0] via signal [SIGKILL]
2022-01-04T10:59:56: PM2 log: App [app:4] exited with code [0] via signal [SIGKILL]
2022-01-04T10:59:56: PM2 log: App [app:5] exited with code [0] via signal [SIGKILL]
2022-01-04T10:59:56: PM2 log: App [app:6] exited with code [0] via signal [SIGKILL]
2022-01-04T10:59:56: PM2 log: App name:app id:4 disconnected
2022-01-04T10:59:56: PM2 log: App name:app id:5 disconnected
2022-01-04T10:59:56: PM2 log: App name:app id:6 disconnected
2022-01-04T10:59:56: PM2 log: App name:app id:0 disconnected
2022-01-04T10:59:56: PM2 log: App [app:0] exited with code [0] via signal [SIGKILL]
2022-01-04T10:59:56: PM2 log: App name:app id:7 disconnected
2022-01-04T10:59:56: PM2 log: App [app:7] exited with code [0] via signal [SIGKILL]
2022-01-04T10:59:56: PM2 log: pid=24 msg=process killed
2022-01-04T10:59:56: PM2 log: pid=31 msg=process killed
2022-01-04T10:59:56: PM2 log: pid=38 msg=process killed
2022-01-04T10:59:56: PM2 log: pid=49 msg=process killed
2022-01-04T10:59:56: PM2 log: pid=60 msg=process killed
2022-01-04T10:59:56: PM2 log: pid=71 msg=process killed
2022-01-04T10:59:56: PM2 log: pid=17 msg=process killed
2022-01-04T10:59:56: PM2 log: pid=82 msg=process killed
2022-01-04T10:59:56: PM2 log: PM2 successfully stopped

OKですね。

コンテナのCPUリソースを制限したい

と思ったのですが、PM2はホスト側のCPU数を見てしまうようです。

PM2 failed at detect cpu core count · Issue #4347 · Unitech/pm2 · GitHub

これは、Node.jsがそうだからみたいです。

Dockerコンテナ内で動作するNode.jsが認識するCPU数、メモリサイズは、ホスト側のものになるという話 - CLOVER🍀

ということは、PM2を使って起動するプロセス数はmaxではなく具体的に指定した方が良さそうですね。

設定ファイルを使う

設定ファイルを使うパターンも試してみましょう。

Docker Integration / Starting a configuration file

こちらで作成。プロセス数は2にしました。

ecosystem.config.js

module.exports = {
  apps : [{
    name   : "express-app",
    script : "./dist/app.js",
    instances: 2,
    exec_mode: "cluster"
  }]
}

DockerfileCOPYの部分と

COPY --from=builder --chown=user:user dist dist
COPY --chown=user:user package*.json ./
COPY --chown=user:user ecosystem.config.js ecosystem.config.js

ENTRYPOINTに反映します。

# ENTRYPOINT ["pm2-runtime", "-i", "max", "dist/app.js"]
ENTRYPOINT ["pm2-runtime", "ecosystem.config.js"]

ビルド。

$ docker image build -t kazuhira/express-pm2:latest .

コンテナを起動すると、設定ファイルの内容を元にプロセスが起動されます。

$ docker container run -it --rm --name app --cpus 2 kazuhira/express-pm2:latest
2022-01-04T12:22:25: PM2 log: Launching in no daemon mode
2022-01-04T12:22:25: PM2 log: App [express-app:0] starting in -cluster mode-
2022-01-04T12:22:25: PM2 log: App [express-app:0] online
2022-01-04T12:22:25: PM2 log: App [express-app:1] starting in -cluster mode-
2022-01-04T12:22:25: PM2 log: App [express-app:1] online
[2022-01-04T12:22:25.774Z] server[0.0.0.0:3000] startup.
[2022-01-04T12:22:25.774Z] server[0.0.0.0:3000] startup.

ログ

最後に、ログを見てみます。pm2-runtimeにオプションを指定することで、「PM2で起動したアプリケーションの」
ログフォーマットを変えられるみたいです。

Docker Integration / Logging Format option

今回は、ENTRYPOINTを使ってコンテナ内のプロセスを起動しているので、コンテナ実行時の引数にそのまま
オプションを指定すればOKです。

試しに、--jsonを指定してみます。

$ docker container run -it --rm --name app --cpus 2 kazuhira/express-pm2:latest --json

こんな感じになりました。

2022-01-04T12:27:47: PM2 log: Launching in no daemon mode
2022-01-04T12:27:47: PM2 log: App [express-app:0] starting in -cluster mode-
2022-01-04T12:27:47: PM2 log: App [express-app:0] online
2022-01-04T12:27:47: PM2 log: App [express-app:1] starting in -cluster mode-
{"timestamp":"2022-01-04T12:27:47.759Z","type":"process_event","status":"start","app_name":"express-app"}
{"timestamp":"2022-01-04T12:27:47.766Z","type":"process_event","status":"online","app_name":"express-app"}
2022-01-04T12:27:47: PM2 log: App [express-app:1] online
{"timestamp":"2022-01-04T12:27:47.791Z","type":"process_event","status":"start","app_name":"express-app"}
{"timestamp":"2022-01-04T12:27:47.795Z","type":"process_event","status":"online","app_name":"express-app"}
{"message":"[2022-01-04T12:27:47.959Z] server[0.0.0.0:3000] startup.","timestamp":"2022-01-04T12:27:47.960Z","type":"out","process_id":0,"app_name":"express-app"}
{"message":"[2022-01-04T12:27:47.979Z] server[0.0.0.0:3000] startup.","timestamp":"2022-01-04T12:27:47.980Z","type":"out","process_id":1,"app_name":"express-app"}
{"message":"[2022-01-04T12:27:54.605Z] access client[172.17.0.1]","timestamp":"2022-01-04T12:27:54.605Z","type":"out","process_id":0,"app_name":"express-app"}

PM2自身のログは、フォーマットを合わせてくれないみたいです…。

デフォルトは、--rawみたいですね。

$ docker container run -it --rm --name app --cpus 2 kazuhira/express-pm2:latest --raw
2022-01-04T12:29:38: PM2 log: Launching in no daemon mode
2022-01-04T12:29:38: PM2 log: App [express-app:0] starting in -cluster mode-
2022-01-04T12:29:38: PM2 log: App [express-app:0] online
2022-01-04T12:29:38: PM2 log: App [express-app:1] starting in -cluster mode-
2022-01-04T12:29:38: PM2 log: App [express-app:1] online
[2022-01-04T12:29:39.061Z] server[0.0.0.0:3000] startup.
[2022-01-04T12:29:39.085Z] server[0.0.0.0:3000] startup.
[2022-01-04T12:29:40.091Z] access client[172.17.0.1]

--formatだと、こんな感じになります。

$ docker container run -it --rm --name app --cpus 2 kazuhira/express-pm2:latest --format
2022-01-04T12:31:13: PM2 log: Launching in no daemon mode
2022-01-04T12:31:13: PM2 log: App [express-app:0] starting in -cluster mode-
2022-01-04T12:31:13: PM2 log: App [express-app:0] online
2022-01-04T12:31:13: PM2 log: App [express-app:1] starting in -cluster mode-
2022-01-04T12:31:13: PM2 log: App [express-app:1] online
timestamp=2022-01-04-12:31:13+0000 app=express-app id=0 type=out message=[2022-01-04T12:31:13.874Z] server[0.0.0.0:3000] startup.
timestamp=2022-01-04-12:31:13+0000 app=express-app id=1 type=out message=[2022-01-04T12:31:13.912Z] server[0.0.0.0:3000] startup.
timestamp=2022-01-04-12:31:15+0000 app=express-app id=0 type=out message=[2022-01-04T12:31:15.448Z] access client[172.17.0.1]

設定ファイルに書く場合は(ドキュメントに書かれていませんが)、log_typeで指定するようです。

ecosystem.config.js

module.exports = {
  apps : [{
    name   : "express-app",
    script : "./dist/app.js",
    instances: 2,
    exec_mode: "cluster",
    log_type: "json"
  }]
}

改行の扱いが微妙なので、オプションで指定した方が良いかもしれません…。

$ docker container run -it --rm --name app --cpus 2 kazuhira/express-pm2:latest
2022-01-04T12:40:42: PM2 log: Launching in no daemon mode
2022-01-04T12:40:42: PM2 log: App [express-app:0] starting in -cluster mode-
2022-01-04T12:40:42: PM2 log: App [express-app:0] online
2022-01-04T12:40:42: PM2 log: App [express-app:1] starting in -cluster mode-
2022-01-04T12:40:42: PM2 log: App [express-app:1] online
{"message":"[2022-01-04T12:40:42.577Z] server[0.0.0.0:3000] startup.\n","timestamp":"2022-01-04T12:40:42.578Z","type":"out","process_id":0,"app_name":"express-app"}
{"message":"[2022-01-04T12:40:42.616Z] server[0.0.0.0:3000] startup.\n","timestamp":"2022-01-04T12:40:42.616Z","type":"out","process_id":1,"app_name":"express-app"}

一応、PM2側のログフォーマットも変えられないか見てみましたが、こういうのなのでムリそうですね…。

    console.log(`App [${env_copy.name}:${env_copy.pm_id}] starting in -cluster mode-`)

https://github.com/Unitech/pm2/blob/5.1.2/lib/God/ClusterMode.js#L36

オマケ: PM2をnpxで使わないのは?

最初はPM2をローカルインストールしてnpxで起動しようと思い、こんな感じにしていたのですが。

ENTRYPOINT ["npx", "pm2-runtime", "-i", "max", "dist/app.js"]

これだと停止時にnpx越しに止めることになり、npxがエラーになります。

2022-01-04T09:50:12: PM2 log: PM2 successfully stopped                                                                                                                        
npm ERR! path /app                                                                                                                                                            
npm ERR! command failed                                                                                                                                                       
npm ERR! signal SIGINT                                                                                                                                                        
npm ERR! command sh -c pm2-runtime "-i" "max" "dist/app.js"

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/user/.npm/_logs/2022-01-04T09_50_12_797Z-debug.log

これが気持ち悪かったので、グローバルインストールに切り替えました…。

RUN npm ci --production && \
    npm i -g pm2@5.1.2

まとめ

Dockerコンテナ内で、PM2を使う方法について調べてみました。

単に起動するだけだったらそんなに苦労しないのですが、作成するコンテナイメージやリソースの設定に踏み込んでみた
結果、予想以上に時間をかけて調べることになりましたが、まあ良いかなと…。

Dockerコンテナ内でPM2を使う時には、覚えておきましょう。