SSH 연결 종료 후에도 프로세스 유지하는 방법
SSH 연결이 끊기면 커널이 SIGHUP 신호를 보내 프로세스가 종료됩니다. nohup, disown, tmux, systemd-run 명령어를 사용하여 세션 종료 후에도 작업을 안전하게 유지하는 방법을 단계별로 설명합니다. 각 도구의 차이점과 상황별 적합한 선택 기준을 확인하십시오.
Why your command dies when SSH disconnects
To keep a command running after an SSH disconnect, the command has to end up somewhere the hangup signal cannot reach it. Every method below is a different way of arranging that, so start with the mechanism.
Your login runs on a pty (pseudo-terminal), a virtual terminal device that sshd creates on the server for your session. It is the controlling terminal of your shell and of every command you start from that shell. If you want the rest of that path, what SSH sets up when you log in covers it. When the TCP connection dies, sshd closes its end and the pty is destroyed. The kernel treats that as the terminal hanging up, so it sends SIGHUP to the foreground process group of that terminal and to the session leader, which is your shell. The default action for SIGHUP is to end the process. Your command was in the foreground process group, so your command dies.
Background jobs are not safe either. A job started with & sits in its own process group, so the kernel does not signal it directly. Bash does. On receiving SIGHUP, an interactive bash resends SIGHUP to every job in its table before exiting. From your side the result looks identical: the job is gone and the log file stops mid-line.
There is an asymmetry here that confuses people. Typing exit does not hang up your background jobs, because bash only does that when the huponexit option is set, and it is off by default. A dropped connection does hang them up. The job that survived you closing the terminal politely can still die when the wifi drops.
Two consequences follow, and they are the whole subject. A process that ignores SIGHUP, or that has no controlling terminal at all, will not be hung up. And a process whose standard output still points at the destroyed pty has nowhere to write: the write fails with EIO (input/output error), and most programs exit at that point. You have to solve both halves. Many recipes solve only the first, which is why people report that "nohup did not work".
If your connection drops several times a day, fix that as well. ServerAliveInterval 60 in ~/.ssh/config stops an idle session from being discarded by a NAT (network address translation) timeout somewhere on the path. A session that never opens at all is a different fault with different causes, which is where the difference between connection refused and connection timed out matters.
SSH 연결이 끊긴 뒤에도 명령을 계속 실행하려면 어떤 방법을 사용해야 합니까?
작업의 중요도에 따라 네 가지 답변을 제시합니다.
nohup또는setsid: 지금 시작하고 나중에 로그를 확인할 일회성 작업에 적합합니다. 출력은 직접 리다이렉션해야 합니다.disown: 이미 시작했지만 보호하는 것을 잊은 작업에 사용합니다. 프로세스를 구조할 수 있지만, 이미 지나간 출력 내용을 다시 보여주지는 못합니다.tmux또는screen: 며칠에 걸쳐 모니터링하고, 중단했다가 다시 돌아와야 하는 작업에 적합합니다.systemd-run또는 실제 unit 파일: 6시간짜리rsync작업이나 밤새 진행되는 데이터베이스 가져오기처럼 로그인 세션보다 오래 유지되어야 하는 모든 작업에 사용합니다.
기억해야 할 규칙은 다음과 같습니다. 작업을 잊어버리는 것이 문제가 된다면, 그 작업은 tmux가 아니라 systemd가 관리해야 합니다. tmux 창은 사람이 직접 기억해야 하는 대상입니다. 반면 unit은 이름, 상태, 로그, 재시작 정책을 가지고 있어 다음 관리자가 별도의 안내 없이도 내용을 파악할 수 있습니다.
nohup과 setsid: 프로세스 실행 후 터미널 종료하기
nohup ./import.sh > ~/import.log 2>&1 &
echo $! > ~/import.pidnohup은(는) SIGHUP의 처리를 무시하도록 설정한 뒤 명령을 실행하므로, 커널이 보내는 hangup 신호가 아무런 영향을 주지 못합니다. 출력 리다이렉션은 사용자가 직접 지정해야 합니다. 표준 출력을 터미널로 그대로 두면 nohup이(가) 자동으로 현재 디렉터리의 nohup.out로 리다이렉트하며, 실패 시 $HOME/nohup.out을(를) 사용하고 다음 메시지를 출력합니다.
nohup: ignoring input and appending output to 'nohup.out'해당 파일은 위치를 놓치기 쉬우므로 직접 이름을 지정하는 것이 좋습니다. $!에는 마지막 백그라운드 작업의 PID(프로세스 식별자)가 저장되므로, 이를 기록해 두면 다시 로그인한 뒤 작업을 확인할 수 있습니다.
setsid은(는) 같은 문제를 다른 방식으로 해결합니다. 제어 터미널이 없는 새로운 세션에서 명령을 실행하므로, 프로세스를 종료시킬 터미널 자체가 존재하지 않게 됩니다.
setsid --fork ./import.sh > ~/import.log 2>&1--fork 옵션을 사용하십시오. 이 옵션이 없으면 setsid는 프로세스 그룹 리더가 아닌 경우(셸 스크립트 내부에서 실행할 때 등) setsid()를 호출하며, 이로 인해 스크립트가 차단된 상태로 멈추게 됩니다. --fork을(를) 사용하면 스크립트와 프롬프트 환경 모두에서 동일하게 동작합니다.
실제 실행 결과를 확인하십시오.
ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat ~/import.pid)"?의 TTY 열에 표시된 값은 해당 프로세스에 제어 터미널이 없음을 의미하며, 따라서 어떤 신호로도 프로세스를 종료할 수 없습니다. nohup 환경에서 TTY 열은 연결이 유지되는 동안 pts/0와(과) 같이 표시되다가, pty가 파괴되면 ?로 변경됩니다. 두 결과 모두 정상입니다. 작업은 안전하게 유지되었습니다.
disown: 이미 시작한 작업 구제하기
2시간이 소요되는 작업을 포그라운드에서 시작한 뒤에야 이 문제를 떠올렸을 수 있습니다. 작업을 강제로 종료하고 처음부터 다시 시작하지 마십시오.
# press Ctrl-Z to suspend the job first
bg
jobs -l
disown -h %1Ctrl-Z은 작업을 일시 중지하고, bg는 해당 작업을 백그라운드에서 재개하며, jobs -l는 작업 번호를 PID 옆에 출력합니다. disown -h %1은 bash가 해당 작업에 SIGHUP 신호를 보내지 않도록 표시를 남깁니다. 단순히 disown %1을 사용하면 bash의 작업 목록에서 해당 작업이 완전히 제거되는데, 이는 SIGHUP 신호에 대해 동일한 효과를 내지만 이후 jobs 명령으로는 해당 작업을 확인할 수 없게 됩니다.
disown이 해결할 수 없는 문제는 출력 경로를 변경하는 것입니다. 프로세스는 여전히 표준 출력으로 pty를 점유하고 있으며, pty가 사라지면 다음 쓰기 작업 시 EIO 오류가 반환됩니다. 따라서 disown는 파일에 기록하는 컴파일 작업처럼 출력이 적은 작업은 안정적으로 유지하지만, 출력이 많은 작업은 종종 실패하게 만듭니다. 작업은 출력할 곳이 없는 상태로 생존하거나, 다음 출력 줄을 시도할 때 종료됩니다.
파일 디스크립터를 구제하기 위한 도구가 있습니다. reptyr은 실행 중인 프로세스를 현재 터미널로 옮겨줍니다. sudo apt install -y reptyr로 설치한 뒤, tmux 창 안에서 reptyr <pid>를 실행하십시오. 이 도구는 ptrace을 통해 작동하며, Ubuntu는 kernel.yama.ptrace_scope = 1을 기본으로 제공합니다. 이는 본인의 자식 프로세스만 추적할 수 있도록 허용하므로, 상속받은 프로세스의 경우 sudo reptyr <pid> 설정이 필요합니다. 이 도구는 비상용으로만 사용하십시오. 일상적인 작업 흐름에 포함해서는 안 됩니다.
tmux: 작업 유지 및 재접속
tmux(terminal multiplexer)는 다른 방식으로 문제를 해결합니다. 프로세스를 pty로부터 보호하는 대신, SSH 세션에 종속되지 않는 pty를 프로세스에 제공합니다. tmux 서버는 세션 외부에서 실행되며 내부의 모든 터미널을 소유합니다. 사용자의 SSH 연결은 단지 서버에 연결된 뷰어일 뿐입니다. 연결을 끊어도 서버는 이를 감지하지 못합니다.
sudo apt update && sudo apt install -y tmux
tmux new -s import해당 창에서 작업을 시작한 뒤, Ctrl-b을 누르고 d을 눌러 세션에서 분리(detach)합니다. 나중에 다시 로그인하여 작업을 이어갈 수 있습니다.
tmux ls
tmux attach -t importtmux ls를 실행하면 import: 1 windows으로 시작하는 줄이 출력되어야 합니다. 만약 no server running on /tmp/tmux-1000/default가 출력된다면, 세션이 생성되지 않았거나 서버가 종료된 상태이므로 연결할 세션이 없는 것입니다.
screen도 다른 키 조합으로 같은 역할을 수행합니다. screen -S import으로 세션을 생성하고, Ctrl-a을 누른 뒤 d을 눌러 분리합니다. screen -ls는 존재하는 세션 목록을 보여주며, screen -r import은 세션에 다시 연결합니다. 두 도구 모두 사용 가능하며, 분리 키를 기억하는 것이 중요합니다.
멀티플렉서는 신호 끊김에도 작업을 유지해야 하는 대화형 작업에 적합한 환경입니다. 이것이 바로 tmux 내부에서 VPS의 Claude Code를 실행하는 것이 일반적인 설정인 이유이며, 몇 분마다 재연결되는 모바일 네트워크 환경에서 휴대전화로 서버 세션을 제어할 수 있게 해주는 핵심 요소입니다.
systemd-run: PID 1에 작업 위임하기
현재 세션과 완전히 독립적으로 실행되어야 하는 작업은 init 시스템에 맡겨야 합니다.
sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/이 명령은 bigsync.service이라는 이름의 일시적인(transient) 서비스 유닛을 생성합니다. 이 유닛은 별도의 cgroup을 할당받으며, 제어 터미널이 없고, 현재 로그인 세션과 아무런 관계를 맺지 않습니다. 명령은 즉시 반환되며 Running as unit: bigsync.service를 출력합니다. 다음 명령으로 상태를 확인하십시오.
systemctl status bigsync
journalctl -u bigsync -f--collect 옵션은 작업이 종료되면(실패하더라도) 해당 유닛을 즉시 제거하도록 systemd에 지시합니다. 이 옵션이 없으면 실패한 일시적 유닛이 로드된 상태로 남아 유닛 이름이 점유되므로, 다음 실행 시 유닛이 이미 존재한다는 오류가 발생합니다. 출력 내용은 타임스탬프와 함께 저널(journal)에 기록됩니다. 저널 항목이 재부팅 후에도 유지되려면 /var/log/journal 디렉터리가 존재해야 합니다. 이를 위해 sudo mkdir -p /var/log/journal를 실행한 뒤 systemd-journald을 재시작하십시오.
일반 사용자 권한으로 sudo 옵션 없이 systemd-run을 호출하면 polkit 인증을 요구하며 ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===를 출력합니다. 시스템 유닛을 대상으로 하려면 sudo 옵션을 사용하십시오.
사용자 관리자(user manager)를 통해 작업을 실행할 수도 있습니다.
systemd-run --user --unit=bigsync --collect /usr/bin/rsync -aH /srv/data/ /mnt/backup/여기에는 주의할 점이 있습니다. 사용자별 관리자인 user@1000.service은 일반적으로 마지막 세션이 종료될 때 함께 중단되며, 이때 모든 사용자 유닛도 함께 종료됩니다. 이를 방지하려면 다음 명령으로 링어링(lingering)을 활성화하십시오.
loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=Linger두 번째 명령은 Linger=yes를 출력해야 합니다. 링어링이 활성화되면 사용자 관리자가 부팅 시점에 시작되어 로그인 여부와 관계없이 계속 실행됩니다. 이 설정이 없으면 systemd-run --user은 nohup와 비교했을 때 아무런 이점이 없습니다.
systemd-run --scope는 다른 용도의 명령입니다. 이 명령은 터미널에 연결된 포그라운드에서 실행되므로 본 목적에는 적합하지 않습니다.
두 번 이상 실행할 작업이라면 매번 일시적인 명령을 입력하지 말고 유닛 파일을 작성하십시오.
반복 실행할 작업을 위한 영구 유닛
[Unit]
Description=Nightly data sync
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=deploy
WorkingDirectory=/srv/data
ExecStart=/usr/local/bin/nightly-sync.sh이 내용을 /etc/systemd/system/nightly-sync.service으로 저장하고, sudo systemctl daemon-reload을 실행한 뒤, sudo systemctl start nightly-sync로 시작하고 journalctl -u nightly-sync로 상태를 확인하십시오. 필요에 따라 특정 시간에 실행되도록 하려면 대응하는 .timer 파일을 추가하십시오.
systemd 서비스 유닛 및 타이머 작성하기에서 파일 형식과 스케줄 구문을 상세히 다룹니다.
출력 대상과 데이터가 사라지는 이유
리다이렉션의 순서는 중요합니다. > file 2>&1은 표준 출력을 파일로 지정한 뒤 표준 에러도 같은 곳으로 지정합니다. 2>&1 > file는 반대로 동작합니다. 표준 에러는 계속 터미널로 향하게 되는데, 터미널은 곧 사라질 대상이기 때문입니다. Bash는 두 스트림을 동시에 처리하기 위해 &> file도 지원합니다.
두 번째로 주의할 점은 버퍼링입니다. 표준 출력이 터미널일 때는 C 라이브러리가 매 줄마다 플러시를 수행합니다. 표준 출력이 파일이면 수 킬로바이트 단위의 블록 버퍼로 전환되므로, tail -f ~/import.log는 몇 분 동안 아무것도 표시하지 않아 작업이 멈춘 것처럼 보일 수 있습니다. stdbuf -oL ./import.sh > ~/import.log 2>&1를 사용하여 라인 버퍼링을 강제하거나, python3 -u 또는 grep --line-buffered과 같이 프로그램 자체 옵션을 사용하십시오.
다음 패턴은 피해야 합니다.
nohup ./import.sh 2>&1 | tee ~/import.log &nohup은 import.sh만 보호합니다. tee은 동일한 파이프라인 내의 별도 프로세스이므로, 행업(hangup) 발생 시 여전히 종료됩니다. 그러면 import.sh은 읽는 주체가 없는 파이프에 데이터를 쓰게 되어 SIGPIPE 신호를 받고 중단됩니다. 전체 파이프라인을 setsid bash -c '...' 내부에 넣거나, 파일에 직접 기록한 뒤 재접속 시 tail -f를 실행하십시오.
rsync 사용 시 한 가지 세부 사항이 더 있습니다. --info=progress2은 터미널에서는 올바르게 보이지만 로그 파일이나 저널에서는 하나의 거대한 줄로 기록되는 캐리지 리턴 스트림을 생성합니다. 자동화된 실행 환경에서는 이를 제거하고 대신 --stats을 사용하십시오.
셸에서 잘 작동하던 작업이 systemd나 cron에서 실패하는 이유
대화형 셸은 /etc/profile, ~/.profile, ~/.bashrc를 읽어 들이므로 사용자의 PATH, 버전 관리자 심(shim), 내보낸(exported) 변수들을 모두 가지고 있습니다. 하지만 systemd 유닛은 이러한 파일을 전혀 읽지 않습니다. cron 역시 마찬가지입니다. Debian 및 Ubuntu에서 cron은 SHELL=/bin/sh와 PATH=/usr/bin:/bin 환경으로 작업을 실행합니다.
systemd 환경에서 나타나는 증상은 systemctl status가 (code=exited, status=203/EXEC)를 보고하는 것입니다. 이는 경로가 잘못되었거나 파일에 실행 권한이 없어 systemd가 해당 파일을 전혀 실행할 수 없음을 의미합니다. cron 환경에서는 보통 command not found 오류가 발생하며, 이는 로컬 메일로 전달되거나 메일 시스템이 설치되지 않은 경우 어디에도 전달되지 않습니다.
무작정 원인을 추측하며 시간을 낭비하기 전에 실제 환경이 어떤지 확인하십시오.
sudo systemd-run --collect --wait --unit=envtest /usr/bin/env
journalctl -u envtest --no-pager위 명령은 작업이 실행될 정확한 환경을 출력합니다. 그 후 차이를 수정하십시오. systemd는 단순히 rsync만 입력하면 고정된 시스템 경로 목록에서만 이를 찾고 사용자의 PATH은 참조하지 않으므로, 직접 만든 모든 항목에는 절대 경로를 사용해야 합니다. 필요한 변수는 명령줄에서 -p Environment="KEY=value"를 사용하거나 유닛 파일 내 EnvironmentFile=/etc/default/myjob 지시어를 통해 전달하십시오. 작업이 반드시 사용자의 로그인 환경을 필요로 한다면, 해당 작업을 /bin/bash -lc 'my-command'로 실행하고 이 작업이 사용자의 도트파일(dotfiles)에 의존하게 됨을 받아들여야 합니다.
분리된 작업이 여전히 종료되는 이유
- 재부팅. tmux의 모든 상태는 메모리에 존재하며 서버는 일반적인 프로세스이므로 재부팅 시 아무것도 유지되지 않습니다. 커널 업데이트는 재부팅을 동반하므로, 쉽게 다시 시작할 수 없는 작업은
systemctl enable를 사용하여 관리해야 합니다. - OOM(Out-of-memory) 킬러.
dmesg -T | grep -i 'killed process'명령을 실행하면 종료된 프로세스 이름을 포함한 관련 정보를 확인할 수 있습니다. 사양이 낮은 VPS에서 대규모 데이터를 가져오는 작업은 OOM 킬러의 주된 대상이 됩니다. - logind 정리 작업.
/etc/systemd/logind.conf에서KillUserProcesses=yes를 설정하면 마지막 세션이 종료될 때 tmux 서버를 포함한 모든 잔여 프로세스가 종료됩니다.loginctl show --property=KillUserProcesses으로 현재 설정을 확인하고,loginctl enable-linger "$USER"을 사용하여 해당 사용자를 예외 처리하십시오. - 디스크 용량 부족. 작업이 중단된 이유는 사용자가 세션을 떠났기 때문이 아니라, 리다이렉션한 로그 파일이 파일 시스템을 가득 채웠기 때문일 수 있습니다. 신호를 탓하기 전에
df -h을 실행하여 확인하십시오.
SSH 연결을 유지하지 않고 작업 시작하기
ssh vps 'sudo systemd-run --unit=import --collect /usr/local/bin/import.sh'systemd-run은 유닛이 시작되자마자 반환되므로 ssh 명령도 즉시 반환되며, 작업은 이를 시작한 세션과 연결되지 않습니다. 이것이 깔끔한 방식입니다.
nohup 버전은 더 주의가 필요합니다.
ssh vps 'nohup ./import.sh > ~/import.log 2>&1 < /dev/null &'리다이렉션이 없으면 이 명령은 멈춘 것처럼 보입니다. sshd는 원격 명령의 표준 출력이나 표준 에러를 사용하는 프로세스가 하나라도 남아 있으면 채널을 열어두는데, 백그라운드 작업은 이 둘을 모두 상속받기 때문입니다. nohup만으로는 해결되지 않습니다. nohup는 출력이 터미널일 때만 리다이렉트하는데, 여기서는 클라이언트로 돌아가는 파이프이기 때문입니다. < /dev/null를 추가하면 입력 측도 닫힙니다. ssh -n도 클라이언트 측에서 동일한 역할을 수행합니다.
FAQ
SSH 연결이 끊기면 왜 명령어가 중단됩니까?
세션이 사용하던 pty(의사 터미널)가 파괴되면 커널은 해당 터미널의 포그라운드 프로세스 그룹에 SIGHUP을 보냅니다. SIGHUP의 기본 동작은 프로세스를 종료하는 것입니다. bash가 종료되기 전에 테이블에 있는 모든 작업에 SIGHUP를 다시 보내기 때문에 백그라운드 작업도 함께 종료됩니다. nohup로 시작한 명령어처럼 SIGHUP을 무시하거나, systemd 유닛처럼 애초에 세션을 공유하지 않은 명령어는 영향을 받지 않습니다.
6시간 동안 실행되는 rsync 작업에는 tmux가 좋습니까, systemd-run이 좋습니까?
systemd-run입니다. tmux 세션은 사용자가 시작한 서버 프로세스에 의존하므로 재부팅 시 종료되며, tmux ls을 실행해야 한다는 사실을 모르는 사람은 해당 세션을 볼 수 없습니다. sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/를 실행하면 상태는 systemctl status bigsync로, 출력은 journalctl -u bigsync으로 관리되므로 다음 관리자가 별도의 안내 없이도 확인할 수 있습니다. 화면을 보면서 직접 입력해야 하는 작업에는 tmux를 사용하십시오.
리다이렉션을 잊은 작업의 출력은 어떻게 확인합니까?
일반적으로는 확인할 수 없습니다. 출력값이 이미 존재하지 않는 터미널로 전달되었기 때문입니다. 프로세스가 실행 중이라면 sudo ls -l /proc/<pid>/fd로 열린 파일을 조사하거나 sudo strace -p <pid>로 시스템 호출을 관찰할 수 있지만, 이미 기록된 텍스트는 사라진 상태입니다. reptyr <pid>를 사용하면 프로세스를 새로운 터미널로 옮길 수 있으나, Ubuntu의 kernel.yama.ptrace_scope = 1 설정 때문에 자신의 자식 프로세스가 아닌 경우에는 sudo 권한이 필요합니다. 이러한 문제를 방지하려면 작업 시작 시 파일로 리다이렉션하고 tail -f 명령어로 해당 파일을 확인하는 습관을 들이십시오.
분리된(detached) tmux 세션은 재부팅 후에도 유지됩니까?
아니요. tmux 서버는 일반적인 프로세스이며 세션은 메모리 내 상태이므로 재부팅 시 둘 다 종료됩니다. 또한 /etc/systemd/logind.conf이 KillUserProcesses=yes를 설정하고 마지막 세션에서 로그아웃할 때도 종료되는데, loginctl enable-linger "$USER" 설정은 이를 방지합니다. 재부팅 후에도 자동으로 다시 시작되어야 하는 작업은 systemd 유닛을 작성하고 systemctl enable을 수행하십시오.
스크립트는 셸에서 잘 실행되는데 왜 systemd 유닛으로 실행하면 실패합니까?
유닛은 /etc/profile이나 ~/.bashrc을 읽지 않으므로 사용자의 PATH 추가 사항이나 내보낸(exported) 변수가 없습니다. (code=exited, status=203/EXEC)이 표시되는 systemctl status 오류는 systemd가 파일을 전혀 실행할 수 없음을 의미하므로, 절대 경로를 사용하고 실행 권한(executable bit)이 설정되었는지 확인하십시오. sudo systemd-run --collect --wait --unit=envtest /usr/bin/env를 실행하고 journalctl -u envtest으로 내용을 읽어보면 작업이 실제로 사용하는 환경을 정확히 파악할 수 있습니다. 누락된 항목은 Environment=나 EnvironmentFile=를 사용하여 제공하십시오.