Understanding SIGHUP and cleanly detaching background services from the terminal
nohup ignores the SIGHUP signal, disown removes a job from the shell's job table after the fact, and setsid starts a brand-new session with no controlling terminal at all. Knowing the difference means picking the right, most robust tool for every background service in a deployment script.
Table of Contents
- 1. The core problem: SIGHUP and the bond to the controlling terminal
- 2. nohup in detail: how it deliberately catches SIGHUP
- 3. disown: removing an already-running job from the shell's job table
- 4. setsid: starting a new session with no controlling terminal
- 5. Detached from the terminal versus having no controlling terminal at all
- 6. Combining nohup, & and disown in practice
- 7. When setsid is really necessary: true daemonizing
- 8. Deployment scripts: which tool fits which background service
- 9. Pitfalls: stream redirection and double forking
- 10. Summary
- 11. FAQ
1. The core problem: SIGHUP and the bond to the controlling terminal
Every interactively started process is normally bound to a controlling terminal, through a session the shell builds at login or when a terminal window opens. When the user closes that terminal or an SSH connection drops, the kernel sends the signal SIGHUP to every process in that session, whose default behavior without an explicit handler is immediate process termination.
For a short, interactive command that behavior is usually desired; for a background deployment process meant to keep running after the SSH session closes, it is a real problem instead. This is exactly where nohup, disown, and setsid come in, each with a different mechanism to protect a process from that signal or to avoid binding it to the terminal in the first place.
2. nohup in detail: how it deliberately catches SIGHUP
nohup starts a command with SIGHUP explicitly set to be ignored, so the kernel still sends the signal to the process once the terminal disconnects, but the process ignores it according to its signal mask and is therefore not terminated. By default, nohup also redirects stdout and stderr into a file called nohup.out, because a process detached from its terminal would otherwise try to write into a terminal that no longer exists, which can trigger another signal.
nohup deliberately does not change the process group or session membership of the started process; the process technically remains part of the same session as the launching shell. If the shell itself keeps running, say because it lives inside a tmux window, a process started with nohup still remains a child of that shell and gets terminated if the shell itself is killed with SIGKILL, a signal that fundamentally cannot be ignored.
nohup ./long-running-deploy.sh > deploy.log 2>&1 &
deploy_pid=$!
echo "Running as PID $deploy_pid, SIGHUP is ignored"
3. disown: removing an already-running job from the shell's job table
disown removes a background job from the current shell's internal job table without affecting the process itself. The key effect: with no entry left in the job table, the shell no longer sends SIGHUP to that process when it exits itself, since it simply no longer recognizes it as one of its own jobs, whereas without disown it forwards SIGHUP by default to every still-running background job.
The crucial difference from nohup: disown only acts retroactively on a background process already started (with &), while nohup must be applied right at launch. Anyone who forgot to start a long-running process with nohup can use disown -h %1 to achieve exactly that effect after the fact, without having to restart the process.
./long-running-deploy.sh &
deploy_pid=$!
# Realized too late that nohup was forgotten - fix it after the fact
disown -h %1
echo "Job removed from the job table, PID $deploy_pid survives SIGHUP"
4. setsid: starting a new session with no controlling terminal
setsid starts a command in a brand-new session, where the new process automatically becomes the session leader and has no controlling terminal from the outset. That is a fundamentally different mechanism from nohup: nohup leaves the process in the same session and only ignores the signal, while setsid detaches the process from the launching shell's session entirely, before SIGHUP could ever become relevant.
Because a process with no controlling terminal cannot receive a SIGHUP from a terminal disconnect in the first place, there is, after all, no terminal left to trigger that signal, setsid is the most robust of the three mechanisms for real background services. The downside: a process started with setsid can no longer be addressed through the shell's normal job control, since it formally no longer belongs to any interactive shell session.
setsid ./deploy-daemon.sh > deploy.log 2>&1 < /dev/null &
echo "New session started, no controlling terminal"
5. Detached from the terminal versus having no controlling terminal at all
nohup and disown solve a different problem than setsid, even though all three are frequently mentioned in the same breath. nohup and disown make sure a process survives a SIGHUP or never receives it at all, but do nothing to change the fact that the process formally remains a member of the original session, with the same controlling terminal, if that terminal still exists.
setsid, by contrast, removes the controlling terminal from the very start, regardless of whether it is disconnected later or not. This difference becomes relevant the moment a process itself tries to interact with the terminal, for example prompting for a password on stdin: a process with no controlling terminal fails at such interactions from the outset, while a process protected only by nohup does not, for as long as its terminal actually still exists.
6. Combining nohup, & and disown in practice
In most deployment scripts, the combination of nohup at launch and the background operator & is entirely sufficient to keep a process running safely past the end of an SSH session. disown is mainly needed in practice when nohup was forgotten for some reason, or when an interactively started job needs to be detached from the current shell after the fact, without a restart.
A robust pattern for a deployment script therefore combines nohup for SIGHUP protection with an explicit redirection of stdin, stdout, and stderr, so the process never attempts under any circumstances to interact with a terminal that may already be closed, even though nohup alone already catches the SIGHUP signal.
nohup ./deploy-worker.sh < /dev/null > deploy.log 2>&1 &
worker_pid=$!
disown "$worker_pid" 2>/dev/null || true
echo "Worker $worker_pid runs independently of this session"
7. When setsid is really necessary: true daemonizing
For most deployment scripts that start a one-off background process, nohup and & are enough, since modern deployment environments rarely want to permanently bind a process to an interactive SSH session anyway. setsid becomes indispensable, however, once a process needs true daemon properties: no terminal binding whatsoever, no accidental job control by a shell, and its own, independent session that fully shields it, for example, from SIGHUP chains triggered by terminal multiplexers like tmux.
In production systemd-based environments, systemd itself already takes over the role of setsid for every service it starts, which is why explicit setsid in such scripts is usually redundant. setsid remains relevant mainly for legacy deployment scripts without systemd integration, or for cases where a process deliberately needs to be fully detached from the calling session out of an interactive cron or CI context.
8. Deployment scripts: which tool fits which background service
For short-lived background processes inside an interactive deployment script that just need to outlive the rest of the script run, nohup combined with & is usually enough, complemented with an explicit redirection of all standard streams. For retroactively detaching a job already running that was forgotten to be protected with nohup, disown is the right, targeted tool, without needing to restart the process.
For true, long-lived background services meant to run permanently and independently of any interactive session, a systemd unit file is really the right solution, since it handles restarts, logging, and resource limits consistently. setsid remains the fitting tool for the transitional case where true daemonizing is needed outside a systemd environment, for example in minimal container base images with no init process of their own.
9. Pitfalls: stream redirection and double forking
The most common mistake with all three tools is not explicitly redirecting stdin. A background process trying to read from stdin while the original terminal has long since closed can block indefinitely or receive an error signal on read, depending on how that particular process handles a closed descriptor, which is why < /dev/null practically always belongs in production scripts.
A second, subtler pitfall concerns double forking during true daemonizing: a process started with setsid is a session leader with no terminal, but under certain circumstances can itself open a terminal again and thereby reacquire a controlling terminal. Classic Unix daemons avoid this with a second fork() after setsid, so the actual daemon process is no longer a session leader itself and therefore cannot acquire a terminal at all, a detail setsid alone does not cover.
| Tool | When applied | Solves SIGHUP | Fully detaches terminal |
|---|---|---|---|
| nohup | At process start | Yes, by ignoring it | No, session stays intact |
| disown | After start, retroactively | Yes, by removing from job table | No, session stays intact |
| setsid | At start, new session | Yes, indirectly | Yes, no terminal from the outset |
| nohup + disown | Combined at start and after | Yes, doubly secured | No, but practically robust |
Mironsoft
Shell automation, DevOps tooling and deployment infrastructure
Shell scripts that hold up in production?
We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.
Code Review
ShellCheck analysis and manual review for critical Bash pattern violations.
Refactoring
Retrofitting error handling, logging and safe file operations.
CI Integration
Wiring ShellCheck and BATS into pipelines and building regression tests.
10. Summary
nohup, disown and setsid: The Essentials at a Glance
SIGHUP cause
Terminal disconnect sends SIGHUP to every process in the session; default behavior is immediate termination.
nohup
Deliberately ignores SIGHUP at process start but leaves session membership unchanged.
disown
Retroactively removes an already-running job from the job table without restarting the process.
setsid
Starts a brand-new session with no controlling terminal, the most robust mechanism for true daemons.