經過前 10 天的奮戰,AI 教練 KAKERU 的 API 已經具備了邏輯、記憶力與防禦機制。本地端開發也算告一段落!
在準備將程式碼部署到雲端之前,我們必須解決一項問題:『在我的電腦上跑得好好的,怎麼一部署就掛了?』
就好像常聽到:『在你們這邊 WiFi 都可以連,為什麼回家就連不到。』
為了解決實際使用跟測試的作業系統與環境差異,今天我們要撰寫 Dockerfile,將這套 Java 21 + Spring Boot 應用程式打包成一個隨處可執行的 Docker Image。
你可以把『在本地建立與執行 Docker』,當作是『上雲端的模擬』。 透過 Docker,我們在自己的電腦裡切出一塊與 GCP 雲端一模一樣的微型環境。如果我們的程式碼能在本地的 Docker 容器裡存活並順利連線,那麼未來將它推上 Cloud Run 時,就能減少一些問題發生!而且,我們還要透過 Multi-stage Build (多階段建置) 的方式,把它做得輕量與安全!
在開始撰寫 Dockerfile 之前,電腦必須先安裝並啟動 Docker 引擎:
docker --version
傳統的 Spring Boot 容器化是本地端先下指令 mvn clean package 產出 .jar 檔,然後複製進 Image 裡。但這種做法有環境依賴與 Image 肥大的缺點。
Multi-stage Build 能夠解決這個問題,在同一個 Dockerfile 定義兩個階段:
.jar。.jar 複製過來執行。這樣最終上雲端的 Image 裡面沒有原始碼、沒有 Maven,體積最小、安全性最高!
要寫出好的 Multi-stage Dockerfile 需要考慮很多底層細節(例如基礎 Image 選擇、使用者權限設定等)。同樣也可以與AI協作,直接將需求轉化為Prompt:
我有一個 Java 21 + Spring Boot 3 專案,準備部署到 Google Cloud Cloud Run。請幫我寫一個 Best Practice 的 Dockerfile。
要求:
- 使用 Multi-stage build 進行 Maven 打包。
- 最終 image 要極度輕量且安全 (建立並使用非 root 的 appuser 執行)。
- 底層 JRE 必須相容 gRPC / Netty (因為專案會連線 Firestore)。」
產生的 Dockerfile如下:
# ==========================================
# Stage 1: Build (編譯階段)
# ==========================================
# 使用官方含有 Maven 與 Java 21 的完整環境作為編譯基底
FROM maven:3.9.6-eclipse-temurin-21 AS builder
# 設定容器內的工作目錄
WORKDIR /app
# 先複製 pom.xml 並下載依賴,善用 Docker Layer Cache
COPY pom.xml .
RUN mvn dependency:go-offline
# 複製所有原始碼進去
COPY src ./src
# 執行打包指令,跳過測試以加速編譯
RUN mvn clean package -DskipTests
# ==========================================
# Stage 2: Run (執行階段)
# ==========================================
# 使用相容 gRPC / Netty Native 的 Java 21 JRE 環境 (Debian/glibc 基礎)
FROM eclipse-temurin:21-jre
# 建立非 root 系統使用者以符合最佳資安規範 (Least Privilege)
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
WORKDIR /app
# 從第一階段 (builder) 將編譯好的 fat jar 複製到最終的 Image 裡
COPY --from=builder /app/target/*.jar app.jar
# 變更檔案擁有者為 appuser
RUN chown -R appuser:appgroup /app
# 切換使用非 root 使用者執行
USER appuser
# 暴露 Spring Boot 預設的 8080 Port
EXPOSE 8080
# 設定容器啟動時執行的指令
ENTRYPOINT ["java", "-jar", "app.jar"]
在根目錄建立 .dockerignore 檔案,避免打包暫存與敏感設定:
target/
!.mvn/wrapper/maven-wrapper.jar
!target/*.jar
*.class
.idea/
.vscode/
*.log
.git/
.gitignore
README.md
開啟終端機,執行以下指令進行打包(標記名稱為 kakeru-ai:1.0.0):
docker build -t kakeru-ai:1.0.0 -t kakeru-ai:latest .
看著 Multi-stage 一步步下載依賴並打包,最後產出的 Image 體積通常相對較小。
接下來:如何把這個 Container 跑起來,並且讓它有權限連線 Firestore?
如果在本地直接執行,程式一啟動就會因為找不到 Google Cloud 權限而崩潰。由於我們的 Docker 使用了安全的「非 root 使用者 (appuser)」,權限路徑的映射非常容易寫錯,所以詢問 AI:
我要在本地端用docker run 啟動剛剛包好的 Image。但我本機有設定 Google Cloud ADC 憑證。該如何把這個憑證掛載到容器內,讓以 appuser (非 root) 執行的 Spring Boot 可以順利讀取到權限?
產生對應的指令:
docker run -d --name my-kakeru-ai \
-p 8080:8080 \
-v ~/.config/gcloud:/home/appuser/.config/gcloud:ro \
-e GOOGLE_APPLICATION_CREDENTIALS=/home/appuser/.config/gcloud/application_default_credentials.json \
kakeru-ai:1.0.0
並可以使用以下指令進行發送 API 測試與日誌檢視,並將curl的內容import到 API 測試工具做測試:
# 查看容器即時日誌
docker logs -f my-kakeru-ai
# 發送測試請求
curl -X POST http://localhost:8080/apiUrl \
-H "Content-Type: application/json" \
-d '{
"targetRace": "半程馬拉松",
"currentLevel": "目前一週可以跑 3 天,四個月後想挑戰半馬"
}'
在測試過程中,會遇到這個報錯:Error response from daemon: Conflict. The container name "/my-kakeru-ai" is already in use.
把錯誤訊息丟給可以看出,這個container已經存在,如果同名的容器已經存在(即便它是停止狀態),就會引發名稱衝突。可以透過以下四個生命週期指令來管理你的 Docker:
docker run:建立並啟動一個全新容器(注意:若同名的容器已存在但處於停止狀態,直接執行 docker run 會出現名稱衝突錯誤)。docker stop my-kakeru-ai:暫停/關閉容器運行。docker start my-kakeru-ai:重新啟動已暫停的現有容器(會保留原本設定好的 Port 與憑證掛載)。docker rm my-kakeru-ai:刪除已關閉的容器。今天在與 AI 的協作下,成功利用 Docker 的 Multi-stage Build,將原本龐大的 Java 專案,濃縮成一個輕量、乾淨且安全的 Docker Image,完成了上雲端前的最後一塊拼圖。
接下來將在明天(Day 12)把這個 Image 推送到 Google Cloud Artifact Registry,並一鍵部署到 Cloud Run 上,讓 KAKERU 的API 正式在網際網路上誕生,敬請期待!