A few days ago I noticed something odd in my Spring Boot app’s logs: a fresh “Initializing Spring embedded WebApplicationContext” line, out of nowhere, in the middle of otherwise normal operation. No stack trace. No OutOfMemoryError. No exception at all — just the app quietly starting up again, as if nothing had happened.
That’s the annoying kind of bug. When the app itself logs an error, you at least know where to look. When it logs nothing, you’re debugging a crime scene with no witnesses.
Here’s how I tracked it down.
Step 1: Ask systemd what it saw
Since the app is run as a systemd service (my-app.service), the first move was to check systemd’s own record of what happened:
systemctl status my-app.service
This showed the service as healthy and running, with an uptime that lined up with the restart — not hugely informative on its own, but it confirmed the service was in fact bounced, not just logging something weird.
Step 2: Pull the journal around the exact restart time
Application logs only tell you what the app chose to log. Systemd’s journal tells you what actually happened to the process — stops, starts, exit codes, signals.
journalctl -u my-app.service --since "<restart-time-minus-2min>" --until "<restart-time-plus-2min>"
e.g.:
journalctl -u my-app.service --since "2026-08-11 15:27:13 UTC" --until "2026-08-11 15:30:13 UTC" -o cat | cat
This is where the trail actually started. The output showed:
systemd[1]: Stopping my-app.service...
systemd[1]: my-app.service: Deactivated successfully.
systemd[1]: Stopped my-app.service.
systemd[1]: my-app.service: Consumed 20min 58s CPU time, 1.8G memory peak, 0B memory swap peak.
systemd[1]: Started my-app.service.
Two things stood out immediately:
- “Deactivated successfully” — this was not a crash. A crash shows up as a nonzero exit code or a kill signal. This was a clean stop, meaning something intentionally told the service to stop.
- 1.8G memory peak — worth noting for later, since my JVM heap was capped at
-Xmx768m. The total footprint (heap + metaspace + thread stacks + native buffers) had crept well past that. Not the cause of this particular incident, but a red flag worth fixing separately.
Step 3: Widen the net — what else happened at that exact second?
A clean stop means something else on the system triggered it. So instead of filtering to just my service, I pulled the entire system journal for that window:
journalctl --since "<restart-time-minus-2min>" --until "<restart-time-plus-2min>"
This was the payoff. The full log showed a very telling sequence, all within the same second:
systemd[1]: Starting apt-daily-upgrade.service...
apt.systemd.daily: [unattended-upgrade running]
systemd[1]: Stopping other-service-A.service...
systemd[1]: Stopping other-service-B.service...
systemd[1]: Stopping my-app.service...
systemd[1]: other-service-A.service: Deactivated successfully.
systemd[1]: Started other-service-A.service.
systemd[1]: other-service-B.service: Deactivated successfully.
systemd[1]: Started other-service-B.service.
systemd[1]: my-app.service: Deactivated successfully.
systemd[1]: Started my-app.service.
Three completely unrelated services — all running on the JVM, all otherwise unconnected to each other — restarted within the same second, immediately after an unattended package upgrade. That’s not a coincidence; that’s a pattern.
Step 4: Confirm the suspect — needrestart
That pattern is the signature of needrestart, a tool that ships with Ubuntu’s unattended-upgrades setup. Its job: after a security patch updates a shared library (things like libssl, libtinfo, libnghttp2), it scans running processes to see which ones are still linked against the old version of that library still sitting in memory, and restarts them so they pick up the patched version.
A quick check of the unattended-upgrades log confirmed the timing lined up exactly with a batch of library upgrades:
cat /var/log/unattended-upgrades/unattended-upgrades.log
INFO Packages that will be upgraded: libncurses6 libnghttp2-14 libtinfo6 ...
INFO All upgrades installed
And checking needrestart’s config confirmed it runs in fully automatic mode when triggered by apt:
cat /etc/needrestart/needrestart.conf | grep -i restart
# UBUNTU: the default restart mode when running as part of the APT hook is 'a',
'a' = automatic. No prompt, no notification, no log line in my app’s own output — it just quietly restarts anything it flags, which is exactly why the only trace in the application log was a fresh startup banner.
Mystery solved: my three JVM-based services all got swept up in a routine security patch restart, triggered by needrestart after unattended-upgrades patched some shared libraries.
Takeaways
- A clean restart with no app-level error is a strong signal to look outside the application — systemd, the kernel, or another process is a more likely culprit than your own code.
journalctl -u <service>narrows the view; a plainjournalctl(no unit filter) widens it. The second one was the key here — it revealed the surrounding system activity that the per-service view couldn’t show.- If multiple unrelated services restart at the exact same moment, look for a shared dependency, not a shared bug. In this case the shared thread was “runs on the JVM,” and the trigger was a routine library patch.
- Automatic security patching is generally a good thing. Once I understood the cause, the fix wasn’t to disable it — it was simply to know that this kind of brief, clean restart is expected behavior, not an incident.
Debugging checklist for “my service restarted and there’s no error”
systemctl status <service>— is it actually a clean stop or a crash/kill?journalctl -u <service> --since ... --until ...— narrow to the exact windowjournalctl --since ... --until ...(no unit filter) — widen to see the whole system at that momentdmesg -T | grep -i oomandjournalctl -k | grep -i oom— rule out the OOM killersystemctl show <service> -p MemoryMax— rule out a cgroup memory limit- Check
/var/log/unattended-upgrades/andneedrestartconfig if the timing lines up with routine patching
Sometimes the most reassuring outcome of a debugging session isn’t finding a bug — it’s finding out there wasn’t one.