用 GCP 免費額度自架 Ghost 部落格,讓 Terraform 自動化幫你部署
如果想要一個自己掌控的部落格:不要被平台綁死,不想每次搬家都重新設定一輪,也希望未來真的要換雲端或換 VPS 時,至少基礎設施不是靠記憶復原。
所以這篇不是「在 GCP Console 點一台 VM 起來」的教學,而是把 Ghost 部落格部署流程整理成 Terraform。你改幾個變數,執行 terraform apply,GCP 上該有的 VM、static IP、防火牆規則與開機安裝腳本就會一起建立。
前提假設你已經申請好 GCP,也已經有一個 Project。還沒做的話,先完成 GCP 帳號、Billing account、Project 的啟用,再回來看這篇。
範例 repo 放在這裡:https://github.com/zhjiangexe/ghost-terraform-gcp-free-tier
裡面有完整的 Terraform 設定、startup script、.gitignore 和 README.md。
免費額度不是魔法
GCP Free Tier 可以讓這個架構接近免費,但不是保證永遠零成本。
我查官方文件時,Compute Engine Free Tier 包含每月 1 台 non-preemptible e2-micro VM,可以放在:
- Oregon:
us-west1 - Iowa:
us-central1 - South Carolina:
us-east1
另外還包含 30 GB-months standard persistent disk,以及 1 GB 從 North America 出站到多數目的地的流量額度。
官方文件也寫有寫:Free Tier 有使用上限,超過就會照標準費率計費,而且 Google 可以調整 Free Tier 條件。部署前請再看一次官方頁面:
https://cloud.google.com/free/docs/free-cloud-features#compute
這篇的範例預設使用 us-west1/us-west1-a。理由很簡單:如果讀者人在台灣,美國西岸通常比美國中部或東岸更近,latency 有機會漂亮一點。但這不是 CDN,也不是保證最快;實際路由還是要看你的 ISP。
還有幾個可能花錢的地方:
- 網域本身通常要申請要錢。
- 流量超過 Free Tier 可能要錢。(初期不太容易不用擔心)
- static IP 如果沒有綁在 VM 上,可能會開始計費。
- 磁碟超過 30 GB 或改用 SSD 會計費。
- 同時跑兩台
e2-micro,超出的那台會計費。
所以我的做法是:Terraform 中以 Free Tier 為主,但不能忘記雲端還是有帳單。
架構長這樣
這個版本的架構很樸素:
讀者瀏覽器
|
DNS A record
|
GCP static external IP
|
Compute Engine e2-micro
|
Nginx 80/443
|
Ghost Docker container
|
SQLite + Docker volume
VM 開機時會執行 GCP metadata 裡的 startup-script,自動完成:
- 安裝 Docker
- 建立 Ghost
docker-compose.yml - 建立 Ghost
.env - 安裝並設定 Nginx
- 預留 Certbot SSL 流程
- 建立每日備份腳本
- 用
agepublic key 加密備份 - 用
rclone上傳備份
Ghost 使用 SQLite,對個人部落格來說夠簡單,也比較符合一台 Free Tier VM 的資源限制。
為什麼用 Terraform
手動點 Console 最大的問題是做完之後就忘記有哪些設定。
今天開了哪些 port?VM 用哪個 image?磁碟是 standard 還是 SSD?Static IP 名稱是什麼?不用太久,要搬家或重建時,差不多就忘光光了。
Terraform 的價值是把這些決定寫進檔案:
terraform/gcp/
├── main.tf
├── variables.tf
├── outputs.tf
├── startup-script.sh.tpl
└── terraform.tfvars.example
main.tf 描述要建立哪些 GCP 資源。variables.tf 定義哪些值需要由使用者提供。terraform.tfvars.example 是可以公開的填寫範本。startup-script.sh.tpl 是 VM 開機後要跑的安裝腳本。
真正的秘密放在 terraform.tfvars,而這個檔案不進 git。
先把會洩漏個人資料的地方抽成變數
這一步很重要。
如果你要把專案放到 GitHub 或寫成文章給讀者參考,不要把自己的資料寫死在 main.tf 裡。至少這些都應該抽成變數:
project_iddomainghost_urlsmtp_usersmtp_passwordssh_public_keybackup_age_recipient- VM 名稱與資源前綴
- region / zone
- SMTP host / port
- 備份目的地
範例的 variables.tf 裡,domain 和 ghost_url 沒有 default,代表使用者一定要自己填。terraform.tfvars.example 只放 example.com 這種 placeholder。
smtp_password 有設定:
sensitive = true
這會避免 Terraform 在 plan 或 apply 輸出中直接印出密碼。不過要注意,它不會加密 Terraform state。只要 secret 有進入 Terraform,state 就可能保存敏感資料,所以 terraform.tfstate 一樣不能公開。
準備本機環境
先登入 GCP:
gcloud auth login
gcloud auth application-default login
gcloud config set project YOUR_PROJECT_ID
啟用 Compute Engine API:
gcloud services enable compute.googleapis.com --project=YOUR_PROJECT_ID
確認你有 SSH public key:
cat ~/.ssh/id_ed25519.pub
如果沒有,就建立一組:
ssh-keygen -t ed25519 -C "your@email.com"
cat ~/.ssh/id_ed25519.pub
備份加密會用到 age public key。你可以在本機產生:
age-keygen -o ~/ghost-backup-age-key.txt
檔案裡的 age1... 是 public key,可以放進 VM。AGE-SECRET-KEY-... 是 private key,要放密碼管理器,不能丟。
填 terraform.tfvars
進入範例 repo:
cd blog/ghost-gcp-terraform/terraform/gcp
cp terraform.tfvars.example terraform.tfvars
打開 terraform.tfvars,把 placeholder 換掉:
project_id = "your-gcp-project-id"
region = "us-west1"
zone = "us-west1-a"
domain = "example.com"
ghost_url = "https://example.com"
mail_from = "hello@example.com"
smtp_host = "smtp-relay.brevo.com"
smtp_port = 587
smtp_user = "your-brevo-account@email.com"
smtp_password = "your-brevo-smtp-key"
ssh_user = "debian"
ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... your@email.com"
backup_age_recipient = "age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
backup_remote = "gdrive:ghost-backups/"
backup_schedule = "0 3 * * *"
如果你目前已經有一台 Free Tier VM 在跑,先不要同時開第二台長時間運作。Free Tier 是每個 billing account 合併計算,不是每個 Project 都送一台。
部署
初始化 Terraform:
terraform init
先看它打算建立什麼:
terraform plan
確認沒有奇怪的資源後再 apply:
terraform apply
完成後 Terraform 會輸出 VM 的 static IP:
vm_external_ip = "xx.xx.xx.xx"
把這個 IP 拿去 DNS provider 設定 A record:
A @ xx.xx.xx.xx
A www xx.xx.xx.xx
DNS 生效後,再到 VM 上跑 Certbot:
sudo certbot --nginx -d example.com -d www.example.com
Ghost 的 url 已經在 .env 裡設定成 https://example.com,所以 SSL 完成後,後台和前台網址會一致。
別忘了處理 SSH 防火牆
Terraform 這邊會建立一條 IAP-only SSH 規則,只允許 Google IAP TCP forwarding 的來源段連到 22 port。
但是 GCP 的 default network 常常會內建這兩條規則:
default-allow-sshdefault-allow-rdp
它們不是這份 Terraform 建的,所以 Terraform 不會自動刪。確認你可以用 IAP 或 Console SSH 連線之後,建議刪掉:
gcloud compute firewall-rules delete default-allow-ssh default-allow-rdp --quiet
公開 SSH 就是在等掃描器敲門;IAP 至少讓 SSH 入口先經過 Google IAM。
Ghost 起來後要檢查什麼
SSH 進 VM 後可以看 startup script log:
sudo tail -f /var/log/ghost-startup.log
看到這行代表安裝跑完:
=== Ghost setup complete: ...
檢查 container:
sudo docker ps
檢查 Nginx 轉發:
curl -I http://localhost/
如果 SMTP 設錯,Ghost 本站通常還是會跑,但忘記密碼、邀請使用者、通知信就會出問題。部署完最好進 Ghost Admin 送一封測試信。
備份:不要等壞掉才想
範例 startup script 會建立 /opt/ghost/backup.sh,每天用 cron 跑一次。
備份流程是:
- 暫停 Ghost container
- 壓縮 Ghost content volume
- 用 age public key 加密
- 啟動 Ghost container
- 用 rclone 上傳到
backup_remote
這裡刻意讓 VM 只持有 public key。能解密備份的 private key 不應該放在 VM 上。
不過 rclone 的遠端設定仍需要你自己完成,因為那會牽涉 Google Drive 或其他備份目的地的授權。部署完請務必手動跑一次備份,確認遠端真的有檔案。
更新和清除
如果只是改 Ghost 的 .env,例如換 SMTP password,進 VM 修改後要用 down + up,不要只用 restart:
sudo docker compose -f /opt/ghost/docker-compose.yml down
sudo docker compose -f /opt/ghost/docker-compose.yml up -d
如果要清掉所有 Terraform 建的資源:
terraform destroy
測試完不用就 destroy。特別是 static IP,沒有綁定 VM 時可能會產生成本。
結語
這套做法不是最豪華的 Ghost 架構,但它很適合個人部落格:成本低、可重現、設定看得見,把部署過程變成可以被閱讀、修改、重跑的程式碼。哪天要換網域、換 SMTP、重建 VM,或把這套搬去別的 Project,至少不需要從瀏覽器點擊歷史裡考古。