<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Legacy on Tarragon</title><link>https://tarrragon.github.io/blog/tags/legacy/</link><description>Recent content in Legacy on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/legacy/index.xml" rel="self" type="application/rss+xml"/><item><title>接手維運：別人建的環境怎麼接管</title><link>https://tarrragon.github.io/blog/infra/takeover/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/takeover/</guid><description>&lt;p>接手維運跟從零建置的差別在於：從零建置時每一個資源都是自己點的，知道它存在、知道為什麼存在；接手時面對的是一個不確定哪些東西還在用、不知道動什麼會壞的環境。第一個要解的問題不是「怎麼做 infra」，而是「現在到底有什麼、它還能不能跑、改了會怎樣」。&lt;/p>
&lt;p>這個模組處理的是接管的操作流程，跟&lt;a href="https://tarrragon.github.io/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">成熟度階梯&lt;/a>平行而非串行 — 接手可能發生在任何成熟度階段：接手一個只有 FTP 存取的 PHP 站、接手一個有 SSH 但沒有 IaC 的雲端環境、接手一個有半套 IaC 但文件缺失的專案。每種情境的約束不同，但操作原則相通：先拍現況、再建維運能力、最後逐步正規化。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">無 SSH 的 FTP / 面板管理環境接管&lt;/a>&lt;/td>
 &lt;td>沒有 SSH、沒有 CLI、只有 FTP 和 phpMyAdmin 的 legacy 環境怎麼接管（總覽）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/legacy-database-backup-migration/" data-link-title="無 SSH 環境的資料庫備份與變更管理" data-link-desc="在只有 phpMyAdmin 或有限遠端連線的無 SSH 環境裡，怎麼建立可靠的資料庫備份策略、schema 變更紀律與還原演練流程">無 SSH 環境的資料庫備份與變更管理&lt;/a>&lt;/td>
 &lt;td>phpMyAdmin 的限制與對策、備份策略、migration 紀律、還原演練&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/legacy-code-versioning-deployment/" data-link-title="程式碼版控與 FTP 部署紀律" data-link-desc="無 SSH 環境的 PHP 專案的程式碼怎麼從 FTP 拉回來建 Git repo、設定檔怎麼分離、FTP 部署怎麼建立可追蹤的流程、以及怎麼用 CI 取代手動上傳">程式碼版控與 FTP 部署紀律&lt;/a>&lt;/td>
 &lt;td>本地 Git 工作流、config 分離、FTP 部署風險控制、CI 化 FTP&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/legacy-php-security-audit/" data-link-title="Legacy PHP 的安全盤點" data-link-desc="接手 legacy PHP 專案後的系統性安全審查：credential 掃描、PHP 版本風險、常見漏洞模式的 grep 偵測、.htaccess 防線、檔案權限、外部依賴與掃描工具">Legacy PHP 的安全盤點&lt;/a>&lt;/td>
 &lt;td>credential 掃描、PHP 版本風險、SQL injection/XSS 模式、.htaccess 防護&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/legacy-external-monitoring/" data-link-title="無 SSH 環境的監控與告警" data-link-desc="無 SSH 環境沒辦法裝 agent、沒辦法串 log pipeline，用外部 HTTP check、錯誤追蹤服務與效能基線建立最低成本的監控能力">無 SSH 環境的監控與告警&lt;/a>&lt;/td>
 &lt;td>外部 HTTP check、錯誤追蹤、效能基線、流量異常偵測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/cloud-no-iac/" data-link-title="有 SSH 但沒有 IaC 的雲端環境接管" data-link-desc="接手一個全手動建立的雲端環境時，怎麼盤點資源、推導依賴關係、收斂 credential、驗證備份、建立變更紀律，以及什麼時候該開始導入 IaC">有 SSH 但沒有 IaC 的雲端環境接管&lt;/a>&lt;/td>
 &lt;td>有 Console 和 CLI 存取、但資源全是手動建的雲端環境怎麼盤點和接管&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/partial-iac-no-docs/" data-link-title="有半套 IaC 但文件缺失的環境接管" data-link-desc="IaC 覆蓋不完整、部分資源在 state 外、文件缺失的環境怎麼盤點差距、修復 state 健康、收斂 drift 並重建文件">有半套 IaC 但文件缺失的環境接管&lt;/a>&lt;/td>
 &lt;td>IaC 覆蓋不完整、部分資源在 state 外、文件缺失的環境怎麼收斂（總覽）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/partial-iac-state-repair/" data-link-title="State 修復與清理" data-link-desc="接手的 Terraform state 損壞、有 orphaned entry、或需要搬遷時，怎麼診斷問題、安全操作、以及從錯誤中回復">State 修復與清理&lt;/a>&lt;/td>
 &lt;td>state 損壞診斷、orphaned entry 清理、state surgery、backend 搬遷&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/partial-iac-drift-triage/" data-link-title="Drift 分類處理指南" data-link-desc="接手半套 IaC 環境時，怎麼讀 plan 輸出分類 drift、判斷保留還是回退、處理 stateful 資源的高風險漂移，以及批次收斂的工作流">Drift 分類處理指南&lt;/a>&lt;/td>
 &lt;td>plan 輸出分類、adopt vs revert 決策、stateful replacement 風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/partial-iac-bulk-import/" data-link-title="Unmanaged Resource 批次 Import 工作流" data-link-desc="把 Terraform state 外的雲端資源有系統地納入 IaC 管理：優先序判斷、import block 語法、generated HCL 的 review 要點、批次策略與常見失敗處理">Unmanaged Resource 批次 Import&lt;/a>&lt;/td>
 &lt;td>優先序、import block、generated HCL review、批次策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/takeover/partial-iac-dual-truth-operation/" data-link-title="兩套真相並存的過渡期操作" data-link-desc="部分資源在 IaC、部分在手動時，怎麼安全操作避免比全手動更危險，以及怎麼縮短這個過渡期">兩套真相並存的過渡期操作&lt;/a>&lt;/td>
 &lt;td>操作規則、ownership 台帳、團隊溝通、import sprint、transition 完成判準&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跟其他模組的關係">跟其他模組的關係&lt;/h2>
&lt;p>接手維運的終點是把環境帶到&lt;a href="https://tarrragon.github.io/blog/infra/before-infra/" data-link-title="模組負一：還沒有 infra 的環境怎麼盡量做好" data-link-desc="手動點起家的環境怎麼守底線、降低未來納管成本、辨識何時該開始導入 IaC — 給還沒有能力上 IaC 的真實起點">模組負一&lt;/a>（可控的手動）或&lt;a href="https://tarrragon.github.io/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一&lt;/a>（最小可行 IaC）的狀態。接手流程本身不做 IaC 導入 — 它的責任是讓接手者理解環境、建立維運能力、確認什麼能動什麼不能動。IaC 導入是接手完成之後的下一步。&lt;/p></description><content:encoded><![CDATA[<p>接手維運跟從零建置的差別在於：從零建置時每一個資源都是自己點的，知道它存在、知道為什麼存在；接手時面對的是一個不確定哪些東西還在用、不知道動什麼會壞的環境。第一個要解的問題不是「怎麼做 infra」，而是「現在到底有什麼、它還能不能跑、改了會怎樣」。</p>
<p>這個模組處理的是接管的操作流程，跟<a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">成熟度階梯</a>平行而非串行 — 接手可能發生在任何成熟度階段：接手一個只有 FTP 存取的 PHP 站、接手一個有 SSH 但沒有 IaC 的雲端環境、接手一個有半套 IaC 但文件缺失的專案。每種情境的約束不同，但操作原則相通：先拍現況、再建維運能力、最後逐步正規化。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">無 SSH 的 FTP / 面板管理環境接管</a></td>
          <td>沒有 SSH、沒有 CLI、只有 FTP 和 phpMyAdmin 的 legacy 環境怎麼接管（總覽）</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/takeover/legacy-database-backup-migration/" data-link-title="無 SSH 環境的資料庫備份與變更管理" data-link-desc="在只有 phpMyAdmin 或有限遠端連線的無 SSH 環境裡，怎麼建立可靠的資料庫備份策略、schema 變更紀律與還原演練流程">無 SSH 環境的資料庫備份與變更管理</a></td>
          <td>phpMyAdmin 的限制與對策、備份策略、migration 紀律、還原演練</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/takeover/legacy-code-versioning-deployment/" data-link-title="程式碼版控與 FTP 部署紀律" data-link-desc="無 SSH 環境的 PHP 專案的程式碼怎麼從 FTP 拉回來建 Git repo、設定檔怎麼分離、FTP 部署怎麼建立可追蹤的流程、以及怎麼用 CI 取代手動上傳">程式碼版控與 FTP 部署紀律</a></td>
          <td>本地 Git 工作流、config 分離、FTP 部署風險控制、CI 化 FTP</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/takeover/legacy-php-security-audit/" data-link-title="Legacy PHP 的安全盤點" data-link-desc="接手 legacy PHP 專案後的系統性安全審查：credential 掃描、PHP 版本風險、常見漏洞模式的 grep 偵測、.htaccess 防線、檔案權限、外部依賴與掃描工具">Legacy PHP 的安全盤點</a></td>
          <td>credential 掃描、PHP 版本風險、SQL injection/XSS 模式、.htaccess 防護</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/takeover/legacy-external-monitoring/" data-link-title="無 SSH 環境的監控與告警" data-link-desc="無 SSH 環境沒辦法裝 agent、沒辦法串 log pipeline，用外部 HTTP check、錯誤追蹤服務與效能基線建立最低成本的監控能力">無 SSH 環境的監控與告警</a></td>
          <td>外部 HTTP check、錯誤追蹤、效能基線、流量異常偵測</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/takeover/cloud-no-iac/" data-link-title="有 SSH 但沒有 IaC 的雲端環境接管" data-link-desc="接手一個全手動建立的雲端環境時，怎麼盤點資源、推導依賴關係、收斂 credential、驗證備份、建立變更紀律，以及什麼時候該開始導入 IaC">有 SSH 但沒有 IaC 的雲端環境接管</a></td>
          <td>有 Console 和 CLI 存取、但資源全是手動建的雲端環境怎麼盤點和接管</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/takeover/partial-iac-no-docs/" data-link-title="有半套 IaC 但文件缺失的環境接管" data-link-desc="IaC 覆蓋不完整、部分資源在 state 外、文件缺失的環境怎麼盤點差距、修復 state 健康、收斂 drift 並重建文件">有半套 IaC 但文件缺失的環境接管</a></td>
          <td>IaC 覆蓋不完整、部分資源在 state 外、文件缺失的環境怎麼收斂（總覽）</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/takeover/partial-iac-state-repair/" data-link-title="State 修復與清理" data-link-desc="接手的 Terraform state 損壞、有 orphaned entry、或需要搬遷時，怎麼診斷問題、安全操作、以及從錯誤中回復">State 修復與清理</a></td>
          <td>state 損壞診斷、orphaned entry 清理、state surgery、backend 搬遷</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/takeover/partial-iac-drift-triage/" data-link-title="Drift 分類處理指南" data-link-desc="接手半套 IaC 環境時，怎麼讀 plan 輸出分類 drift、判斷保留還是回退、處理 stateful 資源的高風險漂移，以及批次收斂的工作流">Drift 分類處理指南</a></td>
          <td>plan 輸出分類、adopt vs revert 決策、stateful replacement 風險</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/takeover/partial-iac-bulk-import/" data-link-title="Unmanaged Resource 批次 Import 工作流" data-link-desc="把 Terraform state 外的雲端資源有系統地納入 IaC 管理：優先序判斷、import block 語法、generated HCL 的 review 要點、批次策略與常見失敗處理">Unmanaged Resource 批次 Import</a></td>
          <td>優先序、import block、generated HCL review、批次策略</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/takeover/partial-iac-dual-truth-operation/" data-link-title="兩套真相並存的過渡期操作" data-link-desc="部分資源在 IaC、部分在手動時，怎麼安全操作避免比全手動更危險，以及怎麼縮短這個過渡期">兩套真相並存的過渡期操作</a></td>
          <td>操作規則、ownership 台帳、團隊溝通、import sprint、transition 完成判準</td>
      </tr>
  </tbody>
</table>
<h2 id="跟其他模組的關係">跟其他模組的關係</h2>
<p>接手維運的終點是把環境帶到<a href="/blog/infra/before-infra/" data-link-title="模組負一：還沒有 infra 的環境怎麼盡量做好" data-link-desc="手動點起家的環境怎麼守底線、降低未來納管成本、辨識何時該開始導入 IaC — 給還沒有能力上 IaC 的真實起點">模組負一</a>（可控的手動）或<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一</a>（最小可行 IaC）的狀態。接手流程本身不做 IaC 導入 — 它的責任是讓接手者理解環境、建立維運能力、確認什麼能動什麼不能動。IaC 導入是接手完成之後的下一步。</p>
<ul>
<li>→ <a href="/blog/infra/before-infra/" data-link-title="模組負一：還沒有 infra 的環境怎麼盡量做好" data-link-desc="手動點起家的環境怎麼守底線、降低未來納管成本、辨識何時該開始導入 IaC — 給還沒有能力上 IaC 的真實起點">模組負一：還沒有 infra 的環境</a>：接手完成後，環境的操作紀律對齊這裡</li>
<li>→ <a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零：infra 是什麼</a>：成熟度階梯作為接手後評估現況的座標</li>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證</a>：接手時的 credential 盤點與輪替</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：接手後的 tagging 與 secret 管理</li>
</ul>
]]></content:encoded></item><item><title>無 SSH 的 FTP / 面板管理環境接管</title><link>https://tarrragon.github.io/blog/infra/takeover/legacy-ftp-no-ssh/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/takeover/legacy-ftp-no-ssh/</guid><description>&lt;p>接手一個只有 FTP 和網頁面板（cPanel / Plesk / phpMyAdmin）存取的 PHP 專案時，面對的約束跟有 SSH 的環境不同：沒辦法登入下指令、沒有 CLI 工具可以批次操作、部署靠 FTP 上傳檔案、資料庫操作靠 phpMyAdmin 的網頁介面。這類環境常見於共享主機，但也可能出現在只安裝了面板的獨立主機或 VPS 上。前一位維護者的「文件」是他的記憶，而這份記憶已經隨著人一起離開。第一步是穩定維運，不是現代化改造。&lt;/p>
&lt;p>這篇文章的操作順序按風險排列：先做不碰 prod 的盤點（零風險），再建本地開發環境（只動本機），然後才是碰 prod 的部署與資料庫紀律。&lt;/p>
&lt;h2 id="拍下完整現況不動-prod">拍下完整現況（不動 prod）&lt;/h2>
&lt;p>接手後的第一個工作日只做一件事：把 prod 的完整狀態拍一份下來存到本地。這一步不改 prod 的任何東西，目的是讓自己手上有一份可對照的快照。&lt;/p>
&lt;p>環境不同，拍照的工具和流程不同。先判斷自己的情境：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>有 cPanel / Plesk 完整備份功能&lt;/strong> → &lt;a href="#%e7%94%a8%e4%b8%bb%e6%a9%9f%e9%9d%a2%e6%9d%bf%e4%b8%80%e6%ac%a1%e6%89%93%e5%8c%85">用主機面板一次打包&lt;/a>&lt;/li>
&lt;li>&lt;strong>只有 FTP 存取&lt;/strong> → &lt;a href="#%e7%94%a8-ftp-%e9%80%90%e5%b1%a4%e6%8b%8d%e7%85%a7">用 FTP 逐層拍照&lt;/a>&lt;/li>
&lt;li>&lt;strong>有 SSH 存取&lt;/strong>（部分 VPS 或獨立主機）→ 改讀&lt;a href="https://tarrragon.github.io/blog/infra/takeover/cloud-no-iac/" data-link-title="有 SSH 但沒有 IaC 的雲端環境接管" data-link-desc="接手一個全手動建立的雲端環境時，怎麼盤點資源、推導依賴關係、收斂 credential、驗證備份、建立變更紀律，以及什麼時候該開始導入 IaC">有 SSH 但沒有 IaC 的雲端環境接管&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="用主機面板一次打包">用主機面板一次打包&lt;/h3>
&lt;p>如果主機有 cPanel，「備份精靈（Backup Wizard）」可以一次打包程式碼 + 資料庫 + email 設定 + cron jobs，是最快的完整快照方式。Plesk 的對應功能在「工具與設定 → 備份管理員」。&lt;/p>
&lt;p>面板備份通常包含：網站檔案（含隱藏檔）、所有 MySQL 資料庫、email 帳戶與轉寄規則、cron job 設定、DNS zone 記錄。下載打包檔後解壓到本地、用 Git 初始化（見下方「初始化 Git repo」段）。&lt;/p>
&lt;p>面板備份可能不包含的：SSL 憑證的私鑰（Let&amp;rsquo;s Encrypt 自動續期的通常不需要手動備份）、PHP 版本與模組設定（需要另外記錄，見&lt;a href="#%e7%92%b0%e5%a2%83%e8%a8%ad%e5%ae%9a%e7%9a%84%e6%8b%8d%e7%85%a7">環境設定的拍照&lt;/a>）、&lt;code>.htaccess&lt;/code> 以外的 Apache/LiteSpeed 自訂設定。拿到面板備份後仍然要跑「環境設定的拍照」段，因為面板備份拍的是檔案、不是環境設定。&lt;/p>
&lt;h3 id="用-ftp-逐層拍照">用 FTP 逐層拍照&lt;/h3>
&lt;p>沒有主機面板（或面板不提供完整備份）時，要用 FTP 和 phpMyAdmin 分別拍程式碼和資料庫。&lt;/p>
&lt;p>&lt;strong>程式碼與靜態資源&lt;/strong>：用 FTP client 把整個網站目錄鏡像到本地。FileZilla 的操作路徑：站台管理員連線後，在遠端面板對根目錄按右鍵 → 「下載」，或用「伺服器 → 同步瀏覽」模式讓本地與遠端目錄結構保持對齊。WinSCP 提供「保持更新（Keep Remote Directory up to Date）」功能，但接手階段只需要一次性的完整下載，不需要持續同步。下載前確認 FTP client 的設定有勾選「顯示隱藏檔案」——&lt;code>.htaccess&lt;/code>、&lt;code>.env&lt;/code>、&lt;code>.user.ini&lt;/code> 這類隱藏檔經常包含關鍵設定。&lt;/p>
&lt;p>&lt;strong>資料庫&lt;/strong>：用 phpMyAdmin 的「匯出」功能匯出完整資料庫（詳見下方「資料庫」段）。FTP 只拍程式碼，資料庫要另外匯出。&lt;/p>
&lt;h3 id="初始化-git-repo">初始化 Git repo&lt;/h3>
&lt;p>不論用面板備份還是 FTP 逐層拍，拿到檔案後都初始化成 Git repo：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mkdir project-takeover &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> project-takeover
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># FTP 下載完整站台到此目錄後&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">git init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">git add -A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;initial snapshot from production FTP&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 commit 是接手的基準線。之後任何改動都能 diff 回這個起點，知道自己改了什麼。&lt;/p>
&lt;h3 id="資料庫">資料庫&lt;/h3>
&lt;p>用 phpMyAdmin 的「匯出」功能：選「自訂」模式 → 勾選所有資料表 → 格式選 SQL → 勾選「加入 DROP TABLE / VIEW / PROCEDURE / FUNCTION / EVENT / TRIGGER 敘述」（讓匯入時能乾淨覆蓋）→ 壓縮選 gzip（大型資料庫避免瀏覽器逾時）→ 編碼選 UTF-8 → 執行。&lt;/p>
&lt;p>phpMyAdmin 的匯出在資料庫超過幾百 MB 時容易因 PHP &lt;code>max_execution_time&lt;/code> 或記憶體限制中斷。替代方案：如果主機有 cPanel，「phpMyAdmin → 匯出」旁邊通常有「MySQL 資料庫備份」或透過 cPanel API 的 &lt;code>mysqldump&lt;/code> 介面，比 phpMyAdmin 的 PHP 層匯出更可靠。另一個選項是本地安裝 DBeaver（免費、跨平台）或 TablePlus（macOS/Windows），用主機提供的遠端 MySQL 連線（cPanel → 遠端 MySQL → 加入本機 IP 白名單）直接從本機執行 &lt;code>mysqldump&lt;/code>。HeidiSQL（Windows 免費）也支援同樣的遠端連線匯出。&lt;/p></description><content:encoded><![CDATA[<p>接手一個只有 FTP 和網頁面板（cPanel / Plesk / phpMyAdmin）存取的 PHP 專案時，面對的約束跟有 SSH 的環境不同：沒辦法登入下指令、沒有 CLI 工具可以批次操作、部署靠 FTP 上傳檔案、資料庫操作靠 phpMyAdmin 的網頁介面。這類環境常見於共享主機，但也可能出現在只安裝了面板的獨立主機或 VPS 上。前一位維護者的「文件」是他的記憶，而這份記憶已經隨著人一起離開。第一步是穩定維運，不是現代化改造。</p>
<p>這篇文章的操作順序按風險排列：先做不碰 prod 的盤點（零風險），再建本地開發環境（只動本機），然後才是碰 prod 的部署與資料庫紀律。</p>
<h2 id="拍下完整現況不動-prod">拍下完整現況（不動 prod）</h2>
<p>接手後的第一個工作日只做一件事：把 prod 的完整狀態拍一份下來存到本地。這一步不改 prod 的任何東西，目的是讓自己手上有一份可對照的快照。</p>
<p>環境不同，拍照的工具和流程不同。先判斷自己的情境：</p>
<ul>
<li><strong>有 cPanel / Plesk 完整備份功能</strong> → <a href="#%e7%94%a8%e4%b8%bb%e6%a9%9f%e9%9d%a2%e6%9d%bf%e4%b8%80%e6%ac%a1%e6%89%93%e5%8c%85">用主機面板一次打包</a></li>
<li><strong>只有 FTP 存取</strong> → <a href="#%e7%94%a8-ftp-%e9%80%90%e5%b1%a4%e6%8b%8d%e7%85%a7">用 FTP 逐層拍照</a></li>
<li><strong>有 SSH 存取</strong>（部分 VPS 或獨立主機）→ 改讀<a href="/blog/infra/takeover/cloud-no-iac/" data-link-title="有 SSH 但沒有 IaC 的雲端環境接管" data-link-desc="接手一個全手動建立的雲端環境時，怎麼盤點資源、推導依賴關係、收斂 credential、驗證備份、建立變更紀律，以及什麼時候該開始導入 IaC">有 SSH 但沒有 IaC 的雲端環境接管</a></li>
</ul>
<h3 id="用主機面板一次打包">用主機面板一次打包</h3>
<p>如果主機有 cPanel，「備份精靈（Backup Wizard）」可以一次打包程式碼 + 資料庫 + email 設定 + cron jobs，是最快的完整快照方式。Plesk 的對應功能在「工具與設定 → 備份管理員」。</p>
<p>面板備份通常包含：網站檔案（含隱藏檔）、所有 MySQL 資料庫、email 帳戶與轉寄規則、cron job 設定、DNS zone 記錄。下載打包檔後解壓到本地、用 Git 初始化（見下方「初始化 Git repo」段）。</p>
<p>面板備份可能不包含的：SSL 憑證的私鑰（Let&rsquo;s Encrypt 自動續期的通常不需要手動備份）、PHP 版本與模組設定（需要另外記錄，見<a href="#%e7%92%b0%e5%a2%83%e8%a8%ad%e5%ae%9a%e7%9a%84%e6%8b%8d%e7%85%a7">環境設定的拍照</a>）、<code>.htaccess</code> 以外的 Apache/LiteSpeed 自訂設定。拿到面板備份後仍然要跑「環境設定的拍照」段，因為面板備份拍的是檔案、不是環境設定。</p>
<h3 id="用-ftp-逐層拍照">用 FTP 逐層拍照</h3>
<p>沒有主機面板（或面板不提供完整備份）時，要用 FTP 和 phpMyAdmin 分別拍程式碼和資料庫。</p>
<p><strong>程式碼與靜態資源</strong>：用 FTP client 把整個網站目錄鏡像到本地。FileZilla 的操作路徑：站台管理員連線後，在遠端面板對根目錄按右鍵 → 「下載」，或用「伺服器 → 同步瀏覽」模式讓本地與遠端目錄結構保持對齊。WinSCP 提供「保持更新（Keep Remote Directory up to Date）」功能，但接手階段只需要一次性的完整下載，不需要持續同步。下載前確認 FTP client 的設定有勾選「顯示隱藏檔案」——<code>.htaccess</code>、<code>.env</code>、<code>.user.ini</code> 這類隱藏檔經常包含關鍵設定。</p>
<p><strong>資料庫</strong>：用 phpMyAdmin 的「匯出」功能匯出完整資料庫（詳見下方「資料庫」段）。FTP 只拍程式碼，資料庫要另外匯出。</p>
<h3 id="初始化-git-repo">初始化 Git repo</h3>
<p>不論用面板備份還是 FTP 逐層拍，拿到檔案後都初始化成 Git repo：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mkdir project-takeover <span class="o">&amp;&amp;</span> <span class="nb">cd</span> project-takeover
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># FTP 下載完整站台到此目錄後</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">git init
</span></span><span class="line"><span class="ln">4</span><span class="cl">git add -A
</span></span><span class="line"><span class="ln">5</span><span class="cl">git commit -m <span class="s2">&#34;initial snapshot from production FTP&#34;</span></span></span></code></pre></div><p>這個 commit 是接手的基準線。之後任何改動都能 diff 回這個起點，知道自己改了什麼。</p>
<h3 id="資料庫">資料庫</h3>
<p>用 phpMyAdmin 的「匯出」功能：選「自訂」模式 → 勾選所有資料表 → 格式選 SQL → 勾選「加入 DROP TABLE / VIEW / PROCEDURE / FUNCTION / EVENT / TRIGGER 敘述」（讓匯入時能乾淨覆蓋）→ 壓縮選 gzip（大型資料庫避免瀏覽器逾時）→ 編碼選 UTF-8 → 執行。</p>
<p>phpMyAdmin 的匯出在資料庫超過幾百 MB 時容易因 PHP <code>max_execution_time</code> 或記憶體限制中斷。替代方案：如果主機有 cPanel，「phpMyAdmin → 匯出」旁邊通常有「MySQL 資料庫備份」或透過 cPanel API 的 <code>mysqldump</code> 介面，比 phpMyAdmin 的 PHP 層匯出更可靠。另一個選項是本地安裝 DBeaver（免費、跨平台）或 TablePlus（macOS/Windows），用主機提供的遠端 MySQL 連線（cPanel → 遠端 MySQL → 加入本機 IP 白名單）直接從本機執行 <code>mysqldump</code>。HeidiSQL（Windows 免費）也支援同樣的遠端連線匯出。</p>
<p>把匯出的 <code>.sql</code> 檔存進 repo：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mkdir db-snapshots
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 把 phpMyAdmin 匯出的檔案存到這裡</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">mv ~/Downloads/production-dump.sql db-snapshots/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>-initial.sql
</span></span><span class="line"><span class="ln">4</span><span class="cl">git add db-snapshots/
</span></span><span class="line"><span class="ln">5</span><span class="cl">git commit -m <span class="s2">&#34;initial database snapshot from phpMyAdmin&#34;</span></span></span></code></pre></div><p>如果主機面板有提供 <code>mysqldump</code> 的 web 介面（部分 cPanel 有），用那個比 phpMyAdmin 的匯出更可靠——phpMyAdmin 在大資料庫上容易因為 PHP 記憶體限制而中斷。</p>
<h3 id="環境資訊記錄">環境資訊記錄</h3>
<p>在 repo 根目錄建一份 <code>ENVIRONMENT.md</code>，記錄以下資訊：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="gu">## Production 環境
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">-</span> **主機商**：[名稱]、方案：[方案名稱]
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">-</span> **PHP 版本**：cPanel/Plesk 的 PHP 設定頁直接顯示；沒有控制面板時，FTP 上傳一個 <span class="sb">`phpinfo.php`</span>（內容 <span class="sb">`&lt;?php phpinfo();`</span>）到站台根目錄、瀏覽器開啟後記錄版本、確認後立刻刪除（phpinfo 會暴露伺服器完整設定）
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">-</span> **MySQL 版本**：phpMyAdmin 首頁顯示
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">-</span> **Web server**：Apache / LiteSpeed / Nginx（控制面板或 response header）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">-</span> **域名 / DNS**：誰管的、nameserver 指向哪裡
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">-</span> **SSL**：Let&#39;s Encrypt 自動續期 / 主機商代管 / 手動上傳
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">-</span> **Cron jobs**：控制面板 → Cron Jobs 頁面截圖或列表
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">-</span> **Email**：有沒有用主機的 email 服務、轉寄規則
</span></span><span class="line"><span class="ln">11</span><span class="cl">- <span class="ge">**</span>.htaccess**：已包含在 FTP 下載中（注意隱藏檔有沒有漏）</span></span></code></pre></div><h3 id="掃描-hardcoded-credential">掃描 hardcoded credential</h3>
<p>PHP 專案常見的做法是把資料庫密碼、API key 直接寫在 <code>config.php</code> 或 <code>wp-config.php</code> 裡。在本地 repo 跑一次掃描：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">grep -rn <span class="s2">&#34;password\|passwd\|secret\|api_key\|apikey\|api_secret&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --include<span class="o">=</span><span class="s2">&#34;*.php&#34;</span> --include<span class="o">=</span><span class="s2">&#34;*.ini&#34;</span> --include<span class="o">=</span><span class="s2">&#34;*.env&#34;</span> .</span></span></code></pre></div><p>把找到的每一筆記錄下來：哪個檔案、什麼 credential、用在哪裡。這份清單是後續 credential 輪替的輸入。</p>
<h3 id="第三方整合清單">第三方整合清單</h3>
<p>翻 code 找出所有對外部服務的呼叫——金流（綠界、藍新、Stripe）、簡訊（Twilio、三竹）、Email（SendGrid、SMTP）、社群登入（Facebook、Google）、CDN、Analytics。每一個整合都有對應的 API key 或 webhook URL，這些都是接手後需要確認存取權的項目。</p>
<h3 id="環境設定的拍照">環境設定的拍照</h3>
<p>程式碼和資料庫之外，伺服器的執行環境本身也要記錄。非 container 環境沒有 <code>docker commit</code> 可以一次打包整台機器，要逐層拍：</p>
<p><strong>PHP 設定</strong>：在站台根目錄上傳一個 <code>phpinfo.php</code>（內容 <code>&lt;?php phpinfo();</code>），用瀏覽器打開後把完整輸出另存為 HTML 檔。記錄完立刻刪掉這個檔案——phpinfo 會暴露伺服器的完整設定與路徑。需要記錄的關鍵項：PHP 版本、載入的模組（<code>mysqli</code>、<code>curl</code>、<code>mbstring</code>、<code>gd</code>、<code>imagick</code>）、<code>upload_max_filesize</code>、<code>post_max_size</code>、<code>max_execution_time</code>、<code>memory_limit</code>、<code>error_reporting</code>、<code>session.save_handler</code>。這些值直接影響程式碼能不能在本地環境重現相同的行為。</p>
<p><strong>Cron jobs</strong>：cPanel 的 Cron Jobs 頁面或 Plesk 的排程工作清單，截圖或逐條抄到 <code>ENVIRONMENT.md</code>。每一條 cron 記錄三項：排程時間、執行的指令（通常是 <code>/usr/local/bin/php /home/user/public_html/cron.php</code>）、這條 cron 的業務用途（如果能從指令或檔案名推斷）。</p>
<p><strong>SSL 憑證</strong>：記錄域名、簽發者（Let&rsquo;s Encrypt / 自購 / 主機商代管）、到期日。瀏覽器的鎖頭圖示可以查看憑證詳情。從本機也可以用 CLI 確認：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">echo</span> <span class="p">|</span> openssl s_client -connect example.com:443 2&gt;/dev/null <span class="p">|</span> openssl x509 -noout -dates -issuer</span></span></code></pre></div><p>如果是 Let&rsquo;s Encrypt 自動續期，要確認續期機制是 cPanel 內建（AutoSSL）還是某個自訂 cron。手動購買的憑證要記錄到期日並設日曆提醒——過期後站台會直接出現瀏覽器安全警告。</p>
<p><strong>.htaccess 規則</strong>：<code>.htaccess</code> 可能散在多個目錄（根目錄、<code>uploads/</code>、<code>wp-admin/</code>、<code>api/</code>）。FTP 下載時已包含在內（前提是 FTP client 有設定顯示隱藏檔案），確認一下這些檔案都在 repo 裡。</p>
<p><strong>外部服務連線</strong>：除了前一節的第三方整合清單，用 grep 掃程式碼找出所有對外 URL。這些連線在未來遷移時要同步處理——搬了伺服器但 callback URL 沒改，金流通知就收不到。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">grep -rn <span class="s2">&#34;https\?://&#34;</span> --include<span class="o">=</span><span class="s2">&#34;*.php&#34;</span> . <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  <span class="p">|</span> grep -v <span class="s2">&#34;localhost\|127\.0\.0\.1\|example\.com&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  <span class="p">|</span> sort -u &gt; _environment/external-urls.txt</span></span></code></pre></div><p><strong>檔案權限</strong>：FileZilla 的遠端檔案清單有權限欄。記錄 <code>uploads/</code>、<code>cache/</code>、<code>sessions/</code>、config 檔案的權限。777 的目錄是安全風險（任何使用者都能寫入），在多租戶的主機上尤其危險——同台主機的其他帳戶也能存取。</p>
<p>把以上資料存進 repo 的 <code>_environment/</code> 目錄：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">_environment/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── phpinfo-20260626.html      # phpinfo 完整輸出
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── cron-jobs.md               # cron 清單
</span></span><span class="line"><span class="ln">4</span><span class="cl">├── ssl-cert-info.txt          # 憑證資訊
</span></span><span class="line"><span class="ln">5</span><span class="cl">├── external-urls.txt          # 外部連線清單
</span></span><span class="line"><span class="ln">6</span><span class="cl">└── file-permissions.txt       # 目錄權限記錄</span></span></code></pre></div><p><code>_environment/</code> 可加進 <code>.gitignore</code>（phpinfo 含敏感資訊），或只 ignore HTML 檔、其餘進 Git。</p>
<h2 id="建立本地開發環境">建立本地開發環境</h2>
<p>本地能跑起來，才有安全的測試空間。目標是在本機重現 prod 的 PHP + MySQL 版本組合。</p>
<h3 id="選型docker-vs-本地堆疊">選型：Docker vs 本地堆疊</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>平台</th>
          <th>費用</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Docker Compose</td>
          <td>跨平台</td>
          <td>免費</td>
          <td>最精確對齊 prod 版本，特別是 PHP 5.6/7.0 這類舊版本</td>
      </tr>
      <tr>
          <td>MAMP Pro</td>
          <td>macOS</td>
          <td>付費（約 $50/年）</td>
          <td>圖形介面切 PHP 版本，不熟 Docker 時最快上手</td>
      </tr>
      <tr>
          <td>Laragon</td>
          <td>Windows</td>
          <td>免費</td>
          <td>比 XAMPP 現代、內建 PHP 版本切換與虛擬網域</td>
      </tr>
      <tr>
          <td>XAMPP</td>
          <td>Windows / macOS / Linux</td>
          <td>免費</td>
          <td>最老牌、社群資源多，但 PHP 版本切換較麻煩</td>
      </tr>
      <tr>
          <td>Laravel Valet</td>
          <td>macOS</td>
          <td>免費</td>
          <td>輕量 CLI 為主，適合已經熟悉 CLI 的開發者</td>
      </tr>
      <tr>
          <td>ServBay</td>
          <td>macOS</td>
          <td>免費版可用</td>
          <td>較新、支援多 PHP 版本共存、內建資料庫管理</td>
      </tr>
  </tbody>
</table>
<p>選型判準：如果 prod 的 PHP 版本是 5.6 或 7.0 這類已停止維護的舊版，Docker 是唯一能精確對齊的選項——MAMP/XAMPP 通常只提供仍在維護的版本。常見版本（7.4、8.0、8.1、8.2）用 MAMP/Laragon 會比 Docker 更快跑起來。</p>
<h3 id="docker-方式">Docker 方式</h3>
<p>Docker Compose V2（<code>docker compose</code> 指令）不需要 <code>version</code> 欄位。如果使用舊版 <code>docker-compose</code> CLI，在檔案開頭加 <code>version: '3.8'</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># docker-compose.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">web</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">php:8.1-apache</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span>- <span class="l">./:/var/www/html</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8080:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mysql:8.0</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">localdev</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">project</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span>- <span class="l">./db-snapshots/initial.sql:/docker-entrypoint-initdb.d/init.sql</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;3306:3306&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">  </span><span class="nt">phpmyadmin</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">phpmyadmin/phpmyadmin</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span><span class="nt">PMA_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8081:80&#34;</span></span></span></code></pre></div><p>PHP 版本要對齊 prod。如果 prod 是 PHP 7.4，本地用 <code>php:7.4-apache</code>。版本差異會導致函式行為不同（<code>str_contains</code> 在 8.0 才有、<code>mysql_*</code> 系列在 7.0 移除），測試通過但 prod 壞掉。phpmyadmin service 讓本地也有跟 prod 相同的資料庫操作介面，方便驗證 phpMyAdmin 上要執行的操作。</p>
<h3 id="匯入資料庫">匯入資料庫</h3>
<p>Docker 啟動後匯入初始快照：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker <span class="nb">exec</span> -i project-db-1 mysql -uroot -plocaldev project &lt; db-snapshots/20260626-initial.sql</span></span></code></pre></div><p>MAMP/Laragon/XAMPP 的匯入方式：開啟對應的 phpMyAdmin（通常在 <code>localhost/phpmyadmin</code>）→ 選資料庫 → 匯入 → 選 <code>.sql</code> 檔案 → 執行。或用 DBeaver/TablePlus 連本地 MySQL 後執行 SQL 檔。</p>
<h3 id="常見的本地跑不起來原因">常見的「本地跑不起來」原因</h3>
<table>
  <thead>
      <tr>
          <th>症狀</th>
          <th>原因</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>白頁或 500</td>
          <td>config 裡寫了 prod 的絕對路徑</td>
          <td>改成相對路徑或用環境變數</td>
      </tr>
      <tr>
          <td>連不上資料庫</td>
          <td>DB host 寫了 <code>localhost</code> 但 Docker 裡 DB 是另一個容器</td>
          <td>改成 Docker service 名稱（<code>db</code>）</td>
      </tr>
      <tr>
          <td>某些功能壞掉</td>
          <td>prod 有裝特定 PHP extension（gd、mbstring、curl）</td>
          <td>Dockerfile 加 <code>docker-php-ext-install</code></td>
      </tr>
      <tr>
          <td>.htaccess rewrite 不生效</td>
          <td>Apache mod_rewrite 沒啟用</td>
          <td>Dockerfile 加 <code>a2enmod rewrite</code></td>
      </tr>
      <tr>
          <td>圖片上傳失敗</td>
          <td>上傳目錄權限不對</td>
          <td><code>chmod 777 uploads/</code>（僅限本地）</td>
      </tr>
  </tbody>
</table>
<p>本地能完整跑起來之後，這個環境就是所有變更的測試場。任何改動都先在這裡驗證。</p>
<h2 id="資料庫變更紀律">資料庫變更紀律</h2>
<p>phpMyAdmin 讓修改 prod DB 只需要幾次點擊，這正是它危險的原因——沒有 preview、沒有 undo、沒有 review。紀律要靠流程補上。</p>
<h3 id="變更流程">變更流程</h3>
<ol>
<li>在本地 DB 寫好 SQL 並執行，確認結果正確</li>
<li>把 SQL 存進 repo 的 <code>migrations/</code> 目錄，檔名帶日期：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># migrations/2026-06-26-add-status-column.sql</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ALTER TABLE orders ADD COLUMN status VARCHAR<span class="o">(</span>20<span class="o">)</span> DEFAULT <span class="s1">&#39;pending&#39;</span><span class="p">;</span></span></span></code></pre></div><ol start="3">
<li>在 phpMyAdmin 上對要改的資料表做匯出（只匯出該表的結構 + 資料），存進 <code>db-snapshots/</code> 作為回退依據</li>
<li>在 phpMyAdmin 的 SQL 頁籤貼上已驗證的 SQL 執行</li>
<li>在 repo 的 <code>CHANGELOG.md</code> 記錄：時間、操作者、改了什麼、為什麼</li>
</ol>
<h3 id="高風險操作的額外防護">高風險操作的額外防護</h3>
<p>修改欄位型別、刪除欄位、刪除資料表、批次更新資料——這些操作在 phpMyAdmin 上執行就生效，沒有乾淨的 undo。額外防護是在執行前先確認：</p>
<ul>
<li>有沒有剛做的該資料表備份（不是上週的，是剛剛做的）</li>
<li>這張表有沒有 foreign key 或觸發器會連帶影響其他表</li>
<li>如果改錯了，回退的具體步驟是什麼（從備份 SQL 重建整張表？還是用 UPDATE 改回來？）</li>
</ul>
<h2 id="部署紀律">部署紀律</h2>
<p>FTP 部署沒有 CI pipeline 的自動化保護，但不代表不能有流程。流程的目標是讓每次部署都可追溯、可回退。</p>
<h3 id="部署步驟">部署步驟</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. git diff HEAD~1 --name-only          # 確認這次改了哪些檔案
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 本地測試通過
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. FTP client 開兩個窗格：左邊本地、右邊 prod
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 用 FileZilla 的目錄比較功能確認差異
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. 只上傳有變更的檔案（不要整站覆蓋）
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. 上傳完在瀏覽器驗證功能
</span></span><span class="line"><span class="ln">7</span><span class="cl">7. git tag deploy-20260626 &amp;&amp; git push   # 標記這次部署的版本</span></span></code></pre></div><h3 id="備份策略">備份策略</h3>
<p>無 SSH 的主機環境通常不提供自動快照。備份要自己做：</p>
<table>
  <thead>
      <tr>
          <th>備份項目</th>
          <th>頻率</th>
          <th>方式</th>
          <th>保留</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>程式碼</td>
          <td>每次部署前</td>
          <td>Git tag</td>
          <td>永久（在 repo 裡）</td>
      </tr>
      <tr>
          <td>資料庫</td>
          <td>每週 + 每次 schema 變更前</td>
          <td>phpMyAdmin 匯出</td>
          <td>至少保留 4 週</td>
      </tr>
      <tr>
          <td>上傳檔案</td>
          <td>每週</td>
          <td>FTP 下載 uploads/ 目錄</td>
          <td>至少保留 4 週</td>
      </tr>
      <tr>
          <td>主機設定</td>
          <td>每次變更</td>
          <td>控制面板截圖 + ENVIRONMENT.md 更新</td>
          <td>在 repo 裡</td>
      </tr>
  </tbody>
</table>
<p>如果主機面板有自動備份功能（cPanel 的 Backup Wizard），確認它有開並且能還原。但不要把它當唯一備份——主機商的備份可能在主機出問題時一起不見。</p>
<h3 id="備份自動化沒-ssh-也能做">備份自動化（沒 SSH 也能做）</h3>
<p>無 SSH 的環境沒有 cron + CLI 的組合，但可以用本機排程 + FTP client 的 CLI 模式達成自動化備份。</p>
<p>用 lftp（macOS/Linux 可透過 Homebrew 或 apt 安裝）做定期站台鏡像：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># backup.sh — 加入本機的 cron 或 launchd 每日執行</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">lftp -e <span class="s2">&#34;mirror --verbose /public_html/ /local/backup/site/; quit&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -u username,password ftp.example.com</span></span></code></pre></div><p>rclone 是另一個選項，支援 FTP/SFTP 且有更好的增量同步（只傳有變更的檔案）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 設定 rclone remote（首次）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">rclone config  <span class="c1"># 選 FTP、填入主機資訊</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 同步（之後每次只傳差異）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">rclone sync myhost:/public_html/ /local/backup/site/ --progress</span></span></code></pre></div><p>macOS 用 launchd plist、Windows 用工作排程器（Task Scheduler）排定每日執行這些腳本，讓備份不再依賴人工記得。</p>
<p>資料庫的自動備份較受限——phpMyAdmin 沒有 CLI 介面。如果主機允許遠端 MySQL 連線，可以在本機 cron 裡加一條 <code>mysqldump</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mysqldump -h mysql.example.com -u dbuser -p<span class="s1">&#39;password&#39;</span> dbname <span class="p">|</span> gzip &gt; /local/backup/db/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.sql.gz</span></span></code></pre></div><p>不允許遠端連線時，退而求其次：每週手動從 phpMyAdmin 匯出一次、存進 repo。</p>
<h3 id="回退方式">回退方式</h3>
<p>FTP 部署沒有 rollback 按鈕。回退的方式是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git checkout deploy-20260625 -- path/to/changed/file.php
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 把特定檔案回到上一次部署的版本，再 FTP 上傳</span></span></span></code></pre></div><p>整站回退則是 checkout 到上一個 deploy tag，再整批 FTP 上傳。這就是為什麼 deploy tag 重要——沒有 tag 就不知道要回退到哪個版本。</p>
<h2 id="credential-盤點與保護">credential 盤點與保護</h2>
<p>接手後要回答的問題是：有哪些 credential、誰有存取權、哪些需要輪替。</p>
<h3 id="盤點清單">盤點清單</h3>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>常見位置</th>
          <th>輪替難度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料庫密碼</td>
          <td><code>config.php</code>、<code>wp-config.php</code>、<code>.env</code></td>
          <td>低（phpMyAdmin + 改 config）</td>
      </tr>
      <tr>
          <td>主機面板登入</td>
          <td>主機商帳號</td>
          <td>中（可能綁前人的 email）</td>
      </tr>
      <tr>
          <td>金流 API key</td>
          <td><code>payment.php</code> 或 config 檔</td>
          <td>中（需要登入金流後台）</td>
      </tr>
      <tr>
          <td>SMTP 密碼</td>
          <td><code>mail.php</code> 或 config 檔</td>
          <td>低</td>
      </tr>
      <tr>
          <td>域名管理</td>
          <td>DNS 服務商帳號</td>
          <td>高（可能綁前人的帳號）</td>
      </tr>
      <tr>
          <td>SSL 憑證</td>
          <td>主機面板或 Let&rsquo;s Encrypt</td>
          <td>低（自動續期則不用管）</td>
      </tr>
  </tbody>
</table>
<p>最高優先輪替的是前人可能仍持有存取權的 credential：主機面板密碼、資料庫密碼。如果前人的離開不是善意的（被解僱、爭端），這些應該在接手的第一天就改。</p>
<h3 id="從-hardcode-到-config-分離">從 hardcode 到 config 分離</h3>
<p>長期目標是把 credential 從 code 裡搬出來。即使在沒有 SSH 的環境也能做：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 改前：password 直接寫在 code 裡
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nv">$db_password</span> <span class="o">=</span> <span class="s1">&#39;p@ssw0rd123&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 改後：從 .env 讀取（用 vlucas/phpdotenv 或手寫 parse）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="nv">$db_password</span> <span class="o">=</span> <span class="nx">getenv</span><span class="p">(</span><span class="s1">&#39;DB_PASSWORD&#39;</span><span class="p">)</span> <span class="o">?:</span> <span class="nx">parse_ini_file</span><span class="p">(</span><span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/.env&#39;</span><span class="p">)[</span><span class="s1">&#39;DB_PASSWORD&#39;</span><span class="p">];</span></span></span></code></pre></div><p><code>.env</code> 放在 webroot 之外（如果主機允許）或在 <code>.htaccess</code> 裡禁止存取：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-apache" data-lang="apache"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">&lt;Files</span> <span class="s">&#34;.env&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nb">Require</span> <span class="k">all</span> denied
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">&lt;/Files&gt;</span></span></span></code></pre></div><h2 id="外部監控prod-不用裝東西">外部監控（prod 不用裝東西）</h2>
<p>無 SSH 的環境裝不了監控 agent，但可以用外部 HTTP 檢查服務從外面看。這類服務從多個地理位置定期對網站發送 HTTP request，回應異常時通知。</p>
<p>UptimeRobot 的免費方案提供 50 個 monitor、每 5 分鐘檢查一次，夠用於一個站台的首頁 + 幾個關鍵頁面（登入頁、API endpoint、金流回呼 URL）。Better Stack（原 Better Uptime）提供類似功能並附帶 status page。兩者都只需要填入 URL 和通知方式（email / Slack / webhook），不需要在 server 上裝任何東西。</p>
<p>設定後至少加三個 monitor：首頁（網站是否活著）、登入或後台入口（PHP 是否正常執行）、以及任何有外部依賴的頁面（金流 callback、API endpoint）。這不是完整的可觀測性，但至少讓「網站掛了」這件事從「使用者打電話來」變成「手機收到通知」。</p>
<h2 id="時程參考">時程參考</h2>
<p>完整走完盤點（FTP mirror + DB dump + 環境記錄）約需半天到一天。本地環境建立與驗證約需半天到一天（取決於 PHP 版本對齊的難度）。紀律建立（changelog + 部署流程）是持續的、但框架搭建約需 2-3 小時。CI 化 FTP 部署約需半天。整體從接手到穩定維運約 2-3 個工作天。</p>
<h2 id="升級路徑的切入點">升級路徑的切入點</h2>
<p>接手穩定後，逐步脫離無 SSH 環境的約束。每一步都獨立且可回退。</p>
<h3 id="最低成本的第一步ci-化-ftp-部署">最低成本的第一步：CI 化 FTP 部署</h3>
<p>在 GitHub repo 設定 GitHub Actions，推到 main 時自動跑測試（如果有的話）+ 自動 FTP 部署。FTP credential 存在 GitHub Secrets 裡，不在 code 裡。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># .github/workflows/deploy.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy via FTP</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">SamKirkland/FTP-Deploy-Action@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">          </span><span class="nt">server</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.FTP_HOST }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">          </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.FTP_USER }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">          </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.FTP_PASS }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">          </span><span class="nt">server-dir</span><span class="p">:</span><span class="w"> </span><span class="l">/public_html/</span></span></span></code></pre></div><p>這一步的價值是部署從「開 FileZilla 手動上傳」變成「push to main 自動部署」，人為失誤的空間顯著縮小。Prod 伺服器不需要任何改動。</p>
<h3 id="下一步遷移到有-ssh-的-vps">下一步：遷移到有 SSH 的 VPS</h3>
<p>當以下任一條件出現時，無 SSH 環境的約束會變成瓶頸：</p>
<ul>
<li>需要 SSH 存取（裝 Git、跑 CLI 工具、設排程）</li>
<li>需要自訂 PHP extension 或 PHP 版本</li>
<li>需要更多的運算資源或記憶體</li>
<li>需要環境分離（dev / staging / prod）</li>
</ul>
<p>遷移到 VPS（DigitalOcean、Linode、AWS Lightsail）後，SSH 存取讓所有雲端環境的工具鏈成為可用——Git on server、composer、artisan、mysqldump CLI、cron 的完整控制。這一步之後，接手維運的環境開始對齊<a href="/blog/infra/before-infra/" data-link-title="模組負一：還沒有 infra 的環境怎麼盡量做好" data-link-desc="手動點起家的環境怎麼守底線、降低未來納管成本、辨識何時該開始導入 IaC — 給還沒有能力上 IaC 的真實起點">模組負一：還沒有 infra 的環境</a>的操作紀律，後續可以按<a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">成熟度階梯</a>逐步往 IaC 推進。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/takeover/cloud-no-iac/" data-link-title="有 SSH 但沒有 IaC 的雲端環境接管" data-link-desc="接手一個全手動建立的雲端環境時，怎麼盤點資源、推導依賴關係、收斂 credential、驗證備份、建立變更紀律，以及什麼時候該開始導入 IaC">有 SSH 但沒有 IaC 的雲端環境接管</a>：搬到 VPS 或雲端後的接管流程</li>
<li>→ <a href="/blog/infra/before-infra/" data-link-title="模組負一：還沒有 infra 的環境怎麼盡量做好" data-link-desc="手動點起家的環境怎麼守底線、降低未來納管成本、辨識何時該開始導入 IaC — 給還沒有能力上 IaC 的真實起點">模組負一：還沒有 infra 的環境</a>：接手完成、環境穩定後，操作紀律對齊這裡</li>
<li>→ <a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零：infra 是什麼</a>：成熟度階梯作為接手後評估現況的座標</li>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：credential 盤點與輪替的系統性設計</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：tagging、secret 管理、成本可見性</li>
</ul>
]]></content:encoded></item></channel></rss>