Run Java 8 and Tomcat 9 in Docker with Logback Configuration

🌏 閱讀中文版本


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:

  1. Verify logback-uat.xml is correctly copied to container
  2. Check if JAVA_OPTS environment variable is properly set
  3. 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:

  1. Logback rolling policy: Set maxHistory and totalSizeCap (see logback-uat.xml above)
  2. 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"
  }
}
  1. 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:

  1. Containerization Benefits: Environment consistency, portability, rapid deployment
  2. Proper Logback Configuration: Use environment variables to specify config file, set rolling policy to prevent log overflow
  3. JVM Tuning: Adjust heap memory and GC parameters based on application characteristics
  4. Health Checks: Ensure containers auto-restart on failure
  5. 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

Leave a Comment