LoginSignup
23
22

More than 1 year has passed since last update.

Docker+nginx+Express(Node.js)で簡易なAPIサーバーを作る

Last updated at Posted at 2022-12-13

概要

この記事は「【マイスター・ギルド】本物のAdvent Calendar 2022」9日目の記事です。

まぁかくかくしかじかありまして、JavaScriptの練習としてAPIを叩いて返ってきた値(JSON)を画面に表示してみよう、そのための環境を作ろうってなったんです。
JavaScriptの練習だからフロントはもちろん使うけど、API側もNodeで書けばより理解も深まるだろうということでExpressなるフレームワークがあると知ってそれを使って環境構築をしてみました。
最近nginxの勉強もしたのでプロキシ設定を使った構成の練習台にもちょうどよかったです。

環境を作ろう

準備

こんな感じのディレクトリ構成にします。今回はデスクトップに作成しました。

ディレクトリ構成
js-practice
├─ backend
├─ docker
│  ├─ nginx
│  └─ node
└─ frontend

nginxの設定

nginxで設定するところは静的ファイルの参照場所とリバースプロキシです。

まずはこの記事を参考にnginxのdefault.confファイルを取り出します(実は自分の記事)。

以下コマンドでnginxのコンテナを立ち上げてコンテナ内に入ります。

コマンドライン
docker run --rm -v /$PWD/docker/nginx:/tmp -it nginx bin/bash

マウントした/tmpdefault.confファイルをコピーしてホスト側に取り出します。

コマンドライン
cp /etc/nginx/conf.d/default.conf /tmp/
exit

これでdocker/nginx配下にdefault.confが入りました。
で、中身を以下のように書き換えます。

docker/nginx/default.conf
server {
    listen       80;
    server_name  localhost;

    root   /usr/share/nginx/html/public;
    index  index.html index.htm;

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    location /api/ {
        proxy_pass http://node:9000;
    }
}

root /usr/share/nginx/html/public;の部分がデフォルトで参照されるディレクトリですね。
proxy_pass http://node:9000;の部分がプロキシの設定です。

またプロキシする場合、バックエンドへのリクエストはnginxから行われることになるためブラウザのリクエストに含まれるヘッダー情報は失われてしまいます。
ブラウザからのヘッダー情報をバックエンドでも参照する場合はproxy_set_headerを使って設定します。
今回は特に設定しませんがブラウザからのヘッダー情報を使用する場合は気をつけてください。

default.conf
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Expressの設定

ExpressではHello,Express!をJSON形式で返すAPIを作ってみます。
ちなみにExpressのことはよく知りません😊

まずはbackendディレクトリに移動してpackage.jsonを用意してからExpressをインストールします。

コマンドライン
cd backend
npm init -y
npm install express

インストールできたらapp.jsというファイルを作成してAPIを作ってみます。

backend/app.js
const express = require('express');
const app = express();
const port = 9000;

app.get('/api/v1/', (req, res) => {
  setTimeout(() => {
    res.json({
      message: 'Hello,Express!'
    });
  }, 500);
});

app.listen(port, () => {
  console.log(`listening on port: ${port}`);
});

Expressの公式を少しいじって500ms後にJSON形式で{message: Hello,Express!}を返します。
(setTimeout関数を使っているのはAPIらしくレスポンスが遅れてくるようにしてみただけで必要な処理ではありません)
ここで一つ問題があって今後app.jsを書き換えるたびにサーバー再起動が必要になります。
サーバー再起動するためにnodeのプロセスをkillしたいのですがnodeコンテナはこれが一筋縄でいかず結局Dockerコンテナの再起動をするしかないようです。
これでは不便なのでapp.jsに変更があればそれを検知して再起動してくれるnodemonを入れます。

コマンドライン
npm install -D nodemon

mainで読み込むファイルはapp.jsなので
nodemonを動かすスクリプトと一緒にpackage.jsonを以下のように変更します。

backend/package.json
___略___
  "main": "app.js",
  "scripts": {
    "start": "nodemon /app"
  },
___略___

Dockerの設定

Dockerの設定ではnginxのWebサーバーとnodeのAPIサーバーをそれぞれ立てます。
まずはそれぞれのDockerfileを作成します。

docker/nginx/Dockerfile
FROM nginx:latest
docker/node/Dockerfile
FROM node:16-alpine

WORKDIR /app

COPY ./backend ./

EXPOSE 9000
CMD ["npm", "start"]

nginxはDockerfile作らなくてもdocker-compose.ymlに収まるんですが、個人的な好みでDockerfileに書き出してます。
次にroot配下にdocker-compose.ymlをつらつらと書きます。

docker-compose.yml
version: '3.9'

services:
  nginx:
    container_name: nginx
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile
    ports:
      - 80:80
    volumes:
      - ./frontend/public:/usr/share/nginx/html/public
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf

  node:
    container_name: node
    build:
      context: .
      dockerfile: ./docker/node/Dockerfile
    ports:
      - 9000:9000
    volumes:
      - ./backend:/app
    tty: true

フロントエンドの設定

フロントエンドと言いつつそれほど大層なものは作らずとりあえずindex.htmlを準備します。
frontend/publicディレクトリの中にindex.htmlを作成します。

frontend/public/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <h1>Hello,World!</h1>
</body>
</html>

ディレクトリ構成確認

ここまでの環境構築でこんな形になってればOKです。

ディレクトリ構成
js-practice
├─ backend
│  ├─ node_modules
│  ├─ app.js
│  ├─ package-lock.json
│  └─ package.json
├─ docker
│  ├─ nginx
│  │  ├─ default.conf
│  │  └─ Dockerfile
│  └─ node
│     └─ Dockerfile
├─ frontend
│  └─ public
│     └─ index.html
└─ docker-compose.yml

動かしてみよう

では準備ができたので、動かしてみましょう。
js-practiceディレクトリ直下に移動してから満を持してdocker-compose up

コマンドライン
docker-compose up

静的ファイルの確認

http://localhostにアクセスしてみて下記画面ならOK、nginxに静的ファイルが正しく配置されています。
スクリーンショット 2022-11-21 22.47.47.png

APIの確認

http:localhost:9000/api/v1/にアクセスしてみて下記画面ならOK、ExpressのAPIサーバーが正しく立ち上がっています。
スクリーンショット 2022-11-21 22.49.42.png

リバースプロキシの確認

http:localhost/api/v1/にアクセスしてみて下記画面ならOK、nginxの設定により/api/のリクエストがAPIサーバーへ正しくプロキシしています。
スクリーンショット 2022-11-21 22.49.42.png

APIの結果を取得しよう

APIを叩く準備

フロントエンドからAPIをコールして結果を表示させてみます。
まずはaxiosを入れたいのでフロント側でもpackage.jsonを作ります。

コマンドライン
cd frontend
npm init -y
npm install axios

あとaxiosをimportして使うのにwebpackに代わってlaravel-mixを使います。

コマンドライン
npm install -D laravel-mix

frontendディレクトリ直下にwebpack.mix.jsを作成して以下のようにします。

frontend/webpack.mix.js
const mix = require('laravel-mix');

mix.disableNotifications(); // windows環境だと通知が邪魔なので通知OFF
mix.js('src/js/main.js', 'public/js/main.js');

src/js/main.jsをコンパイルしてpublic/js/main.jsに出力しなさいということです。
(src/js/main.jsは後で作ります)
フロント側のpackage.jsonscriptも編集します。

frontend/package.json
___略___
  "scripts": {
    "build": "mix",
    "watch": "mix watch"
  },
___略___

これでひとまず準備はOK。

ちなみにlaravel-mixの使い方はこの記事でも書いたので参考までに(また自分の記事)。

APIを叩いてみる

ボタンを押したらgetリクエストのAPIを叩いて、レスポンスを表示するということをやってみます。
レスポンスはbackend/app.jsで定義したJSONですね。

JSONレスポンス
{
  message: "Hello,Express!"
}

index.htmlを更新してsrc/js/main.jsfrontendディレクトリ配下に新たに作成します。

frontend/public/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <h1 id="message">Hello,World!</h1>
  <button id="btn">Message取得</button>
  <script src="./js/main.js"></script>
</body>
</html>
frontend/src/js/main.js
import axios from "axios";

const msg = document.getElementById('message');
const btn = document.getElementById('btn');

const getMessage = async () => {
  const response = await axios.get('/api/v1/');
  return response.data.message;
}

btn.addEventListener('click', async () => {
  await getMessage()
    .then(res => msg.textContent = res)
    .catch(err => console.log(err));
});

そしてfrontend/src/js/main.jsをコンパイルします。

コマンドライン
npm run watch

http://localhostはこんな画面になりました。
スクリーンショット 2022-11-21 23.53.27.png

ボタンを押すと...
スクリーンショット 2022-11-22 0.01.08.png

レスポンスを受け取ってその内容に切り替わりました〜。

まとめ

ちょっとしたきっかけがあって、この環境を作ることになりましたがdocker内のnginxのリバプロの設定でhttp://localhost:9000ではうまくいかないことやnodemonexpressといったツールやフレームワークを知ることができました。(host側のlocalhostとdocker側のlocalhostは違いますよということらしいです)

location /api/ {
  # OK
  proxy_pass http://node:9000; 
  # NG
  # proxy_pass http://localhost:9000;
}

小さな学びがいくつかあったのでよかったです。
これを作った後にフロントをReactに変えてみたりもしました。
環境構築もこなれてきた感があります(嘘)。

今回はシンプルなリクエストを送ってレスポンスを受け取るだけですが、DBを追加してみたりすればよりいい感じのアプリが作れるようになると思います。
フロントとバックのJavaScriptを勉強するための簡易な環境構築でした。

最後にディレクトリ構成はこんな感じになります。

ディレクトリ構成
js-practice
├─ backend
│  ├─ node_modules
│  ├─ app.js
│  ├─ package-lock.json
│  └─ package.json
├─ docker
│  ├─ nginx
│  │  ├─ default.conf
│  │  └─ Dockerfile
│  └─ node
│     └─ Dockerfile
├─ frontend
│  ├─ node_modules
│  ├─ public
│  │  ├─ js/main.js
│  │  └─ index.html
│  ├─ src
│  │  └─ js/main.js
│  ├─ mix-manifest.json
│  ├─ package-lock.json
│  ├─ package.json
│  └─ webpack.mix.js
└─ docker-compose.yml
23
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
22