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.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓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
• 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
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.
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.
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.
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.
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.
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.
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.
close() method fixed it.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.
The Midnight Reboot That Killed the Payment Gateway
- 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.
systemctl status myapp.servicejournalctl -u myapp.service -n 50 --no-pager| File | Command / Code | Purpose |
|---|---|---|
| application.properties | server.port=8080 | Understanding Service Management for Spring Boot |
| myapp.service | [Unit] | What the Official Docs Won't Tell You |
| setup.sh | sudo useradd -r -s /bin/false springboot | systemd Configuration Deep Dive |
| Dockerfile | FROM eclipse-temurin:17-jre-alpine AS builder | Docker with Production Restart Policies |
| MyAppService.xml | Windows Service Wrapper (winsw) Configuration | |
| logback-spring.xml | Monitoring and Logging in Production | |
| GracefulShutdownConfig.java | @Configuration | Graceful Shutdown and Lifecycle Hooks |
| docker-compose.yml | version: '3.8' | Security Hardening for Production Services |
Key takeaways
Interview Questions on This Topic
How do you ensure a Spring Boot application restarts automatically after a crash on Linux?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Boot. Mark it forged?
7 min read · try the examples if you haven't