- Published on
systemd 서비스가 재부팅 후 안 뜰 때 9단계 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버를 재부팅했는데 평소 잘 돌던 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-enabled가enabled가 아니라면:
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단계: 부팅 직후 실패했는지, 아예 시도조차 안 했는지 구분
“안 뜬다”는 말은 두 가지가 섞여 있습니다.
- systemd가 시작을 시도했지만 실패
- 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.target와 Wants=를 함께 사용합니다.
# /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단계에 맞춰 “어디에서 막혔는지”를 근거 기반으로 같이 짚어드릴게요.