Table of Contents
- Introduction
- What Is Daemon Mode?
- Why a Session Supervisor Is Needed
- Popular Session Supervisors
- systemd‑user
- launchd (macOS)
- Upstart & runit
- [Supervisord (Python)]#supervisord-python)
- Designing a Daemon for Supervision
- Practical Example: A Simple Go Service Managed by systemd‑user
- Advanced Topics
- Troubleshooting Common Pitfalls
- Conclusion
- Resources
Introduction
Long‑running background processes—daemons—are the invisible workhorses that keep modern operating systems functional. From web servers and database engines to personal notification agents, daemons provide services without direct user interaction. Yet writing a daemon that behaves well under every circumstance is far from trivial. Over the past two decades, the session supervisor model has emerged as a robust solution to many of the classic daemon‑related headaches.
In this article we will:
- Define daemon mode and explain why the term matters today.
- Examine the role of a session supervisor and how it differs from a traditional init system.
- Compare the most widely used supervisors (systemd‑user, launchd, Upstart, runit, Supervisord).
- Walk through a complete, production‑ready example of a Go service managed by
systemd --user. - Dive into advanced topics such as socket activation, dependency ordering, and resource control.
- Provide a troubleshooting checklist for the most common integration problems.
By the end, you should be able to design, implement, and supervise a daemon that starts reliably, recovers gracefully from failures, and integrates cleanly with the host operating system.
What Is Daemon Mode?
Historical Background
The concept of a daemon originates from early UNIX where background processes were forked, detached from the controlling terminal, and placed into a special “daemon” state. The term itself is a nod to the Greek mythological daemon, an unseen spirit that works behind the scenes. Early daemons (e.g., cron, syslogd) followed a set of conventions:
- Double‑fork to become a child of
init(PID 1). - Close all file descriptors inherited from the parent.
- Redirect standard streams to
/dev/nullor log files. - Run with a restricted user/group for security.
- Handle signals (
SIGTERM,SIGHUP) for reload/restart.
These steps were manually coded into each program, leading to duplicated boilerplate and a high likelihood of subtle bugs.
Key Characteristics of a Daemon
| Characteristic | Description |
|---|---|
| Detachment | The process runs without a controlling terminal. |
| Long‑lived | It stays alive for the system’s uptime or until explicitly stopped. |
| Self‑Management | Handles its own PID file, logging, and signal handling. |
| Autostart | Typically started by init or a similar supervisor at boot. |
| Recovery | Should restart automatically after crashes, if possible. |
When a daemon is well‑behaved, it respects these conventions and coexists peacefully with other services. However, as applications grew more complex (containers, per‑user services, desktop agents), the traditional model showed cracks—especially around session awareness.
Why a Session Supervisor Is Needed
The Limitations of Traditional Daemons
Traditional init systems (sysvinit, early systemd in system mode) launch daemons in the system context (root or a dedicated service account). This works for server‑side components but introduces problems for user‑level services:
- No access to user‑specific resources (e.g., DBus, Wayland sockets, X11).
- Lifecycle mismatch – system daemons survive user log‑out, leading to orphaned processes.
- Permission barriers – a system daemon cannot easily read/write files in a user’s home directory without complex ACLs.
Additionally, many modern tools (e.g., ssh-agent, gpg-agent, notification daemons) need to start once per login session, not once per boot.
User Sessions vs. System Sessions
A session is a collection of processes that share a common login context, environment variables, and access to user‑level IPC mechanisms. In Linux, a session is represented by a session leader (often the login manager like gdm, lightdm, or sshd) and a session ID.
A session supervisor runs inside that session, managing child processes that belong to the same user. It provides the same guarantees that systemd gives for system services—automatic restart, dependency handling, resource limits—while being aware of the user’s environment.
Key benefits:
- Automatic start on login (or on demand) without root privileges.
- Graceful termination on logout (no orphaned background processes).
- Access to per‑user DBus, XDG runtime directories, and secret stores.
- Uniform tooling – the same
systemctl --usercommands work for both system and user services.
Popular Session Supervisors
systemd‑user
systemd has a user mode (systemd --user) that mirrors the system mode but runs under a regular user account. It is started automatically by most modern login managers via a user manager socket ($XDG_RUNTIME_DIR/systemd/user). The command-line interface is the familiar systemctl --user.
- Pros: Seamless integration with the rest of
systemd; supports socket activation, timers, and cgroup v2; works on most modern Linux distros. - Cons: Requires a recent
systemdversion; may be disabled on minimal containers.
launchd (macOS)
macOS uses launchd as both its system and per‑user init system. Per‑user agents are placed in ~/Library/LaunchAgents. launchctl is the command‑line tool.
- Pros: Native to macOS; supports KeepAlive, Throttling, and resource limits out of the box.
- Cons: Proprietary format (plist XML) can be verbose; limited to Apple platforms.
Upstart & runit
Both Upstart (Ubuntu’s former init) and runit (a cross‑platform supervisor) provide per‑user supervision via a user session script. They are less common today but still appear in embedded systems.
- Pros: Simpler than
systemdin some respects; runit’ssvtool is tiny and fast. - Cons: Lacks deep integration with modern Linux features (cgroups, journald).
Supervisord (Python)
supervisord is a language‑agnostic process control system written in Python. While not a native OS supervisor, it can be launched as a per‑user daemon and manage a set of child processes via a simple INI‑style configuration.
- Pros: Portable; rich web UI and XML‑RPC API; easy to embed in Python applications.
- Cons: Doesn’t integrate with systemd’s socket activation; requires a separate Python runtime.
Designing a Daemon for Supervision
Daemonizing vs. “No‑Daemon” Approach
Modern supervisors (systemd, launchd, runit) encourage the “no‑daemon” pattern: the service runs in the foreground, and the supervisor handles detachment, PID tracking, and restarts. This eliminates the need for a double‑fork and reduces complexity.
// Bad: self‑daemonizing
func main() {
if os.Getppid() != 1 {
// fork and exit parent...
}
// rest of service...
}
// Good: foreground process
func main() {
// do not fork; let the supervisor manage it
runServer()
}
When to still daemonize? Rarely—only if you must support legacy init systems that lack supervision capabilities.
Signal Handling & Graceful Shutdown
A well‑supervised daemon must respond to the signals the supervisor sends:
| Signal | Typical Meaning | Supervisor Use |
|---|---|---|
SIGTERM | Request graceful termination | systemctl stop … |
SIGINT | Interrupt (often same as SIGTERM) | Manual kill |
SIGHUP | Reload configuration without exit | systemctl reload … |
SIGUSR1 | Custom – e.g., rotate logs | Optional |
Example in Go:
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer cancel()
go func() {
// Simulated work loop
for {
select {
case <-ctx.Done():
log.Println("Shutdown signal received")
// perform cleanup here
return
default:
// main work
time.Sleep(2 * time.Second)
log.Println("tick")
}
}
}()
<-ctx.Done() // block until signal
log.Println("Exiting")
}
Logging Strategies
Supervisors provide centralized logging:
systemd→journalctllaunchd→/var/log/system.logor per‑user log filessupervisord→ its own log files andtail -f
Best practice: write to stdout/stderr and let the supervisor capture the output. Avoid writing directly to files unless you need persistent logs beyond the supervisor’s retention policy.
log.SetOutput(os.Stdout) // ensures logs go to journal
Practical Example: A Simple Go Service Managed by systemd‑user
We’ll build a tiny HTTP server that reports the current time, then create a systemd --user unit that starts it on login, restarts on failure, and captures logs.
Service Code
Create ~/go/src/daemon-time/main.go:
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func timeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Current time: %s\n", time.Now().Format(time.RFC1123))
}
func main() {
// Log to stdout so systemd captures it
log.SetOutput(os.Stdout)
http.HandleFunc("/", timeHandler)
srv := &http.Server{
Addr: ":8080",
Handler: nil,
}
// Graceful shutdown handling
go func() {
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
sig := <-sigc
log.Printf("Received %s, shutting down...", sig)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Shutdown error: %v", err)
}
}()
log.Println("Starting HTTP server on :8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("ListenAndServe error: %v", err)
}
log.Println("Server stopped")
}
Build the binary:
go build -o $HOME/.local/bin/daemon-time ~/go/src/daemon-time/main.go
chmod +x $HOME/.local/bin/daemon-time
systemd Unit File
Create $HOME/.config/systemd/user/daemon-time.service:
[Unit]
Description=User‑level time‑report HTTP daemon
After=network.target
Wants=network.target
[Service]
ExecStart=%h/.local/bin/daemon-time
Restart=on-failure
RestartSec=5
# Keep stdout/stderr in the journal
StandardOutput=journal
StandardError=journal
# Optional: limit resources
MemoryLimit=100M
CPUQuota=20%
[Install]
WantedBy=default.target
Explanation of important directives:
After=network.target– ensures the network stack is up before the service starts.Restart=on-failure– systemd will automatically restart after a crash.MemoryLimit/CPUQuota– cgroup v2 limits that protect the user’s session from runaway processes.WantedBy=default.target– ties the service to the user’s default target, which is pulled in when the user logs in.
Enabling & Testing
# Reload user manager configuration
systemctl --user daemon-reload
# Enable the service to start on every login
systemctl --user enable daemon-time.service
# Start it immediately for testing
systemctl --user start daemon-time.service
# Check status
systemctl --user status daemon-time.service
# View logs
journalctl --user -u daemon-time.service -f
Open a browser (or curl) to http://localhost:8080/ – you should see the current time.
Simulating a Crash
# Find the PID
pid=$(systemctl --user show -p MainPID daemon-time.service | cut -d= -f2)
# Send SIGSEGV (illegal instruction) to crash
kill -SIGSEGV $pid
Systemd will log the crash and automatically restart the service after the RestartSec interval.
Advanced Topics
Socket Activation
Socket activation lets the supervisor open the listening socket before the service starts, passing the file descriptor to the service. The daemon only runs when there is inbound traffic, reducing idle memory usage.
systemd socket unit (daemon-time.socket):
[Unit]
Description=Socket for daemon-time service
[Socket]
ListenStream=8080
Accept=no
[Install]
WantedBy=sockets.target
Modify daemon-time.service:
[Service]
ExecStart=%h/.local/bin/daemon-time
StandardInput=socket
Now, the service will receive the socket as STDIN, and Go’s net/http can be started with Serve instead of ListenAndServe. This pattern is used by high‑performance services like nginx and systemd-resolved.
Dependency Graphs & Ordering
Complex user environments may have multiple interdependent services (e.g., a DBus service, a notification daemon, a background sync client). Use After= and Requires= to enforce start order and failure propagation.
[Unit]
Requires=dbus.service
After=dbus.service
systemd builds a dependency DAG; cycles are prohibited, and the DAG is evaluated at runtime to start services in parallel when possible.
Resource Limits (cgroups, ulimits)
User managers run under cgroup v2 hierarchy (/sys/fs/cgroup/user.slice/user-1000.slice). You can fine‑tune limits per service:
[Service]
MemoryMax=200M # hard limit
CPUQuota=30% # max 30% of a single CPU
IOWeight=500 # relative I/O priority
These limits protect the desktop environment from a single rogue daemon consuming all resources.
Troubleshooting Common Pitfalls
| Symptom | Likely Cause | Fix |
|---|---|---|
| Service never starts after login | systemd --user not running (e.g., missing pam_systemd entry) | Ensure /etc/pam.d/login includes session required pam_systemd.so and systemd --user is enabled |
| Logs are empty | StandardOutput=journal missing or journalctl not reading user logs | Add ForwardToSyslog=yes or view with journalctl --user |
| Service crashes on first request | Binary expects to open its own socket but socket activation is configured | Either disable socket activation or modify code to accept pre‑opened socket (use net.FileListener) |
| Restart loop (service constantly failing) | Unhandled error causing immediate exit (e.g., missing config file) | Add RestartPreventExitStatus= or fix the underlying error |
| No network on start | After=network.target not sufficient on systems using NetworkManager’s systemd-networkd | Use After=network-online.target and Wants=network-online.target |
Debugging tip: Use systemctl --user show -p ExecMainStatus,ExecMainPID <service> to see exit codes, and journalctl -xe for recent error messages.
Conclusion
Daemon mode and session supervision have converged into a clean, predictable model for background processes. By letting the supervisor handle detachment, restarts, logging, and resource limits, developers can focus on the core business logic of their services. Whether you are building a lightweight per‑user notification agent, a cross‑platform CLI tool with a background sync component, or a full‑blown server daemon, the principles outlined here apply:
- Prefer foreground processes; let the supervisor daemonize for you.
- Implement proper signal handling to cooperate with stop/reload requests.
- Leverage built‑in logging (stdout/stderr) instead of custom file writes.
- Use socket activation when possible to reduce idle resource usage.
- Apply cgroup limits to protect the user session from misbehaving daemons.
With systemd --user as the de‑facto standard on modern Linux distributions, you have a powerful, battle‑tested toolkit at your disposal. The same concepts translate to macOS’s launchd, the portable supervisord library, or minimal supervisors like runit. By mastering these patterns, you’ll write daemons that are robust, maintainable, and friendly to the ecosystems they live in.
Resources
- systemd User Services Documentation – Official guide to
systemd --userand unit file syntax. - Launchd.plist Format Reference – Apple’s documentation for per‑user agents.
- Supervisord Documentation – Comprehensive resource for configuring and using Supervisord.
- Linux Daemon Best Practices (LWN) – In‑depth article on daemonizing and modern service management.
- cgroups v2 & systemd Integration – Details on resource limiting in user slices.