🌏 閱讀中文版本
Why Choose Docker + Java 8 + Tomcat 9 + Logback?
In modern software development, containerized deployment has become the mainstream trend. This technology stack provides a stable and efficient runtime environment for enterprise-level Java web applications.
Three Core Reasons for This Tech Stack
1. Environment Consistency and Portability
Docker containerization solves the classic “works on my machine” problem:
- Development, testing, and production environments are identical
- Eliminates deployment issues caused by system differences
- Easy migration between different cloud platforms
- Team members can quickly set up identical development environments
2. Stability of Java 8 + Tomcat 9
This combination has been validated in the industry for years:
- Java 8: LTS version with extensive ecosystem and community support
- Tomcat 9: Supports Servlet 4.0, JSP 2.3, WebSocket 1.1
- Compatibility: Many enterprise applications still rely on Java 8 APIs
- Performance: Highly optimized over the years with excellent execution efficiency
3. Powerful Logging with Logback
Compared to Log4j, Logback offers more advantages:
- Performance: 10x faster than Log4j with lower memory footprint
- Flexible Configuration: Supports XML and Groovy config, hot reload capability
- Conditional Processing: Adjust logging behavior based on environment variables, time, etc.
- Filtering: Fine-grained control over log output
- Native SLF4J: No additional bridge layer required
Who Should Use This Solution?
Suitable For:
- Development teams deploying Java web applications in containers
- Enterprises maintaining existing Java 8 applications
- Projects requiring unified dev and production environments
- Systems needing flexible log management
Prerequisites:
- Basic Docker concepts (images, containers, Dockerfile)
- Java web development experience
- Understanding of WAR file structure
- Basic Linux command operations
Complete Dockerfile Configuration
Below is a production-ready Dockerfile example with detailed comments:
# Use official Tomcat 9 + JDK 8 base image
# Slim version is more lightweight (~100MB vs 500MB)
FROM tomcat:9-jdk8-openjdk-slim
# Set maintainer information
LABEL maintainer="your-email@example.com"
LABEL version="1.0"
LABEL description="Java 8 + Tomcat 9 with Logback"
# Remove Tomcat default ROOT app and sample apps
# Avoid security risks and resource waste
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
# Create custom log directory
# Centralize logs for easy volume mounting
RUN mkdir -p /var/log/app
# Copy application WAR file to Tomcat webapps directory
# Named ROOT.war to run at root path (http://domain/ instead of http://domain/app)
COPY target/application.war /usr/local/tomcat/webapps/ROOT.war
# Copy Logback configuration file to container
# Place in /usr/local/tomcat/lib to ensure classpath accessibility
COPY logback-uat.xml /usr/local/tomcat/lib/logback-uat.xml
# Set environment variables
# Specify Logback configuration file location
ENV LOGBACK_CONFIG="-Dlogback.configurationFile=/usr/local/tomcat/lib/logback-uat.xml"
# Set JVM parameters
# -Xms: Initial heap memory, -Xmx: Maximum heap memory
# -XX:+UseG1GC: Use G1 garbage collector (recommended for Java 8)
# -XX:MaxGCPauseMillis: Target maximum GC pause time
ENV JAVA_OPTS="-Xms512m -Xmx1024m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-Djava.security.egd=file:/dev/./urandom \
$LOGBACK_CONFIG"
# Expose Tomcat default HTTP port
EXPOSE 8080
# Set working directory
WORKDIR /usr/local/tomcat
# Health check: check every 30s, unhealthy after 3 failures
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/ || exit 1
# Start Tomcat in foreground mode
CMD ["catalina.sh", "run"]
Logback Configuration Example (logback-uat.xml)
Complete Logback configuration file for UAT/production environments:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Define log file path -->
<property name="LOG_HOME" value="/var/log/app" />
<property name="APP_NAME" value="myapp" />
<!-- Console Appender: Output to stdout (collected by 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: Output to file with daily rolling -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}.log</file>
<!-- Rolling policy: Create new file daily -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- Keep 30 days of logs -->
<maxHistory>30</maxHistory>
<!-- Total log size cap at 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: Log ERROR level only -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-error.log</file>
<!-- Filter: ERROR level only -->
<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>
<!-- Package-specific log levels -->
<logger name="org.springframework" level="INFO" />
<logger name="org.hibernate" level="WARN" />
<logger name="com.zaxxer.hikari" level="INFO" />
<!-- Custom application log level -->
<logger name="com.example.myapp" level="DEBUG" />
<!-- Root Logger: Default log level -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>
Build and Run Steps
1. Prepare Project Files
Project directory structure:
my-java-app/
├── Dockerfile
├── logback-uat.xml
├── pom.xml
└── target/
└── application.war
2. Build Docker Image
# Execute in project root directory
docker build -t my-java-app:1.0 .
# View built image
docker images | grep my-java-app
3. Run Container
# Basic run
docker run -d \
--name my-app \
-p 8080:8080 \
my-java-app:1.0
# Advanced run: Mount log directory, set environment variables
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. Verify Deployment
# Check container status
docker ps
# View application logs
docker logs -f my-app
# Test application
curl http://localhost:8080
# Enter container for inspection
docker exec -it my-app bash
Frequently Asked Questions (FAQ)
Q1: Application fails to start with “ClassNotFoundException”?
Cause: Missing required JAR files or dependencies not properly packaged in WAR file.
Solution:
# 1. Check WAR file contents
unzip -l target/application.war | grep -i "missing-class"
# 2. Verify Maven/Gradle dependency configuration
# Ensure dependency scope is not "provided" in pom.xml (unless provided by Tomcat)
<dependency>
<groupId>your-library</groupId>
<artifactId>library</artifactId>
<scope>compile</scope> <!-- Not provided -->
</dependency>
# 3. Rebuild
mvn clean package
Q2: Logback configuration not taking effect, still using default config?
Checklist:
- Verify logback-uat.xml is correctly copied to container
- Check if JAVA_OPTS environment variable is properly set
- Confirm file path is correct
# Enter container to check
docker exec -it my-app bash
# Check if file exists
ls -la /usr/local/tomcat/lib/logback-uat.xml
# Check environment variables
echo $JAVA_OPTS
# View Tomcat startup logs for Logback loading messages
docker logs my-app | grep -i logback
Q3: Container runs out of memory after some time (OutOfMemoryError)?
Solutions:
# 1. Increase JVM heap memory
docker run -d \
--name my-app \
-p 8080:8080 \
-e JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC" \
my-java-app:1.0
# 2. Limit container memory usage
docker run -d \
--name my-app \
-p 8080:8080 \
--memory="4g" \
--memory-swap="4g" \
my-java-app:1.0
# 3. Analyze memory leaks with jmap
docker exec my-app jmap -heap 1
Q4: How to update application without downtime?
Blue-Green Deployment:
# 1. Build new version image
docker build -t my-java-app:1.1 .
# 2. Start new container (different port)
docker run -d \
--name my-app-new \
-p 8081:8080 \
my-java-app:1.1
# 3. Test new version
curl http://localhost:8081
# 4. Switch traffic (using Nginx/HAProxy)
# Or stop old container and start new one on port 8080
# 5. Remove old container
docker stop my-app
docker rm my-app
docker rename my-app-new my-app
Q5: Docker container cannot access external network?
Troubleshooting Steps:
# 1. Check container network mode
docker inspect my-app | grep -i network
# 2. Test network from inside container
docker exec my-app ping google.com
# 3. Check Docker daemon DNS settings
# /etc/docker/daemon.json
{
"dns": ["8.8.8.8", "8.8.4.4"]
}
# 4. Restart Docker
sudo systemctl restart docker
Q6: Log files consuming too much disk space?
Optimization Strategies:
- Logback rolling policy: Set maxHistory and totalSizeCap (see logback-uat.xml above)
- Docker log limits:
# Method 1: Specify at runtime
docker run -d \
--log-opt max-size=10m \
--log-opt max-file=3 \
my-java-app:1.0
# Method 2: Global setting in /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
- Regular cleanup:
# Clean old logs (keep 7 days)
find /var/log/app -name "*.log" -mtime +7 -delete
Q7: How to monitor Tomcat application performance?
Monitoring Solutions:
# 1. Enable JMX monitoring
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. Connect with JConsole/VisualVM
# Host: localhost:9090
# 3. Use Prometheus + Grafana
# Integrate Micrometer in application, expose /actuator/prometheus endpoint
Best Practices
1. Security Recommendations
- Run as non-root user:
# Add to Dockerfile
RUN groupadd -r tomcat && useradd -r -g tomcat tomcat
USER tomcat
- Regularly update base images: Use latest security patch versions
- Scan image vulnerabilities:
docker scan my-java-app:1.0
2. Performance Optimization
- Multi-stage builds: Reduce final image size
# Build stage
FROM maven:3.8-openjdk-8 AS build
COPY . /app
WORKDIR /app
RUN mvn clean package
# Runtime stage
FROM tomcat:9-jdk8-openjdk-slim
COPY --from=build /app/target/application.war /usr/local/tomcat/webapps/ROOT.war
- Leverage Docker layer cache: Place unchanging commands first
3. Log Management
- Use structured logging (JSON format) for easy parsing
- Integrate with ELK Stack (Elasticsearch + Logstash + Kibana) for centralized management
- Set log levels: DEBUG for dev, INFO/WARN for production
Production Deployment Checklist
Pre-deployment Checks:
- [ ] WAR file correctly packaged with all dependencies
- [ ] Logback config suitable for production (log level, rolling policy)
- [ ] JVM parameters adjusted for actual workload
- [ ] Health check endpoint accessible
- [ ] Container memory limits set
- [ ] Log directory mounted to host
- [ ] Backup and restore strategy established
- [ ] Monitoring alerts configured (CPU, memory, response time)
Troubleshooting Tips
Inspect Container Internals
# Enter running container
docker exec -it my-app bash
# Check Tomcat logs
tail -f /usr/local/tomcat/logs/catalina.out
# Check application logs
tail -f /var/log/app/myapp.log
# Check JVM threads
docker exec my-app jstack 1
Network Diagnostics
# Check if port is open
docker exec my-app netstat -tuln | grep 8080
# Test external connection
docker exec my-app curl http://localhost:8080
# Check DNS resolution
docker exec my-app nslookup google.com
Conclusion
This article provides a complete Docker + Java 8 + Tomcat 9 + Logback deployment solution, covering Dockerfile configuration, Logback setup, build/run steps, troubleshooting, and best practices.
Key Takeaways:
- Containerization Benefits: Environment consistency, portability, rapid deployment
- Proper Logback Configuration: Use environment variables to specify config file, set rolling policy to prevent log overflow
- JVM Tuning: Adjust heap memory and GC parameters based on application characteristics
- Health Checks: Ensure containers auto-restart on failure
- Log Mounting: Mount log directory to host for persistence and analysis
With this solution, you can build a stable, maintainable Java web application container environment and deploy confidently in production.
Related Articles
- 在 Docker 中運行 Java 8 和 Tomcat 9,並設置 Logback
- 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