與AI一同建置NGINX + PHP + Certbot的Docker網站

過往在自己的RasperryPi 4中也經常用docker在自行架設NGINX+PHP網站,也試著自行透過Letsencrypt來取得憑證,讓自己的網站具有SSL的安全性認證。但這些都是自己透過查找網路的資料一步步建置起來的。

現在AI這麼流行,就想說再來試一下如過透過跟AI協同運作下建立在Docker中的NGINX + PHP + Certbot的網站服務器是否會比較容易。一方面了解一下AI的輔助能力,以及順便了解一下是否有比先前手動建置網站還要方便的設定方式。

逐步建置

因為我沒有訂閱像claude這樣的AI工具,所以也無法使用Claude Coworker等這類的AI Agent來輔助自動產生所需的功能。所以我只能透過免費的Chat對話功能,來跟AI討論,並將AI產出的結果手動下載及進行測試。

一開始我是針對我所需要的功能逐步跟AI說明及討論,並一步步地修正設定檔,想要完成我所需的功能。我的步驟大約如下:

請產生一份docker file,用ubuntu + NGINX,可以用來提供反向代理服務

於是AI就給了我一些docker file以及NGINX的設定檔,並教我如何執行docker。一開始是可以達到我想要的功能。但我又接著想說要加入SSL的https功能。於是又提出我的需求:

如何支援443 port的SSL

當然AI又給了我修改的設定檔,我又手動設定及測試。

接著我又想要:

憑證可以使用lets encrypt嗎

於是又開始設定及測試。就這樣一次次的迭代進行下去。終於在某一次的需求下,這個網站就不能如我所願的正常運作了。

當然我也有請AI針對錯誤訊息進行修補,但…就是一直都修不好。這裏修好後,就又有某個功能失效。

需求先行

於是我就先暫停下了,思考了一下,這樣一來一往的討論模式為何會失敗。我的想法是因為這樣一步步的討論下,AI或許已忘了前面的需求,所以他在解決新的需求時,沒有辦法有全盤的考量,才會造成最後網站的設定亂了套,終告失敗。

此時我就想到有人在進行Vibe Coding時,有提到SDD(Spec Driven Develpoment)。也就是在真正動手前先將所有需求討論好,再一次開工。

於是這次我就先將我所有的需求先整理好成一份基本的markdown文件,再將這份文件上傳給AI後,接著就詢問:

請參考這份md的需求文件,先幫忙評估,如果有需要補充的,請先詢問。等我確認後,再開始實作

我請AI先閱讀需求文件,並確認他是否都了解需求,透過討論的方式,讓需求更加明確。

一開始我的需求老實說也沒有寫的很複雜,只是大約整理了一下前一次逐步建置的所有功能:

請用docker的compose方式,提供一個可以達到以下功能的NGINX + PHP + CertBot 的組合。

# 基本資料
網域名稱 solo-coding.com
管理員email : xxxx@yyyy.com

## 網站 1
www.solo-coding.com
放置靜態網頁

## 網站 2 
ailab.solo-coding.com 
透過反向代理方式,指向 http://192.168.2.168:3100

## 網站 3
blog.solo-coding.com
可執行wordpress的網站


# 基本功能
1. 可以支援反向代理、可支援SSL 443 ports
2. 可利用certbot功能由letsencrypt進行自動更新憑證
3. PHP服務可提供讓我自行定義的功能,例如可設定傳送的檔案大小、要安裝的擴充套件模組

AI在評估後也詢問了一下算是我當初沒考慮到的需求:

1. WordPress 資料庫:要一起包進 Compose 裡嗎?

2. PHP 擴充套件:除了常規 WordPress 用的套件,有特定要指名安裝哪些嗎?(例如:imagick, redis, gd 等)

3. 初次憑證申請:是否接受配合自動化腳本或分步驟指令來完成第一次的憑證建立?

於是在經驗幾輪的需求討論及確認後,AI對於我的需求有了更進一步的了解。當大致上都差不多後,就可以開始請AI建置。

透過這種先進行需求確認的方式來進行建置,果然成果比逐步建置要好多了。基本上AI所提供的建置資料都可以一次就順利完成所有的需求。

檢視及改善

現在大家都在講Vibe Coding,主打不會程式的人也可以建構應用服務。不可否認的,AI所產生的程式碼或提供的解決方案確認都能達到你要的功能。但是在這些的改善、調整過程中,身為需求提供者的你,還是要仔細地進行檢視,如果你只是看最後的結果,程式可正常執行有基本功能,就以為這樣就好了,那就要小心了。或許AI所提供的解法只是可行解,但不是最佳解。

例如,我有個要求是要能夠自動化地更新Letsencrypt的憑證,結果AI使用了certbot的docker套件,並執行以下的Linux Shell指令:

entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini; docker exec nginx-server nginx -s reload 2>/dev/null || true; sleep 12h & wait $${!}; done'"

如果你對Linux Shell指令有基本的了解的話,你就可以解讀一下這段程式碼的執行邏輯,會發現這段程式是每12小時由certbot確認一下憑證是否快到期,如是的話就會更新。接著…它就會讓NGINX重新載入憑證。請留意,它是每次都會執行讓NGINX重新載入憑證的這個步驟,而不是只有在進行憑證更新後才重新載入。這樣表示每12小時,你的網頁服務就會重啟一次。

於是我就問AI:

entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini; docker exec nginx-server nginx -s reload 2>/dev/null || true; sleep 12h & wait $${!}; done'"

這段的寫法是不論是否renew成功,都會重啟nginx嗎

AI也很老實的回答:

是的,您觀察得非常敏銳。這段寫法不論 Certbot 最終有沒有成功更新憑證(Renew),每次迴圈執行時都會對 Nginx 進行重載(Reload)。

🔍 程式碼執行邏輯拆解
這段指令的核心在於多個 shell 指令是用 分號 ; 連接,而不是用 &&:

當然他也意識到可能我是希望不要每次都重啟NGINX服務,於是他就自己提供了改善的建議:

2. 進階優化寫法(只在成功 Renew 時才重載)
如果您希望做到完美主義,「只有當憑證真正被更新時,才去重載 Nginx」,我們可以利用 Certbot 內建的 --deploy-hook 參數。

這是 Let's Encrypt 官方最推薦的標準作法,請將 docker-compose.yml 中的 entrypoint 修改為以下形式:

entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini --deploy-hook \"docker exec nginx-server nginx -s reload\"; sleep 12h & wait $${!}; done'"

AI改用了delay-hook功能來改善。

這部份我體會到如果你也能有一些基本的程式碼認知的話,就可以協助去Review一下AI產生的結果,就有機會再進行優化。當然,透過優化的過程,我也多學到了,原來certbot還有delay-hook的功能可以這樣來使用。

人工輔助除錯

如果你依AI指示的步驟執行,大致上不太會遇到困難。但馬有失蹄、人有亂手,有時就是會忘了某個步驟而造成問題出現,更慘的是問題出現後,雖然AI也提供了解法,但…就是卡關。

例如,因為我使用的網址服務商是cloudflare,而cloudflare有提供API的服務,所以certbot可以用專用的套件來搭配cloudflare的API,就可全自動地完成萬用憑證的申請。但使用API服務的話就必需要提供API KEY。

AI有指示要將API KEY內入一個指定的檔案:

# 建立並直接寫入內容(請記得替換成您在 Cloudflare 申請的真實 Token)
echo "dns_cloudflare_api_token = 您的_CLOUDFLARE_API_TOKEN_實際密鑰" > /home/ubuntu/Web_App02/certbot/cloudflare.ini

# 安全起見,將權限調整為僅限擁有者可讀寫
chmod 600 /home/ubuntu/Web_App02/certbot/cloudflare.ini

在docker image執行時會將這個檔案掛載進去。結果,我忘了產生這個檔案,所以docker預設會將這個原本是檔案的物件產生了一個同名稱的目錄。後來在發現後,雖然我有手動刪除docker所自動產生的目錄,但後來還是一直執行失敗,一直說無法將檔案掛載成一個目錄。

將錯誤訊息給AI後,他也是說明有個同名稱的目錄存在所以才會有這樣的錯誤。但我想說該刪除的也都刪了,怎麼還有這問題。

最後,還是仔細地看了一下docker compose中的設定檔的掛載目錄,才發現原來有兩個地方,而我只刪了一個。後來將另一個也刪除了才能正常執行。

心得

  • 先討論好需求再請AI開工會比逐步討論及建置來得順利
  • 最好自己也有基本的程式概念,針對AI產生的解決方案可再檢視及確認
  • 有時自己也要加入除錯功能,不然只靠AI的話,可能會卡關