はじめに
下記の記事でも紹介していますが、microCMSを使って自社サービスのLPを作りました。
今回は、そのNuxt.js 3で作ったLP(SSG)を、さくらサーバーにサクッとデプロイするためのシェルスクリプトを組んでみました。
自分用のメモも兼ねて、やったことを共有します。
背景
本当はGitHub Actionsとかでイケてる自動デプロイを組みたかったんですが、時間の都合で今回は見送ることに...。
弊社では基本自動デプロイの構築はインフラチームの担当ですが、インフラエンジニアっていつも大変そうですよね😿
とはいえ、手動デプロイは絶対にやりたくなかったので、ひとまず手元のDocker環境からコマンド一発でデプロイできるシェルスクリプトを作成しました。
シェルスクリプトって何?っていう状態からスタートでした。
ちなみに、さくらサーバーへの接続に使うSSHの秘密鍵はローカルに置いて、そのパスを環境変数(DOCKER_SSH_KEY_PATH)で渡す形にしています。
このやり方がベストプラクティスじゃないのは分かっているんですが、まずは「ミスなく・速く・確実に動くこと」を最優先にしました。
ただ、将来的にGitHub Actionsに移行するときにも、このスクリプトを流用できるように作ってあります。サーバー情報や鍵のパスは全部環境変数で外から渡せるようにしているので、CI/CD環境でもそのまま使えるはずです。
秘密鍵だけGitHub Actionsの環境変数に渡す想定ですね。
デプロイの仕組み
デプロイ先はすごくシンプルで、AWSなどのクラウドは使っておらず、さくらサーバーに静的ファイルを置く場所を借りているだけです。
CyberduckみたいなFTPソフトを使って手動でファイルをアップロードできますが、ファイルの転送が終わるまでに毎回20分くらいかかります。
今回作ったスクリプトだと、rsync(アールシンク)というコマンドで「変更があったファイルだけ」を転送(=差分転送)するので、なんと1分もかからずにデプロイが完了します。
rsyncはDockerファイルでコンテナにインストールしています。
RUN apk add --no-cache rsync
使用技術・環境
- フレームワーク: Nuxt.js 3
- ビルド: SSG(静的サイト生成)
- デプロイ先: さくらサーバー
- 転送方法: rsync(SSH経由)
- 実行環境: Dockerコンテナ内
デプロイスクリプトの全体像
まずは完成したdeploy.shの全体像です。
#!/bin/bash
# --- 設定 ---
# デプロイするディレクトリ(Nuxt3のSSG出力先)
LOCAL_BUILD_DIR="/app/.output/public/"
# さくらサーバー接続情報(.envファイルなどから読み込む)
REMOTE_USER="${DEPLOY_REMOTE_USER}" # ユーザー名
REMOTE_HOST="${DEPLOY_REMOTE_HOST}" # ホスト名
REMOTE_TARGET_DIR="${DEPLOY_REMOTE_TARGET_DIR}" # デプロイ先のディレクトリ
SSH_KEY_PATH="${DOCKER_SSH_KEY_PATH}" # SSH秘密鍵のパス
# --- スクリプト本体 ---
# エラーが起きたら途中で止める(安全装置)
set -eu
echo "🚀 デプロイを開始します..."
echo "-----------------------------------"
# 1. Nuxt3で静的ファイルをビルド
echo "🔨 ビルドプロセスを開始します (npm run generate)"
npm run generate
echo "✨ ビルドが正常に完了しました!"
echo "-----------------------------------"
echo "📦 ビルドメタデータファイルの転送を開始します..."
# 先にメタデータ用のディレクトリを作っておく
# (mkdir -p で、すでに存在していてもエラーにならない)
echo "📁 必要なディレクトリを作成中..."
ssh -i ${SSH_KEY_PATH} ${REMOTE_USER}@${REMOTE_HOST} "mkdir -p ${REMOTE_TARGET_DIR}/_nuxt/builds/meta"
# メタデータファイル(*.json)を先に転送
# --ignore-existing: サーバーに既にあるファイルは上書きしない
echo "🔄 メタデータファイルを転送中..."
rsync -vz --ignore-existing \
-e "ssh -i ${SSH_KEY_PATH} -p 22" \
"/app/.output/public/_nuxt/builds/meta/" \
"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_TARGET_DIR}/_nuxt/builds/meta/"
echo "✅ メタデータファイルの転送が完了しました"
echo "-----------------------------------"
# 2. rsyncでビルドしたファイル全体を転送
echo "📤 生成されたファイルをサーバーに転送中..."
echo "転送先: ${REMOTE_HOST}"
rsync -avz --delete \
-e "ssh -i ${SSH_KEY_PATH} -p 22" \
"${LOCAL_BUILD_DIR}" \
"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_TARGET_DIR}"
echo "-----------------------------------"
echo "🎉 デプロイが正常に完了しました!"
echo "🌐 サイトの更新を確認してください"
スクリプトの処理フロー
スクリプトがやっていることは、大きく分けて次の3ステップです。
-
静的ファイルの生成(
npm run generate) -
メタデータファイルの先行転送(
rsync --ignore-existing) -
全ファイルの同期(
rsync --delete)
それぞれ、もう少し詳しく見ていきます。
1. 静的ファイルの生成
npm run generate
これはおなじみですね。Nuxt.jsのgenerateコマンドで、.output/public/ディレクトリにデプロイに必要な静的ファイル(HTML, CSS, JSなど)を全部書き出します。
2. メタデータファイルの転送
ssh -i ${SSH_KEY_PATH} ... "mkdir -p ..."
rsync -vz --ignore-existing \
-e "ssh -i ${SSH_KEY_PATH} -p 22" \
"/app/.output/public/_nuxt/builds/meta/" \
...
ここが1つ目のポイントです。
rsyncで全ファイルを送る前に、Nuxt.jsが使う「ビルドメタデータ」(_nuxt/builds/meta/にあるjsonファイルなど)だけを先に転送しています。
-
mkdir -p: 転送先ディレクトリがなくても、-pオプションで親ディレクトリごと自動で作ってくれます。これは最初のデプロイで空のサーバーにデプロイするときのために書いています。 -
rsync --ignore-existing: 「サーバー側にすでに同じファイルがあったら上書きしない」というオプションです。メタデータは過去のビルド履歴が重要なので、消さずに残すようにしています。
(※なぜメタデータを先に送るのかは、後で「こだわったポイント」で説明します!)
3. 全ファイルの転送
rsync -avz --delete \
-e "ssh -i ${SSH_KEY_PATH} -p 22" \
"${LOCAL_BUILD_DIR}" \
...
最後に、ビルドしてできた.output/public/の中身すべてを、サーバーに転送します。
ここで使っているrsyncのオプションはこんな意味です。
-
-a(アーカイブモード): ファイルのパーミッション(権限)やタイムスタンプを保持したまま転送します。 -
-v(詳細モード): どのファイルが転送されているか、ログを出してくれます。 -
-z(圧縮モード): 転送中にデータを圧縮してくれるので、スピードが上がります。 -
--delete: 2つ目のポイントです。ローカル(転送元)にないファイルは、サーバー(転送先)から削除します。例えば、ビルドで不要になった古いJSファイルなどを自動で消して、ローカルとサーバーの状態を完全に同期(お揃いに)してくれます。 -
-e "ssh ...": rsyncが通信するときに、どのSSHコマンド(秘密鍵やポート)を使うかを指定しています。
重要なポイント
set -eu は安全装置
set -eu
スクリプトの先頭にあるこれは、一種の「おまじない」であり「安全装置」です。
-
-e: スクリプトの途中でerror(エラー)が起きたら、そこで処理をexit(中断)します。 -
-u: 定義されていない(undefined)変数を使おうとしたら、エラーとして中断します。
これがないと、例えばnpm run generate(ビルド)に失敗したのに、スクリプトが止まらず、古いファイルや空っぽのディレクトリをrsyncしてしまう...といった事故が起きかねません。デプロイスクリプトには必須の設定です。
環境変数で設定を分離
スクリプトの中に、サーバーのホスト名やユーザー名を直接書き込む(ハードコーディングする)のは良くありません。
DEPLOY_REMOTE_USERDEPLOY_REMOTE_HOSTDEPLOY_REMOTE_TARGET_DIRDOCKER_SSH_KEY_PATH
このように環境変数から設定を読み込むようにしておけば、このスクリプト(deploy.sh)自体を一切変更しなくても、ローカル環境・ステージング環境・本番環境で設定を切り替えることができます。
実行方法
1. 環境変数の設定
.envファイルなどに、さくらサーバーの情報を設定します。
DEPLOY_REMOTE_USER=your_username
DEPLOY_REMOTE_HOST=your_hostname.sakura.ne.jp
DEPLOY_REMOTE_TARGET_DIR=/home/your_username/www
DOCKER_SSH_KEY_PATH=/root/.ssh/id_rsa_sakura # Dockerコンテナ内の鍵のパス
2. SSH鍵の準備
さくらサーバーにSSH接続できる秘密鍵を準備し、DOCKER_SSH_KEY_PATHで指定した場所に置きます。
ローカルの秘密鍵をコンテナの中にマウントします。
volumes:
- ..:/app
- /app/node_modules
- ${MY_SSH_KEY_PATH}:${DOCKER_SSH_KEY_PATH}:ro
3. スクリプトに実行権限を付与
chmod +x deploy.sh
4. スクリプトの実行
Dockerコンテナに入って実行するなら、これだけです。
bash deploy.sh
こだわったポイント(メタデータを先に送る理由)
今回、rsyncを2回に分けているのが一番のこだわりポイントです。
# 1回目: メタデータだけ(上書きしない)
rsync -vz --ignore-existing ...
# 2回目: 全ファイル(古いファイルは削除)
rsync -avz --delete ...
なぜこんな面倒なことをしているかというと、デプロイ中にサイトが壊れるのを防ぐためです。
rsyncで全ファイルを一気に転送すると、転送の順番によっては「新しいHTMLファイル」と「古いJavaScriptファイル」がサーバー上に一時的に混在するタイミングが生まれてしまいます。
もし、ユーザーがその瞬間にサイトにアクセスすると、Nuxt.jsが「あれ?HTMLが要求してるJSファイルがない!」となったり、その逆が起きたりして、サイトが真っ白になったり、エラー(リビジョンの不整合)になったりします。
補足:リビジョン(Revision)とは?
ビルドごとに割り当てられる「改訂番号」のようなものです。Nuxt.jsは「このHTML(リビジョンA)には、このJS(リビジョンA)が必要」というのをメタデータで管理しています。
この事故を防ぐために、
- まず、過去のビルド情報も含むメタデータファイルだけ先に転送します。(
--ignore-existingで、過去の履歴を消さないように) - その次に、新しいファイル群を
--deleteオプション付きで一気に同期します。
こうすることで、新しいファイルが転送された時点で、それに対応するメタデータが必ずサーバー上に存在する状態(もしくは、少し古いメタデータが残っている状態)になり、エラーの発生を限りなくゼロに近づけることができます。
rsyncを使う理由
FTP(Cyberduckなど)じゃなくてrsyncにした理由は、速さと正確さです。
主に下記の三つがとても便利でした。
- 差分転送: 変更されたファイルだけを送るので、圧倒的に速い。(20分 → 1分!)
-
同期:
--deleteオプションで、ローカルにないファイル(不要になった古いファイル)をサーバーから自動で削除してくれるのが最高。 - 信頼性: SSH経由なので、安全に転送できます。
まとめ
このスクリプトを組んだことで、
- 手動で20分かかっていたデプロイが、コマンド一発で1分以内に終わるようになった
- エラーが起きたら自動で止まるので、中途半端なデプロイ事故がなくなった
- 設定を環境変数にしたので、他の環境にも応用しやすくなった
-
rsyncのおかげで、サーバーとローカルの状態をきれいに同期できるようになった
デプロイまで自分で組めると、つよつよエンジニアって感じがしていいですね😸
自動デプロイ編に、続く!!!!!