GitHub ActionsからエックスサーバーへFTPSで自動デプロイする方法
作成日:2026.11.08
GitHub ActionsとFTP-Deploy-Actionを使い、静的サイトをエックスサーバーへFTPSで自動デプロイする手順を紹介します。追加FTPアカウントとSecretsの準備、dry-runによる確認、push時の自動実行に加え、証明書検証や削除同期の注意点、530認証エラーの切り分けも解説します。
目次
GitHubで管理している静的サイトを、エックスサーバーへ毎回手動でアップロードするのは少し面倒です。GitHub ActionsからFTPSで転送できれば、変更を反映する作業をまとめられます。
今回は、FTP-Deploy-Actionを使った設定方法を紹介します。最初は手動実行で転送先を確認し、問題がなければmainへのpushで実行する構成へ進みます。
GitHub ActionsのWorkflow・ジョブ・ステップの基本は、以前のGitHub ActionsでPHP・LaravelのCIを始める記事を参照してください。今回は静的ファイルの転送が対象です。PHPアプリの依存関係のインストールやDBマイグレーションは含みません。
FTPSで何を転送するのか
GitHub ActionsのRunnerにリポジトリのファイルを取り出し、公開用ディレクトリの中身をFTP-Deploy-Actionでサーバーへ転送します。FTPSはFTPの通信をTLSで保護する方式です。SSHを利用するSFTPとは別のものですね。
GitHubリポジトリの dist/
↓ checkoutでRunnerへ取得
GitHub ActionsのRunner
↓ FTPSで転送
追加FTPアカウントの接続先 / actions-test/
ここでは、ビルド不要のHTML・CSS・JavaScriptなどをdist/に保存している前提にします。local-dir: ./dist/と指定した場合、転送するのはdistディレクトリそのものではなく、その中身です。
追加FTPアカウントと転送先を準備する
エックスサーバーのサーバーパネルで「FTPアカウント設定」を開き、デプロイ専用の追加FTPアカウントを作成します。「接続先ディレクトリ」は配信対象だけを操作できる範囲に絞っておきましょう。設定画面はエックスサーバー公式のFTPアカウント追加手順で確認できます。
FTPサーバー名・FTPユーザー名・パスワードを確認し、同じアカウントでFTPクライアントからFTPS接続できることを確かめます。サーバー名は接続設定に案内された値を使い、Webサイトのドメイン名を推測で入れないようにします。
最初の転送先は、既存サイトのファイルが混在しない専用ディレクトリにしてください。この記事ではactions-test/とします。
server-dirはFTPログイン先から見たパス
server-dirは、FTPでログインした位置を基準に指定します。たとえば追加FTPアカウントの接続先が公開ディレクトリなら、server-dir: ./actions-test/はその下の検証用ディレクトリです。
FTPでログインした位置
└── actions-test/
├── index.html
└── .ftp-deploy-sync-state.json
server-dir: ./ならログイン先そのものへの転送になります。エックスサーバー側の接続先ディレクトリと、Action側の相対パスをセットで確認してください。local-dirとserver-dirは、どちらも末尾に/が必要です。
FTP制限設定も確認する
接続元IPアドレスを制限している場合、手元のFTPクライアントから接続できても、GitHubのRunnerからは拒否されることがあります。さらに、追加FTPアカウントの接続先ディレクトリによっては、サーバー全体・ドメイン単位のFTP制限が適用されない条件もあります。詳しくはエックスサーバー公式のFTP制限設定と注意事項を確認してください。
一般的なGitHub-hosted runnerの接続元を、手元PCと同じ固定IPとしては扱えません。IP制限が必要な運用では、固定IPを利用できるRunnerなどを検討します。接続を通すために、既存の制限を一律に解除する進め方は避けましょう。GitHub公式のRunnerのIPアドレスに関する説明も参考になります。
接続情報をGitHub Secretsへ登録する
対象リポジトリのSettings → Secrets and variables → Actions → Secrets → New repository secretから、次の3つを登録します。
| Secret名 | 登録する内容 |
|---|---|
| FTP_SERVER | FTP接続先のサーバー名 |
| FTP_USERNAME | 追加FTPアカウントのユーザー名 |
| FTP_PASSWORD | 追加FTPアカウントのパスワード |
Workflowでは${{ secrets.FTP_PASSWORD }}のように参照します。パスワードをYAMLへ直接書くとGit履歴にも残るため、実値は書かないようにしてください。登録手順はGitHub公式のSecretsの使い方で確認できます。
手動実行用のWorkflowを作る
リポジトリを次の構成にします。dist/には、公開してよいファイルだけを置きます。今回はビルド処理を入れないので、dist/index.htmlもGitで管理する前提です。
.github/
└── workflows/
└── deploy.yml
dist/
└── index.html
.github/workflows/deploy.ymlを次の内容で作成します。最初はdry-run: trueにして、変更予定を確認する設定にしています。
name: Deploy static files via FTPS
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: deploy-static-site
cancel-in-progress: false
jobs:
deploy:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Deploy via FTPS
uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with:
server: ${{ secrets.FTP_SERVER }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
protocol: ftps
port: 21
security: strict
local-dir: ./dist/
server-dir: ./actions-test/
dry-run: true
dangerous-clean-slate: false
log-level: standard
main以外を公開しないよう、ジョブのifでもブランチを限定しています。手動実行で別ブランチを選ぶと、このジョブはスキップされます。デフォルトブランチが別の名前なら、ここも合わせて変更してください。
concurrencyは同じ転送先への実行が重ならないようにする設定です。cancel-in-progress: falseで実行中の転送を新しい実行のために中断しないようにしています。ただし、すべての待機中の実行が順番に処理される保証はありません。設定の詳細はGitHub公式の同時実行制御を参照してください。
FTPSと証明書検証は分けて設定する
protocol: ftpsはExplicit FTPSを使う指定です。この記事の例ではポート21を使用します。security: strictはサーバー証明書を検証するために指定しています。
FTP-Deploy-Actionのv4.4.0では、securityの既定値はlooseです。FTPSを指定しただけで、証明書の検証まで有効になるとは限りません。証明書エラーになった場合は、接続先サーバー名と証明書の対応、証明書チェーンを確認してください。エラーを消す目的でlooseへ変更するのは避けたいところです。
設定項目はFTP-Deploy-Action v4.4.0の設定一覧を参照してください。Actionは転送用の認証情報を受け取るため、使用する版と提供元も確認しておきましょう。
変更予定を確認してから実際に転送する
Workflowをデフォルトブランチへ反映したら、GitHubのActionsタブで実行します。workflow_dispatchで手動実行するには、Workflowファイルがデフォルトブランチに存在する必要があります。
- Actionsタブで「Deploy static files via FTPS」を選択します。
- 「Run workflow」を開き、ブランチに
mainを選んで実行します。 - 「Deploy via FTPS」のログで、接続の成否とアップロード・削除の予定を確認します。
- 転送先と予定が正しいことを確認したら、
dry-run: falseへ変更して再度手動実行します。 - FTPクライアントで転送されたファイルを確認し、公開領域ならブラウザーでも表示を確認します。
dry-runは転送・削除の予定を見るためのものです。接続は行うので、認証情報の誤りや証明書エラーを解決してから先へ進みます。また、dry-runの成功だけで、実際の書き込み権限やブラウザーでの表示まで確認できたことにはなりません。
実転送が終わると、通常は転送先に.ftp-deploy-sync-state.jsonも作成されます。これは以前転送したファイルの情報を管理するもので、バックアップではありません。通常の運用で、理由なく編集・削除しないようにします。
dangerous-clean-slate: falseでも削除は起こり得る
名前から少し誤解しやすいのですが、dangerous-clean-slate: falseは「削除同期を無効にする」設定ではありません。転送先全体を消去してから配置するモードを無効にしているだけです。
通常の差分同期でも、以前転送した管理対象ファイルをdist/から削除すると、サーバー側で削除対象になることがあります。最初から他のアプリや手動管理のファイルが混在しない転送先を使いましょう。
初回転送の次は、検証用ディレクトリで内容変更・追加・削除の順に確認すると、同期の範囲を把握しやすくなります。まずdry-runで予定を見てから実転送へ切り替えます。
530 Login incorrectが出たときの切り分け
今回、初回の設定でつまずいたのが、パスワード欄のプレースホルダーでした。接続先には到達しているのに、認証だけが通らない状態です。
当時の記録から接続先・ユーザー名・パスワードに関する行を省くと、失敗した段階は次のログで確認できます。
> AUTH TLS
< 234 AUTH TLS successful
Control socket is using: TLSv1.2
...
< 331 Password required for ***
...
< 530 Login incorrect.
この場合、サーバーへの接続とTLS確立までは進み、FTP認証で拒否されています。331はパスワードを求める応答なので、この行だけでユーザー名が正しいとは判断できません。また、TLS確立の行だけでは、証明書の検証設定が適切だったかまでは分かりません。
原因だったのは、Workflowのパスワード欄へ、次のような文字列をそのまま書いていたことでした。(AIの提案をちゃんと確認せず鵜呑みにしてコピペするとこうなるので気を付けましょう……)
# NG:この文字列がそのままパスワードとして渡る
password: <redacted>
# OK:登録したSecretを参照する
password: ${{ secrets.FTP_PASSWORD }}
<redacted>は伏せ字として使う表現で、GitHubが実際のパスワードへ置き換えてくれる記法ではありません。Secretsを登録していても、Workflow側で参照しなければ使われないわけですね。
530が出る場合は、次の順で確認します。
server・username・passwordが意図したSecretsを参照しているか確認する。- Secret名の誤記や未登録がないか確認する。必要なら値を再登録する。
- 同じアカウントで、手元のFTPクライアントからFTPS接続できるか確認する。
- FTP制限設定と
.ftpaccess、追加FTPアカウントへの適用条件を確認する。
パスワードをログに出して確かめる方法は使わないようにしましょう。未登録のSecretを参照した場合は空文字列になるため、名前の照合も必要です。
| 症状 | 先に確認するところ |
|---|---|
| 接続できない・タイムアウトする | サーバー名、ポート、接続元の制限 |
| TLS・証明書のエラー | FTPS方式、接続先名、証明書チェーン |
| 530で認証が拒否される | Secrets、アカウント、接続元制限 |
| 認証後、ファイル操作で550が出る | 対象パスの存在、アクセス権、操作内容 |
mainへのpushで自動実行する
手動での実転送と表示確認が済んだら、Workflow冒頭のon:を次の内容に置き換えます。転送先が正しいことと、dry-run: falseになっていることも確認してください。
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'dist/**'
- '.github/workflows/deploy.yml'
この例では、mainへのpushのうち、公開用ファイルかWorkflowが変更された場合に実行します。READMEだけの変更などでは動きません。手動実行も残しているので、必要なときに再実行できます。
なお、同じリポジトリでCIが動いていても、このWorkflowがCIの成功を自動的に待つわけではありません。テストやビルドの成功を公開条件にしたい場合は、同じWorkflowでジョブの依存関係を設定するなど、別途つなぐ必要があります。
実際の配信では、dist/とWorkflowを対象にしたpush起動を使用しています。ただし、FTP転送は複数のファイルを一括で切り替える仕組みではないため、転送中や失敗時には新旧ファイルが混ざる可能性があります。公開中のファイルや配布物のバックアップを残し、どの版へ戻すか決めておきましょう。ロールバック手順そのものは今回の確認範囲外です。
まとめ
GitHub Actionsからエックスサーバーへ静的ファイルを反映するなら、専用FTPアカウントとSecretsを用意し、手動実行で転送先を確認するところから始めると進めやすいと思います。FTPSの指定に加えて証明書検証と同期の削除範囲も確認しておきたいところです。
今回の530は、<redacted>をそのままパスワードにしてしまった設定ミスでした。通信がつながっているか、TLSまで進んでいるか、認証で拒否されているかを分けて読むと、調べる場所を絞れます。Secretsを登録するだけでなく、Workflowから正しく参照できているかも忘れずに確認しましょう。
奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。
これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。
【制作会社・企業様向けサポート】
Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。
ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。
Gitでコードの変更履歴を調べる:git blame・git show・git logの使い分け
2026.11.01
このコードはいつ、誰が、どのコミットで変更したのか。git blame・git show・git logを使い、特定行の最終変更や、削除された条件の履歴を調べる方法を紹介します。GitHub・Bitbucket Cloudでの確認方法と、Pull Requestや関連課題から変更理由を調べる流れも説明します。
GitのコンフリクトをVS Codeで解消する:PHPのサンプルで再現からテストまで
2026.10.22
GitHubのPull Requestで発生するコンフリクトを、PHPの小さな送料計算で再現します。mergeの基本から、VS Codeで両方の変更意図を反映する修正、テスト、同じPull Requestへの反映、作業を中止する方法までを解説します。
GitHub経験者向けBitbucket Cloud入門:Repository・Pull Request・ブランチ制限
2026.09.25
GitHubを使ったことがある開発者向けに、Bitbucket CloudのWorkspace・Project・Repositoryの関係と、cloneからPull Request、mergeまでの基本を紹介します。Branch restrictionsとMerge checksの違いや、Freeプランでの制約、HTTPS認証に使うAPI tokenについても整理します。
GitHub ActionsでPHP・LaravelのCIを始める:テスト・Lint・ビルドを自動化するWorkflowの基本
2026.09.13
PHP・Laravelプロジェクトを対象に、GitHub ActionsのWorkflow、イベント、ジョブ、ステップの基本を説明します。Composerのテスト、npm ci、フロントエンドビルド、Laravel PintをCIへ組み込む方法に加え、featureブランチやPull Requestで実行される条件、失敗時のログ確認、Secrets・権限・Actionのバージョン管理についても整理します。