Published on

systemd 서비스가 재부팅 후 안 뜰 때 9단계 진단

Authors

서버를 재부팅했는데 평소 잘 돌던 systemd 서비스가 조용히 사라져 있으면, 대부분은 “서비스 자체 문제”라기보다 부팅 시점의 환경 차이(마운트/네트워크/권한/리소스/의존성)에서 터집니다. 특히 수동으로 systemctl start 하면 잘 뜨는데, 재부팅 후엔 안 뜨는 케이스는 진단 순서가 중요합니다. 아래 9단계는 원인 범위를 빠르게 좁히기 위한 실전 루트이며, 각 단계는 다음 단계로 넘어가기 전에 “증거(로그/상태)”를 남기도록 구성했습니다.

1단계: 정말로 enable/부팅 타깃에 걸려 있는지 확인

가장 먼저 “자동 시작 설정이 되어 있다”는 가정을 깨야 합니다. 서비스가 실행 중이 아니면 보통 enabled가 아니거나, 다른 유닛에 의해 마스킹/비활성화된 경우가 있습니다.

systemctl is-enabled myapp.service
systemctl status myapp.service --no-pager
systemctl list-unit-files | grep -E '^myapp\.service'

체크 포인트:

  • is-enabledenabled가 아니라면:
sudo systemctl enable myapp.service
  • masked라면(강제 비활성):
sudo systemctl unmask myapp.service
  • 템플릿 유닛(myapp@.service)을 쓰고 있는데 인스턴스를 enable 안 한 경우도 흔합니다.
systemctl is-enabled myapp@prod.service
sudo systemctl enable myapp@prod.service

2단계: 부팅 직후 실패했는지, 아예 시도조차 안 했는지 구분

“안 뜬다”는 말은 두 가지가 섞여 있습니다.

  1. systemd가 시작을 시도했지만 실패
  2. systemd가 시작을 시도하지 않음(조건 불만족/의존성/타깃 미도달)
# 이번 부팅에서 해당 유닛 이벤트만
journalctl -b -u myapp.service --no-pager

# 유닛의 최근 실패 이유 요약
systemctl show myapp.service -p ActiveState,SubState,Result,ExecMainStatus

# 실패 유닛 목록
systemctl --failed --no-pager

여기서 로그가 거의 없으면, 다음 단계에서 “조건/의존성 때문에 시작 자체가 스킵”되었는지 확인합니다.

3단계: Unit 파일의 Condition/Assert 때문에 스킵되는지 확인

재부팅 후에만 안 뜨는 서비스는 ConditionPathExists=, ConditionFileNotEmpty=, AssertPathIsMountPoint= 같은 조건에서 많이 걸립니다. 특히 경로가 늦게 생성되거나, 마운트 타이밍이 달라지는 경우가 대표적입니다.

systemctl cat myapp.service
systemctl show myapp.service -p Conditions,Asserts

로그에 아래 같은 문구가 보이면 조건 문제입니다.

  • Condition check resulted in ... being skipped.

해결 방향:

  • 조건이 필요한지 재검토
  • 필요한 파일/디렉터리를 ExecStartPre=에서 생성하거나
  • 실제로는 마운트/네트워크가 준비된 뒤 시작해야 한다면 4~5단계로 이동

4단계: 부팅 타이밍(네트워크/마운트) 의존성 정리

재부팅 직후에는 네트워크가 아직 완전히 준비되지 않았거나, 외부 스토리지/NFS/EBS가 늦게 붙을 수 있습니다. “수동 시작은 성공”은 이 타이밍 이슈의 강력한 신호입니다.

네트워크가 필요하다면

After=network.target만으로는 부족할 때가 많습니다. 실제 연결이 필요하면 network-online.targetWants=를 함께 사용합니다.

# /etc/systemd/system/myapp.service.d/override.conf
[Unit]
Wants=network-online.target
After=network-online.target

적용:

sudo systemctl daemon-reload
sudo systemctl restart myapp.service

특정 마운트가 필요하다면

RequiresMountsFor=가 가장 안전합니다.

[Unit]
RequiresMountsFor=/data
After=local-fs.target

또는 mount 유닛 이름을 직접 의존성으로 걸 수도 있습니다.

systemctl list-units --type=mount | grep data

5단계: 서비스 타입(Type)과 readiness(준비 완료) 신호 점검

부팅 때만 실패하는 서비스 중 상당수가 Type= 설정이 실제 프로세스 동작과 안 맞습니다.

  • Type=simple(기본): ExecStart 프로세스가 뜨면 “시작됨”으로 간주
  • Type=forking: 데몬이 fork로 백그라운드로 가는 구형 패턴
  • Type=notify: 프로세스가 systemd에 READY=1 통지
  • Type=oneshot: 짧게 실행 후 종료하는 작업
systemctl show myapp.service -p Type,NotifyAccess,ExecStart

문제 패턴:

  • 실제로는 포그라운드 앱인데 Type=forking으로 되어 있어 systemd가 PID 추적을 실패
  • Type=notify인데 앱이 sd_notify를 안 해서 시작 타임아웃

타임아웃/시작 상태는 여기서 확인합니다.

systemctl show myapp.service -p TimeoutStartUSec,TimeoutStopUSec
journalctl -b -u myapp.service | tail -n 200

필요하면 Type=simple로 단순화하거나, readiness가 필요하면 헬스체크를 systemd가 아닌 앱/프록시 레벨에서 처리하는 것이 운영상 안정적입니다.

6단계: 실행 경로/환경변수/작업 디렉터리 차이 확인

재부팅 후에만 실패하는 앱은 흔히 “로그인 셸에서만 존재하는 환경”에 의존합니다. 예를 들어 .profile에 PATH를 추가해두고, 서비스에서는 그 PATH가 없어서 실패합니다.

systemctl show myapp.service -p Environment,EnvironmentFile,WorkingDirectory,User,Group

권장 패턴:

  • ExecStart에는 절대 경로 사용
  • 필요한 환경변수는 Environment= 또는 EnvironmentFile=로 명시

예시:

[Service]
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment="NODE_ENV=production"
EnvironmentFile=-/etc/default/myapp
ExecStart=/usr/bin/node /opt/myapp/server.js

EnvironmentFile=-...-는 파일이 없어도 실패하지 않게 합니다(부팅 환경에서 파일이 없을 수 있는 경우 유용).

7단계: 권한/소유권/SELinux(AppArmor) 정책 확인

부팅 후에만 안 뜨는 경우, 로그 파일/소켓/런타임 디렉터리 권한이 초기화되거나, /run 아래 경로가 재부팅으로 날아가서 실패하는 경우가 많습니다.

# 런타임 디렉터리 확인
ls -al /run/myapp || true

# 서비스 유저로 직접 실행 테스트(환경 최소화)
sudo -u myapp -H /usr/bin/node /opt/myapp/server.js

systemd가 런타임 디렉터리를 만들어주게 하려면:

[Service]
RuntimeDirectory=myapp
RuntimeDirectoryMode=0755

SELinux가 켜져 있다면:

getenforce
sudo ausearch -m avc -ts recent | tail -n 50

AppArmor라면:

aa-status
sudo journalctl -k | grep -i apparmor | tail -n 50

8단계: 리소스 부족(OOM/Limit)과 재시작 정책 확인

재부팅 직후는 여러 서비스가 동시에 뜨며 메모리 피크가 커집니다. 이때 OOM Killer가 특정 프로세스를 죽이면 “서비스가 안 뜬다”로 보입니다.

# 커널 로그에서 OOM 흔적
journalctl -b -k | grep -i -E 'oom|killed process|out of memory'

# 유닛의 제한 확인
systemctl show myapp.service -p MemoryMax,CPUQuota,TasksMax,LimitNOFILE

OOM 분석은 별도 체크리스트가 도움이 됩니다.

또한 재시작 정책이 지나치게 공격적이면 부팅 중 연쇄 실패를 숨깁니다.

systemctl show myapp.service -p Restart,RestartSec,StartLimitIntervalUSec,StartLimitBurst

권장 예시:

[Service]
Restart=on-failure
RestartSec=3
StartLimitIntervalSec=60
StartLimitBurst=5

9단계: systemd 자체 관점에서 “왜”를 추적 (analyze/critical-chain)

여기까지 왔으면 개별 유닛 로그만으로는 부족할 수 있습니다. 부팅 전체에서 내 서비스가 언제 시작되었고, 무엇을 기다렸는지 확인합니다.

# 부팅 타임라인(지연 원인)
systemd-analyze blame | head -n 30

# 특정 타깃까지의 크리티컬 체인에서 myapp이 어디에 걸리는지
systemd-analyze critical-chain myapp.service

# 유닛 간 의존성 관계
systemctl list-dependencies myapp.service --reverse
systemctl list-dependencies myapp.service

여기서 network-online.target가 오래 걸리거나, 특정 mount가 지연되는 것이 보이면 4단계로 돌아가 의존성을 더 정확히 모델링하는 것이 정답입니다.

재현과 수정을 빠르게 하는 운영 팁

드롭인(override)로 안전하게 수정

배포 패키지의 유닛 파일을 직접 수정하면 업데이트 때 덮어씌워집니다. 드롭인으로 관리하세요.

sudo systemctl edit myapp.service
# 에디터에서 override.conf 작성
sudo systemctl daemon-reload
sudo systemctl restart myapp.service

부팅 환경과 최대한 비슷하게 테스트

로그인 셸에서 잘 되는 명령은 서비스에서 실패할 수 있습니다. 최소 환경으로 재현합니다.

sudo -u myapp env -i \
  PATH=/usr/bin:/bin \
  NODE_ENV=production \
  /usr/bin/node /opt/myapp/server.js

마무리: “재부팅 후만 실패”는 타이밍·환경·리소스 문제다

정리하면, 재부팅 후 systemd 서비스가 안 뜰 때는 (1) enable 여부 → (2) 시도/실패 구분 → (3) 조건 스킵 → (4) 네트워크/마운트 타이밍 → (5) Type/readiness → (6) 환경/경로 → (7) 권한/보안 정책 → (8) OOM/Limit → (9) 부팅 체인 분석 순으로 보면 가장 빨리 좁혀집니다.

만약 서비스가 “일정 횟수 재시작 후 포기”하거나, “실패 로그는 있는데 원인이 애플리케이션 내부 예외”라면 컨테이너 환경에서의 재시작 루프 진단법도 사고방식이 유사합니다.

원하시면, 실제 myapp.service 유닛 파일과 journalctl -b -u myapp.service 로그 일부(민감정보 제거)를 주시면 위 9단계에 맞춰 “어디에서 막혔는지”를 근거 기반으로 같이 짚어드릴게요.