from setsid and PID files to a systemd unit
Starting a script in the background with & does not make it a daemon. A true daemon detaches completely from the terminal, prevents double starts through a PID file, reacts in a controlled way to signals like SIGTERM and SIGHUP, and writes its log somewhere that stays reachable even without a terminal.
Table of Contents
- 1. What sets a daemon apart from a background script
- 2. Double-fork and setsid in detail
- 3. PID files and locking against double starts
- 4. Signal handling: SIGTERM, SIGHUP and clean shutdown
- 5. Logging without a terminal: files and syslog
- 6. Health checks and restart strategies
- 7. systemd as a modern alternative
- 8. Common mistakes building a Bash daemon
- 9. Daemonization approaches compared
- 10. Summary
- 11. FAQ
1. What sets a daemon apart from a background script
A daemon is a process that runs permanently and independently of any terminal in the background, typically for the entire lifetime of the system or until it is explicitly stopped. A plain ./script.sh & is not yet a daemon, because the process remains bound to the starting terminal and receives SIGHUP when it closes, which in most cases terminates it immediately.
A true daemon needs several properties at once: complete detachment from the terminal, a new session without a controlling terminal, redirected standard streams, a known working directory, and usually a PID file that other processes can use to identify and control it. None of these properties arise automatically just by starting something in the background, they must be implemented in the script itself.
In practice, the question often comes up whether to hand-build a daemon in pure Bash or rely on systemd, which handles many of these tasks. This article shows both paths: the classic manual daemonization for environments without systemd or for portable scripts, and the move to systemd as a more robust modern alternative.
2. Double-fork and setsid in detail
The classic technique for daemonizing a process is the double-fork, combined with setsid. The first fork creates a child process and immediately terminates the parent, making the child an orphan that gets adopted by init or systemd. setsid in the child creates a new session in which the process becomes the session leader, but does not yet own a controlling terminal. A second fork prevents the process from later accidentally acquiring a controlling terminal, since only session leaders can acquire one and the second fork's descendant is not one.
In Bash, this classic double-fork approach can be replicated, though setsid as an external command is usually simpler than the raw fork approach, since Bash itself offers no direct fork builtin. setsid command starts the command in a new session without a terminal, which is already enough for most daemon use cases, without replicating the full complexity of a real double-fork written in C.
#!/usr/bin/env bash
set -euo pipefail
daemonize() {
local cmd="$1"
local pidfile="$2"
local logfile="$3"
# setsid detaches from the controlling terminal and starts a new session
setsid bash -c "
exec >> '$logfile' 2>&1
exec < /dev/null
echo \$\$ > '$pidfile'
exec $cmd
" &
disown
echo "Daemon started, see $logfile"
}
daemonize "./worker-loop.sh" "/var/run/myapp.pid" "/var/log/myapp.log"
What matters in the example is the combination of setsid for the new session, redirecting all standard streams before the actual exec, and writing the PID to a file once the new process has its final identity. exec replaces the bash -c wrapper process with the actual daemon command, without spawning another child process.
3. PID files and locking against double starts
A PID file stores the process ID of a running daemon, usually under /var/run/ or /run/, and serves two purposes: letting other scripts stop the daemon deliberately or send signals to it, and letting the daemon itself check on startup whether an instance is already running. Without this check, several instances of the same daemon could run in parallel, causing conflicts over exclusive resources such as ports or lock files.
The robust check reads the PID from the file if present, and verifies with kill -0 whether the process actually still exists. An orphaned PID file left over from a crashed daemon must not cause a restart to be refused, which is why the existence check must always be added on top of the plain file check. flock on the PID file itself offers an additional, race-condition-free safeguard compared to a plain PID check.
#!/usr/bin/env bash
set -euo pipefail
PIDFILE="/var/run/myapp.pid"
LOCKFILE="/var/run/myapp.lock"
check_already_running() {
if [[ -f "$PIDFILE" ]]; then
local existing_pid
existing_pid="$(cat "$PIDFILE")"
if kill -0 "$existing_pid" 2>/dev/null; then
echo "[ERROR] Already running with PID $existing_pid" >&2
exit 1
else
echo "[WARN] Stale PID file found, removing" >&2
rm -f "$PIDFILE"
fi
fi
}
# Additional race-condition-free guard using flock
exec 9>"$LOCKFILE"
flock -n 9 || { echo "[ERROR] Could not acquire lock, already running" >&2; exit 1; }
check_already_running
echo $$ > "$PIDFILE"
trap 'rm -f "$PIDFILE" "$LOCKFILE"' EXIT
4. Signal handling: SIGTERM, SIGHUP and clean shutdown
A cleanly implemented daemon reacts specifically to at least two signals: SIGTERM for a controlled shutdown, and SIGHUP often for reloading configuration without fully restarting the process. Without an explicit trap for SIGTERM, the default handler would terminate the process immediately without cleanup, potentially leaving behind open files, locks or incomplete write operations.
The trap function for SIGTERM should wrap up ongoing work in a controlled manner, release open resources and remove the PID file before the process actually exits. SIGHUP is traditionally used to tell a daemon to re-read its configuration file without interrupting ongoing connections or processing, a pattern many classic Unix daemons like nginx or rsyslog implement.
#!/usr/bin/env bash
set -euo pipefail
running=true
config_file="/etc/myapp/config.conf"
load_config() {
# shellcheck disable=SC1090
source "$config_file"
echo "$(date -Iseconds) Config reloaded" >> /var/log/myapp.log
}
shutdown_gracefully() {
echo "$(date -Iseconds) Received SIGTERM, finishing current task" >> /var/log/myapp.log
running=false
}
reload_config() {
echo "$(date -Iseconds) Received SIGHUP, reloading config" >> /var/log/myapp.log
load_config
}
trap shutdown_gracefully TERM
trap reload_config HUP
load_config
while $running; do
process_next_task
sleep 1
done
echo "$(date -Iseconds) Daemon stopped cleanly" >> /var/log/myapp.log
5. Logging without a terminal: files and syslog
Without a terminal connection, a daemon's output goes nowhere, unless it is explicitly redirected. The simplest approach redirects stdout and stderr straight to a log file, either already at daemon startup or with exec > logfile 2>&1 as the first line in the script itself. For log rotation, the daemon must additionally react to SIGHUP or a dedicated signal to reopen the log file, otherwise it keeps writing to the old, already renamed file descriptor after a rotation.
A more robust alternative is logger, which forwards messages directly to syslog or journald, without the daemon itself having to manage rotation. echo "message" | logger -t myapp writes a log entry tagged myapp, viewable through journalctl -t myapp or classic syslog files. This approach fully delegates rotation, compression and retention periods to the systemd journal or syslog infrastructure.
#!/usr/bin/env bash
set -euo pipefail
log_message() {
local level="$1"
local message="$2"
# Delegate rotation and retention to syslog/journald instead of
# writing raw files that need manual rotation handling
logger -t myapp -p "user.${level}" "$message"
}
log_message "info" "Daemon started with PID $$"
process_item() {
local item="$1"
if ! do_work "$item"; then
log_message "err" "Failed to process $item"
return 1
fi
log_message "info" "Processed $item successfully"
}
6. Health checks and restart strategies
A production daemon should expose its own health status externally, so monitoring can tell whether the process is still doing meaningful work, not merely whether it still exists. A simple method is a heartbeat file that the daemon updates at regular intervals with the current timestamp. An external monitoring script then checks whether this file was updated within an expected window, and raises an alert if the daemon exists as a process but is hanging internally.
For automatic restarts after crashes, a Bash-built daemon alone is not enough, because a crashed process cannot restart itself. This requires either a higher-level watchdog instance monitoring the daemon and restarting it as needed, or switching to a process supervisor such as systemd, which handles exactly this task by default.
7. systemd as a modern alternative
For most new projects, a systemd service is the more robust alternative to manual Bash daemonization, since systemd already provides PID file management, restart strategies, logging through journald and signal handling as built-in features. The Bash script itself no longer needs to implement daemon mode at all, it can run in the foreground while systemd handles process supervision through Type=simple or Type=notify.
The switch reduces your own code considerably: no double-fork, no manual PID file, no manual log rotation handling. Restart=on-failure automatically triggers a restart after a crash, something a plain Bash daemon without an external watchdog cannot achieve. For new projects on Linux systems with systemd, this path is almost always preferable, while the manual double-fork technique remains relevant for portable scripts or containers without systemd.
#!/usr/bin/env bash
set -euo pipefail
# Generate a systemd unit instead of hand-rolling daemonization logic
cat > /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Bash worker daemon
After=network.target
[Service]
Type=simple
ExecStart=/opt/myapp/worker-loop.sh
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now myapp.service
systemctl status myapp.service --no-pager
8. Common mistakes building a Bash daemon
The most common mistake is not redirecting stdin. A daemon without exec < /dev/null can unexpectedly block on an accidental stdin read, or receive SIGTTIN if it is still attached to a terminal. The second common mistake is writing the PID file too early, before the actual target process has reached its final PID via exec, leading to a PID file containing the wrapper process's PID instead of the actual daemon's.
A third mistake concerns missing signal handling: a daemon without a trap for SIGTERM gets terminated abruptly on stop, without cleanup, without closing open files, and without removing the PID file. This leads to an orphaned PID file on the next start that falsely suggests a process is already running, if the existence check with kill -0 is not implemented carefully.
9. Daemonization approaches compared
The table below compares the main approaches to running a process permanently in the background.
| Approach | Automatic restart | Log management | Effort |
|---|---|---|---|
| & alone | No | None | Minimal, but unreliable |
| setsid + PID file | No (without a watchdog) | Manual, rotation needed | Medium |
| setsid + logger/syslog | No (without a watchdog) | Via syslog/journald | Medium |
| systemd service | Yes, configurable | Automatic via journald | Low |
For new Bash daemons on a system with systemd, the last row is almost always the right choice. The manual combination of setsid, PID file and signal handling remains relevant for containers without an init system, older systems, or portable scripts that must run on multiple platforms without systemd.
Mironsoft
Shell automation, DevOps tooling and deployment infrastructure
Bash scripts that need to run reliably as a service?
We build robust daemonization with PID files, signal handling and logging into your Bash scripts, or migrate them to systemd units with automatic restart and centralized logging via journald.
Daemon review
Check existing background scripts for robustness
systemd migration
Set up units with a restart strategy and journald logging
Signal handling
Clean shutdown and config reload without a process restart
10. Summary
A true daemon needs more than a trailing &: complete detachment from the terminal via setsid, a PID file with a robust existence check against double starts, clean signal handling for SIGTERM and SIGHUP, and logging that stays reachable even without a terminal. Every one of these properties must be explicitly implemented in the Bash script, they do not arise automatically from simply starting something in the background.
For new projects on systems with systemd, a systemd service is in most cases the more robust and lower-maintenance alternative, since automatic restart, logging via journald and process supervision are already available as built-in features. Manual daemon implementation remains relevant for portable scripts, containers without an init system, and environments where systemd is unavailable.
Running Bash Scripts as a Daemon — Key Takeaways
Terminal detachment
setsid starts a new session without a controlling terminal, exec < /dev/null prevents stdin blocks.
PID file
Existence check with kill -0 before starting, flock as an additional race-condition-free safeguard.
Signals
trap for SIGTERM for clean shutdown, SIGHUP often for config reload without a restart.
Alternative
systemd handles restart, logging and process supervision as built-in features, usually the more robust choice.