iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Modern Web

前端來點 AWS 技能樹!系列 第 22 篇

Day22 - 安裝 Caddy:讓 EC2 上的 Next.js 用 HTTPS 開站

  • 分享至 

  • xImage
  •  

昨天認識了 HTTPS、TLS 憑證和 Reverse Proxy,而今天就實際把 Caddy 裝進 EC2,讓 Next.js 網站可以用 https:// 打開ㄅ!
做完就是昨天這張圖的樣子:
Caddy 與 Next.js 的架構

這次沿用前幾篇所開的 EC2 Server 和 DNS 的設定。文中會用 example.com 當作範例,要記得換成自己的 Domain 喔!

確認網域指向這台 EC2

在自己電腦的終端機(我是 Mac)查詢:

dig example.com A +short
dig example.com AAAA +short

第一行要看到自己的 EIP,第二行應該什麼都沒有。如果第二行出現 IPv6 位址,多半是註冊商停放頁留下的舊 Record,要先到 Cloudflare 刪掉:Let's Encrypt 會優先走 IPv6,所以驗證會跑到別台 Server 上。Cloudflare 的 Record 也要維持是 DNS only。

Windows 可以用 nslookup -type=A example.com;而下面的 curl 在 PowerShell 要打成 curl.exe。

安全群組開放 80 和 443

Day13 的 HTTP 規則只開給「My IP」。而今天網站要公開,要讓 Let's Encrypt 也可以連進來驗證,所以要改成開放給所有人。
到 EC2 → 執行個體,選擇之前開的 Instance,在安全性(Security)Tab 中點安全群組的連結,按編輯傳入規則(Edit inbound rules):

類型(Type) 連接埠(Port) 來源(Source)
HTTP 80 Anywhere-IPv4(0.0.0.0/0)
HTTPS 443 Anywhere-IPv4(0.0.0.0/0)

原本那條 HTTP 把來源改掉,再新增一條 HTTPS;3000 和 SSH 的 22 都不用開。
https://ithelp.ithome.com.tw/upload/images/20261006/20179793HhNRmisKri.png

為什麼不只開給 Let's Encrypt 就好?因為它不會公布驗證用的 IP,還會從好幾個地點同時連進來檢查。像在我的 side project 申請和續期時,logs 就記到 5 次驗證請求。

不過 0.0.0.0/0 只適合網站用的 80、443;而 22、3000 或資料庫的 port 千萬不要這樣開。
改完回到自己的電腦上確認外面連得到:

curl -I --max-time 10 http://example.com

看到 HTTP/1.1 200 OK 就可以繼續往下做;如果等了 10 秒出現 timed out,就要回頭檢查 DNS 和 Security groups。還有如果用 .dev、.app Domain 的話,瀏覽器會強制 HTTPS,這一步只能用 curl。

安裝 Caddy

接下來到"回到 Mac 驗收",都在 EC2 的 Session Manager 終端機操作。

  1. 我當時試過 COPR 套件來源,在 Amazon Linux 2023 上裝不起來,所以直接下載官方的執行檔:
cd ~
curl --fail --location --output caddy 'https://caddyserver.com/api/download?os=linux&arch=amd64'
sudo install -m 0755 caddy /usr/local/bin/caddy
rm caddy
/usr/local/bin/caddy version

install 會把檔案放進 /usr/local/bin 並設成可執行。網址裡的 amd64 就是 x86_64(如果 uname -m 顯示 aarch64,要改成 arm64)。而這個網址會給最新版,記得記下 version 顯示的版本號。

  1. 裝好後再替 Caddy 建一個專用帳號,不要讓它用 root 執行:
sudo groupadd --system caddy
sudo useradd --system \
  --gid caddy \
  --create-home \
  --home-dir /var/lib/caddy \
  --shell /usr/sbin/nologin \
  caddy
sudo mkdir -p /etc/caddy

/var/lib/caddy 是這個帳號的家目錄,Caddy 會把憑證存在這裡;nologin 表示這個帳號不能拿來登入。

我踩過的坑:忘了準備家目錄

我當時建帳號漏了 --create-home 和 --home-dir,Caddy 想把憑證存進 /home/caddy 卻沒有權限,logs 出現:

example.com: obtaining certificate: failed storage check: mkdir /home/caddy: permission denied - storage is probably misconfigured

我補好家目錄、重新啟動後,5 秒就拿到憑證了。
所以遇到一樣的錯誤,先用 getent passwd caddy 看家目錄是不是 /var/lib/caddy;如果要改帳號設定的話,記得要先停掉 Caddy。

讓 Caddy 開機自動啟動

  1. 寫一份 systemd 服務設定,用來告訴系統用哪個帳號、怎麼啟動 Caddy:
sudo tee /etc/systemd/system/caddy.service > /dev/null <<'EOF'
[Unit]
Description=Caddy web server
Wants=network-online.target
After=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force
AmbientCapabilities=CAP_NET_BIND_SERVICE
PrivateTmp=true
ProtectSystem=full
TimeoutStopSec=5s

[Install]
WantedBy=multi-user.target
EOF

看懂這幾行就好:

  • User=caddy:用剛建立的帳號執行。
  • ExecStart:啟動 Caddy,讀取 /etc/caddy/Caddyfile。
  • ExecReload:之後改設定用 sudo systemctl reload caddy 套用,網站不用停。
  • AmbientCapabilities:Linux 預設只有 root 能用 1024 以下的連接埠,這行讓 caddy 帳號也能用 80 和 443。

寫 Caddyfile

  1. Caddyfile 是 Caddy 的設定檔。記得把裡面的 example.com 換成自己的 Domain 再貼上:
sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
example.com {
    reverse_proxy 127.0.0.1:3000
}
EOF

第一行只寫 Domain、不加 http://,Caddy 就會自動申請憑證,並把 HTTP 轉到 HTTPS。reverse_proxy 則把請求轉給同一台主機的 3000,也就是等一下的 Next.js。

  1. 接著整理格式、檢查設定:
sudo /usr/local/bin/caddy fmt --overwrite /etc/caddy/Caddyfile
sudo -u caddy -H /usr/local/bin/caddy validate --config /etc/caddy/Caddyfile

fmt 會把縮排整理成 Caddy 的格式,沒整理的話,每次啟動都會在 logs 中提醒 Caddyfile input is not formatted。validate 用 caddy 帳號檢查設定,看到 Valid configuration 就沒問題,它不會真的去申請憑證。指令寫完整路徑,是因為 sudo 不一定會到 /usr/local/bin 找指令。

把 80 讓給 Caddy

現在 container 用 -p 80:3000 佔著主機的 80。所以我們要讓 container 只在主機內部開 3000,並把 80 交給 Caddy。

  1. 但是目前 port 設定改不了,改的話要重建 container。所以我們繼續沿用目前的 image,不用重新 build 或登入 ECR:
APP_IMAGE=$(sudo docker inspect --format '{{.Image}}' my-app)
echo "$APP_IMAGE"

看到 sha256: 開頭的一串 ID 後,把舊 container 停掉、改名留著當備份,再建立新 container:

sudo docker stop my-app
sudo docker rename my-app my-app-old
sudo docker run -d \
  --name my-app \
  --restart unless-stopped \
  -p 127.0.0.1:3000:3000 \
  "$APP_IMAGE"

從這裡到 Caddy 啟動完成,網站會暫時打不開。 中途斷線、APP_IMAGE 不見了,就用 APP_IMAGE=$(sudo docker inspect --format '{{.Image}}' my-app-old) 再拿一次。
127.0.0.1:3000 表示主機的 3000 只開給 EC2 自己用,外面連不到,但同一台主機上的 Caddy 連得到。Day9 寫在 Dockerfile 的 HOSTNAME=0.0.0.0 也還在 image 裡,不用改。

  1. 確認新 container:
sudo docker port my-app
curl -I http://127.0.0.1:3000

預期看到 3000/tcp -> 127.0.0.1:3000 和 HTTP/1.1 200 OK。剛啟動時如果出現 Empty reply 或 Connection reset,等幾秒再試。

啟動 Caddy

  1. 執行:
sudo systemctl daemon-reload
sudo systemctl enable --now caddy
sudo journalctl -u caddy -f

daemon-reload 讓系統讀到新的服務設定,enable --now 是設定開機啟動並立刻啟動。最後一行會持續顯示 logs,看到 certificate obtained successfully 就是拿到憑證了,按 Ctrl+C 離開。
第一次啟動時會出現 HTTP/2 skipped because it requires TLS 這類看起來像錯誤的訊息,這說的是 port 80 那個只負責轉址的 Server,所以不用擔心。另外,服務顯示 active 這只是代表 Caddy 在跑,至於憑證有沒有拿到還是要看 logs。

回到 Mac 驗收

  1. 回到自己的終端機下:
curl -I --max-time 10 http://example.com
curl -I --max-time 10 https://example.com
  • 第一個要看到 308 Permanent Redirect 和 Location: https://example.com/,代表 HTTP 會自動轉到 HTTPS。
  • 第二個的第一行出現 200(通常是 HTTP/2 200)就成功了。curl 預設會檢查憑證,不要加 -k 跳過檢查。

最後用瀏覽器開 https://自己的 Domain,點網址列左邊的圖示確認連線安全;再點進憑證,就能看到 Day21 說的發給哪個 Domain、誰簽發(Let's Encrypt)和有效期間。

加碼:讓 www 也能用

Day20 有設定 www 的話,先在 Mac 確認:

dig www.example.com A +short

會先看到 example.com.(CNAME 的目標),下一行才是 EIP。接著回 EC2 改 Caddyfile、套用:

sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
example.com, www.example.com {
    reverse_proxy 127.0.0.1:3000
}
EOF
sudo /usr/local/bin/caddy fmt --overwrite /etc/caddy/Caddyfile
sudo -u caddy -H /usr/local/bin/caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

reload 會直接套用新設定,網站不用停。Caddy 會替 www 另外申請一張憑證,也就是 Day21 說的「分別使用不同的憑證」。最後對 www 重做"回到 Mac 驗收"。
完成後兩個名稱會開同一個網站,但不會自動跳成同一個網址。

卡住時先看這裡

狀況 先檢查
address already in use 80 或 443 還被佔著,通常是舊 container 沒停;在 EC2 用 sudo ss -lntp 看是誰
permission denied 家目錄(我踩過的坑)、Caddyfile 能不能讀、服務檔的 AmbientCapabilities
likely firewall problem 或 timeout Security group 的 80、443 是不是 0.0.0.0/0;自己連得到,不代表 Let's Encrypt 連得到
NXDOMAIN、DNS problem A Record 沒設好,或 Cloudflare 不是 DNS only
HTTPS 回 502 Next.js 沒回應:在 EC2 跑 curl -I http://127.0.0.1:3000、看 sudo docker logs --tail 50 my-app
瀏覽器顯示憑證錯誤 網址要用 Caddyfile 裡的 Domain,用 IP 開一定會錯;不要直接略過警告

前四種都會出現在 Caddy 的 logs 裡。修好之後,可以用 sudo systemctl restart caddy 讓它馬上重啟一次;重試時 logs 出現 staging(Let's Encrypt 的測試環境)是正常的。
這邊要注意不要一直重來或刪掉憑證資料,會碰到 Let's Encrypt 的申請次數限制。

之後要注意的事

  • 憑證會自動續期:憑證存在 /var/lib/caddy/.local/share/caddy,這個資料夾要留著。我的 side project 7 月拿到的憑證,9 月 Caddy 就自己續了,到期日往後延了兩個月,我完全沒動手。前提是 EC2 和 Caddy 都在跑、DNS 正確,80 和 443 也保持開放。
  • 開站改用 Domain:用 IP 開會出現憑證錯誤,因為憑證只發給 Domain。在 EC2 檢查 Next.js 也要改打 curl -I http://127.0.0.1:3000,之前用的 http://127.0.0.1 現在接的是 Caddy。
  • 停機也會停續期:練習完把 EC2 停掉的話,網站和續期都會暫停,下次啟動後再跑一次"回到 Mac 驗收"的步驟。
  • Caddy 要自己更新:這次裝的是單一執行檔,所以不會跟著系統一起更新。
  • Domain 會被看見:憑證發出後,Domain 會被記進公開的憑證透明度(Certificate Transparency)紀錄,掃描程式很快就會找上門,別把只想給自己看的東西放在這個 Domain 上。

我原本專案的自動部署也是這樣分工:每次只重建 Next.js container(一樣綁 127.0.0.1:3000),Caddy 不用動,最後用 https 網址確認網站是否有回應。

到這裡,Domain、EC2、Docker 和 HTTPS 就接起來了!下一篇來看改完程式後,要怎麼把新版本送上這台主機ㄅ!

參考資料


上一篇
Day21 - HTTPS 與 TLS 憑證:讓網站連線加上保護
下一篇
Day23 - IaC 與 CI/CD:把手動的步驟交給程式
系列文
前端來點 AWS 技能樹! 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言