はじめに
今回はNext.jsとtailwindcssでポートフォリオサイトを作成してGithub Actionsを用いてVPSに自動デプロイできるようにしたので,
その際の手順やymlファイルを共有します.
コードの全体像
next.confing.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output:'standalone'
};
export default nextConfig;
Github Actionsのmain.yml
name: Build and Deploy to VPS
on:
push:
branches: [ main ]
jobs:
build_and_deploy:
runs-on: ubuntu-latest
env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_PORT: ${{ secrets.SSH_PORT }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.17.1
- run: npm ci
- run: npm run build
- name: Prepare artifact
run: |
mkdir -p out_artifact
cp -ra .next/standalone/. out_artifact/
mkdir -p out_artifact/.next/static
cp -ra .next/static/. out_artifact/.next/static/
cp -r public out_artifact/public || true
- name: Setup SSH KEY
run: |
install -m 700 -d ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p "$SSH_PORT" "$SSH_HOST" >> ~/.ssh/known_hosts
- name: Deploy to VPS
run: |
set -euo pipefail
RELEASE="${GITHUB_SHA}"
ssh -i ~/.ssh/deploy_key -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
"sudo install -d -o $SSH_USER -g $SSH_USER -m 775 '$DEPLOY_PATH' '$DEPLOY_PATH/releases' '$DEPLOY_PATH/releases/$RELEASE'"
rsync -az --partial --delete --info=progress2 \
-e "ssh -p ${SSH_PORT} -i ~/.ssh/deploy_key" \
out_artifact/ "$SSH_USER@$SSH_HOST:$DEPLOY_PATH/releases/$RELEASE/"
- name: Start server
run: |
RELEASE="${GITHUB_SHA}"
ssh -i ~/.ssh/deploy_key -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "
set -e
sudo ln -sfn \"$DEPLOY_PATH/releases/$RELEASE\" \"$DEPLOY_PATH/current\"
if pm2 describe portfolio > /dev/null 2>&1; then
pm2 delete portfolio
fi
cd \"$DEPLOY_PATH/current\"
pm2 start ./server.js --name portfolio
pm2 save
"
- name: Delete key
if: always()
run: sudo rm -f ~/.ssh/deploy_key
コードの解説
next.config.ts
output:'standalone'
上記のコードを追記することでnext buildした際に最小限のファイルのみが生成されるかつそれらのファイルのみで動作が可能となります.
具体的には以下のようなディレクトリ構成のフォルダが生成されます.
起動時はserver.jsを指定することでサイトが立ち上がります.
standalone
├── node_modules
├── package.json
├── public
└── server.js
main.yml
- 実行のトリガー
on:
push:
branches: [ main ]
これによりmainブランチにpushされたときに実行されるようになります.
環境変数の設定
env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_PORT: ${{ secrets.SSH_PORT }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
これらの環境変数は当該リポジトリの「settings」→「Secrets and variables」→「Actions」→「Repository secrets」から登録できます.
ここに登録した変数は一度登録すると,登録した内容を見ることはできないので記述する際には注意して記述するようにしましょう.
SSH_USERはデプロイ先サーバのユーザ名
SSH_PORTはデプロイ先サーバにSSH接続するためのポート番号(デフォルトだと22番)
DEPLOY_PATHはデプロイ先サーバのどこにサイトの各種ファイルを置くかを指定
SSH_PRIVATE_KEYはデプロイ先サーバにSSHログインする際に使用する秘密鍵
SSH_HOSTはデプロイ先サーバにSSHログインする際のユーザ名です.
リポジトリのチェックアウト・必要なnode.jsのセットアップ・ビルド
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.17.1
- run: npm ci
- run: npm run build
npm ciでpackage-lock.json に固定されたバージョンをもとに依存関係を解決(クリーンインストール).
npm run buildでstandaloneファイルを生成してくれます.
各種ディレクトリの作成
- name: Prepare artifact
run: |
mkdir -p out_artifact
cp -ra .next/standalone/. out_artifact/
mkdir -p out_artifact/.next/static
cp -ra .next/static/. out_artifact/.next/static/
cp -r public out_artifact/public || true
buildした結果生成されたファイルをコピーするための各種ディレクトリを作成します.(これしなくてもできそうですよね)
SSH鍵のセットアップ
- name: Setup SSH KEY
run: |
install -m 700 -d ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p "$SSH_PORT" "$SSH_HOST" >> ~/.ssh/known_hosts
まず.sshディレクトリを権限700で作成します.
次に環境変数に設定した秘密鍵(SSH_PRIVATE_KEY)を登録します
登録した秘密鍵の権限を600にします.
ssh-keyscanでサーバの公開鍵フィンガープリントを取得することで以降の接続でホスト認証の確認を自動化します.(初めて接続するときにyes/noを聞かれないようにする)
VPSへファイルをコピー
- name: Deploy to VPS
run: |
set -euo pipefail
RELEASE="${GITHUB_SHA}"
ssh -i ~/.ssh/deploy_key -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
"sudo install -d -o $SSH_USER -g $SSH_USER -m 775 '$DEPLOY_PATH' '$DEPLOY_PATH/releases' '$DEPLOY_PATH/releases/$RELEASE'"
rsync -az --partial --delete --info=progress2 \
-e "ssh -p ${SSH_PORT} -i ~/.ssh/deploy_key" \
out_artifact/ "$SSH_USER@$SSH_HOST:$DEPLOY_PATH/releases/$RELEASE/"
今回はデプロイ実行ごとにサーバー側でフォルダを分けるようにしています.具体的にはコピー先を$DEPLOY_PATH/releases/$RELEASE/${GITHUB_SHA}とすることでトリガー時のコミットハッシュ値で分けるようにしています.
まず最初にサーバ側でデプロイ先となるフォルダをパーミッションまで指定して作成します.
ファイルのコピー方法はrsyncを用いており,--deleteで転送先にあって転送元にないファイルを削除,-aなどでパーミッション・シンボリックリンク・タイムスタンプなどを保持しつつコピーしています.
起動
- name: Start server
run: |
RELEASE="${GITHUB_SHA}"
ssh -i ~/.ssh/deploy_key -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "
set -e
sudo ln -sfn \"$DEPLOY_PATH/releases/$RELEASE\" \"$DEPLOY_PATH/current\"
if pm2 describe portfolio > /dev/null 2>&1; then
pm2 delete portfolio
fi
cd \"$DEPLOY_PATH/current\"
pm2 start ./server.js --name portfolio
pm2 save
"
現在動いているバージョンを指定するためにcurrentというディレクトリを作成し,対象となるハッシュ値を持つディレクトリからシンボリックリンクを張っています.
サーバの起動はpm2を使用しています.過去にすでにデプロイされていた場合(if pm2 describe portfolio > /dev/null 2>&1; then)にpm2 reloadではなく,pm2 deleteしているのは,今回シンボリックリンクを張っているcurrentディレクトリを指定して起動していることから,2度目のデプロイの際にcurrentディレクトリの中身は変わっても,pm2が示している実行ファイル(server.js)のパスが過去のものを指してしまうということを防ぐためです.(回りくどいコードになってしまいましたが・・・)
秘密鍵の削除
- name: Delete key
if: always()
run: sudo rm -f ~/.ssh/deploy_key
最後に秘密鍵を削除します.ここでif: always()とすることで,テストが失敗したかどうかを問わず鍵を削除するようにします.
最後に
自身の勉強の記録を残す場所,アウトプットする場所としてポートフォリオを充実させていくことはキャリアを通じても大事なことだと思います.
この記事が誰かの参考になれば幸いです.