Express + Next.js をコンテナで動かしたい
皆さん、こんにちは。技術開発グループのn-ozawanです。
9月ですね。年を重ねると時間経つのが早く感じる現象を「ジャネーの法則」といいます。
本題です。
Express + Next.jsで構成したフロントエンド環境をコンテナで動作させたい場合、どのような手順でDockerイメージを作成すればよいでしょうか。今回は、Express + Next.jsで構成したフロントエンド環境をイメージ化するまでの手順をまとめたいと思います。
目次
Express + Next.js プロジェクトの準備
まずはExprerss + Next.jsで構成されたプロジェクトを準備します。以下のコマンドでNext.jsを構築します。途中の質問は全てデフォルトのままでOKです。
npx create-next-app@latest express-server-app
次にExpressをインストールします。
cd express-server-app
npm i express
npm i -D ts-node @types/express
プロジェクトのルート直下にserver.ts
を追加します。内容は以下の通りです。
import express, { json } from 'express';
import next from "next";
const port = parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";
const server = express();
const app = next({ dev });
async function startupServer() {
await app.prepare();
const handle = app.getRequestHandler();
server.get('*',(req, res) => handle(req, res));
server.listen(port, () => {
console.log(
`> Server listening at http://localhost:${port} as ${dev ? "development" : process.env.NODE_ENV}`,
);
});
}
startupServer();
7行目のnext({ dev });
でNext.jsのサーバーを生成しています。引数にdev
を渡していますが、ローカル環境で動作するときはdev: true
を、本番環境で動作するときはdev: false
を指定する必要があります。このdev
の中身は5行目で判断しています。
10行目のapp.prepare();
でNext.jsのサーバーを立ち上げます。13行目ではExpressサーバーが受信したリクエストを、Next.jsサーバーへ渡します。
tsconfig.server.json
を追加します。内容の説明は省きます。
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "dist",
"lib": ["es2019"],
"target": "es2019",
"noEmit": false
},
"include": ["server.ts"]
}
あとは起動するコマンドを修正すればOKです。package.json
のscriptsを修正します。
"dev": "ts-node -P ./tsconfig.server.json ./server.ts",
以下のコマンドでExpress + Next.jsの環境が動作します。
npm run dev

Dockerfileの作成
ローカル環境での動作が確認できましたので、本番環境用に、このプロジェクトのDockerイメージを作成します。Next.jsのDockerイメージを作成するDockerfileは公式より公開されています。このDockerfileを参考に構築します。
ビルド
Dockerfileを作成します。まずはpackage.json
とpackage-lock.json
をイメージにコピーして、npm ci
により依存関係のパッケージをインストールします。
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
次にソースコードなどをイメージにコピーして、npm run build
によりビルドを実行します。
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
npm run build
は以下のように修正します。next.jsのビルドと、Expressのビルドを実行しています。
"build": "next build && tsc --project tsconfig.server.json",
Next.jsをビルドする際は、next.config.mjs
にoutput: "standalone"
を追加しておきます。standalone
で出力すると、node_moduleを含む、Next.jsを実行するために必要なファイルがstandalone
というフォルダにまとめて出力してくれます。
const nextConfig = {
output: "standalone",
};
Next.jsのファイルをイメージにコピーする
Next.jsをビルドすると、ビルド結果は.next
フォルダに出力されます。.next
に出力されたファイルをイメージにコピーします。
# Production image, copy all the files (express + next.js) and run express
FROM base AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
Expressのファイルをイメージにコピーする
Expressをビルドすると、ビルド結果はdist
フォルダに出力されます。dist
に出力されたファイルをイメージにコピーします。コピー後は、npm ci --omit dev
で実行に必要なパッケージをインストールします。
COPY --from=builder /app/dist ./
COPY --from=builder /app/package-lock.json ./
RUN npm ci --omit dev
サーバーを実行する
最後はnpm run start
によりサーバーを実行します。
USER nextjs
EXPOSE 3000
ENV PORT=3000
# server.js is created by next build from the standalone output
ENV HOSTNAME="0.0.0.0"
CMD ["npm", "run", "start"]
npm run start
の内容は以下のように修正します。
"start": "NODE_ENV=production node ./server.js",
イメージ化してコンテナを起動する
Dockerfileの編集は以上です。実際にイメージ化してコンテナを起動してみましょう。
docker build -t express-server-app .
docker run -p 3000:3000 -it express-server-app

おわりに
今回はExpress + Next.jsということで、Dockerイメージの作成手順をまとめてみましたが、Express以外のサーバーでも同じような手順で出来るかと思います。
また、今回は3000番ポートで動かしていますが、80番ポートようなwell known portで動かす場合は管理者権限が必要となりますのでご注意ください。USER nextjs
でnextjsユーザーで起動するようにしていますので、おそらく80番ポートでの起動は出来ないです。その場合はUSER nextjs
をコメントアウトしてください。
ではまた。