在 Docker 中運行 Java 8 和 Tomcat 9,並設置 Logback

🌏 Read the English version


為什麼選擇 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 配置未生效,仍使用預設配置?

檢查清單:

  1. 確認 logback-uat.xml 已正確複製到容器
  2. 檢查 JAVA_OPTS 環境變數是否正確設定
  3. 確認檔案路徑無誤
# 進入容器檢查
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: 日誌檔案佔用過多磁碟空間?

優化策略:

  1. Logback 滾動策略:設定 maxHistory 和 totalSizeCap(參見上方 logback-uat.xml)
  2. 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"
  }
}
  1. 定期清理:
# 清理舊日誌(保留 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 設定、建置運行步驟,到常見問題排查與最佳實踐。

關鍵重點回顧:

  1. 容器化優勢:環境一致性、可移植性、快速部署
  2. 正確配置 Logback:使用環境變數指定配置檔案,設定滾動策略避免日誌爆滿
  3. JVM 調校:根據應用特性調整堆記憶體與 GC 參數
  4. 健康檢查:確保容器異常時能自動重啟
  5. 日誌掛載:將日誌目錄掛載到宿主機,方便持久化與分析

透過這個方案,您可以建立穩定、可維護的 Java Web 應用容器環境,並在生產環境中安心部署。

Related Articles

Leave a Comment