“VPN内のサーバーへ自動デプロイする方法 ― GitHub Actionsが繋がらない原因と現実的な解決策”

  • 2026年5月30日
  • 2026年5月30日
  • etc
etc

「コードをpushしたら、自動でサーバーへ反映される」状態を作りたい。よくある要望ですが、デプロイ先がVPN経由でしかアクセスできないサーバーだと、定番の手順が途中でつまずきます。

この記事は、GitHub Actions で自動デプロイを組んだものの最後の接続だけが失敗し、原因を追ったらネットワーク(VPN)だった、という記録です。環境固有の情報は伏せ、汎用的な内容としてまとめています。

やりたかったこと

やりたかったのはシンプルで、「コードをpushしたら、SFTP でサーバーへ自動アップロードする」こと。毎回手作業でファイルを上げる運用をやめたい、という目的でした。

構成は、GitHub へ push → GitHub Actions が起動 → SFTP でサーバーへアップロード、という流れ。ワークフロー(YAML)を書き、接続情報は GitHub Secrets に登録しました。

つまずいたエラー

ワークフローの読み込み、Secrets の読み込み、デプロイ処理の開始までは問題なく進みます。ところが、最後の接続でこのエラーが出ました。

ssh: connect to host *** port 22: Operation timed out

YAML を見直し、Secrets を確認し、鍵の形式も直しました。それでも、接続だけが通りません。

結論:原因は設定ではなく「ネットワーク到達性」

先に結論です。原因は YAML でも Secrets でも鍵形式でもなく、実行している場所からサーバーへ届かなかったことでした。

ポイントは、このタイムアウトが「認証より前の TCP 接続の段階」で出ていることです。つまり、鍵やパスワードを試す手前で止まっている。これは設定の問題ではない、という有力なサインです。

なぜ繋がらなかったのか

サーバーを「入館証がないと入れない部屋」だとイメージすると分かりやすいです。VPN がその入館証にあたります。

  • 自分のPCは VPN(入館証)を持っている → だから手作業ならアップロードできる
  • GitHub Actions はクラウド上で動く”代理ロボット”で、入館証を持っていない → ドアの前までは行くが開かず、返事を待ち続けて時間切れ

GitHub がホストするランナーは、自分の手元ではなくクラウド上で動きます。そのため、ローカルで張った VPN 接続は引き継がれません。手元の Mac や PC からは届くのに、Actions からは届かない、という状態が起きていたわけです。

解決の方向は大きく2つ

VPN 内のサーバーへ自動で届けるには、考え方として次のどちらかになります。

  • 実行する側(実行係)を VPN の中に置く
  • サーバー側に 取りに来てもらう(pull 型)

具体的な選択肢を整理すると、こうなります。

方式反映タイミングアップロードされる内容備考
セルフホストランナーを VPN 内に常駐push直後・ほぼ即時コミットした確定版企業構成の正攻法。実行マシンを常時起動しておく前提
GitHub Actions + VPN参加(Tailscale 等)push直後・ほぼ即時コミットした確定版クラウドランナーのまま。サーバー側にも VPN 導入が必要
pull型(端末やサーバーが Git から取得)数分間隔コミットした確定版外部からの到達問題が起きない。リアルタイムではない
半自動(VPN接続端末でスクリプトを手動実行)実行したとき手元の内容簡単・安全だが自動ではない

どれが最適かは、常時起動できるマシンがあるか、リアルタイム性が必要か、サーバー側を触れるか、といった条件で変わります。

今回採った方法:Git → 定期pull → SFTP(VPN内の端末で)

今回は、外部の CI を使わず、VPN 内の端末が Git リポジトリを定期的に pull し、変更があれば SFTP で反映する「pull型」を採用しました。

  • メリット:push 済みの確定版だけが反映される(本番向き)。外部クラウドから VPN 内へ届かない問題が起きない。
  • デメリット:反映は push 直後ではなく「数分間隔」になる(リアルタイムではない)。
  • 前提:その端末が起動中・VPN 接続中のあいだに動く(先に確認した「端末からサーバーへ届く」状態をそのまま活かせる)。

デプロイの大まかな手順

  1. デプロイ専用フォルダに Git リポジトリを clone する(初回だけ対話的に認証を通し、以降の自動 pull で聞かれないようにする)。
  2. パスワード認証の SFTP を自動化できるツールを用意する(Windows なら WinSCP など。標準コマンドの sftp/scp でも可)。初回接続でホスト鍵を信頼し、フィンガープリントを控える。
  3. パスワードなどの秘密情報はスクリプトに直書きせず、環境変数や秘密情報ストアで管理する。
  4. 「pull → 変更があればアップロード」を行うスクリプトを用意する。差分アップロードにすると、rsync のように変更分だけが送られる。
  5. OS のスケジューラ(Windows はタスクスケジューラ、Mac/Linux は cron)に登録し、数分間隔の定期実行にする。

スクリプトの考え方(PowerShell の例)

実際のホスト名・パスは伏せて、骨組みだけ示します。秘密情報は環境変数から読み込む形にしています。

powershell

# 設定(自分の値に置き換え)
$RepoDir    = "C:\deploy\site"
$Branch     = "main"
$RemotePath = "/var/www/html"
$HostName   = "<サーバーのホスト名>"
$UserName   = "<SFTPユーザー>"
$Password   = $env:DEPLOY_SFTP_PASS              # 秘密情報は環境変数から取得
$HostKey    = "ssh-ed25519 256 <フィンガープリント>"

# 1) 最新を取得し、変更があったかを確認
Set-Location $RepoDir
$before = git rev-parse HEAD
git pull origin $Branch | Out-Null
$after  = git rev-parse HEAD
if ($before -eq $after) { return }               # 変更がなければ何もしない

# 2) 変更あり → WinSCP で差分アップロード(.git などは除外)
Add-Type -Path "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
$opts = New-Object WinSCP.SessionOptions -Property @{
    Protocol = [WinSCP.Protocol]::Sftp
    HostName = $HostName; UserName = $UserName; Password = $Password
    SshHostKeyFingerprint = $HostKey
}
$session = New-Object WinSCP.Session
$session.Open($opts)

$t = New-Object WinSCP.TransferOptions
$t.FileMask = "|.git/;.vscode/"                  # アップロードから除外
# 第4引数 $False = リモートの余分なファイルは削除しない(安全側)
$session.SynchronizeDirectories(
    [WinSCP.SynchronizationMode]::Remote,
    $RepoDir, $RemotePath, $False, $False,
    [WinSCP.SynchronizationCriteria]::Time, $t).Check()
$session.Dispose()

Mac や Linux で同じことをするなら、cron + rsync を使うと、より少ない設定で同じ運用が作れます。

補足:VS Code の SFTP プラグインとの違い

「保存したら自動でアップロード」できる VS Code の SFTP プラグインもあります。手軽でリアルタイムなのが魅力ですが、性質が少し異なります。

プラグインは手元のファイルを保存と同時に送るものです。Git を経由しないため、コミット前の書きかけや、うっかり保存した変更もそのまま反映されます。一方、pull 型や CI はコミットした確定版だけを反映します。

そのため、使い分けるのが現実的です。

  • テスト/ステージング:保存即アップは確認が速く便利
  • 本番:コミット起点(pull 型や CI)で、確定版だけを反映する方が安全

なお、プラグインの設定ファイルにサーバー情報や鍵・パスワードを書くと、リポジトリに混ざって流出する恐れがあります。秘密情報は書かず、設定ファイルはバージョン管理から除外しておきましょう。

ハマりどころと運用のコツ

  • まず到達性を確認するTest-NetConnection <ホスト> -Port 22(Windows)などで、その端末からサーバーへ届くかを先に確かめる。ここが通らなければ、自動化以前のネットワークの問題。
  • 秘密情報は直書きしない。環境変数や Secrets で管理し、もし露出したら速やかにローテーション(再発行)する。
  • 同期はまず「消さない」設定から。サーバー側に手動で置いたファイル(アップロード画像など)を誤って消さないよう、最初は「リモートの余分なファイルを削除しない」設定で始める。
  • 本番は専用ブランチにする。確認できたものだけをデプロイ対象のブランチへ反映すると、レビュー前のコードが公開される事故を防げる。

まとめ

  • port 22: timed out が出たら、設定ミスより先に「実行している場所が VPN の外にいないか」を疑う。
  • 外部クラウドの CI から VPN 内のサーバーへは、直接は届かない。解決の方向は「実行係を VPN 内に置く」か「サーバー側に取りに来てもらう」のどちらか。
  • リアルタイム性と「確定版だけ公開する安全性」はトレードオフ。用途に合わせて方式を選ぶ。

同じ「自動化」でも、ネットワークの前提が一つ変わるだけで最適な手段は変わります。まずは到達性の確認から始めると、遠回りが減らせます。