又搬家了:從 Zeabur 遷移到 AWS Lightsail

Share
又搬家了:從 Zeabur 遷移到 AWS Lightsail
Photo by Brett Jordan / Unsplash

是的,又搬家了。

前一篇「部落格遷移紀錄」才記錄了從 WordPress 搬到 Ghost,主機也從 SugarHosts、GCP 一路輾轉到了 Zeabur。原本覺得以這個部落格的流量與使用方式來說,Zeabur 簡單方便,應該可以安穩地用上一段時間。

近期 Zeabur 發生的資安事件,剛好讓我重新思考一直沒去想想這個筆記到底放哪好的問題,能符合掛這個小站的經濟效益,我又能在有餘裕時利用主機做一些嘗試,最後決定搬到 AWS Lightsail。

選擇 Lightsail

其實沒有太多 AWS 的使用經驗,一開始在查看時,想過是不是直接用 EC2,但研究了一會,對我這種單純放部落格的小站來說,建一個 EC2 能調整的東西很多,相對也代表需要理解與管理的東西更多,殺雞何需牛刀呢..

於是又看到了 Lightsail ,看起來比較接近我需要的樣子:一台固定規格的 VM、一個 Static IP,加上相對單純的計價方式(😄)。

最後選擇 Hong Kong Region 的 Ubuntu 主機:

  • 2 vCPU
  • 2 GB RAM
  • 60 GB SSD
  • Ubuntu 24.04

對我的小部落格來說已經很夠用了,價格也略低於 Zeabur 停用共享叢集及漲價後的總計,藉著 AI 的幫助,花了大約 2 個小時的時間完成備份與搬家。

新的環境維持原有架設在 Zeabur 的版本:

  • Ghost 6.43.1
  • MySQL 8.0

只是沒有 Zeabur 的方便了,需要小小學習一下大致的架構:

Internet → AWS Lightsail [Caddy → Ghost → MySQL]

Ghost、MySQL 與 Caddy 都透過 Docker Compose 管理。其中只有 Caddy 對外開放 443,Ghost 與 MySQL 都只留在 Docker 內部網路。

我對 Caddy 其實是不熟悉的,讓 AI 帶著實際設定後,發現它就是站在 Ghost 前面接收 HTTPS Request,再把流量轉給 Ghost,跟我以往熟悉的 Nginx 做的事一樣,只是配置上單純許多,連 Let's Encrypt Certificate 的申請與續期都可以自動完成。

準備 Docker Compose

前置工作是建立一個 Lightsail 執行個體後,先在 Lightsail 安裝 Docker,接著透過 docker compose 建立服務,過程中是逐步進行的,一開始先建立好 MySQL 和 Ghost,最初的 compose 大致如下:

services:

  mysql:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql

  ghost:
    image: ghost:6.43.1
    restart: unless-stopped
    depends_on:
      - mysql
    environment:
      database__client: mysql
      database__connection__host: mysql
      database__connection__port: 3306
      database__connection__user: ${MYSQL_USER}
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ${MYSQL_DATABASE}
      url: https://josnote.com
    volumes:
      - ghost_content:/var/lib/ghost/content

volumes:
  mysql_data:
  ghost_content:

ghost url 要換成自己的網址,docker compose file 中的 ${變數} ,都是存放在 .env檔案裡,不直接寫進 compose。

備份與還原資料

Ghost 需要搬兩個部分:

  1. MySQL Database
  2. /var/lib/ghost/content

Database 包含文章、設定等資料,而 content 裡則有 images、themes 等檔案。

從 Zeabur 備份

MySQL Database

mysqldump -u"$MYSQL_USERNAME" -p"$MYSQL_PASSWORD" --single-transaction --quick --lock-tables=false --default-character-set=utf8mb4 "$MYSQL_DATABASE" > /tmp/josnote.sql

Ghost content

cd /var/lib/ghost
tar -czf /tmp/josnote-content.tar.gz content

最後得到兩個檔案:

# MySQL Database 匯出的備份
josnote.sql

# Ghost content 壓縮檔備份 
josnote-content.tar.gz

下載後透過 SCP 傳到 Lightsail。

還原到 Lightsail
資料庫

Lightsail 上的 Ghost 在第一次啟動時已經建立了一套新的 Database,所以正式還原前先停掉 Ghost,重新建立乾淨的 Database。

接著將 MySQL Database 備份檔透過以下的指令還原到 Lightsail 上的 MySQL:

docker compose exec -T mysql sh -c 'mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' < backup/josnote.sql

完成後簡單確認文章筆數:

docker compose exec mysql sh -c 'mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" -e "SELECT COUNT(*) AS posts FROM posts;"'

執行後會看到類似以下的結果:

+-------+
| posts |
+-------+
|    31 |
+-------+

數值大於 0 也就代表有匯入成功了。

還原 Ghost Content

透過以下指令,把原本新 Ghost 建立的 content 清掉,再將備份解壓縮還原回來。

docker run --rm -v josnote_ghost_content:/target -v /opt/josnote/backup:/backup alpine sh -c 'tar -xzf /backup/josnote-content.tar.gz -C /target --strip-components=1'

確認 images、themes 等目錄都有回來後,重新啟動 Ghost。

docker compose start ghost

到這裡,文章資料與圖片其實都已經在 AWS Lightsail 上了,只是外面的流量還進不來。

讓 Caddy 接收請求轉發到 Ghost

建立 Caddyfile ,並且在 compose 加上 Caddy 的服務。

Caddyfile 只有很簡單的設定,以我的網站為例:

josnote.com {
    reverse_proxy ghost:2368
}

意思就是:收到 josnote.com 的請求,就轉發給 Ghost 的 2368 port。

Docker Compose 新增 caddy service,對外開 443 port,經由 Caddyfile 的設定,請求會轉發到 Ghost ,讓 Ghost 不需要直接對外。

  caddy:
    image: caddy:2
    restart: unless-stopped
    depends_on:
      - ghost
    ports:
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

原本想像 HTTPS 可能會是搬家比較麻煩的一段,結果 Caddy 幾乎都幫忙處理掉了。

小插曲:DNS Cache。


當我將 josnote.com 的 DNS 指向 Lightsail Static IP 後,網站其實已經可以正常開啟,但 Caddy 的 log 一直出現 Let's Encrypt Certificate 申請失敗:

authorization failed
remote error: tls: no application protocol

錯誤訊息裡顯示 Let's Encrypt 連線的 IP,竟然還是 Zeabur 原本的 IP。

檢查了 Google DNS 與 Cloudflare DNS,都已經解析到新的 Lightsail IP

nslookup josnote.com 8.8.8.8
nslookup josnote.com 1.1.1.1

只有在 Lightsail 主機裡,仍然得到 Zeabur 的 IP

nslookup josnote.com

這是 Ubuntu 本機的 DNS Cache,在 AI 的指導下,透過以下指令清掉:

sudo resolvectl flush-caches

重新查詢後就變成新的 Lightsail IP。

後記

好像每搬一次家,就會多理解到一些網站平常不太需要注意的東西,但希望不用常常透過搬家來了解,希望這次可以住久一點😅

Read more

桌面上的筆電顯示程式碼,旁邊放著咖啡杯,象徵日常部署與開發工作流

[紀錄] OpenClaw 部署指定模型

上一篇先記了我初試 OpenClaw 的過程,這一輪則是把原本的 docker compose 再往前補一些,順手把預設模型也一起放進去。 這次選擇的是 Ollama,預設模型設成 minimax-m2.5:cloud。 原本以為把 .env 補好、compose 啟動,接著就能開始用了。做了才知道事情沒有我想得順利,仍然還是需要手動進 container 執行指令。 因為這次在 docker compose 想放進預設模型,所以整個配置也跟著多補了一些。原本比較單純的 OpenClaw 部署,後來變成 openclaw + ollama 的配置,讓 OpenClaw 啟動後能直接接上模型。 不過模型名稱先放進去,事情也沒這麼順。 Ollama 要使用 cloud model 得先登入。第一次啟動後,要先進到 Ollama 容器裡跑

By Jo Assistant, Jo
[紀錄] 初試 OpenClaw

[紀錄] 初試 OpenClaw

夯了很久的 OpenClaw,近期開始出現了退安裝潮,我卻正要開始嘗試使用。 前幾天花了一點時間簡易安裝看看傳說中的龍蝦 (OpenClaw) 要怎麼用,略有點覺得值得再往後嘗試時,才開始認真看看安裝方式,在小心為上的前提下,我採用 docker 建置在自己閒置的電腦。 在 docker-compose.yaml 的準備過程,原先只是不斷試錯調整,過了好段時間才有點意識到該好好利用身邊的資源,於是集幾個 AI 模型問答之大成來建置初版,當 OpenClaw 建起來後,又透過跟它的互動,協助我寫一版可整合 Discord 的 Openclaw docker-complase.yaml 自用。(參考) Gateway Token & Pairing 如果沒有特別改設定,當啟動 container 後,透過 http://localhost:16789 會導向登入頁 登入時會遇到 2 個情況

By Jo
Data Platform 筆記#02:從可行到可承接

Data Platform 筆記#02:從可行到可承接

在初版架構逐漸成形後,時間也差不多過了一年。 架構可以跑、資料可以流動,但我仍然不確定它能不能真正落地。這條路必須要團隊可以承接、可以擴展,數據才有機會真正發揮價值。 很慶幸的是,我的主管願意投資時間,讓這個方向能繼續推進。也正是在那段時間,我的思考開始出現轉變... 前一篇的重點,是讓流程從「能跑」走向「能持續」。 而接下來我開始思考:如果這件事要由團隊一起做下去,現在的做法夠不夠讓人接手? 轉變的核心 回頭看那一年,大多數時間其實是在解問題。 但接下來,我該解的是另一個問題:怎麼讓別人不用再解一次同樣的問題? 於是投入了約莫三個月、壓力值很高的一段時間,開始把原本依賴個人經驗與記憶的做法,收斂成可以被團隊理解與複製的形式。 這個收斂,後來具體落在幾個方向上: * 把 Data Center 的部署方式收斂成一致做法,降低環境轉換成本 * 把資料整理作業轉變為配置驅動,讓流程與部署有規則可循 * 整理 DDL 轉換規則與範本,讓團隊能共用同一套方法 * 把知識系統化交付出去 這些事情的唯一核心是 讓方法大於個人。 從個人經驗,到規則明確 第一個改變:

By Jo