A comprehensive guide covering all essential considerations for stable production deployment of Spring Boot applications with embedded web servers.
Sources: Official Spring Boot Documentation, Apache Tomcat Documentation, Baeldung Guides
Core Concept: Every Spring Boot web application includes an embedded web server, eliminating the need for external server deployment.
Default Servers:
- Servlet Stack (spring-boot-starter-web): Tomcat (default)
- Reactive Stack (spring-boot-starter-webflux): Reactor Netty (default)
Alternative Servers Available: Jetty, Undertow
Key Learning: You can swap the default embedded server by excluding the default dependency and adding your preferred server.
Steps for Maven:
- Exclude the default server starter (e.g.,
spring-boot-starter-tomcat) - Add your preferred server starter (e.g.,
spring-boot-starter-jetty)
Steps for Gradle:
- Use
excludein your implementation block - Add the alternative server implementation
| Configuration | Property | Description |
|---|---|---|
| Change default port | server.port=9090 |
Changes from default 8080 |
| Random port | server.port=0 |
OS assigns available port |
| Disable HTTP endpoints | server.port=-1 |
Useful for testing |
| Disable web server entirely | spring.main.web-application-type=none |
Application runs without web |
Runtime Port Discovery:
- Use
@LocalServerPortannotation in tests - Listen to
WebServerInitializedEvent - Access via
WebServerApplicationContext
Enable: server.compression.enabled=true
Default Settings:
- Minimum response size: 2048 bytes
- Supported MIME types: text/html, text/xml, text/plain, text/css, text/javascript, application/javascript, application/json, application/xml
Customization Properties:
server.compression.min-response-sizeserver.compression.mime-types
server.port=8443
server.ssl.key-store=classpath:keystore.jks
server.ssl.key-store-password=secret
server.ssl.key-password=another-secretserver.port=8443
server.ssl.certificate=classpath:my-cert.crt
server.ssl.certificate-private-key=classpath:my-cert.key
server.ssl.trust-certificate=classpath:ca-cert.crtserver.port=8443
server.ssl.bundle=exampleServer Name Indication (SNI): Supported by Tomcat and Netty for serving multiple SSL certificates by hostname.
Enable: server.http2.enabled=true
| Mode | Description | Requirements |
|---|---|---|
| h2 | HTTP/2 over TLS | SSL must be enabled |
| h2c | HTTP/2 over TCP (cleartext) | Used when SSL is disabled |
Server-Specific Notes:
- Tomcat 11.0.x: h2c and h2 supported out-of-the-box
- Jetty: Requires
jetty-http2-serverdependency - Reactor Netty: Native support; optional native libraries for performance
- Define
@Beanfor Servlet, Filter, or Listener - Use
FilterRegistrationBeanorServletRegistrationBeanfor mappings/init parameters - Disable auto-registration by setting
registration.setEnabled(false)
- Annotate classes with
@WebServlet,@WebFilter,@WebListener - Add
@ServletComponentScanto a@Configurationclass
server.tomcat.basedir=my-tomcat
server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.pattern=%t %a %r %s (%D microseconds)server.jetty.accesslog.enabled=true
server.jetty.accesslog.filename=/var/log/jetty-access.logProblem: Behind a proxy, request info (host, port, scheme) may change.
Solution: Configure forwarded headers strategy.
| Strategy | Property Value | Use Case |
|---|---|---|
| Native | server.forward-headers-strategy=NATIVE |
Web server handles headers |
| Framework | server.forward-headers-strategy=FRAMEWORK |
Spring handles headers |
| None | server.forward-headers-strategy=NONE |
Disable forwarded headers |
Tomcat-Specific Customization:
server.tomcat.remoteip.remote-ip-header=x-your-remote-ip-header
server.tomcat.remoteip.protocol-header=x-your-protocol-header
server.tomcat.remoteip.internal-proxies=192\.168\.\d{1,3}\.\d{1,3}For configurations not available via properties, implement WebServerFactoryCustomizer:
@Component
public class MyTomcatWebServerCustomizer
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
// custom configuration
}
}
```
#### Multiple Connectors (Tomcat)
Add additional HTTP/HTTPS connectors programmatically using `WebServerFactoryCustomizer`.
#### MBean Registry (Tomcat)
Enable for metrics exposure: `server.tomcat.mbeanregistry.enabled=true`
---
### Module 11: WebSocket Support
**Requirement:** Declare `ServerEndpointExporter` bean for `@ServerEndpoint` support in embedded containers.
```java
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
```
*Note: Not required when deployed to standalone servlet container.*
---
### Quick Reference Cheat Sheet
| Task | Property/Approach |
|------|------------------|
| Change port | `server.port=9090` |
| Random port | `server.port=0` |
| Enable SSL | `server.ssl.*` properties |
| Enable HTTP/2 | `server.http2.enabled=true` |
| Enable compression | `server.compression.enabled=true` |
| Disable web server | `spring.main.web-application-type=none` |
| Enable access logs | `server.tomcat.accesslog.enabled=true` |
| Handle proxy headers | `server.forward-headers-strategy=NATIVE` |
| Custom server config | Implement `WebServerFactoryCustomizer` |
---
## Part 2: Production Deployment Guide
### Table of Contents
1. [Thread Pool & Connection Management](#1-thread-pool--connection-management)
2. [Timeout Configuration](#2-timeout-configuration)
3. [Graceful Shutdown](#3-graceful-shutdown)
4. [SSL/TLS Security](#4-ssltls-security)
5. [HTTP/2 Configuration](#5-http2-configuration)
6. [Response Compression](#6-response-compression)
7. [Request Size Limits](#7-request-size-limits)
8. [Access Logging](#8-access-logging)
9. [Health Checks & Monitoring](#9-health-checks--monitoring)
10. [Metrics & Observability](#10-metrics--observability)
11. [Proxy & Load Balancer Configuration](#11-proxy--load-balancer-configuration)
12. [Session Management](#12-session-management)
13. [Error Handling](#13-error-handling)
14. [JVM & Memory Tuning](#14-jvm--memory-tuning)
15. [Server Selection & Switching](#15-server-selection--switching)
---
## 1. Thread Pool & Connection Management
### Why It Matters
Thread pool configuration directly impacts your application's ability to handle concurrent requests. Misconfiguration can lead to thread exhaustion, request queuing, or resource waste.
### Key Properties
```yaml
server:
tomcat:
threads:
max: 200 # Maximum worker threads
min-spare: 10 # Minimum threads always kept running
max-queue-capacity: 2147483647 # Max queue before rejection
max-connections: 8192 # Max connections server accepts
accept-count: 100 # Queue length when max-connections reached
```
### Production Patterns
**Pattern 1: High-Throughput API Server**
```yaml
server:
tomcat:
threads:
max: 400
min-spare: 50
max-connections: 10000
accept-count: 200
connection-timeout: 20000
```
**Pattern 2: Resource-Constrained Environment**
```yaml
server:
tomcat:
threads:
max: 100
min-spare: 10
max-connections: 2000
accept-count: 50
```
**Pattern 3: Using Virtual Threads (Java 21+)**
```yaml
spring:
threads:
virtual:
enabled: true
server:
tomcat:
threads:
max: 200 # Ignored when virtual threads enabled
```
### Programmatic Configuration
```java
@Component
public class TomcatCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector -> {
ProtocolHandler handler = connector.getProtocolHandler();
if (handler instanceof AbstractProtocol<?> protocol) {
protocol.setMaxThreads(300);
protocol.setMinSpareThreads(25);
protocol.setAcceptCount(150);
protocol.setMaxConnections(10000);
}
});
}
}
```
### Jetty Configuration
```yaml
server:
jetty:
threads:
max: 200
min: 8
idle-timeout: 60000
acceptors: -1 # Auto-detect based on CPU
selectors: -1 # Auto-detect based on CPU
max-connections: -1 # Unlimited
```
---
## 2. Timeout Configuration
### Why It Matters
Proper timeout settings prevent resource leaks from slow clients and ensure predictable behavior under load.
### Key Properties
```yaml
server:
tomcat:
connection-timeout: 20s # Time to wait for request URI after connection
keep-alive-timeout: 20s # Time to wait between requests
max-keep-alive-requests: 100 # Max requests per connection (-1 = unlimited)
# Jetty equivalent
jetty:
connection-idle-timeout: 30s
```
### Production Patterns
**Pattern 1: Public-Facing API**
```yaml
server:
tomcat:
connection-timeout: 10s # Aggressive timeout for public traffic
keep-alive-timeout: 15s
max-keep-alive-requests: 50
```
**Pattern 2: Internal Microservice**
```yaml
server:
tomcat:
connection-timeout: 60s # More lenient for internal traffic
keep-alive-timeout: 60s
max-keep-alive-requests: 200
```
**Pattern 3: File Upload Service**
```yaml
server:
tomcat:
connection-timeout: 300s # Long timeout for uploads
max-http-form-post-size: 100MB
max-swallow-size: 100MB
```
### Async Request Timeout
```yaml
spring:
mvc:
async:
request-timeout: 30s # Timeout for async operations
```
```java
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("Async method {} threw exception: {}", method.getName(), ex.getMessage());
};
}
}
```
---
## 3. Graceful Shutdown
### Why It Matters
Graceful shutdown ensures in-flight requests complete before the server stops, preventing data loss and client errors during deployments.
### Configuration
```yaml
server:
shutdown: graceful # Enable graceful shutdown
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # Grace period for existing requests
```
### Production Pattern with Health Check Integration
```yaml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 45s
management:
endpoint:
health:
probes:
enabled: true
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
```
### Kubernetes Integration
```yaml
# Kubernetes deployment configuration
spec:
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"] # Allow time for load balancer to drain
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 3
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
failureThreshold: 3
periodSeconds: 5
```
### Custom Shutdown Logic
```java
@Component
public class GracefulShutdownHandler implements ApplicationListener<ContextClosedEvent> {
@Autowired
private DataSource dataSource;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("Initiating graceful shutdown...");
// Close database connections gracefully
if (dataSource instanceof HikariDataSource hikari) {
hikari.close();
}
log.info("Graceful shutdown complete");
}
}
```
---
## 4. SSL/TLS Security
### Why It Matters
Proper SSL configuration is critical for data security, compliance, and preventing man-in-the-middle attacks.
### Option A: Java KeyStore
```yaml
server:
port: 8443
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: ${SSL_KEYSTORE_PASSWORD}
key-store-type: PKCS12
key-alias: tomcat
protocol: TLS
enabled-protocols: TLSv1.3,TLSv1.2
ciphers: TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256
```
### Option B: PEM-Encoded Certificates
```yaml
server:
port: 8443
ssl:
certificate: classpath:server.crt
certificate-private-key: classpath:server.key
trust-certificate: classpath:ca.crt
```
### Option C: SSL Bundles (Spring Boot 3.1+)
```yaml
spring:
ssl:
bundle:
pem:
server:
keystore:
certificate: classpath:server.crt
private-key: classpath:server.key
truststore:
certificate: classpath:ca.crt
jks:
client:
keystore:
location: classpath:client.p12
password: ${CLIENT_KEYSTORE_PASSWORD}
type: PKCS12
server:
port: 8443
ssl:
bundle: server
```
### Production Pattern: Mutual TLS (mTLS)
```yaml
server:
port: 8443
ssl:
enabled: true
key-store: classpath:server.p12
key-store-password: ${SSL_KEYSTORE_PASSWORD}
key-store-type: PKCS12
trust-store: classpath:truststore.p12
trust-store-password: ${SSL_TRUSTSTORE_PASSWORD}
trust-store-type: PKCS12
client-auth: need # Require client certificates
```
### Server Name Indication (SNI)
```yaml
server:
port: 8443
ssl:
bundle: primary
server-name-bundles:
- server-name: api.example.com
bundle: api-bundle
- server-name: admin.example.com
bundle: admin-bundle
```
### SSL Health Monitoring
```yaml
management:
health:
ssl:
enabled: true
certificate-validity-warning-threshold: 14d # Warn 14 days before expiry
```
---
## 5. HTTP/2 Configuration
### Why It Matters
HTTP/2 provides multiplexing, header compression, and server push, significantly improving performance.
### Basic Configuration
```yaml
server:
http2:
enabled: true
ssl:
enabled: true # Required for h2 (HTTP/2 over TLS)
```
### Cleartext HTTP/2 (h2c)
```yaml
server:
http2:
enabled: true
# No SSL = h2c mode (useful behind TLS-terminating proxy)
```
### Tomcat HTTP/2 with Native Library
```java
@Component
public class Http2Customizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector -> {
connector.addUpgradeProtocol(new Http2Protocol());
});
}
}
```
### Jetty HTTP/2
Add dependency:
```xml
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-server</artifactId>
</dependency>
```
---
## 6. Response Compression
### Why It Matters
Compression reduces bandwidth usage and improves response times, especially for text-based content.
### Configuration
```yaml
server:
compression:
enabled: true
min-response-size: 1KB # Only compress responses > 1KB
mime-types:
- text/html
- text/xml
- text/plain
- text/css
- text/javascript
- application/javascript
- application/json
- application/xml
excluded-user-agents:
- MSIE 6.0 # Exclude problematic clients
```
### Production Pattern
```yaml
server:
compression:
enabled: true
min-response-size: 2KB
mime-types:
- application/json
- application/xml
- application/javascript
- text/html
- text/xml
- text/plain
- text/css
- text/javascript
- application/graphql+json
```
### Important Note
Compression is mutually exclusive with `sendfile`. When compression is enabled, files larger than 48KB may be sent uncompressed if sendfile is also enabled.
---
## 7. Request Size Limits
### Why It Matters
Proper limits prevent denial-of-service attacks and resource exhaustion from oversized requests.
### Configuration
```yaml
server:
tomcat:
max-http-form-post-size: 2MB # Form post body limit
max-swallow-size: 2MB # Max aborted upload data to swallow
max-http-response-header-size: 8KB # Response header size
max-http-request-header-size: 8KB # Request header size
spring:
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 50MB
file-size-threshold: 2KB # Write to disk after this threshold
```
### Production Pattern: File Upload Service
```yaml
server:
tomcat:
max-http-form-post-size: 200MB
max-swallow-size: 200MB
max-parameter-count: 10000
max-part-count: 100
max-part-header-size: 8KB
spring:
servlet:
multipart:
enabled: true
max-file-size: 100MB
max-request-size: 200MB
file-size-threshold: 10MB
location: /tmp/uploads # Temp directory for large uploads
```
### Jetty Configuration
```yaml
server:
jetty:
max-http-form-post-size: 200000B
max-http-response-header-size: 16KB
max-form-keys: 1000
```
---
## 8. Access Logging
### Why It Matters
Access logs are essential for debugging, security auditing, and performance analysis.
### Tomcat Access Log
```yaml
server:
tomcat:
basedir: /var/log/tomcat
accesslog:
enabled: true
directory: logs
prefix: access_log
suffix: .log
file-date-format: .yyyy-MM-dd
pattern: '%h %l %u %t "%r" %s %b %D'
rotate: true
max-days: 30
buffered: true
```
### Access Log Patterns
| Pattern | Description |
|---------|-------------|
| `%h` | Remote host |
| `%l` | Remote logical username |
| `%u` | Remote user |
| `%t` | Date/time of request |
| `%r` | First line of request |
| `%s` | HTTP status code |
| `%b` | Bytes sent |
| `%D` | Time to process (ms) |
| `%T` | Time to process (s) |
| `%I` | Current thread name |
### Production Pattern: JSON Access Logs
```yaml
server:
tomcat:
accesslog:
enabled: true
pattern: '{"timestamp":"%{yyyy-MM-dd HH:mm:ss.SSS}t","client":"%h","method":"%m","uri":"%U","query":"%q","status":%s,"bytes":%b,"duration":%D,"userAgent":"%{User-Agent}i"}'
```
### Jetty Access Log
```yaml
server:
jetty:
accesslog:
enabled: true
filename: /var/log/jetty/access.log
```
---
## 9. Health Checks & Monitoring
### Why It Matters
Health checks enable orchestrators to manage application lifecycle and route traffic appropriately.
### Basic Configuration
```yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when-authorized
show-components: always
probes:
enabled: true
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
```
### Health Groups
```yaml
management:
endpoint:
health:
group:
readiness:
include: db,redis,kafka
show-details: always
liveness:
include: ping,diskSpace
show-details: always
custom:
include: db,redis
show-components: always
status:
http-mapping:
down: 503
out-of-service: 503
```
### Custom Health Indicator
```java
@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
private final ExternalServiceClient client;
@Override
public Health health() {
try {
boolean isHealthy = client.healthCheck();
if (isHealthy) {
return Health.up()
.withDetail("service", "external-api")
.withDetail("status", "reachable")
.build();
}
return Health.down()
.withDetail("service", "external-api")
.withDetail("status", "unreachable")
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
```
### Reactive Health Indicator
```java
@Component
public class AsyncServiceHealthIndicator implements ReactiveHealthIndicator {
@Override
public Mono<Health> health() {
return checkServiceHealth()
.map(status -> Health.up()
.withDetail("latency", status.getLatency())
.build())
.onErrorResume(ex -> Mono.just(
Health.down(ex).build()));
}
}
```
---
## 10. Metrics & Observability
### Why It Matters
Metrics provide visibility into application performance and enable proactive issue detection.
### Micrometer Configuration
```yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
environment: ${ENVIRONMENT:development}
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 50ms,100ms,200ms,500ms,1s
```
### Tomcat Metrics
```yaml
server:
tomcat:
mbeanregistry:
enabled: true # Enable Tomcat MBeans for Micrometer
```
### Prometheus Integration
```yaml
management:
prometheus:
metrics:
export:
enabled: true
descriptions: true
step: 30s
```
### Custom Metrics
```java
@Component
public class BusinessMetrics {
private final Counter ordersCounter;
private final Timer orderProcessingTimer;
private final Gauge activeUsersGauge;
public BusinessMetrics(MeterRegistry registry) {
this.ordersCounter = Counter.builder("orders.placed")
.description("Number of orders placed")
.tag("type", "total")
.register(registry);
this.orderProcessingTimer = Timer.builder("orders.processing.time")
.description("Time to process orders")
.publishPercentileHistogram()
.register(registry);
this.activeUsersGauge = Gauge.builder("users.active", this::getActiveUserCount)
.description("Number of active users")
.register(registry);
}
public void recordOrder() {
ordersCounter.increment();
}
public void recordProcessingTime(Duration duration) {
orderProcessingTimer.record(duration);
}
}
```
---
## 11. Proxy & Load Balancer Configuration
### Why It Matters
Correct proxy configuration ensures accurate client information and proper URL generation.
### Forward Headers Strategy
```yaml
server:
forward-headers-strategy: native # Let server handle X-Forwarded-* headers
# Alternative: framework (Spring handles headers)
```
### Tomcat Proxy Configuration
```yaml
server:
tomcat:
remoteip:
remote-ip-header: X-Forwarded-For
protocol-header: X-Forwarded-Proto
protocol-header-https-value: https
internal-proxies: 192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3}
```
### Production Pattern: Behind AWS ALB
```yaml
server:
forward-headers-strategy: native
tomcat:
remoteip:
remote-ip-header: X-Forwarded-For
protocol-header: X-Forwarded-Proto
internal-proxies: >
10\.\d{1,3}\.\d{1,3}\.\d{1,3}|
192\.168\.\d{1,3}\.\d{1,3}|
169\.254\.\d{1,3}\.\d{1,3}|
127\.\d{1,3}\.\d{1,3}\.\d{1,3}
```
### Production Pattern: Behind Nginx
```yaml
server:
forward-headers-strategy: native
tomcat:
remoteip:
remote-ip-header: X-Real-IP
protocol-header: X-Forwarded-Proto
```
---
## 12. Session Management