- Published on
systemd 서비스가 무한 재시작되는 원인 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 "서비스가 계속 죽고 다시 뜬다"는 문제는 겉으로 보기엔 단순하지만, systemd의 재시작 정책(Restart=), 시작 판정(Type=), 상태 감시(WatchdogSec=), 종료 코드/시그널 처리까지 겹치면 원인 추적이 빠르게 난이도 상승합니다. 특히 Start request repeated too quickly 또는 Scheduled restart job이 반복된다면, systemd가 의도적으로 재기동 루프를 만들고 있는지, 아니면 프로세스가 즉시 종료되는지를 먼저 분리해야 합니다.
이 글에서는 운영에서 가장 자주 만나는 systemd 서비스 무한 재시작 원인 7가지를 증상→확인법→해결책 순으로 정리합니다. (Kubernetes의 CrashLoopBackOff와 유사한 패턴도 많습니다. 컨테이너 관점의 체크는 EKS CrashLoopBackOff인데 로그가 0줄? 원인 8가지도 함께 참고하면 좋습니다.)
먼저: “무한 재시작”을 확정하는 1분 진단
1) 상태/이력 확인
systemctl status myapp.service --no-pager
systemctl show myapp.service -p Restart -p RestartUSec -p StartLimitIntervalUSec -p StartLimitBurst
Active: activating (auto-restart)/Scheduled restart job가 반복되면 systemd 재시작 정책이 작동 중입니다.StartLimit...값이 작으면 빠르게 재시작 제한에 걸릴 수 있습니다.
2) 로그는 journalctl로 “이번 부팅부터” 추적
journalctl -u myapp.service -b --no-pager -n 200
journalctl -u myapp.service -b -f
code=exited, status=1/FAILURE→ 애플리케이션이 종료 코드로 죽음code=killed, status=9/KILL→ SIGKILL(대개 OOMKill 또는 systemd 강제 종료)Watchdog timeout→ watchdog 원인 가능성
원인 1) 애플리케이션이 “정상 종료가 아닌 종료 코드”로 끝남
가장 흔한 케이스입니다. 앱이 시작 직후 설정 오류/포트 바인딩 실패/DB 연결 실패 등으로 종료 코드 1을 반환하고, unit에 Restart=always 또는 Restart=on-failure가 설정되어 있으면 systemd는 계속 재시작합니다.
전형적인 로그
Main process exited, code=exited, status=1/FAILURE- 애플리케이션 로그에
bind: address already in use,permission denied,config parse error등
확인 포인트
systemctl cat myapp.service
journalctl -u myapp.service -b --no-pager | tail -n 200
Restart=always면 **정상 종료(0)**여도 재시작합니다.
해결책
- 정상 종료를 재시작으로 취급하지 않기
# /etc/systemd/system/myapp.service
[Service]
Restart=on-failure
SuccessExitStatus=0 143
- 앱이 SIGTERM(143)로 정상 종료하는 패턴(예: Java/Spring Boot)이라면
SuccessExitStatus=143를 추가해 “실패로 오인”하지 않게 합니다.
> DB 연결 실패가 원인이라면, 애플리케이션 레벨에서 재시도(backoff)와 systemd 레벨의 재시작 간 충돌이 루프를 악화시킬 수 있습니다. 특히 커넥션 누수/풀 고갈로 시작 직후 죽는다면 Spring Boot 3.2 HikariCP 커넥션 누수 경고 추적법처럼 애플리케이션 내부 원인도 같이 점검하세요.
원인 2) Type=과 실제 실행 방식이 불일치 (특히 Type=forking/simple/notify)
systemd는 Type=에 따라 “서비스가 정상 기동했다”고 판단하는 기준이 달라집니다. 이 값이 실제 프로세스 동작과 다르면, systemd는 기동 실패로 판단하고 재시작을 반복할 수 있습니다.
대표 실수
- 데몬이 포그라운드로 도는데
Type=forking을 사용 Type=notify를 설정했지만 앱이sd_notify()를 호출하지 않음PIDFile=을 지정했지만 PID 파일이 생성되지 않거나 경로/권한 문제
증상/로그
Failed to start ...직후 재시작Service hold-off time over, scheduling restart.Can't open PID file ... (yet?) after start: No such file or directory
해결책 예시
- 포그라운드 실행이면 보통
Type=simple이 안전합니다.
[Service]
Type=simple
ExecStart=/usr/local/bin/myapp --foreground
Restart=on-failure
Type=notify를 쓰려면 실제로 notify가 필요합니다(예: nginx는 포그라운드+simple로도 운영 가능). notify가 불필요하면 제거하세요.
원인 3) ExecStartPre/ExecStartPost/ExecStopPost 스크립트 실패
많은 팀이 마이그레이션, 디렉터리 생성, 권한 조정 등을 ExecStartPre=에 넣습니다. 여기서 한 줄이라도 실패하면 서비스 전체가 실패로 처리되어 재시작 루프가 됩니다.
전형적인 패턴
ExecStartPre=/usr/bin/bash -c '...'내부에서set -e로 인해 중간 실패 시 즉시 종료curl/wget같은 네트워크 호출이 간헐적으로 실패chmod/chown대상 경로가 없거나 권한 없음
확인 방법
systemctl cat myapp.service
journalctl -u myapp.service -b --no-pager | grep -E "ExecStartPre|ExecStartPost|ExecStopPost|status="
해결책
- 실패해도 무시해도 되는 pre-step이면
-prefix를 사용합니다.
[Service]
ExecStartPre=-/usr/bin/mkdir -p /var/lib/myapp
ExecStartPre=-/usr/bin/chown -R myapp:myapp /var/lib/myapp
- 네트워크 의존 pre-step은 systemd가 아니라 앱 내부로 옮기거나, 재시도 로직을 넣고 타임아웃을 명시하세요.
원인 4) 환경 변수/WorkingDirectory/권한(User, Group) 문제로 즉시 종료
수동으로 쉘에서 실행하면 잘 뜨는데, systemd로만 실행하면 죽는 경우의 상당수는 환경이 다르기 때문입니다.
흔한 차이
PATH가 다름 →ExecStart에서 실행 파일/의존 커맨드 못 찾음WorkingDirectory가 다름 → 상대 경로 설정/파일 로드 실패User=로 실행 시 접근 권한 부족(로그/소켓/데이터 디렉터리).env파일을 기대하지만 systemd에서는 로드 안 됨
확인 포인트
systemctl show myapp.service -p Environment -p EnvironmentFile -p WorkingDirectory -p User
해결책 예시
[Service]
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/opt/myapp/bin/myapp
# 로그/데이터 경로 권한을 systemd가 보장하게 만들 수도 있음
StateDirectory=myapp
CacheDirectory=myapp
LogsDirectory=myapp
StateDirectory= 등을 쓰면 /var/lib/myapp 같은 경로를 systemd가 생성/권한 부여해 운영 실수를 줄일 수 있습니다.
원인 5) WatchdogSec=/헬스체크(ready/notify) 미응답으로 systemd가 강제 재시작
systemd watchdog를 켜면, 서비스는 주기적으로 "나 살아있다"를 systemd에 알려야 합니다. 이를 안 하면 systemd가 hung으로 판단해 프로세스를 죽이고 재시작합니다.
증상
- 로그에
Watchdog timeout또는killing관련 메시지 - 일정한 주기(예: 30초/60초)로 정확히 재시작
확인
systemctl show myapp.service -p WatchdogUSec -p Type -p NotifyAccess
해결책
- watchdog를 쓰지 않을 거면 끄기
[Service]
WatchdogSec=0
Type=simple
- watchdog를 쓸 거면 앱에서
sd_notify("WATCHDOG=1")를 주기적으로 호출하거나, systemd-notify를 활용(단, 이 경우에도 앱이 실제로 살아있는지 의미 있게 설계해야 함)
원인 6) OOMKill/리소스 제한(MemoryMax, TasksMax, LimitNOFILE)으로 커널 또는 systemd가 종료
무한 재시작처럼 보이지만, 실제로는 리소스 제한에 걸려 강제 종료되는 경우가 있습니다. 특히 메모리 스파이크, 파일 디스크립터 고갈, 스레드/프로세스 수 폭증이 흔합니다.
전형적인 로그
code=killed, status=9/KILLOut of memory: Killed process ...(커널 로그)Too many open files
확인 방법
# unit에 걸린 제한 확인
systemctl show myapp.service -p MemoryMax -p TasksMax -p LimitNOFILE -p LimitNPROC
# OOM 관련 커널 로그
journalctl -k -b --no-pager | grep -i -E "oom|out of memory|killed process"
해결책
[Service]
# 임시 완화(근본 원인은 앱 메모리/FD 사용량)
MemoryMax=2G
LimitNOFILE=65535
TasksMax=4096
Restart=on-failure
리소스 제한을 올리는 것은 “증상 완화”일 수 있습니다. 누수/폭증의 근본 원인을 함께 잡아야 재시작 루프가 사라집니다.
원인 7) Start limit(재시작 제한) 설정이 부적절하거나, 재시작 간격이 너무 짧음
Restart= 자체가 문제가 아니라 재시작 템포가 문제인 경우가 있습니다. 빠르게 죽는 프로세스를 Restart=always로 돌리면, 로그가 폭발하고 서비스가 불안정해집니다. systemd는 이를 막기 위해 start limit을 적용하지만, 설정이 애매하면 “무한 재시작 → 제한 걸림 → 운영자가 재시작 → 다시 무한 재시작” 패턴이 반복됩니다.
증상
Start request repeated too quickly.Failed with result 'start-limit-hit'.
확인
systemctl show myapp.service -p StartLimitIntervalUSec -p StartLimitBurst -p RestartUSec
해결책
- 재시작 간격을 늘려서(백오프) 외부 의존성(DB, 네트워크)이 회복될 시간을 줍니다.
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=10s
- “외부 의존성이 준비될 때까지 기다리는” 의존성 설계를 추가합니다.
[Unit]
After=network-online.target
Wants=network-online.target
[Service]
ExecStartPre=/usr/bin/bash -c 'for i in {1..30}; do nc -z 127.0.0.1 5432 && exit 0; sleep 1; done; exit 1'
다만 ExecStartPre에서 네트워크 체크를 과도하게 넣으면 원인 3처럼 또 다른 실패 지점이 되므로, 짧고 명확한 타임아웃을 두는 것이 중요합니다.
재시작 루프를 끊고 원인을 “고정”하는 운영 팁
재시작을 잠시 멈춰 로그/상태를 확보
# 즉시 중지
systemctl stop myapp.service
# 자동 재시작 비활성화(원인 분석 중에 유용)
systemctl edit myapp.service
오버라이드 파일에 아래를 넣습니다.
[Service]
Restart=no
unit 파일 검증(문법/경로)
systemd-analyze verify /etc/systemd/system/myapp.service
“실행 환경”을 systemd와 동일하게 재현
User=로 지정된 계정으로 직접 실행WorkingDirectory로 이동 후 실행EnvironmentFile을 로드한 상태로 실행
sudo -u myapp -H bash -lc 'cd /opt/myapp && source /etc/myapp/myapp.env && ./bin/myapp'
마무리: 원인별로 “systemd 문제”와 “앱 문제”를 분리하자
systemd 무한 재시작은 대체로 다음 두 부류로 나뉩니다.
- systemd 설정/판정 문제:
Type=불일치, notify/watchdog, pre/post 스크립트 실패, start limit/재시작 정책 - 애플리케이션/런타임 문제: 종료 코드, 환경 변수/권한, 포트 충돌, OOM/FD 고갈 등
가장 빠른 접근은 journalctl -u ... -b로 **죽는 이유(Exit code/Signal/Watchdog/OOM)**를 먼저 확정하고, 그 다음 systemctl cat으로 unit 설정이 그 실패를 “무한 재시작”으로 증폭시키는지 확인하는 것입니다. 이 순서만 지켜도, 재시작 루프의 80%는 10~20분 안에 원인에 도달합니다.