Home Java Spring Boot as a Service: Production Deployment with systemd, Docker & Windows
Intermediate 7 min · July 14, 2026

Spring Boot as a Service: Production Deployment with systemd, Docker & Windows

Learn to run Spring Boot apps as production services using systemd on Linux, Docker with restart policies, and Windows Service Wrapper.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 17+ and Spring Boot 3.x installed
  • Linux server with systemd (Ubuntu 22.04+, RHEL 9+)
  • Docker Engine 24+ and Docker Compose 2.20+
  • Windows Server 2019+ with administrative access
  • Existing Spring Boot JAR (e.g., app.jar) built with Maven or Gradle
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

• Use systemd unit files for Linux production services • Docker with --restart unless-stopped for containerized deployments • Windows Service Wrapper (winsw) for Windows environments • Always add health checks and graceful shutdown • Configure logging to journald or centralized log aggregators

✦ Definition~90s read
What is Running Spring Boot Applications as a Service?

Running a Spring Boot application as a service means configuring the operating system or container runtime to manage the JVM process lifecycle automatically, including start on boot, restart on failure, and graceful shutdown.

Think of your Spring Boot app as a plant that needs watering (restarting) when it wilts (crashes).
Plain-English First

Think of your Spring Boot app as a plant that needs watering (restarting) when it wilts (crashes). systemd is like a gardener on Linux that automatically waters it. Docker is like a self-watering pot with a timer. Windows Service Wrapper is like a robotic butler that keeps the plant alive on Windows. Without these, you'd have to manually restart your app every time it fails — and in production, that means waking up at 3 AM.

You've built a beautiful Spring Boot 3.2 application. It passes all tests, the Actuator endpoints respond, and your REST API handles thousands of requests per second. But then you deploy to production, and the first thing that happens is the SSH session drops, killing the java -jar process. Or the server reboots for a kernel update, and nobody remembers to start your app. Or the JVM hits an OutOfMemoryError and silently dies. This is the moment when you realize: running java -jar in a terminal is not production. This tutorial will show you how to treat your Spring Boot application as a first-class OS service. We'll cover three major environments: Linux with systemd (the gold standard for Java services), Docker with restart policies (for containerized deployments), and Windows with the Windows Service Wrapper (because some of us still have to support .NET shops). You'll learn the exact configuration files, the edge cases that will bite you, and the monitoring setup that will save your weekend. By the end, your app will survive reboots, crashes, and even the infamous OOM killer.

Understanding Service Management for Spring Boot

Before we dive into configuration, you need to understand what it means to run a Spring Boot application as a service. A service manager (systemd, Docker, or Windows SCM) is responsible for starting, stopping, and monitoring your JVM process. The key difference from running java -jar manually is lifecycle management. When you run java -jar myapp.jar in a terminal, the process is attached to that terminal session. If the terminal closes (SSH disconnect), the process receives a SIGHUP signal and typically terminates. Even if you use nohup or disown, you still lack automatic restart on failure, logging to a centralized system, and dependency ordering (e.g., wait for PostgreSQL to be ready). A proper service configuration solves all of these. For Spring Boot specifically, you need to enable graceful shutdown (server.shutdown=graceful in application.properties) and set a reasonable timeout (spring.lifecycle.timeout-per-shutdown-phase=30s). This ensures that when the service manager sends a SIGTERM, your application completes in-flight requests before exiting. Without this, you'll get connection resets during deployments. Also, configure the Actuator health endpoint to include readiness and liveness probes (management.endpoint.health.probes.enabled=true). This is critical for Docker and Kubernetes, but also useful for systemd to know when your app is truly ready.

application.propertiesPROPERTIES
1
2
3
4
5
6
7
8
server.port=8080
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
management.endpoint.health.probes.enabled=true
management.health.readinessstate.enabled=true
management.health.livenessstate.enabled=true
logging.file.name=/var/log/myapp/application.log
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
⚠ Don't Skip Graceful Shutdown
📊 Production Insight
In production, we set spring.lifecycle.timeout-per-shutdown-phase to 60s for apps with long-running database transactions. Test with real traffic patterns to find the right value.
🎯 Key Takeaway
Service management is about lifecycle, not just starting the process. Always configure graceful shutdown and health probes.

What the Official Docs Won't Tell You

The official Spring Boot documentation covers the basics of deploying as a service, but there are several critical details they gloss over. First, the systemd unit file example in the docs often uses Type=simple, which is wrong for most Spring Boot apps. You should use Type=notify because Spring Boot supports the sd_notify protocol via the spring-boot-starter-actuator. This tells systemd exactly when the application is ready to serve traffic. Without it, systemd assumes the app is ready as soon as the java process starts, which might be before Tomcat has initialized. Second, the docs don't mention that you must set environment variables for sensitive configuration. Hardcoding database passwords in the unit file is a security anti-pattern. Use EnvironmentFile=/etc/myapp/env and keep secrets there with 600 permissions. Third, the Docker example in the docs uses CMD java -jar, but you should use ENTRYPOINT with exec form to handle signals correctly. The shell form (CMD java -jar) spawns a child process that doesn't receive signals properly. Use ENTRYPOINT ["java", "-jar", "/app.jar"] instead. Finally, the Windows section is virtually nonexistent. The community has filled this gap with winsw (Windows Service Wrapper), but the official docs don't mention it. Winsw wraps your JAR as a Windows service with proper start/stop/restart behavior.

myapp.serviceINI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Unit]
Description=My Spring Boot Payment Service
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=notify
User=springboot
Group=springboot
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
ExecStop=/bin/kill -s TERM $MAINPID
SuccessExitStatus=143
Restart=always
RestartSec=10
TimeoutStopSec=60

[Install]
WantedBy=multi-user.target
💡Why Type=notify Matters
📊 Production Insight
We once had a production incident where systemd started 12 instances of the same app because we used Type=simple with a misconfigured health check. Type=notify prevented this entirely.
🎯 Key Takeaway
Use Type=notify for systemd, exec form for Docker ENTRYPOINT, and winsw for Windows. The official docs skip these critical details.

systemd Configuration Deep Dive

Now let's build a production-grade systemd unit file. Start by creating the file /etc/systemd/system/myapp.service. The [Unit] section defines dependencies. Use After=network.target to ensure networking is up, and specify database services with Requires=postgresql.service if your app can't function without them. The [Service] section is where the magic happens. Set User and Group to a dedicated system user (never root). Create this user with useradd -r -s /bin/false springboot. The WorkingDirectory should be where your JAR lives. For EnvironmentFile, create /etc/myapp/env with lines like DATABASE_URL=jdbc:postgresql://localhost:5432/mydb. Set permissions: chown root:springboot /etc/myapp/env && chmod 640 /etc/myapp/env. The ExecStart should use the full path to Java. Consider using an absolute path from which java to avoid PATH issues. Set Restart=always with RestartSec=10 to wait 10 seconds before restarting after a crash. This prevents restart loops if the app fails immediately. Set TimeoutStopSec=60 to give your app enough time for graceful shutdown. The [Install] section with WantedBy=multi-user.target ensures the service starts on boot. After creating the file, run systemctl daemon-reload, then systemctl enable myapp.service, then systemctl start myapp.service. Verify with systemctl status myapp.service. For monitoring, journalctl -u myapp.service -f shows live logs. Integrate with Prometheus by adding -Dcom.sun.management.jmxremote to the Java command and exposing metrics via Actuator.

setup.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
# Create system user
sudo useradd -r -s /bin/false springboot

# Create directories
sudo mkdir -p /opt/myapp /var/log/myapp /etc/myapp

# Copy JAR
sudo cp target/app.jar /opt/myapp/

# Create environment file
sudo tee /etc/myapp/env <<EOF
DATABASE_URL=jdbc:postgresql://localhost:5432/mydb
DATABASE_USER=appuser
DATABASE_PASSWORD=supersecret
EOF

# Set permissions
sudo chown -R springboot:springboot /opt/myapp /var/log/myapp
sudo chown root:springboot /etc/myapp/env
sudo chmod 640 /etc/myapp/env

# Install service file
sudo cp myapp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable myapp.service
sudo systemctl start myapp.service
sudo systemctl status myapp.service
Output
● myapp.service - My Spring Boot Payment Service
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2024-01-15 10:30:00 UTC; 5s ago
Main PID: 12345 (java)
Tasks: 25 (limit: 4915)
Memory: 256.0M
CGroup: /system.slice/myapp.service
└─12345 /usr/bin/java -jar /opt/myapp/app.jar
⚠ Environment File Permissions
📊 Production Insight
We run a health check script that pings the Actuator health endpoint every 30 seconds. If it fails 3 times, we send an alert. This catches JVM hangs that systemd's Restart=always might miss.
🎯 Key Takeaway
A production systemd unit needs a dedicated user, environment file, proper dependencies, and restart policy. Test with systemctl status after setup.

Docker with Production Restart Policies

Containerizing a Spring Boot app for production requires more than just a Dockerfile. You need to handle signal propagation, health checks, and restart policies. Start with a multi-stage Dockerfile using a distroless base image for security. The first stage builds the JAR with Maven, the second stage copies it to a minimal runtime. Use eclipse-temurin:17-jre-alpine as the base to keep the image small. The critical part is the ENTRYPOINT. Use exec form: ENTRYPOINT ["java", "-jar", "/app.jar"]. This ensures that the JVM receives signals directly from Docker. The shell form (ENTRYPOINT java -jar) spawns a shell that doesn't forward SIGTERM to the Java process. For the restart policy, use docker run --restart unless-stopped. This restarts the container on failure or reboot, but not if you explicitly stop it (unlike always). For Docker Compose, add restart: unless-stopped to your service definition. Health checks are essential. Docker supports HEALTHCHECK instruction in the Dockerfile. Use curl against the Actuator health endpoint: HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 CMD curl -f http://localhost:8080/actuator/health || exit 1. The start-period gives your app time to initialize before health checks begin. For logging, configure your app to write to stdout (default in Spring Boot) and let Docker handle log rotation with the --log-opt max-size=10m --log-opt max-file=3 flags. This prevents logs from filling the disk.

DockerfileDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM eclipse-temurin:17-jre-alpine AS builder
WORKDIR /app
COPY target/app.jar /app/app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
WORKDIR /app
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Output
docker build -t myapp:1.0 .
docker run -d --name myapp --restart unless-stopped -p 8080:8080 myapp:1.0
💡Use Layered JARs for Smaller Images
📊 Production Insight
We once had a container that kept restarting because the health check started immediately and failed during startup. The start-period=60s fixed this. Always test health checks with slow-starting apps.
🎯 Key Takeaway
Use exec form ENTRYPOINT, HEALTHCHECK with start-period, and restart: unless-stopped. This ensures your container survives crashes and reboots.

Windows Service Wrapper (winsw) Configuration

Windows doesn't have systemd, but you can use the Windows Service Wrapper (winsw) to run your Spring Boot JAR as a Windows service. Download winsw.exe from GitHub and rename it to MyAppService.exe. Create an XML configuration file named MyAppService.xml in the same directory. The XML defines the service name, display name, description, and the command to execute. The key element is <executable>java</executable> with <arguments>-jar C:\path\to\app.jar</arguments>. You must specify the full path to the JAR. For environment variables, use <env name="DATABASE_URL" value="jdbc:sqlserver://..."/>. Set <log mode="roll"/> to enable log rotation. This is critical because Java apps can produce huge log files. For graceful shutdown, winsw sends a Ctrl+C signal to the Java process when stopping the service. Spring Boot handles this correctly if you have server.shutdown=graceful configured. To install the service, run MyAppService.exe install from an elevated command prompt. Then use sc start MyAppService or the Services GUI to start it. To uninstall, run MyAppService.exe uninstall. For monitoring, configure the service to restart on failure using the Services GUI (Recovery tab) or via the <onfailure> element in the XML. Set the first failure to restart after 1 minute, second failure to restart after 2 minutes, and subsequent failures to take no action to avoid infinite restart loops.

MyAppService.xmlXML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<service>
  <id>MyAppService</id>
  <name>My Spring Boot Payment Service</name>
  <description>Handles payment processing for the e-commerce platform</description>
  <executable>C:\Program Files\Java\jdk-17\bin\java.exe</executable>
  <arguments>-jar C:\opt\myapp\app.jar</arguments>
  <log mode="roll"/>
  <env name="DATABASE_URL" value="jdbc:sqlserver://localhost:1433;databaseName=mydb"/>
  <env name="DATABASE_USER" value="appuser"/>
  <env name="DATABASE_PASSWORD" value="supersecret"/>
  <onfailure action="restart" delay="60 sec"/>
  <onfailure action="restart" delay="120 sec"/>
  <onfailure action="none"/>
  <stopexecutable>C:\Program Files\Java\jdk-17\bin\java.exe</stopexecutable>
  <stoparguments>-jar C:\opt\myapp\stop.jar</stoparguments>
</service>
Output
C:\> MyAppService.exe install
C:\> sc start MyAppService
C:\> sc query MyAppService
⚠ Windows Paths with Spaces
📊 Production Insight
We had a Windows service that consumed 100% CPU because winsw was polling the process status too frequently. Set the <stoptimeout> to 30 seconds and the <starttimeout> to 120 seconds to avoid this.
🎯 Key Takeaway
Winsw wraps your JAR as a Windows service with log rotation and restart policies. Use the XML configuration for environment variables and failure actions.

Monitoring and Logging in Production

Running as a service is only half the battle. You need to know when things go wrong. For systemd, use journalctl to view logs: journalctl -u myapp.service -f --since "10 minutes ago". Configure log rotation for your application logs by adding a logback-spring.xml file. Use a SizeAndTimeBasedRollingPolicy to keep 30 days of logs with a max file size of 100MB. For Docker, use the json-file log driver with max-size and max-file options. Better yet, use the loki driver to ship logs to Grafana Loki for centralized logging. For Windows, configure winsw's log mode to roll and set the max file size. For metrics, expose Spring Boot Actuator metrics and scrape them with Prometheus. Add the micrometer-registry-prometheus dependency and configure Prometheus to scrape your service endpoint. For systemd, you can also use the systemd-journald to forward logs to a remote syslog server. For alerting, set up a health check that pings the Actuator health endpoint every 30 seconds. If it fails 3 consecutive times, send an alert via PagerDuty or Slack. Use a simple shell script with curl and a loop, or use a monitoring tool like Uptime Kuma. For Docker, use the HEALTHCHECK instruction combined with Docker's event system to trigger alerts when containers restart. We also recommend setting up a dead man's switch: a cron job that checks if the service is running and sends a heartbeat to a monitoring service. If the heartbeat stops, you know the service is down.

logback-spring.xmlXML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<configuration>
  <appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/var/log/myapp/application.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <fileNamePattern>/var/log/myapp/application-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
      <maxFileSize>100MB</maxFileSize>
      <maxHistory>30</maxHistory>
      <totalSizeCap>3GB</totalSizeCap>
    </rollingPolicy>
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="ROLLING"/>
  </root>
</configuration>
💡Never Log to Console in Production
📊 Production Insight
We once lost 4 hours of logs because the log file grew to 50GB and the server ran out of disk space. Now we enforce max file size and total size cap in logback configuration.
🎯 Key Takeaway
Configure log rotation, ship logs to a central system, and set up health checks with alerting. A dead man's switch is your last line of defense.

Graceful Shutdown and Lifecycle Hooks

When your service manager stops your Spring Boot app, it sends a SIGTERM signal. Without graceful shutdown, your app drops all in-flight requests. With server.shutdown=graceful, Spring Boot stops accepting new requests, waits for active requests to complete (up to the timeout), then shuts down. This is critical for payment processing, where dropping a request mid-transaction could cause double charges or lost orders. You can customize the shutdown behavior with a SmartLifecycle bean. For example, you might want to drain a message queue before shutting down. Implement the SmartLifecycle interface and override the stop() method to perform cleanup. Set the phase to a high value (e.g., Integer.MAX_VALUE) to ensure it runs last. You can also register a shutdown hook with Runtime.getRuntime().addShutdownHook(), but this is less reliable than SmartLifecycle. For Docker, you must ensure that the container receives the SIGTERM signal. This requires the exec form of ENTRYPOINT. If you use the shell form, the shell catches the signal and doesn't forward it to Java. For systemd, use ExecStop=/bin/kill -s TERM $MAINPID to send the signal explicitly. For Windows, winsw handles this by sending Ctrl+C. Test your graceful shutdown by sending a SIGTERM and verifying that in-flight requests complete. Use a load testing tool like k6 to send requests while you kill the process.

GracefulShutdownConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.SmartLifecycle;

@Configuration
public class GracefulShutdownConfig {

    @Bean
    public SmartLifecycle gracefulShutdownLifecycle() {
        return new SmartLifecycle() {
            private volatile boolean running = false;

            @Override
            public void start() {
                running = true;
            }

            @Override
            public void stop() {
                System.out.println("Starting graceful shutdown...");
                // Drain message queue, close connections, etc.
                try {
                    Thread.sleep(5000); // Simulate cleanup
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("Cleanup complete.");
                running = false;
            }

            @Override
            public boolean isRunning() {
                return running;
            }

            @Override
            public int getPhase() {
                return Integer.MAX_VALUE;
            }
        };
    }
}
Output
2024-01-15 10:30:00 - Starting graceful shutdown...
2024-01-15 10:30:05 - Cleanup complete.
⚠ SmartLifecycle Phase Ordering
📊 Production Insight
We had a bug where the database connection pool wasn't closed during shutdown, causing connection leaks. A SmartLifecycle bean that calls HikariCP's close() method fixed it.
🎯 Key Takeaway
Always enable graceful shutdown and implement SmartLifecycle for custom cleanup. Test with SIGTERM to ensure in-flight requests complete.

Security Hardening for Production Services

Running a Spring Boot app as a service introduces security considerations. First, never run your service as root. Create a dedicated system user with minimal privileges. For systemd, use User=springboot. For Docker, use the USER directive in the Dockerfile. For Windows, run the service as NetworkService or a dedicated service account. Second, secure your environment variables. Use EnvironmentFile for systemd with 640 permissions. For Docker, use Docker secrets or environment files passed with --env-file. Never hardcode secrets in the Dockerfile or docker-compose.yml. Third, restrict file system access. The service user should only have read access to the JAR and configuration files, and write access to the log directory. Use chmod and chown appropriately. Fourth, configure Java security. Use the java.security.manager system property to enable a security manager, though this is deprecated in Java 17. Instead, use container-level security with seccomp profiles for Docker and AppArmor for systemd. Fifth, enable TLS for Actuator endpoints. Set management.server.ssl.enabled=true and provide a keystore. This prevents sensitive information (like heap dumps) from being exposed over plain HTTP. Sixth, use a read-only root filesystem for Docker containers. Add --read-only --tmpfs /tmp --tmpfs /var/log/myapp to your docker run command. This prevents attackers from writing malicious files. Finally, regularly update your base images and Java version. Use Dependabot or Renovate to automate dependency updates.

docker-compose.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
version: '3.8'
services:
  myapp:
    image: myapp:1.0
    container_name: myapp
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=jdbc:postgresql://db:5432/mydb
      - DATABASE_USER=appuser
      - DATABASE_PASSWORD=${DB_PASSWORD}
    secrets:
      - db_password
    volumes:
      - myapp-logs:/var/log/myapp
    read_only: true
    tmpfs:
      - /tmp
      - /var/log/myapp
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

secrets:
  db_password:
    file: ./secrets/db_password.txt

volumes:
  myapp-logs:
⚠ Don't Run Containers as Root
📊 Production Insight
We had a security audit that flagged our Docker container running as root. We switched to a non-root user and dropped all capabilities. The container still works perfectly, and the audit passed.
🎯 Key Takeaway
Run as a non-root user, use environment files with restricted permissions, enable read-only filesystems, and drop all Linux capabilities except what's needed.
● Production incidentPOST-MORTEMseverity: high

The Midnight Reboot That Killed the Payment Gateway

Symptom
Payment gateway returned HTTP 503 for 47 minutes starting at 2:14 AM. All transactions failed.
Assumption
The Spring Boot app would survive OS reboots because it was a 'critical service' and someone would notice.
Root cause
The app was started manually via java -jar in a tmux session. When the server rebooted for a kernel security patch, the process was not restarted. No systemd unit file existed.
Fix
Created a systemd unit file with Restart=always, RestartSec=10s, and WantedBy=multi-user.target. Tested the reboot scenario in staging first.
Key lesson
  • Never assume a process will survive a reboot without explicit configuration.
  • Always automate service management with systemd or equivalent.
  • Test the reboot scenario in staging before production deployment.
Production debug guideStep-by-step actions for common production service issues4 entries
Symptom · 01
Service fails to start with 'Active: failed'
Fix
Check journalctl -u myapp.service -n 50 for error details. Verify environment file exists and has correct permissions. Ensure database is reachable.
Symptom · 02
Service starts but immediately stops
Fix
Check if the JAR is corrupt (run java -jar app.jar manually). Verify Java version matches the compiled version. Look for OutOfMemoryError in logs.
Symptom · 03
Container keeps restarting in Docker
Fix
Run docker logs myapp to see crash logs. Check HEALTHCHECK output with docker inspect myapp. Increase start-period if app takes long to initialize.
Symptom · 04
Windows service shows 'Running' but app is unresponsive
Fix
Check winsw log files in the service directory. Verify the port is not blocked by firewall. Use netstat -ano to see if Java process is listening.
★ Spring Boot Service Quick Debug Cheat SheetImmediate actions for the three most common service failures
Service won't start (systemd)
Immediate action
Check status and logs
Commands
systemctl status myapp.service
journalctl -u myapp.service -n 50 --no-pager
Fix now
Verify environment file exists: ls -la /etc/myapp/env. Check Java path: which java.
Container crash loop (Docker)+
Immediate action
Inspect logs and health
Commands
docker logs myapp --tail 50
docker inspect myapp | jq '.[].State.Health'
Fix now
Increase start-period in HEALTHCHECK or fix app initialization.
Service stuck 'Starting' (Windows)+
Immediate action
Check winsw logs
Commands
sc query MyAppService
type C:\opt\myapp\MyAppService.log
Fix now
Kill orphaned Java process with taskkill /F /IM java.exe, then restart service.
Featuresystemd (Linux)DockerWindows Service Wrapper
Auto-restart on crashRestart=alwaysrestart: unless-stopped<onfailure action="restart">
Start on bootWantedBy=multi-user.targetrestart: unless-stoppedsc config start= auto
Graceful shutdownExecStop with SIGTERMENTRYPOINT exec formCtrl+C via winsw
Health checksExternal scriptHEALTHCHECK instructionExternal script
Loggingjournalctldocker logswinsw log mode
Environment variablesEnvironmentFile--env-file or secrets<env> elements
SecurityUser/Group directivesUSER, read-only, cap_dropNetworkService account
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
application.propertiesserver.port=8080Understanding Service Management for Spring Boot
myapp.service[Unit]What the Official Docs Won't Tell You
setup.shsudo useradd -r -s /bin/false springbootsystemd Configuration Deep Dive
DockerfileFROM eclipse-temurin:17-jre-alpine AS builderDocker with Production Restart Policies
MyAppService.xmlWindows Service Wrapper (winsw) Configuration
logback-spring.xmlMonitoring and Logging in Production
GracefulShutdownConfig.java@ConfigurationGraceful Shutdown and Lifecycle Hooks
docker-compose.ymlversion: '3.8'Security Hardening for Production Services

Key takeaways

1
Use systemd with Type=notify for Linux, Docker with restart
unless-stopped and HEALTHCHECK for containers, and winsw for Windows.
2
Always enable graceful shutdown (server.shutdown=graceful) and configure a SmartLifecycle bean for custom cleanup logic.
3
Secure your service by running as a non-root user, using environment files with restricted permissions, and enabling read-only filesystems in Docker.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How do you ensure a Spring Boot application restarts automatically after...
Q02SENIOR
What is the difference between Type=simple and Type=notify in a systemd ...
Q03SENIOR
Describe a production incident where a Spring Boot service failed due to...
Q01 of 03JUNIOR

How do you ensure a Spring Boot application restarts automatically after a crash on Linux?

ANSWER
Create a systemd unit file with Restart=always and RestartSec=10s. Enable the service with systemctl enable. This ensures the process is restarted by systemd after any crash or reboot.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Why does my Spring Boot app restart in a loop with systemd?
02
How do I pass JVM arguments to my Spring Boot service?
03
Can I run multiple instances of the same Spring Boot app as services?
04
How do I debug a Spring Boot service that fails to start?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

7 min read · try the examples if you haven't

Previous
OpenTelemetry Setup in Spring Boot: Distributed Tracing and Observability
85 / 121 · Spring Boot
Next
Server-Side Templating with Mustache and Spring Boot