Learn to deploy production-ready Spring Boot applications using Jetty embedded server with Docker containerization. This tutorial covers multi-stage Docker builds, production configuration, and deployment with Docker Compose for scalable Java applications.
Prerequisites
- Root or sudo access
- At least 4GB RAM
- Docker and Java 21 installed
What this solves
Spring Boot applications with embedded Jetty server provide a lightweight alternative to traditional Tomcat deployments. Docker containerization enables consistent deployments across environments while multi-stage builds optimize image sizes for production use.
Step-by-step installation
Install Docker and Java development tools
Start by installing Docker Engine and Java JDK for building Spring Boot applications.
sudo apt update && sudo apt upgrade -y
sudo apt install -y docker.io docker-compose openjdk-21-jdk maven
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
Verify Java and Docker installation
Confirm all required tools are properly installed and accessible.
java --version
mvn --version
docker --version
docker compose version
Create Spring Boot project structure
Generate a new Spring Boot project with Jetty dependencies using Maven.
mkdir springboot-jetty-app
cd springboot-jetty-app
mvn archetype:generate -DgroupId=com.example -DartifactId=jetty-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
Configure Maven dependencies for Spring Boot with Jetty
Replace the default pom.xml with Spring Boot starter dependencies and exclude Tomcat in favor of Jetty.
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.2.1
com.example
jetty-app
1.0.0
jar
21
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-tomcat
org.springframework.boot
spring-boot-starter-jetty
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-maven-plugin
Create Spring Boot application class
Create the main application class with a sample REST endpoint for testing.
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@GetMapping("/")
public String home() {
return "Spring Boot with Jetty Server - Running in Docker Container";
}
@GetMapping("/health")
public String health() {
return "OK - Jetty Server is running";
}
}
Configure application properties
Set Jetty server configuration and production settings in application properties.
# Server configuration
server.port=8080
server.jetty.threads.max=200
server.jetty.threads.min=10
server.jetty.threads.idle-timeout=60000
server.jetty.connection-idle-timeout=30000
Actuator endpoints
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
management.server.port=8081
Logging configuration
logging.level.org.eclipse.jetty=INFO
logging.level.com.example=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
Create multi-stage Dockerfile
Build an optimized Docker image using multi-stage builds to minimize the final image size.
# Build stage
FROM openjdk:21-jdk-slim AS build
WORKDIR /app
Copy pom.xml and download dependencies
COPY pom.xml .
RUN apt-get update && apt-get install -y maven
RUN mvn dependency:go-offline -B
Copy source code and build application
COPY src ./src
RUN mvn clean package -DskipTests
Runtime stage
FROM openjdk:21-jre-slim AS runtime
Create non-root user for security
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
Copy JAR from build stage
COPY --from=build /app/target/jetty-app-1.0.0.jar app.jar
Change ownership to non-root user
RUN chown -R appuser:appgroup /app
USER appuser
Expose ports
EXPOSE 8080 8081
Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8081/actuator/health || exit 1
Run application
ENTRYPOINT ["java", "-jar", "-Djava.security.egd=file:/dev/./urandom", "app.jar"]
Create Docker Compose configuration
Set up Docker Compose for easy deployment with environment-specific configurations.
version: '3.8'
services:
springboot-jetty:
build:
context: .
dockerfile: Dockerfile
image: springboot-jetty:latest
container_name: springboot-jetty-app
ports:
- "8080:8080"
- "8081:8081"
environment:
- SPRING_PROFILES_ACTIVE=docker
- JAVA_OPTS=-Xmx512m -Xms256m
restart: unless-stopped
networks:
- app-network
volumes:
- ./logs:/app/logs
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
nginx:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- springboot-jetty
restart: unless-stopped
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
logs:
driver: local
Configure Nginx reverse proxy
Create Nginx configuration to proxy requests to the Spring Boot application with load balancing capabilities.
events {
worker_connections 1024;
}
http {
upstream springboot {
server springboot-jetty:8080;
}
server {
listen 80;
server_name localhost;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
location / {
proxy_pass http://springboot;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
location /actuator/health {
proxy_pass http://springboot-jetty:8081/actuator/health;
access_log off;
}
}
}
Create production environment configuration
Add production-specific application properties for Docker deployment.
# Production configuration for Docker
server.jetty.threads.max=300
server.jetty.threads.min=20
Logging configuration for production
logging.level.com.example=INFO
logging.file.name=/app/logs/application.log
logging.logback.rollingpolicy.max-file-size=100MB
logging.logback.rollingpolicy.max-history=30
Actuator security
management.endpoints.web.base-path=/actuator
management.endpoint.health.show-components=always
Build and deploy the application
Build the Docker images and start the complete application stack.
cd jetty-app
Build the application
docker compose build
Start the services
docker compose up -d
View logs
docker compose logs -f springboot-jetty
Verify your setup
Test the Spring Boot application and verify all components are working correctly.
# Check container status
docker compose ps
Test application endpoint
curl http://localhost/
Check health endpoint
curl http://localhost/actuator/health
View application metrics
curl http://localhost:8081/actuator/metrics
Check Jetty server info in logs
docker compose logs springboot-jetty | grep -i jetty
You should see responses indicating the Spring Boot application is running with Jetty server. The health check should return status "UP" and metrics should show Jetty-specific measurements.
Production optimization
Configure JVM memory settings
Optimize JVM memory allocation for containerized environments.
version: '3.8'
services:
springboot-jetty:
environment:
- JAVA_OPTS=-Xmx1g -Xms512m -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0
- JVM_ARGS=-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
Add container resource limits
Set resource constraints to prevent container resource exhaustion.
services:
springboot-jetty:
deploy:
resources:
limits:
cpus: '1.5'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Port binding failed | Port already in use | sudo netstat -tlnp | grep :8080 to find conflicting process |
| Application won't start | Insufficient memory | Increase Docker memory limits or reduce -Xmx value |
| Container exits immediately | Java application error | docker compose logs springboot-jetty to check startup errors |
| Health check failing | Management port not accessible | Verify port 8081 is exposed and actuator endpoints enabled |
| Nginx 502 Bad Gateway | Backend connection failed | Check if springboot-jetty container is healthy and network connectivity |
| Build fails with Maven errors | Dependencies not downloaded | Run docker compose build --no-cache to rebuild images |
Next steps
- Monitor Docker containers with Prometheus and Grafana for comprehensive application monitoring
- Configure advanced NGINX caching and load balancing for better performance
- Set up Spring Boot clustering with Hazelcast for horizontal scaling
- Implement Spring Boot security with JWT authentication
- Configure centralized logging with ELK stack integration
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
# Configuration
readonly APP_NAME="springboot-jetty-app"
readonly APP_USER="jetty"
readonly APP_DIR="/opt/${APP_NAME}"
readonly SERVICE_NAME="springboot-jetty"
# Print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Cleanup function for rollback
cleanup() {
print_error "Installation failed. Performing cleanup..."
systemctl stop docker 2>/dev/null || true
userdel -r "$APP_USER" 2>/dev/null || true
rm -rf "$APP_DIR" 2>/dev/null || true
exit 1
}
trap cleanup ERR
# Check if running with sufficient privileges
check_privileges() {
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root or with sudo"
exit 1
fi
}
# Detect distribution and set package manager
detect_distro() {
if [[ ! -f /etc/os-release ]]; then
print_error "Cannot detect Linux distribution"
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
JAVA_PKG="openjdk-21-jdk"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
JAVA_PKG="java-21-openjdk-devel"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
JAVA_PKG="java-21-amazon-corretto-devel"
;;
*)
print_error "Unsupported distribution: $ID"
exit 1
;;
esac
print_status "Detected distribution: $PRETTY_NAME"
print_status "Using package manager: $PKG_MGR"
}
# Install Docker and Java development tools
install_dependencies() {
print_status "[1/8] Installing Docker and Java development tools..."
$PKG_UPDATE
case "$PKG_MGR" in
apt)
$PKG_INSTALL docker.io docker-compose "$JAVA_PKG" maven curl wget
;;
dnf|yum)
$PKG_INSTALL docker docker-compose "$JAVA_PKG" maven curl wget
;;
esac
systemctl enable --now docker
print_status "Docker and Java tools installed successfully"
}
# Create application user
create_app_user() {
print_status "[2/8] Creating application user..."
if ! id "$APP_USER" &>/dev/null; then
useradd -r -s /bin/false -d "$APP_DIR" "$APP_USER"
usermod -aG docker "$APP_USER"
print_status "Created user: $APP_USER"
else
print_warning "User $APP_USER already exists"
fi
}
# Verify installations
verify_installation() {
print_status "[3/8] Verifying Java and Docker installation..."
java --version
mvn --version
docker --version
docker compose version
print_status "All tools verified successfully"
}
# Create Spring Boot project structure
create_project_structure() {
print_status "[4/8] Creating Spring Boot project structure..."
mkdir -p "$APP_DIR"
cd "$APP_DIR"
# Create Maven project structure
mkdir -p src/main/java/com/example
mkdir -p src/main/resources
mkdir -p target
chown -R "$APP_USER:$APP_USER" "$APP_DIR"
}
# Create Maven configuration
create_maven_config() {
print_status "[5/8] Creating Maven configuration..."
cat > "$APP_DIR/pom.xml" << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>jetty-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
EOF
}
# Create Spring Boot application
create_application() {
print_status "[6/8] Creating Spring Boot application..."
cat > "$APP_DIR/src/main/java/com/example/Application.java" << 'EOF'
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@GetMapping("/")
public String home() {
return "Spring Boot with Jetty Server - Running in Docker Container";
}
@GetMapping("/health")
public String health() {
return "OK - Jetty Server is running";
}
}
EOF
cat > "$APP_DIR/src/main/resources/application.properties" << 'EOF'
# Server configuration
server.port=8080
server.jetty.threads.max=200
server.jetty.threads.min=10
server.jetty.threads.idle-timeout=60000
server.jetty.connection-idle-timeout=30000
# Actuator endpoints
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
management.server.port=8081
# Logging configuration
logging.level.org.eclipse.jetty=INFO
logging.level.com.example=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
EOF
}
# Create Dockerfile
create_dockerfile() {
print_status "[7/8] Creating multi-stage Dockerfile..."
cat > "$APP_DIR/Dockerfile" << 'EOF'
# Build stage
FROM openjdk:21-jdk-slim AS build
WORKDIR /app
# Copy pom.xml and download dependencies
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Copy source code and build
COPY src ./src
RUN mvn clean package -DskipTests
# Runtime stage
FROM openjdk:21-jre-slim
WORKDIR /app
# Create non-root user
RUN useradd -r -s /bin/false jetty
# Copy JAR from build stage
COPY --from=build /app/target/*.jar app.jar
# Set ownership
RUN chown jetty:jetty app.jar
USER jetty
EXPOSE 8080 8081
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8081/actuator/health || exit 1
CMD ["java", "-jar", "app.jar"]
EOF
cat > "$APP_DIR/docker-compose.yml" << 'EOF'
version: '3.8'
services:
jetty-app:
build: .
container_name: springboot-jetty-app
ports:
- "8080:8080"
- "8081:8081"
environment:
- SPRING_PROFILES_ACTIVE=production
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
EOF
# Set proper permissions
chown -R "$APP_USER:$APP_USER" "$APP_DIR"
chmod 644 "$APP_DIR"/{pom.xml,Dockerfile,docker-compose.yml}
chmod 644 "$APP_DIR/src/main/java/com/example/Application.java"
chmod 644 "$APP_DIR/src/main/resources/application.properties"
}
# Configure firewall
configure_firewall() {
print_status "[8/8] Configuring firewall..."
if command -v ufw &> /dev/null; then
ufw allow 8080/tcp
ufw allow 8081/tcp
elif command -v firewall-cmd &> /dev/null; then
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --permanent --add-port=8081/tcp
firewall-cmd --reload
fi
print_status "Firewall configured for ports 8080 and 8081"
}
# Verify deployment
verify_deployment() {
print_status "Verifying deployment..."
cd "$APP_DIR"
# Build and test the application
sudo -u "$APP_USER" mvn clean package -DskipTests
sudo -u "$APP_USER" docker build -t springboot-jetty-app .
print_status "${GREEN}Installation completed successfully!${NC}"
print_status "Project location: $APP_DIR"
print_status "To start the application:"
print_status " cd $APP_DIR"
print_status " docker compose up -d"
print_status "Application will be available at:"
print_status " Main app: http://localhost:8080"
print_status " Health check: http://localhost:8081/actuator/health"
}
# Main execution
main() {
print_status "Starting Spring Boot with Jetty and Docker installation..."
check_privileges
detect_distro
install_dependencies
create_app_user
verify_installation
create_project_structure
create_maven_config
create_application
create_dockerfile
configure_firewall
verify_deployment
print_status "${GREEN}Spring Boot with Jetty deployment setup completed successfully!${NC}"
}
main "$@"
Review the script before running. Execute with: bash install.sh