프로세스가 죽었는데 아무도 안 살려줬다
새벽에 앱 프로세스가 조용히 죽고, 아침에 출근해서야 "서비스가 안 되네요" 문의를 받는 상황. 리눅스 서버에 자체 데몬을 올려 운영하다 보면 한 번쯤 겪게 되는 일이다. 다행히 systemd를 쓰고 있다면 이 문제는 유닛 파일 한두 줄로 끝난다.
컨테이너·쿠버네티스가 표준처럼 보여도, 베어메탈과 VM 위에서 돌아가는 워크로드에서는 여전히 systemd가 사실상의 프로세스 슈퍼바이저다. 쿠버네티스의 restartPolicy로 넘어가기 전 단계, 혹은 하이브리드 환경에서 "죽으면 알아서 살아나는" 서비스를 만드는 건 여전히 기본기다.
이 글에서는 복붙해서 바로 쓰는 유닛 파일 템플릿부터 시작해, Restart 옵션 6종을 상황별로 정확히 고르는 법, 그리고 잘못 설정하면 발생하는 무한 재시작 루프까지 막는 실전 설정을 한 번에 정리한다.
- 적용 범위: systemd 기반 배포판(Ubuntu 18.04+, RHEL/Rocky 8+, Debian 10+ 등), systemd 245 이상 기준. 250+에서 안정화된
StartLimitIntervalSec표기를 중심으로 설명한다.
최소 복붙 템플릿: 3줄이면 자동재시작 완성
먼저 가장 단순한 형태부터 보자. /etc/systemd/system/myapp.service 파일을 만들고 아래 내용을 붙여넣으면 된다.
[Unit]
Description=My App Daemon
After=network.target
[Service]
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target핵심은 [Service] 섹션의 세 줄이다.
ExecStart— 실행할 명령. 반드시 절대경로로 적는다.myapp처럼 상대경로나 PATH 의존 명령을 쓰면 실행이 안 된다.Restart=on-failure— 비정상 종료 시 다시 살린다.RestartSec=5— 죽고 나서 5초 뒤에 재시작. 값이 없으면 기본 100ms라 크래시 루프 시 CPU를 태울 수 있어 명시하는 게 좋다.
루프 방지까지 넣은 실전 완전 버전
실무에서는 여기에 무한 재시작을 막는 안전장치까지 넣어 쓴다. 아래가 그대로 프로덕션에 붙여도 되는 완전한 유닛 파일이다.
[Unit]
Description=My App Daemon
After=network.target
# 60초 창 안에서 3번까지만 재시작 허용
StartLimitIntervalSec=60
StartLimitBurst=3
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp --config /opt/myapp/config.yaml
Restart=on-failure
RestartSec=5
# 로그를 journal로 흘려보냄
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target버전 주의:
StartLimitIntervalSec와StartLimitBurst는[Unit]섹션에 위치한다([Service]아님). systemd 230 미만 구버전에서는StartLimitInterval(뒤에Sec없음)로 표기했는데, 최신 버전은 둘 다 인식하지만 신규 작성 시StartLimitIntervalSec를 권장한다.systemctl --version으로 버전을 먼저 확인하자.
Restart 옵션 완전 비교: 웹서버는 always, 배치잡은 on-failure
Restart=에는 6가지 값이 있고, 종료 방식(정상/비정상/시그널)에 따라 재시작 여부가 달라진다. 잘못 고르면 "정상 종료했는데 계속 살아난다"거나 "죽었는데 안 살아난다" 같은 혼란이 생긴다.
| Restart 값 | 정상종료(exit 0) | 비정상종료(exit≠0) | SIGTERM 등 시그널 | watchdog 타임아웃 | 대표 사용처 |
|---|---|---|---|---|---|
no (기본) | ✗ | ✗ | ✗ | ✗ | 일회성 스크립트, 수동 관리 |
on-success | ✓ | ✗ | SIGHUP/TERM/INT/PIPE만 ✓ | ✗ | 주기 재실행이 필요한 정상종료 잡 |
on-failure | ✗ | ✓ | ✓ | ✓ | 배치잡, CLI 데몬 |
on-abnormal | ✗ | ✗ | ✓ | ✓ | 크래시·킬만 복구, 종료코드는 무시 |
on-watchdog | ✗ | ✗ | ✗ | ✓ | watchdog 연동 헬스체크 서비스 |
always | ✓ | ✓ | ✓ | ✓ | 웹서버, 상시 데몬 |
선택 가이드는 이렇게 정리하면 된다.
- 웹서버·API 서버처럼 절대 멈추면 안 되는 상시 데몬 →
Restart=always. 어떤 이유로 종료되든 무조건 살린다. - 정상 종료(exit 0)가 "일이 끝났다"는 의미인 배치·워커 →
Restart=on-failure. 실패했을 때만 재시도하고, 정상 완료 시엔 조용히 끝낸다. - 종료코드는 신경 안 쓰고 크래시/강제 킬만 복구하고 싶을 때 →
on-abnormal.
여기서 헷갈리는 포인트: systemctl stop으로 서비스를 내리는 것은 어떤 Restart 값이든 재시작을 트리거하지 않는다. systemd가 의도적으로 보낸 stop 시그널은 "정상적인 중지"로 간주되기 때문이다. 그래서 always를 걸어도 관리자가 stop 하면 얌전히 멈춘다. 이건 버그가 아니라 설계다.
무한 재시작 루프 방지: StartLimit의 동작 원리
Restart=always를 걸었는데 앱이 설정 오류로 실행 즉시 죽는다면? RestartSec 간격으로 영원히 재시작을 반복하며 journal 로그와 CPU를 갉아먹는다. 이걸 막는 게 StartLimitIntervalSec와 StartLimitBurst다.
동작 원리는 단순하다.
StartLimitIntervalSec(시간 창) 안에서 StartLimitBurst(횟수)를 초과해 재시작이 발생하면, systemd가 재시작을 포기하고 서비스를
failed상태로 고정한다.
예를 들어 위 예제의 StartLimitIntervalSec=60, StartLimitBurst=3 조합은 이렇게 계산된다.
- 60초라는 시간 창을 본다.
- 그 안에서 시작 시도가 3번까지는 허용.
- 4번째 시도가 60초 창 안에 들어오면 → 재시작 중단,
start-limit-hit.
앱이 실행 즉시 죽고 RestartSec=5라면: 0초 시작 → 죽음 → 5초 재시작 → 죽음 → 10초 재시작 → 죽음 → 15초에 4번째... 이 시점이 60초 창 안이므로 여기서 멈춘다.
start-limit-hit 로그 확인
한계에 도달하면 systemctl status에서 아래와 같은 출력을 볼 수 있다.
● myapp.service - My App Daemon
Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
Active: failed (Result: start-limit-hit) since Fri 2026-07-10 09:14:22 KST
Duration: 15s
Jul 10 09:14:22 host systemd[1]: myapp.service: Scheduled restart job, restart counter is at 3.
Jul 10 09:14:22 host systemd[1]: Stopped My App Daemon.
Jul 10 09:14:22 host systemd[1]: myapp.service: Start request repeated too quickly.
Jul 10 09:14:22 host systemd[1]: myapp.service: Failed with result 'start-limit-hit'.
Jul 10 09:14:22 host systemd[1]: Failed to start My App Daemon.핵심 문구는 **Start request repeated too quickly**와 **Result: start-limit-hit**다. 이게 보이면 "죽어서 안 살아나는" 게 아니라 "너무 자주 죽어서 systemd가 포기한" 상태다. 근본 원인(설정 오류, 포트 충돌 등)을 고치지 않으면 재시작해봤자 또 멈춘다.
복구: reset-failed
원인을 고친 뒤 카운터를 초기화하고 다시 시작하려면:
# 실패 카운터를 리셋 (start-limit-hit 상태 해제)
sudo systemctl reset-failed myapp
# 다시 시작
sudo systemctl start myappreset-failed 없이 그냥 start만 하면, 시간 창이 지나기 전엔 여전히 한계에 걸려 다시 start-limit-hit이 뜰 수 있다.
적용·검증 명령 런북
유닛 파일을 저장했다면 이제 아래 순서대로 적용하고 검증한다. 순서가 중요하다.
# 1) 유닛 파일을 새로 만들거나 수정한 뒤 반드시 실행
# systemd가 디스크의 .service 파일을 다시 읽어 메모리에 반영
sudo systemctl daemon-reload
# 2) 부팅 자동시작 등록(enable) + 지금 즉시 실행(--now)을 한 번에
sudo systemctl enable --now myapp
# 3) 현재 상태 확인 — active(running)인지, Restart 설정이 먹었는지
systemctl status myapp
# 4) 실시간 로그 추적 — 크래시 원인·재시작 흐름을 눈으로 확인
journalctl -u myapp -f
# 5) start-limit-hit 등 failed 상태에서 카운터 초기화 후 복구
sudo systemctl reset-failed myapp각 명령의 예상 정상 결과는 다음과 같다.
daemon-reload— 출력이 없으면 성공. 에러가 뜨면 유닛 파일 문법 오류이니 메시지의 라인 번호를 확인한다.status—Active: active (running)이 초록색으로 보이면 정상.Loaded: ... enabled면 부팅 자동시작도 켜진 것.journalctl -u myapp -f— 앱의 정상 기동 로그가 흘러야 한다. 반복적으로Scheduled restart job이 찍히면 크래시 루프 신호다.
예상과 다를 때 분기:
status가inactive (dead)→start를 안 했거나enable만 한 경우.sudo systemctl start myapp.failed (Result: exit-code)→ 앱 자체가 비정상 종료.journalctl -u myapp -n 50으로 앱 에러를 확인한다.failed (Result: start-limit-hit)→ 위 StartLimit 섹션 참고, 원인 수정 후reset-failed.
자주 묻는 질문 (FAQ)
Q1. 유닛 파일을 수정했는데 반영이 안 돼요. Restart 값을 바꿔도 그대로예요.
sudo systemctl daemon-reload를 실행하지 않았기 때문이다. systemd는 디스크의 .service 파일을 매번 읽지 않고 메모리에 로드된 상태를 쓴다. 파일을 수정했다면 반드시 daemon-reload로 다시 읽게 한 뒤 sudo systemctl restart myapp으로 재적용해야 한다. "수정 → daemon-reload → restart" 3단계를 세트로 기억하자.
Q2. Restart=always인데 프로세스가 정상 종료(exit 0)한 뒤 안 살아나거나, 반대로 계속 살아나요.
always는 정상 종료를 포함해 모든 종료에서 재시작한다. 만약 배치잡이 정상 종료 후 자꾸 다시 뜨는 게 문제라면 Restart=on-failure로 바꿔야 한다. 반대로 정상 종료 후 안 살아나는 상황이라면 Restart=no(기본값)이거나, 관리자가 systemctl stop으로 내린 경우다. stop은 어떤 Restart 값이든 재시작을 트리거하지 않는다는 점을 기억하자.
Q3. 재시작이 몇 번 반복되다가 갑자기 멈춰버려요.
StartLimitIntervalSec/StartLimitBurst 한계에 걸린 것이다. systemctl status myapp에서 Result: start-limit-hit가 보이면 확실하다. 근본 원인을 고친 뒤 sudo systemctl reset-failed myapp → sudo systemctl start myapp 순으로 복구한다. 한계 자체를 완화하려면 [Unit] 섹션에서 StartLimitBurst 값을 늘리거나 StartLimitIntervalSec를 조정한다.
Q4. enable과 start의 차이가 뭔가요?
enable은 부팅 시 자동시작 등록이고, start는 지금 당장 실행이다. 둘은 독립적이라 enable만 하면 재부팅 전까진 안 뜨고, start만 하면 지금은 돌지만 재부팅하면 안 뜬다. 둘 다 원하면 sudo systemctl enable --now myapp으로 한 번에 처리하는 게 실무 표준이다.
여기까지 적용했다면 죽어도 알아서 살아나고, 그렇다고 무한 루프에 빠지지도 않는 서비스가 완성된다. 크래시 원인 자체를 파고들려면 "journalctl 로그 분석으로 서비스 크래시 원인 찾기", 상태값 해석이 필요하면 "systemctl status 상태값(failed/activating/dead) 읽는 법", 부팅 순서 의존성 문제는 "리눅스 서버 부팅 시 서비스 자동시작 순서(After=/Requires=)" 글을 이어서 참고하면 트러블슈팅 런북이 완성된다. 정확한 옵션 동작은 man systemd.service와 systemd 공식 문서(freedesktop.org)에서 사용 중인 버전 기준으로 확인하는 것을 권장한다.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.