nohup and Keeping Processes Alive

When you run a command in a terminal and then close that terminal — by logging out over SSH, closing the terminal emulator window, or losing your network connection — the shell’s controlling terminal normally sends a signal called SIGHUP (“hangup”) to the processes attached to it, and those jobs die along with it. nohup is a small utility whose entire job is to make a command immune to that one signal, so a long-running process — a backup script, a data import, a small server — keeps running after you disconnect. This lesson covers exactly how nohup works under the hood, how to combine it correctly with backgrounding and output redirection, and the related tools (disown, setsid, and terminal multiplexers) that solve the same survival problem in different ways.

Overview: Why Processes Die When You Log Out

Every process you start from an interactive shell belongs to a session tied to a controlling terminal — the pseudo-terminal device your terminal emulator or SSH connection created. When that terminal goes away, the kernel sends SIGHUP to the processes still attached to it. By default, a process that receives SIGHUP and does not explicitly handle it terminates. This is why a script started with just ./backup.sh & and left running can vanish the moment you log out, even though it looked safely backgrounded.

nohup solves this by starting your command with SIGHUP‘s disposition set to “ignore” before your program’s own code ever runs. Internally, the nohup process calls signal(SIGHUP, SIG_IGN) and then uses exec to replace itself with your command — so your command inherits that ignored-signal disposition and even keeps the same process ID nohup had. Crucially, nohup does not detach the process from the session, put it in a new process group, or background it for you. It only changes how the process reacts to one specific signal. That’s why you still need the shell’s & operator to run it in the background; nohup and backgrounding are two separate, complementary steps that beginners frequently conflate.

nohup has one more behavior worth knowing: if the command’s standard output is still connected to a terminal (you didn’t redirect it), nohup automatically redirects stdout to a file named nohup.out in the current directory (or $HOME/nohup.out if the current directory isn’t writable), and standard error is appended there too unless redirected separately. This exists so output isn’t silently lost once the terminal disappears — but relying on the default file is a mistake in practice; see Common Mistakes below.

It’s also worth being precise about what nohup does not protect against. It only ignores SIGHUP. A kill (which sends SIGTERM by default) or kill -9 (SIGKILL, which cannot be caught or ignored by any process) will still stop the job. nohup also does not survive a system reboot or crash — for that you need a real service manager like systemd, which is covered in a later lesson.

Syntax

The general form is:

nohup "<command>" ["<arguments...>"] &
Part Meaning
nohup The command itself — makes the process it launches ignore SIGHUP.
command The program or script to run, e.g. ./backup.sh or python3 worker.py.
arguments Any arguments, passed through unchanged to command.
& Not part of nohup — this is the shell’s background operator. Without it, nohup still runs the command in the foreground and blocks your terminal until it finishes.
> file 2>&1 Optional but recommended: redirect stdout and stderr to a log file you choose, instead of relying on the default nohup.out.

nohup‘s own exit status tells you whether it managed to start the command at all: 127 means the command wasn’t found, 126 means it was found but not executable, and otherwise nohup exits with whatever status the command itself returned.

Examples

Example 1: The basic case

nohup ./backup.sh &

Output:

[1] 21345
nohup: ignoring input and appending output to 'nohup.out'

Bash prints the job number ([1]) and process ID (21345) because of the trailing &. nohup reports that it’s sending output to nohup.out since stdout wasn’t redirected. You can now close the terminal, and backup.sh keeps running.

Example 2: Redirecting output explicitly (recommended)

nohup ./backup.sh > /var/log/backup.log 2>&1 &

Output:

[1] 21402

Here stdout goes to /var/log/backup.log and 2>&1 sends stderr to the same place. Because stdout is no longer connected to the terminal, nohup has nothing to redirect on its own, so it prints no extra message — you get one predictable, named log file instead of a nohup.out that could land in different directories depending on where you happened to run the command from.

Example 3: A small wrapper script that captures the PID

#!/usr/bin/env bash
set -euo pipefail

LOG_FILE="/var/log/data-sync.log"

nohup ./sync_data.sh > "$LOG_FILE" 2>&1 &
SYNC_PID=$!

echo "Started sync_data.sh with PID $SYNC_PID, logging to $LOG_FILE"

Output:

Started sync_data.sh with PID 21520, logging to /var/log/data-sync.log

$! holds the PID of the most recently backgrounded job, captured immediately so it can’t be overwritten by a later command. Saving that PID lets you check on the process later with ps or stop it deliberately with kill instead of hunting for it by name.

You can confirm the process survived by searching for it after logging back in:

ps -ef | grep sync_data.sh

Output:

root     21520     1  0 14:12 ?        00:00:01 /bin/bash ./sync_data.sh
root     21977 21901  0 14:15 pts/0    00:00:00 grep --color=auto sync_data.sh

Notice the PPID column shows 1: once your original shell exited, the orphaned child was reparented to init (PID 1, or a systemd user instance on many modern distros) instead of being killed — and it only got the chance to be reparented because nohup had already made it immune to the SIGHUP that would otherwise have killed it first.

Example 4: Fixing a job you forgot to nohup, with disown

./nightly_report.sh > /var/log/nightly_report.log 2>&1 &
disown

Output:

[1] 22014

If you already started a job in the background without nohup and now need to log out, disown removes it from your shell’s job table so the shell won’t try to manage or signal it on exit. Run disown -h %1 instead of plain disown if you want the job to stay visible in jobs while still being protected — -h marks it to ignore a future SIGHUP without fully detaching it from the table.

Example 5: Full detachment with setsid

setsid ./web_server.sh > /var/log/web_server.log 2>&1 < /dev/null &

Output:

[1] 22188

setsid goes a step further than nohup: it runs the command in a brand-new session with no controlling terminal at all, so there’s no terminal to ever send it SIGHUP in the first place. Redirecting stdin from /dev/null is standard practice here, since a process with no controlling terminal that still tries to read from the terminal will hang or error.

How nohup Works Step by Step

Walking through nohup ./backup.sh > /var/log/backup.log 2>&1 &:

  • Bash parses the line and, because of the trailing &, decides to run it as a background job instead of waiting for it.
  • Bash fork()s a child process. That child sets up the requested redirections (stdout and stderr now point at /var/log/backup.log) before doing anything else.
  • The child then exec()s nohup, replacing its own program image with the nohup binary but keeping the same PID and the redirections already in place.
  • nohup calls signal(SIGHUP, SIG_IGN) to make the process ignore hangups from this point forward, then exec()s ./backup.sh — again replacing the running program’s image while keeping the same PID and the now-ignored SIGHUP disposition.
  • Bash prints the job number and PID and returns control of the terminal to you immediately, since the job is running in the background.
  • Later, when the terminal session ends (you close the window, the SSH connection drops), the kernel sends SIGHUP to the processes still attached to that session. backup.sh receives it, does nothing (because it’s set to be ignored), and keeps running.
  • Once its original parent shell is gone, the kernel reparents the now-orphaned process to init (PID 1) or an equivalent reaper process, which will simply wait() on it when it eventually exits.

Common Mistakes

Mistake 1: Forgetting the trailing &

Wrong — this blocks your terminal:

nohup ./backup.sh

nohup only changes signal handling; it does not background the process. Without &, your shell waits for backup.sh to finish before giving you a prompt back.

Corrected:

nohup ./backup.sh &

Mistake 2: Relying on the default nohup.out forever

Wrong — output accumulates indefinitely in an easy-to-forget file:

nohup ./monitor.sh &

Every run appends to nohup.out in whatever directory you happened to be in, with no size limit and no rotation. Over weeks this can quietly fill a disk, and it’s easy to lose track of which nohup.out belongs to which job.

Corrected — redirect to a named, predictable log file:

nohup ./monitor.sh >> /var/log/monitor.log 2>&1 &

Note the double >> here: a single > truncates the file every time the job restarts, discarding prior log history, while >> appends to it. Pick whichever matches your intent, and pair long-lived logs like this with logrotate so they don’t grow forever either.

Mistake 3: Passing an unquoted variable as the command

Wrong — word splitting breaks a path containing a space:

SCRIPT="./run backup.sh"
nohup $SCRIPT &

Without quotes, $SCRIPT is split on whitespace, so bash tries to run a program literally named ./run with one argument, backup.sh — not the file ./run backup.sh.

Corrected:

SCRIPT="./run_backup.sh"
nohup "$SCRIPT" &

Mistake 4: Forgetting execute permission

Wrong:

nohup ./deploy.sh &

Output:

nohup: failed to run command './deploy.sh': Permission denied

A script needs its execute bit set before it can be run directly, whether or not nohup is involved.

Corrected:

chmod +x deploy.sh
nohup ./deploy.sh &

Best Practices

  • Always pair nohup with & — it makes a process immune to SIGHUP, it does not background it.
  • Redirect stdout and stderr explicitly to a named log file rather than relying on the default nohup.out.
  • Capture $! right after backgrounding a job if you’ll need its PID later — read it before running any other command that could change $? or clobber your intent.
  • Use disown (or disown -h) to rescue a job you already started without nohup and need to detach before logging out.
  • Reach for setsid, or a terminal multiplexer like tmux or screen, when you want a process fully detached from the terminal’s session, not just protected from one signal.
  • Rotate or cap any log file a long-running nohup‘d process writes to — nothing does this for you automatically.
  • For anything that must survive a reboot, restart after a crash, or run on a schedule, use a real systemd service or a cron job instead of nohup.
  • Don’t nohup interactive programs (editors, REPLs, anything expecting keyboard input) — it’s meant for unattended, batch-style jobs.

Practice Exercises

  • Write a script long_task.sh that runs sleep 120 and then appends "done" to a file. Start it with nohup and redirected output, close your terminal (or SSH session) before the 120 seconds are up, log back in, and confirm with ps -ef that it finished successfully.
  • Start ./import.sh & without nohup. Realizing you need to close your session, use disown to protect it. Confirm with jobs that it no longer appears in your shell’s job table, then confirm with ps that it’s still running.
  • Run one command with plain nohup and no redirection, and a second with explicit > file 2>&1 redirection. Locate the resulting nohup.out file, then explain in your own words why the explicitly redirected version is easier to manage in a real deployment.

Summary

  • SIGHUP is sent to a process’s session when its controlling terminal goes away; left unhandled, it terminates the process.
  • nohup makes a command ignore SIGHUP so it survives logout — it does not background the job for you, so you still need a trailing &.
  • Without redirection, nohup sends output to nohup.out; always redirect explicitly (> file 2>&1) to a real, named log file instead.
  • $! captures the PID of the most recently backgrounded job so you can track or stop it later.
  • disown detaches an already-running job from the shell’s job table when you forgot to nohup it up front.
  • setsid goes further than nohup by giving the process its own session with no controlling terminal at all.
  • None of these tools survive a reboot or a crash — for that, use systemd or cron.