Capstone — Building a Real Multi-Unit Service Stack

systemd in Depth

Chapter 11 · Capstone — Building a Real Multi-Unit Service Stack

The final chapter. A real small app — notesapp — deployed as a properly dependent, resource-limited service with a companion maintenance timer, logged and verified using every tool this course has built. This is where ws1's and ansible1-10's own unexplained systemctl/service calls finally get their full answer.

The Target — A Real Small App: notesapp

A small notes-taking web service: a long-lived process, needs the network genuinely up before starting, running on a modest, shared VPS where resource limits matter, and needing a periodic cleanup job to purge notes older than 90 days.

Step 1 — The Service Unit

# notesapp.service [Unit] Description=Notes App Service After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/notesapp Restart=on-failure RestartSec=5 User=notesapp [Install] WantedBy=multi-user.target

Note the paired Wants= and After= for network-online.targetsystemd1-4's own central lesson, applied for real: After= alone would never guarantee networking is actually started; the pair together genuinely guarantees both that it starts and that it starts first.

Step 2 — Resource Limits

CPUQuota=50% MemoryMax=512M MemoryHigh=384M

These values were chosen only after observing notesapp's own real memory and CPU usage via systemd-cgtop over a representative period — systemd1-9's own warn-box, applied honestly here rather than picking arbitrary numbers.

Step 3 — Applying and Verifying

sudo systemctl daemon-reload sudo systemctl enable --now notesapp systemctl status notesapp systemctl show notesapp -p MemoryMax

daemon-reload is not optional (systemd1-3's own gotcha); the final command confirms the resource limit actually took effect, exactly systemd1-9's own verification step.

Step 4 — The Companion Maintenance Timer

# notesapp-cleanup.service [Unit] Description=Purge notes older than 90 days [Service] Type=oneshot ExecStart=/usr/local/bin/notesapp-cleanup
# notesapp-cleanup.timer [Unit] Description=Run notesapp-cleanup.service daily [Timer] OnCalendar=daily Persistent=true [Install] WantedBy=timers.target

systemd1-7's own two-unit pattern, applied for real — the cleanup logic is a genuine, independently-testable Type=oneshot service; the timer only decides when it runs.

Step 5 — Reading the Logs

journalctl -u notesapp -f journalctl -u notesapp-cleanup -b

The first watches notesapp live after its first start; the second confirms the daily cleanup timer's own most recent run actually executed and logged correctly — systemd1-6's own tools, put to real use.

Step 6 — Verifying Boot Impact

systemd-analyze critical-chain notesapp.service

An honest use of systemd1-10's own tool — confirming notesapp's real position (or lack of one) in the boot's actual critical path, rather than assuming a new service must automatically be a boot-time concern just because it's new.

Closing the Loop on ws1 and ansible1-10

Every systemctl command run throughout this capstone — status, restart, enable, daemon-reload — is now fully understood. This resolves exactly what ws1's own hardening chapters and ansible1-10's own capstone left unexplained at the very start of this course.

PieceChapter
Unit file structure, systemctl basicssystemd1-2
Type=simple, Restart=, daemon-reloadsystemd1-3
Paired Wants=/After=systemd1-4
WantedBy=multi-user.target / timers.targetsystemd1-5
journalctl verificationsystemd1-6
The .timer + .service patternsystemd1-7
CPUQuota=/MemoryMax=/MemoryHigh=, verified via systemctl showsystemd1-9
systemd-analyze critical-chain verificationsystemd1-10
Still out of scope, honestly
notesapp itself deliberately does not use socket activation — systemd1-8's own material is a real, honest non-fit here: an always-on, frequently-hit web-facing app doesn't benefit from on-demand startup the way that chapter's own rarely-used-service example did. No systemd-managed sandboxing directives (ProtectSystem=, NoNewPrivileges=, and similar real, common production-hardening options) were covered anywhere in this course and aren't used here either. Logging stays local to journald — no centralized Loki-style shipping is actually configured, only discussed as a real possibility back in systemd1-6. And this capstone is single-host only, with no clustering or multi-host coordination — genuine next steps, not oversights.

Hands-On Exercises

Exercise 1

Explain why notesapp.service uses both Wants= and After= for network-online.target rather than just After= alone, using this chapter's own reasoning.

📄 View solution
Exercise 2

Explain why the notesapp-cleanup task is written as a separate Type=oneshot service paired with a timer, rather than being built directly into notesapp.service's own ExecStart= somehow.

📄 View solution
Exercise 3

A colleague suggests socket-activating notesapp.service to save resources. Give an honest answer, using this chapter's own scope note, on whether that's a good idea for this specific app.

📄 View solution

Chapter 11 Quick Reference — systemd in Depth Complete

  • notesapp.service combines paired Wants=/After=, Restart=on-failure, and a dedicated User= — every piece from systemd1-2 through systemd1-4
  • CPUQuota=/MemoryMax=/MemoryHigh= chosen only after observing real usage via systemd-cgtop, per systemd1-9's own discipline
  • notesapp-cleanup.timer/.service — a real .timer + .service pairing, daily, with Persistent=true catch-up
  • journalctl and systemd-analyze critical-chain both verify the deployment honestly, not just assumed to be correct
  • Every systemctl command from ws1's and ansible1-10's own unexplained calls is now fully understood
  • Honest scope note: no socket activation (a real non-fit here), no sandboxing directives, journald only (no centralized shipping configured), single-host
  • The full systemd in Depth course — 11 chapters — is now complete.