GitHub の main ブランチにコミット(または PR マージ)が入るたびに、リポジトリの中身を Xserver へ自動でアップロードする仕組みを解説します。静的サイトや PHP ツール(HTML / PHP ベース)の配信に使えます。実際に Xserver で構築した際にハマったポイントと、その回避策までまとめました。
結論:Xserver への自動デプロイは「lftp + FTPS」が正解
先に答えを書きます。Xserver への自動デプロイは lftp + FTPS が正解です。既製の FTP アクションや SSH(SFTP) は Xserver 相手だと詰まりやすく、次のような結果になりました。

- SSH(SFTP):使えない。Xserver は GitHub Actions(海外IP)からの SSH を、鍵を受理した直後に切断する。クラウド/海外IPの SSH を拒否しているため設定では回避できない。
- FTPS を既製アクションで送る:失敗。
425 Unable to build data connection: Operation not permittedでデータ通信が失敗する(FTPS のデータ通信の TLS セッション再利用にライブラリが対応できない)。 - lftp + FTPS:成功。lftp は FTPS の TLS セッション再利用に正しく対応するため 425 を回避。FTP は海外IPからもログインできるので Xserver でも通る。暗号化(FTPS)を保ったまま配置できる。
なぜ FTP なら通って SSH は通らないのか:Xserver は FTP/FTPS のログインは海外IPからも許可するが、SSH は海外IPからのアクセスを拒否しているためです。GitHub Actions の実行サーバーは海外にあるので、SSH 方式は使えません。
セットアップ手順(4ステップ)
1. Xserver で FTP 接続情報を確認する
Xserver のサーバーパネルで、次の4点を控えます。
- FTPホスト名:「FTPアカウント設定」→ 対象ドメイン →「FTPソフト設定」に表示される
svXXXX.xserver.jp。 - FTPユーザー名:同画面のユーザー名(=サーバーID、または作成した FTP アカウント名)。
- FTPパスワード:メインの FTP ならサーバーパスワード。専用 FTP アカウントを作るとより安全。
- 配置先フォルダ(相対パス):FTP ログイン直後からの相対パス。Xserver は
ドメイン/public_html/配下が公開領域。例:example.com/public_html/tools/(末尾スラッシュ必須)。フォルダは自動作成されるので事前作成は不要。
2. GitHub に Secret を 4 つ登録する
リポジトリの Settings → Secrets and variables → Actions →「New repository secret」から登録します。
| Secret名 | 入れる値 |
|---|---|
| FTP_SERVER | FTPホスト名(例 svXXXX.xserver.jp) |
| FTP_USERNAME | FTPユーザー名 |
| FTP_PASSWORD | FTPパスワード |
| FTP_SERVER_DIR | 配置先フォルダ(相対パス・末尾スラッシュ。例 example.com/public_html/tools/) |
3. ワークフローファイルを置く
リポジトリに .github/workflows/deploy.yml を作成します(中身は次の章の完成版をコピー)。main ブランチに置くこと。
4. push して反映
git push origin main
push 後、GitHub の Actions タブで処理が緑(成功)になればデプロイ完了です。以降は main への push / PR マージのたびに自動でデプロイされます。関係者以外に見せたくない場合は、Xserver のサーバーパネルで対象フォルダに Basic 認証をかけます。
完成版ワークフロー deploy.yml

.github/workflows/deploy.yml として配置します。main への push / 手動実行で起動し、lftp + FTPS でリポジトリの内容を Xserver へ同期します。
name: Deploy to server (FTPS/lftp)
on:
push:
branches: [ main ]
workflow_dispatch: # Actionsタブから手動実行も可
concurrency:
group: deploy
cancel-in-progress: false # デプロイ同士を直列化
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# シークレット未設定なら停止(意図しない場所へ配置しない安全弁)
- name: Check required secrets
env:
S_SERVER: ${{ secrets.FTP_SERVER }}
S_USER: ${{ secrets.FTP_USERNAME }}
S_PASS: ${{ secrets.FTP_PASSWORD }}
S_DIR: ${{ secrets.FTP_SERVER_DIR }}
run: |
missing=""
[ -z "$S_SERVER" ] && missing="$missing FTP_SERVER"
[ -z "$S_USER" ] && missing="$missing FTP_USERNAME"
[ -z "$S_PASS" ] && missing="$missing FTP_PASSWORD"
[ -z "$S_DIR" ] && missing="$missing FTP_SERVER_DIR"
if [ -n "$missing" ]; then
echo "::error::FTPシークレットが未設定です:$missing"
exit 1
fi
- name: Install lftp
run: sudo apt-get update -qq && sudo apt-get install -y -qq lftp
- name: Deploy via lftp (FTPS)
env:
FTP_HOST: ${{ secrets.FTP_SERVER }}
FTP_USER: ${{ secrets.FTP_USERNAME }}
FTP_PASS: ${{ secrets.FTP_PASSWORD }}
FTP_DIR: ${{ secrets.FTP_SERVER_DIR }}
run: |
set -euo pipefail
# パスワードは LFTP_PASSWORD 経由で渡す(コマンド文字列に出さない)
env LFTP_PASSWORD="$FTP_PASS" lftp --env-password -u "$FTP_USER" "$FTP_HOST" -e "
set ftp:ssl-force true;
set ftp:ssl-protect-data true;
set ftp:ssl-auth TLS;
set ssl:verify-certificate no;
set ftp:passive-mode true;
set net:max-retries 2;
set net:timeout 20;
mirror -R --delete --verbose \
--exclude '\.git' \
--exclude 'orders\.json\$' \
--exclude 'invoices\.json\$' \
./ \"$FTP_DIR\";
bye
"
各設定の意味
- on: push: branches: [main] … main にコミット/PRマージが入ると起動。
workflow_dispatchで手動実行も可能。 - Check required secrets … 4つの Secret が未設定なら停止。配置先未設定のまま同期して事故るのを防ぐ安全弁。
- パスワードの渡し方 …
LFTP_PASSWORD環境変数+lftp --env-passwordで渡す。コマンド文字列に書かない。--env-passwordは lftp のコマンドライン引数であり、openの中では効かない点に注意。 - ssl-force / ssl-protect-data … ログイン(制御通信)とデータ通信を TLS 暗号化。
verify-certificate noは共有ホストで証明書のホスト名が一致しない場合の検証エラー回避(暗号化は維持)。 - mirror -R / –delete / –exclude … ローカル→サーバーへアップロード。
--deleteで余分なファイルも同期削除。--excludeしたファイルは転送も削除もされないので、保存データ(アプリが書き出す JSON 等)の保護に使う。
トラブルシューティング(ハマり所と回避策)
1. SSH が「鍵を受理した直後に切断」される → SSH は使えない
verbose ログで Server accepts key(公開鍵は受理)の直後に Connection closed by <Xserver IP> port 10022 となる。RSA でも ed25519 でも同一症状=鍵の問題ではない。同じIPから FTP ログインは成功するので、Xserver が海外IPからの SSH を拒否しているのが原因。SSH は諦めて FTP(FTPS) にするのが正解。
2. FTPS を既製アクションで送ると 425 エラー
Node 実装の FTP アクションでは、ログインは通るが転送時に 425 Unable to build data connection: Operation not permitted で失敗する。データ通信の TLS セッション再利用に対応できないのが原因。lftp に切り替えると解決する。
3. lftp で「mirror: Not connected」
ログインエラーも出ずに mirror: Not connected で失敗する場合、open コマンド内で --env-password を使っているのが原因(これは lftp のコマンドライン引数で、open 内では無効)。次のようにコマンドライン引数として渡す。
env LFTP_PASSWORD="$FTP_PASS" lftp --env-password -u "$FTP_USER" "$FTP_HOST" -e "set ...; mirror ...; bye"
4. 証明書の検証エラー
共有ホストでは証明書のホスト名と接続先が一致しないことがある。set ssl:verify-certificate no を入れると、暗号化は維持したままホスト名の検証だけを無効化できる。
5. 【最重要】保存データ(JSON 等)を消さない
mirror --delete は「ローカルに無いファイルをサーバーから削除」する。アプリがサーバー上に書き出す保存データは Git 管理外=ローカルに無いため、放置するとデプロイのたびに削除される。--exclude で除外すれば転送も削除もされず保持される。自分のアプリの保存ファイル名に合わせて指定する(例:--exclude 'orders\.json$')。
6. 特定のファイル名(例: index.php)だけアップロードできない
ログ上は成功表示なのに、そのファイルだけサーバーに実体が残らず 403 や表示崩れになることがある。共有ホストのセキュリティ(改ざん対策など)で特定のファイル名のアップロードがブロックされている場合がある(拡張子でなく名前固有)。トップが純粋な HTML(動的処理は api.php 等に分離)なら index.php → index.html にリネームして配置すれば回避できる。内部リンクを href="サブフォルダ/" のディレクトリ参照にしておけば影響しない。デプロイ後に配置確認のステップを入れると早期発見できる。
7. main への push がブロックされる
ブランチ保護や運用ポリシーで main への直接 push が禁止されている場合は、ワークフローの設置・修正コミットは作れても push 自体は権限を持つ人が行う。手動実行(gh workflow run)や Secret 設定(gh secret set)は push 不要で実行できる。
まとめ(最短手順)
- Xserver で FTP 接続情報(ホスト・ユーザー・パスワード・配置先パス)を確認する。
- GitHub に 4 つの Secret(FTP_SERVER / FTP_USERNAME / FTP_PASSWORD / FTP_SERVER_DIR)を登録する。
.github/workflows/deploy.yml(lftp + FTPS 版)をリポジトリに置く。mainに push する → 以降は push / マージのたびに自動デプロイ。

コメント