tmux/screen 없이 SSH 끊겨도 서버 작업 유지하는 nohup 활용법

Views: 0

원격 서버에서 긴 작업을 돌리다 연결이 끊기면 프로세스가 함께 종료되는 일이 잦다. 이는 로그인 세션이 끊길 때 커널이 세션의 자식 프로세스에게 SIGHUP을 보내기 때문이다. nohup은 이 신호를 무시하도록 설정하고 표준 출력·오류를 파일로 돌려 안정적으로 작업을 지속시킨다. 이 글은 nohup의 동작 원리, 올바른 사용법, 로그 설계, 실패 사례의 원인과 해결, 그리고 setsid, disown, systemd-run 같은 실무 대안을 한 번에 정리한다.

왜 SSH가 끊기면 프로세스가 죽는가

  • 로그인 셸은 부모 세션의 프로세스 그룹을 관리한다.
  • 세션이 종료되면 커널이 해당 그룹에 SIGHUP을 브로드캐스트한다.
  • 많은 프로그램이 기본 처리로 SIGHUP에서 종료한다.
    핵심은 “세션에 묶이지 않게 만들고(HUP 무시), 입출력 스트림을 터미널에서 떼는 것”이다.

nohup의 동작 원리

nohup은 실행 전 다음을 수행한다.

  • SIGHUP 무시: signal(SIGHUP, SIG_IGN)
  • 표준 출력/오류 재지정: 지정이 없다면 nohup.out로 보냄
  • 표준 입력: 터미널로 남아있으면 예상치 못한 블로킹이 생길 수 있으므로 /dev/null로 바꾸는 습관이 안전하다.

기본 예시

nohup ./long_job.sh &

위처럼만 써도 동작하지만, 실무에서는 로그와 입력을 명시해 예측 가능하게 만든다.

실무에서 안전한 실행 패턴

다음 1줄을 템플릿처럼 외워두면 불상사를 줄인다.

nohup <cmd> </dev/null > job.out 2>&1 &
  • < /dev/null: 표준 입력을 닫아 대기 상태 방지
  • > job.out 2>&1: 출력과 오류를 한 파일로 합쳐 분석 일관성 확보
  • &: 백그라운드 실행. nohup만으로는 포그라운드에 남을 수 있다.

로그를 날짜별로 분리하고 싶다면:

TS=$(date +%F_%H%M%S)
nohup <cmd> </dev/null > logs/<name>.$TS.log 2>&1 &

nohup.out의 위치와 권한

  • 파일을 지정하지 않으면 CWD에 nohup.out이 생성된다. CWD에 쓰기 권한이 없으면 $HOME/nohup.out로 떨어질 수 있다.
  • 컨테이너·크론 환경처럼 CWD가 애매한 경우, 반드시 경로를 절대경로로 지정한다.

파이프라인과 nohup

파이프 양쪽 중 어느 쪽이 HUP을 받느냐에 따라 결과가 달라진다. 가장 쉬운 해법은 “전체 파이프라인을 셸 한 번으로 감싸고 그 셸을 nohup 처리”하는 것이다.

nohup bash -c 'producer | filter | consumer' </dev/null > pipe.log 2>&1 &

개별 커맨드에 nohup을 중첩하는 것보다 오류 원인을 줄인다.

중간에 nohup 없이 시작했다면: disown

작업을 이미 시작했고 로그도 잘 나오는데, 갑자기 연결을 끊어야 한다면 셸의 잡 제어로 세션 연결을 끊을 수 있다.

# 1) 현재 포그라운드 잡을 백그라운드로
^Z
bg %1

# 2) HUP 무시(선택)
trap '' HUP

# 3) 셸이 해당 잡과의 관계를 끊음
disown -h %1

-h는 HUP 무시 플래그를 남긴다. 다만 일부 프로그램은 이미 표준 출력을 터미널에 묶어둔 채 버퍼링하고 있을 수 있으니, 처음부터 리다이렉션과 함께 실행하는 편이 안정적이다.

세션에서 완전히 분리: setsid

세션 리더가 아닌 새 세션에서 실행해 SIGHUP·CONT 등 세션 신호를 차단한다. nohup과 궁합이 좋다.

setsid nohup <cmd> </dev/null > run.log 2>&1 &

setsid만 쓰면 HUP 무시는 보장되지 않으므로, 장시간 배치라면 nohup과 함께 사용한다.

systemd-run으로 “작은 서비스”처럼 돌리기

서버가 systemd라면 SSH와 무관한 독립 유닛으로 띄우는 것이 가장 견고하다. 로그도 저널로 수집되고, 재부팅 이후 정책도 설정할 수 있다.

# 현재 사용자 스코프로 실행
systemd-run --user --scope --unit=adhoc-backup bash -lc \
'./backup.sh </dev/null > ~/logs/backup.$(date +%F).log 2>&1'

# 데몬화(백그라운드 유지), 재시작 정책과 시간 제한 예시
systemd-run --unit=myjob --property=Restart=on-failure \
--property=RuntimeMaxSec=8h --collect bash -lc '<cmd>' 

실행 후 상태와 로그 확인:

systemctl --user status adhoc-backup
journalctl --user -u adhoc-backup -f

루트 단위로 돌릴 땐 --user를 빼고, 접근 권한과 보안 정책을 점검한다.

크론·타이머와의 차이

  • 크론은 기본적으로 메일로 출력 결과를 보낸다. 잡별로 명시적으로 리다이렉션하자.
  • systemd-timer는 유닛과 세트로 관리되어 재현성과 관측성이 우수하다. ad-hoc 작업이 반복 패턴으로 굳어지면 타이머로 승격하는 전략이 관리 비용을 줄인다.

로그 설계와 회전

장시간 작업은 로그가 급격히 커질 수 있다.

  • 파일 로그: logrotate 정책을 준비한다.
  • 저널: SystemMaxUse, RuntimeMaxUse 등 용량 제한을 설정한다.
  • 실시간 추적: tail -F job.out처럼 파일 회전에도 끊기지 않는 옵션을 사용한다.

리소스 한도와 ulimit

SSH가 끊겨도 작업이 계속되려면 자원 한도에도 걸리지 않아야 한다. 시작 전에 확인한다.

ulimit -a
# 특히 nofile, nproc, as, fsize

서비스화(systemd-run)할 땐 유닛 속성으로 한도를 명시한다.

systemd-run --unit=myjob -p LimitNOFILE=200000 -p TasksMax=infinity bash -lc '<cmd>'

신호와 종료 처리

nohup은 HUP만 무시하도록 설정한다. SIGTERM·SIGINT·SIGKILL은 여전히 유효하다. 종료 훅을 활용하려면 애플리케이션에서 신호 핸들러를 갖추거나, 래퍼 스크립트에 트랩을 둔다.

#!/usr/bin/env bash
set -euo pipefail
cleanup(){ echo "cleanup..."; }
trap cleanup TERM INT
<main logic>

실패 사례와 해결

  • 작업이 멈춘 것처럼 보임: 표준 입력이 터미널에 남아 read 대기 중일 수 있다 → < /dev/null 필수.
  • 로그가 비어 있음: 버퍼링으로 즉시 쓰지 않는 프로그램일 수 있다 → stdbuf -oL -eL로 줄단위 버퍼링 또는 프로그램 자체 옵션 사용.
nohup stdbuf -oL -eL <cmd> </dev/null > job.out 2>&1 &
  • nohup인데도 종료됨: 래퍼 스크립트가 내부에서 새 프로세스를 만들어 세션 신호를 별도로 처리했을 수 있다 → setsid를 병행하거나 systemd-run으로 격리.
  • 권한 문제로 nohup.out이 엉뚱한 곳에 생김: 항상 절대경로 로그를 지정.

보안·운영 관점 체크리스트

  • 실행 사용자와 권한: 민감 파일을 쓰는 작업은 최소 권한 원칙 적용
  • 자원 한도: ulimit와 cgroup(systemd 속성) 확인
  • 로깅: 위치·보존 주기·회전 정책 명시
  • 재부팅 내구성: ad-hoc이면 nohup, 반복이면 systemd-timer
  • 관측성: 진행률 출력, 부분 결과 저장, 실패 시 알림 경로 마련

빠르게 쓰는 스니펫 모음

# 가장 안전한 1줄
nohup <cmd> </dev/null > /var/log/<app>/run.$(date +%F_%H%M%S).log 2>&1 &

# 이미 돌아가는 잡을 세션에서 분리
bg %1 && disown -h %1

# 세션 완전 분리
setsid nohup <cmd> </dev/null > run.log 2>&1 &

# 파이프라인 전체를 보호
nohup bash -c 'producer | filter | consumer' </dev/null > pipe.log 2>&1 &

# systemd로 “작은 서비스”처럼 실행하고 추적
systemd-run --user --unit=adhoc-job bash -lc '<cmd>'
journalctl --user -u adhoc-job -f

마무리

nohup은 SIGHUP 무시와 입출력 분리를 통해 “세션과 운명을 같이하지 않는 작업”을 간단히 만든다. 여기에 < /dev/null과 명시적 로그 리다이렉션을 습관화하면 대부분의 원격 작업은 안정적으로 살아남는다. 파이프라인·세션·재부팅까지 고려해야 하는 시나리오라면 setsidsystemd-run을 조합해 “작업을 서비스처럼” 다루는 것이 운영 비용과 실패 확률을 함께 줄여준다.