為什麼選擇 Docker + Java 8 + Tomcat 9 + Logback?
在現代軟體開發中,容器化部署已成為主流趨勢。這個技術組合為企業級 Java Web 應用提供穩定、高效的運行環境。
技術選型的三大核心理由
1. 環境一致性與可移植性
Docker 容器化解決了「在我的機器上可以運行」的經典問題:
- 開發、測試、生產環境完全一致
- 消除因系統差異導致的部署問題
- 輕鬆在不同雲端平台間遷移
- 團隊成員快速建立相同開發環境
2. Java 8 + Tomcat 9 的穩定性
這個組合在業界已有多年驗證:
- Java 8:LTS 版本,擁有龐大的生態系統和社群支持
- Tomcat 9:支持 Servlet 4.0、JSP 2.3、WebSocket 1.1
- 相容性:許多企業級應用仍依賴 Java 8 API
- 效能:經過多年優化,執行效率高
3. Logback 強大的日誌管理
相較於 Log4j,Logback 提供更多優勢:
- 效能:比 Log4j 快 10 倍,記憶體占用更少
- 靈活配置:支持 XML 和 Groovy 配置,可動態重載
- 條件處理:根據環境變數、時間等條件調整日誌行為
- 過濾功能:精細控制日誌輸出內容
- 原生 SLF4J:無需額外轉接層
誰適合使用這個方案?
適合對象:
- 需要容器化部署 Java Web 應用的開發團隊
- 維護既有 Java 8 應用的企業
- 希望統一開發與生產環境的專案
- 需要靈活日誌管理的系統
前置知識:
- 基本 Docker 概念(鏡像、容器、Dockerfile)
- Java Web 開發經驗
- 了解 WAR 檔案結構
- 基本 Linux 指令操作
完整 Dockerfile 配置
以下是生產環境可用的 Dockerfile 範例,包含詳細註解:
# 使用官方 Tomcat 9 + JDK 8 基礎鏡像
# Alpine 版本更輕量(約 100MB vs 500MB)
FROM tomcat:9-jdk8-openjdk-slim
# 設定維護者資訊
LABEL maintainer="your-email@example.com"
LABEL version="1.0"
LABEL description="Java 8 + Tomcat 9 with Logback"
# 刪除 Tomcat 預設的 ROOT 應用與範例應用
# 避免安全風險與資源浪費
RUN rm -rf /usr/local/tomcat/webapps/ROOT
&& rm -rf /usr/local/tomcat/webapps/docs
&& rm -rf /usr/local/tomcat/webapps/examples
&& rm -rf /usr/local/tomcat/webapps/host-manager
&& rm -rf /usr/local/tomcat/webapps/manager
# 建立自訂日誌目錄
# 將日誌統一存放,方便掛載 volume
RUN mkdir -p /var/log/app
# 複製應用的 WAR 檔案到 Tomcat webapps 目錄
# 命名為 ROOT.war 使應用在根路徑運行(http://domain/ 而非 http://domain/app)
COPY target/application.war /usr/local/tomcat/webapps/ROOT.war
# 複製 Logback 配置檔案到容器
# 放在 /usr/local/tomcat/lib 確保 classpath 可以讀取
COPY logback-uat.xml /usr/local/tomcat/lib/logback-uat.xml
# 設定環境變數
# 指定 Logback 配置檔案位置
ENV LOGBACK_CONFIG="-Dlogback.configurationFile=/usr/local/tomcat/lib/logback-uat.xml"
# 設定 JVM 參數
# -Xms: 初始堆記憶體, -Xmx: 最大堆記憶體
# -XX:+UseG1GC: 使用 G1 垃圾回收器(Java 8 推薦)
# -XX:MaxGCPauseMillis: GC 最大暫停時間目標
ENV JAVA_OPTS="-Xms512m -Xmx1024m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-Djava.security.egd=file:/dev/./urandom
$LOGBACK_CONFIG"
# 暴露 Tomcat 預設 HTTP 端口
EXPOSE 8080
# 設定工作目錄
WORKDIR /usr/local/tomcat
# 健康檢查:每 30 秒檢查一次,3 次失敗視為不健康
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3
CMD curl -f http://localhost:8080/ || exit 1
# 啟動 Tomcat(前景模式)
CMD ["catalina.sh", "run"]
Logback 配置範例(logback-uat.xml)
完整的 Logback 配置檔案範例,適用於 UAT/生產環境:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 定義日誌檔案路徑 -->
<property name="LOG_HOME" value="/var/log/app" />
<property name="APP_NAME" value="myapp" />
<!-- Console Appender:輸出到標準輸出(Docker logs 會收集) -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- File Appender:輸出到檔案,每日滾動 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}.log</file>
<!-- 滾動策略:每日產生新檔案 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 保留 30 天的日誌 -->
<maxHistory>30</maxHistory>
<!-- 總日誌大小不超過 3GB -->
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- Error Appender:僅記錄 ERROR 級別日誌 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-error.log</file>
<!-- 過濾器:只記錄 ERROR 級別 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 特定套件的日誌級別設定 -->
<logger name="org.springframework" level="INFO" />
<logger name="org.hibernate" level="WARN" />
<logger name="com.zaxxer.hikari" level="INFO" />
<!-- 自訂應用程式日誌級別 -->
<logger name="com.example.myapp" level="DEBUG" />
<!-- Root Logger:預設日誌級別 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>
建置與運行步驟
1. 準備專案檔案
專案目錄結構:
my-java-app/
├── Dockerfile
├── logback-uat.xml
├── pom.xml
└── target/
└── application.war
2. 建置 Docker 鏡像
# 在專案根目錄執行
docker build -t my-java-app:1.0 .
# 檢視建置的鏡像
docker images | grep my-java-app
3. 運行容器
# 基本運行
docker run -d
--name my-app
-p 8080:8080
my-java-app:1.0
# 進階運行:掛載日誌目錄,設定環境變數
docker run -d
--name my-app
-p 8080:8080
-v /opt/app/logs:/var/log/app
-e JAVA_OPTS="-Xms1g -Xmx2g"
--restart unless-stopped
my-java-app:1.0
4. 驗證部署
# 檢查容器狀態
docker ps
# 檢視應用日誌
docker logs -f my-app
# 測試應用
curl http://localhost:8080
# 進入容器內部檢查
docker exec -it my-app bash
常見問題 FAQ
Q1: 應用啟動失敗,顯示「ClassNotFoundException」?
原因:缺少必要的 JAR 檔案或依賴未正確打包到 WAR 檔案中。
解決方法:
# 1. 檢查 WAR 檔案內容
unzip -l target/application.war | grep -i "missing-class"
# 2. 確認 Maven/Gradle 依賴配置
# pom.xml 中確保依賴 scope 不是 "provided"(除非是 Tomcat 已提供的)
<dependency>
<groupId>your-library</groupId>
<artifactId>library</artifactId>
<scope>compile</scope> <!-- 不是 provided -->
</dependency>
# 3. 重新打包
mvn clean package
Q2: Logback 配置未生效,仍使用預設配置?
檢查清單:
- 確認 logback-uat.xml 已正確複製到容器
- 檢查 JAVA_OPTS 環境變數是否正確設定
- 確認檔案路徑無誤
# 進入容器檢查
docker exec -it my-app bash
# 檢查檔案是否存在
ls -la /usr/local/tomcat/lib/logback-uat.xml
# 檢查環境變數
echo $JAVA_OPTS
# 查看 Tomcat 啟動日誌,確認 Logback 載入訊息
docker logs my-app | grep -i logback
Q3: 容器運行一段時間後記憶體不足(OutOfMemoryError)?
解決策略:
# 1. 增加 JVM 堆記憶體
docker run -d
--name my-app
-p 8080:8080
-e JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC"
my-java-app:1.0
# 2. 限制容器記憶體使用上限
docker run -d
--name my-app
-p 8080:8080
--memory="4g"
--memory-swap="4g"
my-java-app:1.0
# 3. 使用 jmap 分析記憶體洩漏
docker exec my-app jmap -heap 1
Q4: 如何在不停機的情況下更新應用?
藍綠部署方式:
# 1. 建置新版本鏡像
docker build -t my-java-app:1.1 .
# 2. 啟動新容器(使用不同端口)
docker run -d
--name my-app-new
-p 8081:8080
my-java-app:1.1
# 3. 測試新版本
curl http://localhost:8081
# 4. 切換流量(使用 Nginx/HAProxy)
# 或直接停止舊容器,啟動新容器在 8080 端口
# 5. 移除舊容器
docker stop my-app
docker rm my-app
docker rename my-app-new my-app
Q5: Docker 容器無法訪問外部網路?
檢查步驟:
# 1. 檢查容器網路模式
docker inspect my-app | grep -i network
# 2. 測試容器內部網路
docker exec my-app ping google.com
# 3. 檢查 Docker daemon DNS 設定
# /etc/docker/daemon.json
{
"dns": ["8.8.8.8", "8.8.4.4"]
}
# 4. 重啟 Docker
sudo systemctl restart docker
Q6: 日誌檔案佔用過多磁碟空間?
優化策略:
- Logback 滾動策略:設定 maxHistory 和 totalSizeCap(參見上方 logback-uat.xml)
- Docker 日誌限制:
# 方式一:運行時指定
docker run -d
--log-opt max-size=10m
--log-opt max-file=3
my-java-app:1.0
# 方式二:全域設定 /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
- 定期清理:
# 清理舊日誌(保留 7 天)
find /var/log/app -name "*.log" -mtime +7 -delete
Q7: 如何監控 Tomcat 應用效能?
監控方案:
# 1. 啟用 JMX 監控
docker run -d
--name my-app
-p 8080:8080
-p 9090:9090
-e JAVA_OPTS="-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9090
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false"
my-java-app:1.0
# 2. 使用 JConsole/VisualVM 連接
# Host: localhost:9090
# 3. 使用 Prometheus + Grafana
# 在應用中整合 Micrometer 庫,暴露 /actuator/prometheus 端點
最佳實踐
1. 安全性建議
- 使用非 root 使用者運行:
# 在 Dockerfile 中新增
RUN groupadd -r tomcat && useradd -r -g tomcat tomcat
USER tomcat
- 定期更新基礎鏡像:使用最新的安全補丁版本
- 掃描鏡像漏洞:
docker scan my-java-app:1.0
2. 效能優化
- 多階段建置:減少最終鏡像大小
# 建置階段
FROM maven:3.8-openjdk-8 AS build
COPY . /app
WORKDIR /app
RUN mvn clean package
# 運行階段
FROM tomcat:9-jdk8-openjdk-slim
COPY --from=build /app/target/application.war /usr/local/tomcat/webapps/ROOT.war
- 利用 Docker layer cache:將不常變動的指令放前面
3. 日誌管理
- 使用結構化日誌(JSON 格式)方便解析
- 整合 ELK Stack(Elasticsearch + Logstash + Kibana)集中管理
- 設定 log level:開發 DEBUG,生產 INFO/WARN
生產環境部署檢查清單
部署前必查:
- [ ] WAR 檔案已正確打包,包含所有依賴
- [ ] Logback 配置適合生產環境(log level, 滾動策略)
- [ ] JVM 參數已根據實際負載調整
- [ ] 健康檢查端點可正常訪問
- [ ] 容器記憶體限制已設定
- [ ] 日誌目錄已掛載到宿主機
- [ ] 備份與還原策略已建立
- [ ] 監控告警已設定(CPU、記憶體、回應時間)
故障排查技巧
檢視容器內部
# 進入運行中的容器
docker exec -it my-app bash
# 檢查 Tomcat 日誌
tail -f /usr/local/tomcat/logs/catalina.out
# 檢查應用日誌
tail -f /var/log/app/myapp.log
# 檢查 JVM 執行緒
docker exec my-app jstack 1
網路診斷
# 檢查端口是否開啟
docker exec my-app netstat -tuln | grep 8080
# 測試外部連線
docker exec my-app curl http://localhost:8080
# 檢查 DNS 解析
docker exec my-app nslookup google.com
結論
本文提供了完整的 Docker + Java 8 + Tomcat 9 + Logback 部署方案,從 Dockerfile 配置、Logback 設定、建置運行步驟,到常見問題排查與最佳實踐。
關鍵重點回顧:
- 容器化優勢:環境一致性、可移植性、快速部署
- 正確配置 Logback:使用環境變數指定配置檔案,設定滾動策略避免日誌爆滿
- JVM 調校:根據應用特性調整堆記憶體與 GC 參數
- 健康檢查:確保容器異常時能自動重啟
- 日誌掛載:將日誌目錄掛載到宿主機,方便持久化與分析
透過這個方案,您可以建立穩定、可維護的 Java Web 應用容器環境,並在生產環境中安心部署。
Related Articles
- Run Java 8 and Tomcat 9 in Docker with Logback Configuration
- Timezone Configuration in Cloud Services: From VMs to Containers (English)
- Choosing AWS Container Services: Kubernetes vs Amazon ECS Complete Comparison Guide
- 選擇 AWS 容器服務:Kubernetes vs Amazon ECS 完整對比指南
- Multiple Methods for Image Upload in Java Web Applications