node.jsでベクトルタイルサーバを作成（ローカルホスト編）

# はじめに
ベクトルタイルの配信について知識を深めていますが、今回はNode.jsのモジュールであるexpressを用いたベクトルタイル配信について記事を書きます。一つのタイルが一つのファイルとなっているpbfファイル形式は特別な実装が不要な静的フォルダから配信することが出来るので、まずはそれについて記載し、その後MBTiles形式のデータからベクトルタイルを配信する方法について記載します。本記事は[こちら](https://qiita.com/T-ubu/items/545d9f995ef7496a2ec4)の記事を参考に、自分の中の整理も兼ねて記載しています。
最後に、pm2及びcronを利用したnode.jsサーバの永続化についても記載しています。

まずは、自身のPCをサーバとする方法をこちらの記事の記載します。今後、レンタルサーバからの記載を方法をいずれ記載します。

[こちら](https://github.com/k96mz/20240722hostingPracticeLocalhost)のGitHubのページにコードがあります。

# 静的フォルダからベクトルタイルを配信
まずは静的なフォルダからベクトルタイルの配信を行います。

 ```javascript:app0.js
// Set Module
const config = require('config');
const express = require('express');
const morgan = require('morgan');
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

// Configure constant
const port = config.get('port');
const htdocsPath = config.get('htdocsPath');
const morganFormat = config.get('morganFormat');
const logDirPath = config.get('logDirPath');

// Logger configuration
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new DailyRotateFile({
      filename: `${logDirPath}/server-%DATE%.log`,
      datePattern: 'YYYY-MM-DD',
    }),
  ],
});

logger.stream = {
  write: message => {
    logger.info(message.trim());
  },
};

// Middleware
const app = express();
app.use(
  morgan(morganFormat, {
    stream: logger.stream,
  })
);
app.use(express.static(htdocsPath));

app.listen(port, () => {
  console.log(`Starting server at port ${port}`);
});
```

```./config/default.hjson
{
  htdocsPath: htdocs
  port: 3000
  logDirPath: log
  morganFormat: tiny
  mbtilesDir: mbtiles
}
```
## コードの解説
メインのファイルがapp0.jsですが、そのファイルが参照している設定ファイルが、./config/default.hjsonです。このファイルの設定を変えることで、ログを出力するフォルダ、ポート番号などを変更することが出来ます。

### 【default.hson】
htdocsPath: htdocs 
静的フォルダを設定しています。このフォルダ以下に置かれたファイルがウェブ上で配信されます。

port: 3000
ポート番号を指定します。今回は3000としました。

logDirPath: log
ログが出力されるフォルダを指定します。

morganFormat: tiny
morganモジュールで出力するログのオプションを設定しています。「tiny」はその名の通り最小限のアウトプットをする設定で、リクエストのあったURL、レスポンスのステータスなどを返します。

mbtilesDir: mbtiles
ここでは使用していませんが、後ほど使用します。

### 【app0.js】
モジュールの説明

| 名前 | 説明 |
|:-:|:-:|
| config  | 設定用 |
| express  | Node.jsで利用できるフレームワーク  |
| morgan  | ログアプリ（HTTPリクエストとレスポンスに特化） |
| winston  | ログアプリ（様々な場所にログを出力出来る）  |
| DailyRotateFile  | Winstonのログの出力先の一つ。日毎にログファイルを切り替えながら保存  |

◯ロガーについてのイメージ
morganでログを出力し、winstonでコンソールとファイルにログを送信、DailyRotateFileで日毎に保存しています。morganとwinstonの組み合わせによって、リアルタイムのログ出力と長期的なログ保存の両方を実現しています。


 ```javascript:
// Logger configuration
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new DailyRotateFile({
      filename: `${logDirPath}/server-%DATE%.log`,
      datePattern: 'YYYY-MM-DD',
    }),
  ],
});
```
winston.createLoggerによってロガーを作成します。transportsオプションで出力先を指定します。new winston.transports.Console()は、ログメッセージをコンソールに出力します。

winston-daily-rotate-fileモジュールを使用して、日毎にログファイルをローテーション（切り替え）します。
filenameオプションで、ログファイルのパスと名前を指定します。%DATE%プレースホルダーは、datePatternオプションで指定された日付フォーマットに基づいて置き換えられます。ここでは、YYYY-MM-DD形式で日付を設定します。

 ```javascript
logger.stream = {
  write: message => {
    logger.info(message.trim());
  },
};

// Middleware
const app = express();
app.use(
  morgan(morganFormat, {
    stream: logger.stream,
  })
);
app.use(express.static(htdocsPath));
```

app.use(morgan...でmorganミドルウェアを設定しています。この記述をapp.use(express...の記述より上に記載する必要があります。これを下に書いていたためベクトルタイルが正常に200のステータスで返ってきているのにそのログが出力されず、データが存在しないベクトルタイルのルートから404のエラーログのみ返ってくるエラーが発生しました。ミドルウェアは記載する順番が重要です。
morganがログを吐き出すべきタイミングが来た時にオプション{stream: logger.stream}のwriteメソッドが呼び出されます。（本来はStreamオブジェクトを入れなければならないので良くない書き方ですが、おそらくここではStreamでなくても問題ないのでこのままでいきます。）
logger.info(message.trim())は、取得したメッセージをwinstonのinfoレベルのログとして記録します。message.trim()で余分な空白や改行を取り除いています。

以下のコマンドで実行できます。
```
npm install
node app0.js
```
http://localhost:3000/
を見に行くと以下のようなサイトが表示されます。

![スクリーンショット 2024-08-14 15.32.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/7d5e9d72-a8d0-e4f6-20dd-846ba64040f9.png)

./VTpractice/1/1/1.pbf
をクリックすると、ベクトルタイルファイルがダウンロードされます。

また、
./map/mapO-pbf.html
をクリックすると、以下のような地図が表示されます。

![スクリーンショット 2024-08-14 15.35.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/853d8b53-5226-66a6-ad43-d1e8e74a93c2.png)


ログファイル(./log/server-2024-08-14.log）（日付はその日の日付になります。）には、以下のようなものが書き出されます。コンソールについても同様です。

![スクリーンショット 2024-08-14 15.38.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/96b6de69-2565-f05a-f01d-f0c206ebcae6.png)

ファイル数が少ないと上記のような静的なホスティングで問題ありませんが、ファイルの数が多くなってくる、もしくは重くなってくると、ファイルのコピーや移動に支障が出てきます。それを解決するためにMBTilesを下記で扱います。

# MBTilesファイルからベクトルタイルを配信
pbfファイル形式のベクトルタイルは、静的フォルダに置くだけでベクトルタイルを配信でき便利ですが、世界全体などの大きなデータを扱う場合には、ファイル数が膨大になり扱いづらいです。そこで、pbfファイルを一つのファイルにまとめたMBTilesを使うとこの問題を解決できます。しかし、MBTilesの配信にはサーバ実装が必要です。以下がコードとなります。


 ```javascript:app.js
// Set Module
const config = require('config');
const express = require('express');
const morgan = require('morgan');
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

// Configure constant
const port = config.get('port');
const htdocsPath = config.get('htdocsPath');
const morganFormat = config.get('morganFormat');
const logDirPath = config.get('logDirPath');

// Logger configuration
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new DailyRotateFile({
      filename: `${logDirPath}/server-%DATE%.log`,
      datePattern: 'YYYY-MM-DD',
    }),
  ],
});

logger.stream = {
  write: message => {
    logger.info(message.trim());
  },
};

// Middleware
const app = express();
const VTRouter = require('./routes/VT');
app.use(
  morgan(morganFormat, {
    stream: logger.stream,
  })
);
app.use(express.static(htdocsPath));
app.use('/VT', VTRouter);

app.listen(port, () => {
  console.log(`Starting server at port ${port}`);
});
```
app0.jsとほとんど同じですが、以下の２つの行の記載により、「http\://localhost:3000/VT」以下にリクエストがあった場合は、./routes/VT.jsが参照されます。
const VTRouter = require('./routes/VT');
app.use('/VT', VTRouter);

 ```javascript:./routes/VT.js
// Set Module
const express = require('express');
const router = express.Router();
const config = require('config');
const fs = require('fs');
const MBTiles = require('@mapbox/mbtiles');

// Configure constant
const mbtilesDir = config.get('mbtilesDir');

// Global variables
let mbtilesPool = {};
let busy = false;

// Specify mbtiles
const getMBTiles = async (t, z, x, y) => {
  let mbtilesPath = `${mbtilesDir}/${t}.mbtiles`;
  return new Promise((resolve, reject) => {
    if (mbtilesPool[mbtilesPath]) {
      resolve(mbtilesPool[mbtilesPath].mbtiles);
    } else {
      if (fs.existsSync(mbtilesPath)) {
        new MBTiles(`${mbtilesPath}?mode=ro`, (err, mbtiles) => {
          if (err) {
            reject(new Error(`${mbtilesPath} could not open.`));
          } else {
            mbtilesPool[mbtilesPath] = {
              mbtiles: mbtiles,
              openTime: new Date(),
            };
            resolve(mbtilesPool[mbtilesPath].mbtiles);
          }
        });
      } else {
        reject(new Error(`${mbtilesPath} was not found.`));
      }
    }
  });
};

// Get tile
const getTile = async (mbtiles, z, x, y) => {
  return new Promise((resolve, reject) => {
    mbtiles.getTile(z, x, y, (err, tile, headers) => {
      if (err) {
        reject();
      } else {
        resolve({ tile: tile, headers: headers });
      }
    });
  });
};

router.get('/zxy/:t/:z/:x/:y.pbf', async (req, res) => {
  busy = true;
  const t = req.params.t;
  const z = parseInt(req.params.z);
  const x = parseInt(req.params.x);
  const y = parseInt(req.params.y);

  // console.log(`t: ${t}, z: ${z}, x: ${x}, y: ${y}`);

  getMBTiles(t, z, x, y)
    .then(mbtiles => {
      // console.log(mbtiles);
      getTile(mbtiles, z, x, y)
        .then(r => {
          // console.log(`---------before------------`);
          // console.log({ tile: r.tile, headers: r.headers });
          // console.log(`---------beforeend------------`);
          if (r.tile) {
            res.set('content-type', 'application/vnd.mapbox-vector-tile');
            res.set('content-encoding', 'gzip');
            res.set('last-modified', r.headers['Last-Modified']);
            res.set('etag', r.headers['ETag']);
            // console.log(`---------after------------`);
            // console.log(res.getHeaders());
            // console.log(`---------afterend------------`);
            res.send(r.tile);
            busy = false;
          } else {
            res
              .status(404)
              .send(`tile not found: /zxy/${t}/${z}/${x}/${y}.pbf`);
            busy = false;
          }
        })
        .catch(e => {
          res
            .status(404)
            .send(
              `tile not found (getTile error): /zxy/${t}/${z}/${x}/${y}.pbf`
            );
          busy = false;
        });
    })
    .catch(e => {
      res
        .status(404)
        .send(`mbtiles not found for /zxy/${t}/${z}/${x}/${y}.pbf`);
    });
});

module.exports = router;

```

## コードの解説

VT.jsでは、特定のタイル（.pbfファイル）をMBTilesデータベースから取得し、HTTPリクエストに応じてそれをクライアントに返しています。


 ```javascript:./routes/VT.js
const router = express.Router();
```
・express.Routerは、ルーティングを管理するためのミニアプリケーションを作成します。
・ルート定義を分離し、モジュールとしてエクスポートすることができます。
・メインのExpressアプリケーション（app.js）にマウントして使用します。


 ```javascript:./routes/VT.js
const MBTiles = require('@mapbox/mbtiles');
```
MapboxのMBTiles形式のデータベースを操作するためのモジュールです。

```javascript:./routes/VT.js
const mbtilesDir = config.get('mbtilesDir');
```
mbtilesDir：MBTilesファイルが格納されているディレクトリのパスを設定ファイルから取得します。今回は、「mbtiles」というディレクトリ名にしています。


```javascript:./routes/VT.js
let mbtilesPool = {};
```
mbtilesPool：MBTilesオブジェクトのキャッシュを格納するためのオブジェクト

```javascript:./routes/VT.js
let busy = false;
```
busy：サーバーの状態を示すフラグ。（必要なのかどうか不明です。）


getMBTiles関数は、作成されたmbtilesインスタンスを取得し、キャッシュに保存しています。この関数の引数の例としては、t = VTpracticegzip, z = 1, x = 1, y = 1となります。「t」の名称は「mbtiles」というディレクトリの下に格納されているファイル名です。スタイルファイル（今回の例では./htdocs/map/styleO-mbtiles.json）にも記載されています。
例としては、mbtilesPathは、
let mbtilesPath =　mbtiles/VTpracticegzip.mbtiles
となり、mbtilesのファイル名が入ります。

 ```javascript:./routes/VT.js
if (mbtilesPool[mbtilesPath]) {
      resolve(mbtilesPool[mbtilesPath].mbtiles);
    }
```

もし、mbtilesPoolオブジェクトに、mbtilesPath（ =　mbtiles/VTpracticegzip.mbtiles）が存在したら、mbtilesオブジェクト（new MBTiles()によって作成されるMBTilesファイル（地図タイルの格納ファイル）を開くためのインスタンス（オブジェクト））を返します。つまり、あるタイルが読み込まれて、一度mbtilesファイルの場所が判明したら、そのファイルが返されます。


 ```javascript:./routes/VT.js
else {
      if (fs.existsSync(mbtilesPath)) {
        new MBTiles(`${mbtilesPath}?mode=ro`, (err, mbtiles) => {
          if (err) {
            reject(new Error(`${mbtilesPath} could not open.`));
          } else {
            mbtilesPool[mbtilesPath] = {
              mbtiles: mbtiles,
              openTime: new Date(),
            };
            resolve(mbtilesPool[mbtilesPath].mbtiles);
          }
        });
      } else {
        reject(new Error(`${mbtilesPath} was not found.`));
      }
    }
```
そうではない場合には、fs.existsSync(mbtilesPath)によって、mbtilesPathが存在することを確認し、存在するなら、MBTiles コンストラクタを使って、指定された mbtiles ファイルを開きます。mbtilesPath にクエリパラメータ ?mode=ro を付加しているため、読み取り専用モードでファイルを開こうとしています。コールバック関数 (err, mbtiles) は、ファイルを開いた後に呼び出されます。引数のerr はエラーがあればその内容が格納され、引数のmbtiles には開いた mbtiles オブジェクトが渡されます。作成されたオブジェクト（インスタンス）mbtilesを「mbtilesPool[mbtilesPath].mbtiles」に保存して、それを返します。mbtilesPathが存在しない場合は、rejectを返します。

※fs.existsSync は、指定されたパスにファイルやディレクトリが存在するかどうかを確認するために使用されます。この関数は、指定されたパスが存在すれば true を返し、存在しなければ false を返します。

 ```javascript:./routes/VT.js
const getTile = async (mbtiles, z, x, y) => {
  return new Promise((resolve, reject) => {
    mbtiles.getTile(z, x, y, (err, tile, headers) => {
      if (err) {
        reject();
      } else {
        resolve({ tile: tile, headers: headers });
      }
    });
  });
};
```

指定されたズームレベル、x座標、およびy座標に基づいてmbtilesファイル（オブジェクト）からタイルを取得します。ここでmbtilesは、ファイル名ではなく、mbtilesインスタンス（@mapbox/mbtilesモジュールによって作成されるインスタンス）のことです。mbtilesインスタンスのgetTileメソッド(https://www.npmjs.com/package/@mapbox/mbtiles#reading)
を使用し、該当タイルと、そのヘッダーをオブジェクトに入れて返しています。上記、URLを見ると、「mbtiles.getTile(z, x, y, function(err, data, headers) {　});」と記載があり、コールバック関数の引数として、err, data, headersが使用されることが分かります。


 ```javascript:./routes/VT.js
router.get('/zxy/:t/:z/:x/:y.pbf', async (req, res) => {
  busy = true;
  const t = req.params.t;
  const z = parseInt(req.params.z);
  const x = parseInt(req.params.x);
  const y = parseInt(req.params.y);
```

パスパラメータ（t, z, x, y）を取得します。先程も記載したように、この関数の引数の例としては、t = VTpracticegzip, z = 1, x = 1, y = 1となります。parseIntをつけることで、整数値に修正（例えば、2.9は2となる。）しています。


 ```javascript:./routes/VT.js
getMBTiles(t, z, x, y)
    .then(mbtiles => {
      // console.log(mbtiles);
      getTile(mbtiles, z, x, y)
        .then(r => {
          // console.log(`---------before------------`);
          // console.log({ tile: r.tile, headers: r.headers });
          // console.log(`---------beforeend------------`);
          if (r.tile) {
            res.set('content-type', 'application/vnd.mapbox-vector-tile');
            res.set('content-encoding', 'gzip');
            res.set('last-modified', r.headers['Last-Modified']);
            res.set('etag', r.headers['ETag']);
            // console.log(`---------after------------`);
            // console.log(res.getHeaders());
            // console.log(`---------afterend------------`);
            res.send(r.tile);
            busy = false;
          } else {
            res
              .status(404)
              .send(`tile not found: /zxy/${t}/${z}/${x}/${y}.pbf`);
            busy = false;
          }
        })
        .catch(e => {
          res
            .status(404)
            .send(
              `tile not found (getTile error): /zxy/${t}/${z}/${x}/${y}.pbf`
            );
          busy = false;
        });
    })
    .catch(e => {
      res
        .status(404)
        .send(`mbtiles not found for /zxy/${t}/${z}/${x}/${y}.pbf`);
    });
```

getMBTiles(t, z, x, y)
でmbtilesファイルのオブジェクトを返し、
getTile(mbtiles, z, x, y)
で「r = { tile: tile, headers: headers }」として、該当タイルとそのヘッダーを返しています。
このヘッダーに以下の設定を追加しています。

```javascript:./routes/VT.js
res.set('content-type', 'application/vnd.mapbox-vector-tile')
```
• 役割: レスポンスの内容の種類（メディアタイプ）を指定します。
• 値: 'application/vnd.mapbox-vector-tile'
• 詳細: application/vnd.mapbox-vector-tileは、Mapbox Vector TileのMIMEタイプです。このヘッダーを設定することで、クライアントはレスポンスがベクトルタイルであることを理解し、適切に処理できます。


```javascript:./routes/VT.js
res.set('content-encoding', 'gzip')
```
• 役割: レスポンスのコンテンツがどのようにエンコードされているかを指定します。
• 値: 'gzip'
• 詳細: gzipは、コンテンツがgzip圧縮されていることを示します。このヘッダーを設定することで、クライアントは受信したデータを解凍する必要があることを認識します。gzip圧縮により、データの転送サイズが小さくなり、通信の効率が向上します。

:::note info
ここで注意すべき点は、tippecanoeでmbtilesデータを作成する際に、gzip圧縮で作成すること（--no-tile-compression（-pC)オプションをつけないようにする）です。これで、大分時間が取られました。。
:::

```javascript:./routes/VT.js
res.set('last-modified', r.headers['Last-Modified']);
res.set('etag', r.headers['ETag']);
```
このコードでは、ヘッダーとして、'last-modified'プロパティにr.headers['Last-Modified']、'etag'プロパティにr.headers['ETag']を設定しています。


静的フォルダの時と同様に、以下のコマンドで実行できます。
```
node app.js
```
index.htmlのページの
/VT/zxy/VTpracticegzip/1/1/1.pbf
をクリックすると、ベクトルタイルファイルがダウンロードされます。

また、
./map/mapO-mbtiles.html
をクリックすると、静的フォルダの時と同様の地図が表示されます。見た目は同じですが、今回はMBTilesのベクトルタイルが消費されています。

ログファイル(./log/server-2024-08-14.log）（日付はその日の日付になります。）には、以下のようなものが書き出されます。コンソールについても同様です。

![スクリーンショット 2024-08-14 16.49.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/ec484ebb-fa2b-7a77-1efa-0748ee7a7a43.png)


### コードの流れを理解するためのコンソールへの出力
```
console.log(mbtiles);
```
とすることで、getMBTiles(t, z, x, y)関数の戻り値であるmbtilesを表示します。getMBTiles(t, z, x, y)関数における「mbtilesPool[mbtilesPath].mbtiles」です。以下が返ってきます。これはMapboxのモジュールにより作成されたインスタンスです。

```
MBTiles {
  _maxListeners: 0,
  filename: 'mbtiles/VTpracticegzip.mbtiles',
  _batchSize: 100,
  _db: Database {},
  _stat: Stats {
    dev: 16777233,
    mode: 33188,
    nlink: 1,
    uid: 502,
    gid: 20,
    rdev: 0,
    blksize: 4096,
    ino: 5172124,
    size: 290816,
    blocks: 568,
    atimeMs: 1723662154983.752,
    mtimeMs: 1721931404748.3792,
    ctimeMs: 1722307029151.0254,
    birthtimeMs: 1721931404581.561,
    atime: 2024-08-14T19:02:34.984Z,
    mtime: 2024-07-25T18:16:44.748Z,
    ctime: 2024-07-30T02:37:09.151Z,
    birthtime: 2024-07-25T18:16:44.582Z
  },
  open: true
}
```

```
console.log(`---------before------------`);
console.log({ tile: r.tile, headers: r.headers });
console.log(`---------beforeend------------`);
```
とすることで、取得されたタイルデータ、そのヘッダーの情報をコンソールに出力します。以下のような情報が得られます。

```
---------before------------
{
  tile: <Buffer 1f 8b 08 00 00 00 00 00 00 13 93 6a 62 ac 60 e2 62 4b ce 4f 2c 2e c9 d1 68 50 90 e2 2c 4e 4e cc 49 2d 4a cc cb 96 e2 4a 4b 4d 2c 29 2d 4a 4d ce 49 94 ... 163 more bytes>,
  headers: {
    'Content-Type': 'application/x-protobuf',
    'Content-Encoding': 'gzip',
    'Last-Modified': 'Thu, 25 Jul 2024 18:16:44 GMT',
    ETag: '290816-1721931404748'
  }
}
---------beforeend------------
```

```
console.log(`---------after------------`);
console.log(res.getHeaders());
console.log(`---------afterend------------`);
```
res.getHeaders()は、expressで使用出来るサーバーからクライアントに送信されるヘッダー情報をオブジェクト形式で返すメソッドです。
res.setでヘッダーの設定を上書きしているので、以下の通りそれが反映されているのが分かります。

```
---------after------------
[Object: null prototype] {
  'x-powered-by': 'Express',
  'content-type': 'application/vnd.mapbox-vector-tile',
  'content-encoding': 'gzip',
  'last-modified': 'Thu, 25 Jul 2024 18:16:44 GMT',
  etag: '290816-1721931404748'
}
---------afterend------------
```


# PM2を利用したNode.jsサーバの永続化
PM2とは、Nodeサーバを永続化し、異常終了した場合にアプリケーションの再始動を行ってくれるプロセス管理のアプリケーションです。
pm2を使うことで、nodejsサーバーをバックグラウンドで実行できます。デーモンプロセスになるのでサーバーからログアウトしても大丈夫です。
今回はpm2のインストール、サーバの起動、停止、削除の方法を記載します。

まずは、PM2を
```
npm install pm2 -g
```
としてインストールします。

その後、
```
pm2 start app.js  --name server-test-01
```
として、サーバーを起動します。
--name server-test-01
とすることで、起動したアプリケーションのインスタンスの名前をserver-test-01と指定しています。

![スクリーンショット 2024-08-16 10.11.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/937e6001-3ab7-0e68-bd95-2bb7c48f3349.png)

これで、
http://localhost:3000/
のサイトや、地図のサイトがみられます。
なぜか、時々見られない時があり、不安定な挙動も見られました。

```
pm2 monit
```
サーバを監視するためのモニタリングボードが表示されます。

![スクリーンショット 2024-08-16 10.10.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/e36846c5-989d-18dd-d1ae-c4046f1cafa4.png)


```
pm2 stop server-test-01
```
サーバを停止します。

![スクリーンショット 2024-08-16 10.12.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/c5a6d31a-ee1a-44a8-fe9e-95c3d8b09610.png)

```
pm2 delete server-test-01
```
登録されたサーバ名を削除します。

![スクリーンショット 2024-08-16 10.14.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/339be1ff-9ec6-a260-84ab-2a587171261d.png)

# crontabを利用した定期的なサーバの再起動
cron（crond）とは、多くのUNIX系OSで標準的に利用される常駐プログラム(デーモン）の一種で、利用者の設定したスケジュールに従って指定されたプログラムを定期的に起動してくれるものです。
利用者は「crontab」（“cron table”の略）コマンドによって実行したいプログラムやコマンド、シェルスクリプトなどと実行日時を指定すると、同名のテキストファイル(crontabファイル）に設定が保存されます。


```
crontab -e
```
crontabエディタが起動します。viがデフォルトではないかと思います。
例えば、1分毎に/tmp/hello.txtに"hello."と書き出すコマンドは以下のとおりです。
```
*/1 * * * * echo "hello." >> /tmp/hello.txt
```

基本フォーマット
```
分 時 日 月 曜日 コマンド
```
分：0-59
時：0-23
日：1-31
月：1-12
曜日：0-7（日曜日は0または7）
コマンド：実行するコマンド


```
crontab -l
```
現在設定されているcronジョブがリスト表示されます。

```
crontab -r
```
作成したcronを削除することができます。


### シェルスクリプトの作成
```sh:pmreload.sh
#!/bin/bash

export PATH=$PATH:/Users/astro/.nvm/versions/node/v20.15.1/bin

/Users/astro/.nvm/versions/node/v20.15.1/bin/pm2 reload --update-env /Users/astro/Documents/GitHub/20240722hostingPracticeLocalhost/app.js  --name server-test-01
# /Users/astro/.nvm/versions/node/v20.15.1/bin/pm2 monit
date
```
export PATH=$PATH:/Users/astro/.nvm/versions/node/v20.15.1/bin
については、
•環境変数 PATH に、Node.js のバイナリディレクトリを追加しています。
•Node.js や pm2 のコマンドをこのスクリプト内で直接使用できるようにするためです。
 
pm2コマンドの絶対パス、またそれに付随してnvm?の絶対パスを記載しないと実行出来ませんでした。cronを使用しているため、cron上でのパスを指定しないといけないのだと思います。さらにmonitを実行するとログファイルには何も表示されなくなってしまうため、コメントアウトしています。最後にdateをつけることで、いつに実行されたものか分かるように工夫しています。
「pm2 restart」と「pm2 reload」がありますが、「pm2 reload」の方がゼロダウンタイムで再起動出来るのでこちらを選択しています。「--update-env」は環境変数を変更した場合、その変更を即座に反映させたいときに使用します。


シェルスクリプトに対して実行権限を付与します。これをしないと実行出来ないので、忘れずに付与します。
```
chmod +x pmreload.sh
```

cronエディタに以下のように記載するとシェルスクリプトが１分毎に実行されます。
```
*/1 * * * * /Users/astro/Documents/GitHub/20240722hostingPracticeLocalhost/pmreload.sh >> /tmp/cron_output.txt 2>&1
```
「>>」はファイルに標準出力（stdout）を追加で書き込みます。
また、「2>&1」で、標準エラー出力（stderr）を標準出力（stdout）にリダイレクトします。これにより、エラーメッセージも含めてすべての出力が/tmp/cron_output.txtに記録されます。

再起動が出来ているか、pm2 monitで確認してみましたが、右下の方にあるUptimeが設定した時間間隔（１分毎）で０になっているので、無事に再起動している模様です。右上にWARNINGが出ていますが、問題ないようです。

![スクリーンショット 2024-08-17 11.40.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/283aff74-ba61-b128-76bc-fa4ce3d167fe.png)



# まとめ
本記事では、ローカルホスト（自分自身のPCをサーバとする）でnode.jsを使用してベクトルタイルサーバを作成しました。いろいろなライブラリが出てきて、ややこしいですが、少しずつ理解出来てきている気がします。
また、pm2、cronの使い方もまとめました。思ったより難しくなくて、理解出来ました。サーバを永続的に使用出来るのは非常に便利だと思います。

# Reference

https://qiita.com/T-ubu/items/545d9f995ef7496a2ec4