Node.js+Expressな環境をDockerで構築する

はじめに

1年前に フロントエンド開発のためのセキュリティ入門 知らなかったでは済まされない脆弱性対策の必須知識 を買ったのだが、それを読んでおらず、最近になって読み始めた。
この中で、Node.js + Express でハンズオンの環境を構築しているので、書籍の通り構築してみる。
※ どこでも環境を作れるようにDockerで構築を試してみる。

環境

MacOS sonoma 14.2.1

Docker Desktop 4.28.0 (139021)

Server: Docker Desktop 4.28.0 (139021)
 Engine:
  Version:          25.0.3
  API version:      1.44 (minimum version 1.24)
  Go version:       go1.21.6
  Git commit:       f417435
  Built:            Tue Feb  6 21:14:25 2024
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.28
  GitCommit:        ae07eda36dd25f8a1b98dfbf587313b99c0190bb
 runc:
  Version:          1.1.12
  GitCommit:        v1.1.12-0-g51d5e94
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

docker initを使って構築する

実は docker initを試してみるでも、docker initを使って、Node.js+Expressの環境を構築していた。

https://www.publickey1.jp/blog/24/dockerdocker_initdockerfilecompose.html によると、docker initは2024/01/25に正式リリースされたので、以前の記事の時点では実験的リリースだったようだ。
成果物に変わりがありそうなので、再度docker initで構築を試してみる。

ワークディレクトリの作成

今回は、~/workspace/nodejs-express-handsonというパスでディレクトリを作成し、そこで作業をする。

mkdir -p ~/workspace/nodejs-express-handson && cd $_

docker init の実行

docker initでの構築を実施してみる。

docker init
Welcome to the Docker Init CLI!

This utility will walk you through creating the following files with sensible defaults for your project:
  - .dockerignore
  - Dockerfile
  - compose.yaml
  - README.Docker.md

Let's get started!

プロジェクトにどのアプリケーションプラットフォームを使うのかを聞かれるので、Node を選択する。

? What application platform does your project use?  [Use arrows to move, type to filter]
  Go - suitable for a Go server application
  Python - suitable for a Python server application
> Node - suitable for a Node server application
  Rust - suitable for a Rust server application
  ASP.NET Core - suitable for an ASP.NET Core application
  PHP with Apache - suitable for a PHP web application
  Java - suitable for a Java application that uses Maven and packages as an uber jar
  Other - general purpose starting point for containerizing your application
  Don't see something you need? Let us know!
  Quit

Node のバージョンを聞かれる。 https://nodejs.org/en/about/previous-releasesを見ると、LTSかつ最新なものは20.11.1となるので、これにする。

? What version of Node do you want to use? (8.9.4)  20.11.1

パッケージマネージャは何を使うと聞かれる。 pnpm使ってみたいしこれにしよう。

? Which package manager do you want to use?  [Use arrows to move, type to filter]
  npm - (detected)
  yarn
> pnpm

pnpmのバージョン何にすると聞かれる。 https://github.com/pnpm/pnpm/releasesのGitHubのリポジトリのReleasesのページを見てみた。
v8.15.4が最新らしい。(v9.0.0もあるがアルファ版)

? What version of pnpm do you want to use? 8.15.4

どういうコマンドでアプリケーションが起動するか教えてと聞かれる。
前回は、node index.js みたいなコマンドだったので、それにしておこう。

? What command do you want to use to start the app? [tab for suggestions] node index.js

ポートは何番を使うかを聞かれる。
3000番としておく。

? What port does your server listen on? 3000

できた

✔ Your Docker files are ready!

Take a moment to review them and tailor them to your application.

WARNING: The following files required to run your application were not found. Be sure to create them before running your application:
  - package.json
  - pnpm-lock.yaml

When you're ready, start your application by running: docker compose up --build

Your application will be available at http://localhost:3000

Consult README.Docker.md for more information about using the generated files.

成果物を確認していこう、このあたりができている。

.dockerignore  Dockerfile  README.Docker.md  compose.yaml

環境の起動

docker compose up -d --build 
failed to solve: failed to compute cache key: failed to calculate checksum of ref 100ebf9f-d869-4ab8-a483-2915b8123053::ejlz08rw16fqm6bbzjdwc1yl2: "/package.json": not found

package.jsonがなくて怒られる。

package.jsonやその他ファイルがある前提でDockerfileが作成されるので初期構築が少し難しい。

そのため、修正を加えることにする。

docker init 成果物の修正とindex.jsの追加

Dockerfile

# syntax=docker/dockerfile:1

# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/

# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7

ARG NODE_VERSION=20.11.1
ARG PNPM_VERSION=8.15.4

FROM node:${NODE_VERSION}-alpine

# Use production node environment by default.
ENV NODE_ENV production

# Install pnpm.
RUN --mount=type=cache,target=/root/.npm \
    npm install -g pnpm@${PNPM_VERSION}

WORKDIR /usr/src/app

# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.local/share/pnpm/store to speed up subsequent builds.
# Leverage a bind mounts to package.json and pnpm-lock.yaml to avoid having to copy them into
# into this layer.
#RUN --mount=type=bind,source=package.json,target=package.json \
#    --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
#    --mount=type=cache,target=/root/.local/share/pnpm/store \
#    pnpm install --prod --frozen-lockfile

# Run the application as a non-root user.
USER node

# Copy the rest of the source files into the image.
COPY . .

# Expose the port that the application listens on.
EXPOSE 3000

# Run the application.
CMD ["tail", "-f", "/dev/null"]
#CMD node index.js

としておく。

compose.yml

# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/

# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
  server:
    build:
      context: .
    environment:
      NODE_ENV: production
    ports:
      - 3000:3000

# The commented out section below is an example of how to define a PostgreSQL
# database that your application can use. `depends_on` tells Docker Compose to
# start the database before your application. The `db-data` volume persists the
# database data between container restarts. The `db-password` secret is used
# to set the database password. You must create `db/password.txt` and add
# a password of your choosing to it before running `docker-compose up`.
#     depends_on:
#       db:
#         condition: service_healthy
#   db:
#     image: postgres
#     restart: always
#     user: postgres
#     secrets:
#       - db-password
#     volumes:
#       - db-data:/var/lib/postgresql/data
#     environment:
#       - POSTGRES_DB=example
#       - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
#     expose:
#       - 5432
#     healthcheck:
#       test: [ "CMD", "pg_isready" ]
#       interval: 10s
#       timeout: 5s
#       retries: 5
# volumes:
#   db-data:
# secrets:
#   db-password:
#     file: db/password.txt

こっちは特に変更を入れていない。

index.jsの作成

const http = require('http');
const express = require('express');

const app = express();

app.get("/", function(req, res){
    return res.send("Hello World");
});

const server = http.createServer(app);
server.listen(3000);

修正後の作業

この状態で、docker compose up -d --buildとし、コンテナを起動。
エントリーポイントが CMD ["tail", "-f", "/dev/null"] の部分となっているので停止せずに動いている状態となる。

起動したら、下記でコンテナ内に入る。

docker compose exec --user=root server sh

その後、pnpm initを実行する。

pnpm init
/usr/src/app # pnpm init
Wrote to /usr/src/app/package.json

{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
/usr/src/app # ls
README.Docker.md  index.js          package.json

その後、pnpm add expressexpressも入れておこう。

pnpm add express
/usr/src/app # pnpm install express
Packages: +64
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 64, reused 0, downloaded 64, added 64, done

dependencies:
+ express 4.18.3

devDependencies: skipped because NODE_ENV is set to production

Done in 2.6s

できたので、これをホスト側のPCに持ってくる。
docker cpを使う。

docker ps

CONTAINER ID   IMAGE                           COMMAND                   CREATED         STATUS         PORTS                    NAMES
986d5bc0a0f9   nodejs-express-handson-server   "docker-entrypoint.s…"   9 minutes ago   Up 9 minutes   0.0.0.0:3000->3000/tcp   nodejs-express-handson-server-1

CONTAINER ID986d5bc0a0f9なので、

docker cp 986d5bc0a0f9:/usr/src/app/package.json .
docker cp 986d5bc0a0f9:/usr/src/app/pnpm-lock.yaml .

で持ってこられる。

Dockerfileを元に戻す

# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/

# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7

ARG NODE_VERSION=20.11.1
ARG PNPM_VERSION=8.15.4

FROM node:${NODE_VERSION}-alpine

# Use production node environment by default.
ENV NODE_ENV production

# Install pnpm.
RUN --mount=type=cache,target=/root/.npm \
    npm install -g pnpm@${PNPM_VERSION}

WORKDIR /usr/src/app

# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.local/share/pnpm/store to speed up subsequent builds.
# Leverage a bind mounts to package.json and pnpm-lock.yaml to avoid having to copy them into
# into this layer.
-#RUN --mount=type=bind,source=package.json,target=package.json \
-#    --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
-#    --mount=type=cache,target=/root/.local/share/pnpm/store \
-#    pnpm install --prod --frozen-lockfile
+RUN --mount=type=bind,source=package.json,target=package.json \
+    --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
+    --mount=type=cache,target=/root/.local/share/pnpm/store \
+    pnpm install --prod --frozen-lockfile

# Run the application as a non-root user.
USER node

# Copy the rest of the source files into the image.
COPY . .

# Expose the port that the application listens on.
EXPOSE 3000

# Run the application.
-CMD ["tail", "-f", "/dev/null"]
+CMD node index.js

起動

docker compose up -d --build

ちゃんと起動した〜

express-01

今回作成したもの

下記に成果物を入れておいた。
https://github.com/katsuobushiFPGA/nodejs-express-with-docker-handson

おわりに

書籍のハンズオンをこれからやっていきたい。
そして、フロントエンド技術を深く理解していきたい。
pnpmとか使ったことなかったので良い機会になるかも。

あと、docker initで生成した成果物は全部production環境で動作するようにできているみたいだ。
開発環境であれば、ソースコードをマウントしたりした方が開発効率は良いので、そうできるように環境ごとのymlを作ったり、うまいこと切り替える機構を使ってみるかな。
ということで、まだ改善の余地があり。
※現状だと、ソースコード直すたびにコンテナビルドが必要になる。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。