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
sudo dnf update -y
sudo dnf install -y docker docker-compose java-21-openjdk-devel 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.
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