Next.js のプロジェクトを VPS 上の Docker にデプロイする【その4】

Next.js のプロジェクトを VPS 上の Docker にデプロイする【その1】【その2】【その3】の続きです。

前回は実際に本番環境へのデプロイを行いました。
しかし、前回の GitHub のリポジトリを本番環境へ直接 pull するやり方では懸念点が多いため、今回はよりよいデプロイ方法について追求していこうと思います。

デプロイ方法の調査

Next.js プロジェクトを VPS 上の Docker へデプロイする方法について改めて調査してみます。

実際にやりたいことは「Deploying Next.js apps to a VPS using Github actions and Docker」の記事通りなので、今回は主にこの記事を参考に考えていきます。

上記記事の方法では、GitHub Actions と GitHub Container Registry を使って、自動で GitHub のリポジトリに変更があった場合(プッシュやマージ等)に VPS 上の Docker にデプロイしています。

GitHub Actions とは

GitHub Actionsは CI / CDツールです。ソフトウェア開発のワークフローをリポジトリの中で自動化して、実行することができます。
例えばリポジトリに対してプッシュがあった時に、自動でビルドを開始する、テストを行う等のアクションを自動化することが可能になります。

GitHub Actions のドキュメント

GitHub Container Registry とは

GitHub Container Registryは Docker イメージのレジストリ(保管場所)です。
GitHub が提供するレジストリなので、GitHub Actions との組み合わせが行いやすくなります。
docker loginコマンドで、GitHub の Personal Token を使って認証を行うので、他のサービスとの連携等が不要になります。

参考記事のコードを読む

Deploying Next.js apps to a VPS using Github actions and Docker」の GitHub Actions の自動化コードを読んで、実際にどのようなことを行っているのか調査してみます。

GitHub Actions のドキュメントをみながらdeploy.ymlの中身を読みます。

deploy.yml
# name: GitHub リポジトリの [アクション] タブに表示されるワークフローの名前(省略可能)
name: Build and Deploy

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the main branch
# on: ワークフローのトリガーを指定
on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      logLevel:
        description: 'Log level'
        required: true
        default: 'warning'

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
# ワークフローで実行されるすべてのジョブをグループ化
jobs:
  # This workflow contains a single job called "build"
  # 任意の名前のジョブを定義
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest
    container: node:14

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v2
      - name: Build and Publish to Github Packages Registry
        uses: elgohr/Publish-Docker-Github-Action@master
        env:
          NEXT_PUBLIC_BACKEND_URL: ${{ secrets.APP_NEXT_PUBLIC_BACKEND_URL }}
          NEXT_PUBLIC_META_API_KEY: ${{ secrets.APP_NEXT_PUBLIC_META_API_KEY }}
        with:
          name: my_github_username/my_repository_name/my_image_name
          registry: ghcr.io
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets. GITHUB_TOKEN }}
          dockerfile: Dockerfile
          buildargs: NEXT_PUBLIC_BACKEND_URL,NEXT_PUBLIC_META_API_KEY
          tags: latest

      - name: Deploy package to digitalocean
        uses: appleboy/ssh-action@master
        env:
          GITHUB_USERNAME: ${{ secrets.USERNAME }}
          GITHUB_TOKEN: ${{ secrets. GITHUB_TOKEN }}
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          port: ${{ secrets.DEPLOY_PORT }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          envs: GITHUB_USERNAME, GITHUB_TOKEN
          script: |
            docker login ghcr.io -u $GITHUB_USERNAME -p $GITHUB_TOKEN
            docker pull ghcr.io/my_github_username/my_repository_name/my_image_name:latest
            docker stop containername
            docker system prune -f
            docker run --name containername -dit -p 3000:3000 ghcr.io/my_github_username/my_repository_name/my_image_name:latest

大まかな構成

アクションを追加したいリポジトリに.github/workflows/ディレクトリを作成し、yaml ファイルを作成します。

まずは、name, on, jobsに分割してみていきます。
それぞれ以下を定義しています。

任意の名前.yml
# name: GitHub リポジトリの [アクション] タブに表示されるワークフローの名前(省略可能)
name: Build and Deploy

# on: ワークフローのトリガーを指定
on:

# ワークフローで実行されるすべてのジョブをグループ化
jobs:

トリガー指定

onはどのタイミングで jobs を実行するかを定義します。

on に書かれている概要は以下の通りです。

  • メインブランチにプッシュされた時
  • 手動実行時にどの粒度のログを表示させるかについての設定を行う
deploy.yml
on:
  # main ブランチにプッシュされた時
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      logLevel:
        description: 'Log level'
        required: true
        default: 'warning'
workflow_dispatch

workflow_dispatchはワークフローを手動でトリガーする場合に利用します。

ブラウザからワークフローを実行する場合に、ワークフローを実行する前に、必要な値を手動で入力する必要があります。

ジョブの指定

jobs ではワークフローで実際に行いたいアクションを定義します。

buildという名前のジョブを定義して、実行環境や実行内容を定義しています。

stepsでは、ジョブで実行されるすべてのステップをグループ化します。
stepsで入れ子になった各項目は、個別のアクションです。

deploy.yml
# ワークフローで実行されるすべてのジョブをグループ化
jobs:
  # 任意の名前のジョブを定義
  build:
    # Ubuntu Linux ランナーの最新バージョンで実行されるようにジョブを構成
    runs-on: ubuntu-latest
    # 実行環境のコンテナイメージを指定
    container: node:14

    # ジョブで実行されるすべてのステップをグループ化します。 
    steps:
ワークフローの実行環境

jobs.{job_id}.runs-onでジョブを実行する環境の定義ができます。

実行環境の種類は、GitHub ホステッド ランナーの選択にて確認することができます。

また、jobs.{job_id}.containerを設定することで、コンテナを作成して実行できるようになります。

jobs.{job_id}.runs-onで指定した Ubuntu Linux ランナー上の Docker Engine 上に
jobs.{job_id}.containerで指定したイメージよりコンテナを作成します。

steps はこの作成されたコンテナ内で実行されます。

Steps

stepsの中身を見てみます。

jobs.build の中には、3つのステップが定義されています。

usesでは、ジョブでステップの一部として実行されるアクションを選択します。

STEP.1
チェックアウトする

uses: actions/checkout@v2でアクションを使用する前にリポジトリをチェックアウトする必要があるそうです。ここ難しい。

deploy.yml
      # STEPワークフローと同じリポジトリにあるアクションの使用
      - uses: actions/checkout@v2
STEP.2
GitHub Packages Container Registry にビルドして公開する

elgohr/Publish-Docker-Github-Actionアクションを使って、GitHub Container Registry にビルドしていきます。

envでビルド時に利用する環境変数の指定ができます。

withで、入力パラメータを設定します。
ここで入力する内容は各アクションによって異なります。

今回利用しているelgohr/Publish-Docker-Github-Actionで指定されている各パラメータの設定を行います。

deploy.yml
      # ステップに名前をつける
      - name: Build and Publish to Github Packages Registry
        # パブリック アクションの使用
        uses: elgohr/Publish-Docker-Github-Action@master
        # 環境変数を設定
        env:
          NEXT_PUBLIC_BACKEND_URL: ${{ secrets.APP_NEXT_PUBLIC_BACKEND_URL }}
          NEXT_PUBLIC_META_API_KEY: ${{ secrets.APP_NEXT_PUBLIC_META_API_KEY }}
        # アクションによって定義される入力パラメーターを設定
        with:
          # イメージ名
          name: my_github_username/my_repository_name/my_image_name
          # イメージを格納したいレジストリ(GitHub Packages Container Registryを指定)
          registry: ghcr.io
          # GitHub のユーザ名
          username: ${{ secrets.USERNAME }}
          # GitHub のトークン
          password: ${{ secrets.GITHUB_TOKEN }}
          # Dockerfile の指定
          dockerfile: Dockerfile
          # 上記 env で指定した環境変数を利用
          buildargs: NEXT_PUBLIC_BACKEND_URL,NEXT_PUBLIC_META_API_KEY
          # イメージのタグを指定
          tags: latest
secrets の設定

環境変数等の secrets の設定は、GitHub 上で設定ができます。

.envファイルに書いている内容等を GitHub 上で指定してビルドを行います。
自サーバーに直接.envファイルを置かなくていいので、セキュリティ的にも安心感があります。

STEP.3
VPSへのデプロイ

次にVPS(記事の例ではDigitalOcean)へのデプロイを行います。

SSH接続をするためにappleboy/ssh-action@masterアクションを利用します。

VPS にSSH接続をして、STEP.2 でビルドしたイメージを pull してからコンテナを作り直すスクリプトが書かれています。

deploy.yml
      # ステップに名前をつける
      - name: Deploy package to digitalocean
        # パブリックアクションの使用
        uses: appleboy/ssh-action@master
        # 環境変数の設定
        env:
          GITHUB_USERNAME: ${{ secrets.USERNAME }}
          GITHUB_TOKEN: ${{ secrets. GITHUB_TOKEN }}
        # アクションによって定義される入力パラメーターを設定
        with:
          # デプロイ先のホストの指定
          host: ${{ secrets.DEPLOY_HOST }}
          # デプロイ先のポート番号の指定
          port: ${{ secrets.DEPLOY_PORT }}
          # デプロイ先のユーザを指定
          username: ${{ secrets.DEPLOY_USER }}
          # デプロイ先のキーを指定(SSH接続の秘密鍵)
          key: ${{ secrets.DEPLOY_KEY }}
          # 利用する環境変数の指定
          envs: GITHUB_USERNAME, GITHUB_TOKEN
          # 接続後に行うコマンド
          script: |
            docker login ghcr.io -u $GITHUB_USERNAME -p $GITHUB_TOKEN
            docker pull ghcr.io/my_github_username/my_repository_name/my_image_name:latest
            docker stop containername
            docker system prune -f
            docker run --name containername -dit -p 3000:3000 ghcr.io/my_github_username/my_repository_name/my_image_name:latest

ワークフローを定義する

さて、参考記事の自動化コードを読んで、なんとなくわかった気になったので実際にワークフローを書いていきます。

# 作業ディレクトリに移動する
$ cd ~/NextProjects/donuts-site

# ワークフローを配置するディレクトリを作成
$ mkdir -p .github/workflows/

# ワークフローファイルを作成する
$ touch .github/workflows/deploy-vps.yml

イメージのビルドには、Docker 公式が提供しているdocker/login-actiondocker/build-push-actionを用いてみました。

deploy-vps.yml
name: Deploy to Sakura VPS

# main ブランチにプッシュされた時に実行する
on: 
  push:
    branches: [main]
  # 手動実行時のログを設定
  workflow_dispatch:
    inputs:
      logLevel:
        description: 'Log level'
        required: true
        default: 'warning'
jobs:
  deploy:
    runs-on: ubuntu-latest
    container: node:16
    steps:
      # チェックアウト
      - uses: actions/checkout@v3
      # GitHub Container Registry へのログイン
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      # イメージのビルド
      - uses: docker/build-push-action@v3
        with:
          context: .
          # マルチステージビルドでのステージ名
          target: runner
          push: true
          tags: ghcr.io/<GitHubのユーザ名>/donuts-site-image:latest
      # VPSへのデプロイ
      - name: Deploy to Sakura VPS
        uses: appleboy/ssh-action@master
        env:
          GITHUB_USERNAME: ${{ secrets.USERNAME }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          port: ${{ secrets.DEPLOY_PORT }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          envs: GITHUB_USERNAME, GITHUB_TOKEN
          script: |
            docker login ghcr.io -u $GITHUB_USERNAME -p $GITHUB_TOKEN
            docker image pull ghcr.io/<GitHubのユーザ名>/DonutsSite/donuts-site-image:latest
            docker container stop donuts-site
            docker container rm donuts-site
            docker system prune -f
            docker container run --name donuts-site -dit -p 3000:3000 ghcr.io/<GitHubのユーザ名>/DonutsSite/donuts-site-image:latest

書いたら保存して GitHub にプッシュしておきます。

GitHub 上で Action タブよりワークフローが実行されたことを確認します。

secrets の設定をする

現状では secrets の設定をしていないので上記画像のようにエラーが起きています。

リポジトリの Settings > Secrets > Actions に必要な secrets を指定していきます。

以下の4つを設定します。

# さくらVPSのIPアドレス
DEPLOY_HOST
# SSH接続用のポート
DEPLOY_PORT
# SSH接続するユーザ
DEPLOY_USER
# SSH接続の秘密鍵
DEPLOY_KEY

GitHub のトークン(GITHUB_TOKEN)は自動生成されるので設定は必要ありません。

Actions タブより再度実行してみました。

エラーが出ました。

Error: Unable to locate executable file: docker. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.

まず、docker/login-actionを使うにはdocker/setup-buildx-actionを使ってねとのことなので追記します。(参考:「https://dev.classmethod.jp/articles/github-actions-docker-build-ecr/

jobs:
  deploy:
    runs-on: ubuntu-latest
    container: node:16
    steps:
      # チェックアウト
      - uses: actions/checkout@v3
      # Docker Buildx のセットアップ
      - uses: docker/setup-buildx-action@v1

上記エラーは消えないのでもう少し調べてみたところ、GitHub Container Registry を使う場合には 一度 ghcr.io にログインする必要があるようです。(参考:GitHub Container Registry(ghcr.io)にDockerイメージをpushする手順

上記エラーを一度もみ消して、参考記事通りにelgohr/Publish-Docker-Github-Actionを使ってみます。

deploy-vps.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    container: node:16
    steps:
      # チェックアウト
      - uses: actions/checkout@v3
      - name: Build and Publish to Github Container Registry
        uses: elgohr/Publish-Docker-Github-Action@master
        with:
          name: <GitHubのユーザ名>/DonutsSite/donuts-site-image
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          dockerfile: prod.Dockerfile
          tags: latest

SSH 接続できるようにする

GitHub Container Registry へのイメージのビルドと公開には成功しました。次は VPS へのデプロイでエラーが出ています。

GitHub Container Registry を確認する

GitHub Container Registry に格納されたイメージは、リポジトリの右側のメニューにあるpackagesより確認することができます。

SSH接続で失敗しているので改めて、appleboy/ssh-action の README.mdの確認をします。

どうやら秘密鍵と公開鍵が逆かもしれない…手順通りに生成してみます。

STEP.1
ローカルPCでキーの作成をする

まずは、ローカルPCでキーの作成を行います。

# キーの作成をする
$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
STEP.2
秘密鍵をGitHubリポジトリのsecretに登録する

生成された秘密鍵の方を 先ほどの GitHub のリポジトリの secrets に登録します。

STEP.3
公開鍵をVPSに配置する

公開鍵は VPS 上に配置します。

# VPSにログインする
$ ssh -p ポート番号 ユーザ名@IPアドレス

# ~/.ssh に配置
% cd ~/.ssh
% vi authorized_keys2
# ペーストする

# 権限を変更する
% chmod 700 ~/.ssh
% chmod 640 ~/.ssh/authorized_keys2

再度ワークフローの実行をしてみます。

Actions > 失敗したアクション > Re-run failed jobs を選択します。

ログインに成功しましたがまだエラーが出ています。

err: docker: invalid reference format: repository name must be lowercase.

Docker のイメージの名前とユーザ名は lowercase で書く必要があるそうなので変更します。

正しいイメージ名に変更したところ、無事に成功しました😊

VPSに接続してコンテナの稼働を確認する

最後にVPSに接続してコンテナを確認してみましょう。

# VPSにログインする
$ ssh -p ポート番号 ユーザ名@IPアドレス

# コンテナの確認をする
% docker container ls

CONTAINER ID   IMAGE                                                  COMMAND                  CREATED          STATUS          PORTS                                         NAMES
61a7e88d0003   ghcr.io/ユーザ名/donutssite/donuts-site-image:latest   "docker-entrypoint.s…"   22 minutes ago   Up 22 minutes   0.0.0.0:3000->3000/tcp                        donuts-site

# イメージの確認をする
% docker image ls

REPOSITORY                                      TAG             IMAGE ID       CREATED         SIZE
ghcr.io/ユーザ名/donutssite/donuts-site-image   latest          010e2e102c13   7 minutes ago   121MB

無事にコンテナの確認ができました。http://IPアドレス:3000 にアクセスしてサイトも表示されました😊

最後に、スタイルの変更をして main ブランチに push して変更が自動で反映されるか確認してみたところ、無事に変更がされました。

最終的なワークフロー

最後に Docker Compsoe 時に使用していたrestart: alwaysの指定を追記しました。

deploy.yml
name: Deploy to Sakura VPS

# main ブランチにプッシュされた時に実行する
on: 
  push:
    branches: [main]
  # 手動実行時のログを設定
  workflow_dispatch:
    inputs:
      logLevel:
        description: 'Log level'
        required: true
        default: 'warning'
jobs:
  deploy:
    runs-on: ubuntu-latest
    container: node:16
    steps:
      # チェックアウト
      - uses: actions/checkout@v3
      # Github Container Registry へビルドしてイメージを格納
      - name: Build and Publish to Github Container Registry
        uses: elgohr/Publish-Docker-Github-Action@master
        with:
          name: GitHubのユーザ名(lowercase)/donutssite/donuts-site-image
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          dockerfile: prod.Dockerfile
          tags: latest
      # VPSへのデプロイ
      - name: Deploy to Sakura VPS
        uses: appleboy/ssh-action@master
        env:
          GITHUB_USERNAME: ${{ github.actor }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          port: ${{ secrets.DEPLOY_PORT }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          envs: GITHUB_USERNAME, GITHUB_TOKEN
          script: |
            docker login ghcr.io -u $GITHUB_USERNAME -p $GITHUB_TOKEN
            docker image pull ghcr.io/GitHubのユーザ名(lowercase)/donutssite/donuts-site-image:latest
            docker container stop donuts-site
            docker container rm donuts-site
            docker system prune -f
            docker container run --name donuts-site -dit -p 3000:3000 --restart=always ghcr.io/GitHubのユーザ名(lowercase)/donutssite/donuts-site-image:latest

おわりに

無事にデプロイを自動化することに成功しました。

結果的に Docker Compose も使わなくなってしまいましたが、目標であった自動デプロイまでできました!

次回は、独自ドメインの設定とポート番号の変更等、最終的な調整をしていこうと思います。