Next.js のプロジェクトを VPS 上の Docker にデプロイする【その1】
前回のシリーズの続きです。
- Next.js のプロジェクトを作成して開発環境の Docker で動くようにする【その1】
- Next.js のプロジェクトを作成して開発環境の Docker で動くようにする【その2】
- Next.js のプロジェクトを作成して開発環境の Docker で動くようにする【その3】
前回は Next.js の開発環境を Docker 上に構築しました。
今回のシリーズでは、前回作ったプロジェクトを本番環境の Docker にデプロイすることを目標とします。
タスク整理
「Next.js で作った自作サイトをVPSで公開する方法について考えてみた」のタスクの洗い出しより、さらに細かいタスクをの整理をしていきます。現在は「3.Next.js プロジェクトを Docker 上で動かせるようにする。」の部分まで終わっている状態です。
以下の図のように、GitHub を通してサイトの更新を行なっていくことを目標としています。
- GitHub にリポジトリを作成する
- 本番環境用の Docker イメージを作成する
- 本番環境でサイトを起動するために Docker Compose を利用する
- 本番環境に Docker 環境を用意する
- 本番環境の Docker で動かす
図の①->②の自動化は大変なので、次回のシリーズでやることにします。
今回のシリーズでは、本番環境でサイトが表示されること、手動で GitHub 上に push/pull を行なってサイトの更新ができることの2点を目指します。
GitHub にリポジトリを作成する
# 作業ディレクトリに移動
$ cd ~/NextProjects/donuts-site
# Next.js のプロジェクト作成時に .git も作成されているので git init 等は必要ない
# リモートリポジトリ(GitHub)との紐付けを行う
$ git remote add origin git@github.com:ユーザ名/DonutsSite.git
これで GitHub のリポジトリに push できる状態になりました。
Next.js ではプロジェクト作成時に自動で .gitignore
ファイルも作成されます。
内容は以下の通りです。
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
今回は使わないものもありますが、そのままにしておきます。(.vercel 等)
node_modules
等はアップロードしてしまうととても膨大になってしまいますし、本番環境でも膨大なファイルを pull してきてしまうことになるので、.gitignore
にちゃんと書かれていることを確認しておきます。
.gitignore
の確認ができたら、GitHub にプッシュします。
# GitHub にプッシュする
$ git push -u origin main
本番環境用の Docker イメージを作成する
Dockerfile に本番環境用のマルチステージビルドを利用した記述を行います。
Next.js 公式サンプルの Dockerfile を読み解いてみた時に、マルチステージビルドについても詳しく調査を行っています。
# 作業ディレクトリに移動
$ cd ~/NextProjects/donuts-site
# 本番環境用の Dockerfile を作成
$ touch prod.Dockerfile
##### deps ステージ #####
FROM node:16-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
else echo "Lockfile not found." && exit 1; \
fi
##### builder ステージ #####
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
##### runner ステージ #####
FROM node:16-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
Dockerfile から正しくイメージが作成され、サイトが起動できることを確認します。
# 作業ディレクトリに移動
$ cd ~/NextProjects/donuts-site
# docker build -f Dockerfile のファイル名 -t 作成するするイメージ名 もとになるDockerfileの場所
$ docker build -f prod.Dockerfile -t donut-site-image .
# image が作成できたかの確認
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
donut-site-image latest 7b4c93a5e0a8 11 seconds ago 916MB
# イメージからコンテナを作成して起動する
$ docker run --name donut-site donut-site-image
Listening on port 3000
http://localhost:3000 を見ても表示されませんでした。
Docker のキャッシュを削除して再度上記のコードブロックを実行しました。(参考: Dockerでビルドした際に作られるBuild Cacheを削除する
# Docker の キャッシュを削除する
$ docker builder prune
しかし表示されない…😢
サイトを表示できるようにする
上記メモにて Dockerfile よりイメージを作成してサイトを起動後にブラウザにアクセスしても、うまく表示ができませんでした。
前回の記事時点からnode_modules
フォルダを削除したことを思い出しました。実際にはnode_modules
はdeps ステージ
にてインストールされるはず…。
なのでnode_modules
を復活させるのではなく、根本の原因を探すため、ライブラリ周りから調査してみます。
コンテナ内を探索する
# 作業ディレクトリに移動
$ cd ~/NextProjects/donuts-site
# docker build -f Dockerfile のファイル名 -t 作成するするイメージ名 もとになるDockerfileの場所
$ docker build -f prod.Dockerfile -t donut-site-image .
# image が作成できたかの確認
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
donut-site-image latest 7b4c93a5e0a8 11 seconds ago 916MB
# イメージからコンテナを作成して起動する(バックグラウンド -d で起動する)
$ docker run --name donut-site -d donut-site-image
# コンテナの状態を確認
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6cc446e57ada donut-site-image "docker-entrypoint.s…" 3 minutes ago Up 2 minutes 3000/tcp donut-site
コンテナの中に入り、実際のツリー構造をみてみます。
# コンテナの中にはいる
$ docker container exec -it donut-site sh
# app内を確認する
/app $ ls
node_modules package.json public server.js
# node_modules の中身を確認
/app $ cd node_modules/
/app/node_modules $ ls
@next @swc next react react-dom use-sync-external-store
# .next の中身を確認
/app $ cd .next
/app/.next $ ls
BUILD_ID package.json react-loadable-manifest.json routes-manifest.json static
build-manifest.json prerender-manifest.json required-server-files.json server
-i
オプションは、コンテナに操作端末(キーボード)を繋ぎます。
-t
オプションは、特殊キーを使用可能にします。
この2つのオプションをまとめて-it
として指定することで、コンテナの中身をキーボードで操作することができます。
docker exec のドキュメント
docker container exec
を bash で実行しようとした場合に以下のエラーが出ました。
$ docker container exec -it donut-site bash
OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "bash": executable file not found in $PATH: unknown
bash が利用できない場合は sh
を利用すると、コンテナ内の操作ができるようになります。
前回の開発環境で Docker Compose を用いた場合はうまく起動しているので、そちらと見比べてみました。
# コンテナの情報
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b44921f166f7 donuts-site_donuts-site "docker-entrypoint.s…" 6 minutes ago Up 4 minutes 0.0.0.0:3000->3000/tcp donuts-site
# コンテナの中にはいる
$ docker container exec -it donuts-site sh
# app 内
/app # ls
next-env.d.ts next.config.js node_modules package.json pages public styles tsconfig.json yarn.lock
# node_modules 内
/app # cd node_modules/
/app/node_modules # ls
@babel eslint-plugin-import is-glob prelude-ls
... たくさん
一向に解決できないので一度公式サンプルを動かしてみることにします。
# 作業ディレクトリに移動
$ cd ~/NextProjects
# サンプルから作成
$ npx create-next-app --example with-docker nextjs-docker
# 出来上がったフォルダに移動
$ cd nextjs-docker
# イメージの作成
$ docker build -t nextjs-docker .
# コンテナの作成と起動
$ docker run -p 3000:3000 nextjs-docker
http://localhost:3000/ にアクセスして表示されました。
バックグラウンドで起動して中に入り、中身の確認を行ってみます。
# バックグラウンドでコンテナを起動
$ docker run -p 3000:3000 -d nextjs-docker
# コンテナの確認
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
726632b1e953 nextjs-docker "docker-entrypoint.s…" 43 seconds ago Up 42 seconds 0.0.0.0:3000->3000/tcp wizardly_goldwasser
# コンテナの中に入る
$ docker container exec -it wizardly_goldwasser sh
# app の中身を確認
/app $ ls
node_modules package.json public server.js
# node_modules の中身を確認
/app $ cd node_modules/
/app/node_modules $ ls
@next next react use-sync-external-store
@swc object-assign react-dom
特に相違はなさそうです。
違いといえば、コンテナ起動時のdocker run -p 3000:3000 -d nextjs-docker
です。
元プロジェクトに戻ってポート指定を試してみます😢
ポートの指定
ファイル等は特に変えずに、コンテナ起動時にポートの指定-p 3000:3000
を追加して実行してみます。
# 作業ディレクトリに移動
$ cd ~/NextProjects/donuts-site
# docker build -f Dockerfile のファイル名 -t 作成するするイメージ名 もとになるDockerfileの場所
$ docker build -f prod.Dockerfile -t donut-site-image .
# image が作成できたかの確認
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
donut-site-image latest 14a4d9642166 42 minutes ago 121MB
# イメージからコンテナを作成して起動する
$ docker run --name donut-site -p 3000:3000 donut-site-image
表示できました😂数時間ハマってたのに😂
ポート指定をせずにコンテナを起動した場合は、3000/tcp
ポート指定をしてコンテナを起動した場合は、0.0.0.0:3000->3000/tcp
となっていました。
ポート番号はホストのポート番号:コンテナのポート番号
という指定になります。
ポート番号が同じ場合には->
移行は表示されません。
つまり、ホスト側のポートが3000/tcp
指定されていたためhttp://localhost:3000
にアクセスしても表示できなかったわけです。
この3000/tcp
が意味することとはなんでしょうか?
Docker Docs のコンテナーのネットワークページによると、-p 8080:80/tcp
は、コンテナーの TCP ポート 80 を Docker ホスト上のポート 8080 に割り当てますという意味になります。TCP ポートの他に UDP ポートも利用できます。
今回の3000/tcp
は、コンテナーの TCP ポート 3000ということになります。
これまでのシリーズでは特に指定なしで動いていたような気もしますが、ちゃんと指定しているサンプルを動かしているパターンと docker-compose.yml の方で指定していたパターンで動いていたのかもしれません。
基本的には、Docker コンテナ内で通信を行う場合には、ポートフォワーディング(ホストマシンのポートをコンテナポートに紐付けて、コンテナの外から来た通信をコンテナポートに紐づけることができる機能)を設定が必須であるという認識でよさそうです。
おわりに
沼ってしまったので、今回の記事はここまでにします。
たったひとつのポート指定漏れで泣いちゃうところでしたが、理解できてよかったです。
今回のように、コマンド実行時のポート指定の有無でサイトが表示できない…といったことも Docker Compose を用いて docker-compose.yml に定義してあげれば、ミスも減りそうですね。
ということで、次回は本番環境用のdocker-compose.yml
を用意していこうと思います。