Published on

systemd 서비스가 재부팅 후 안 뜰 때 12단계 점검

Authors

서버 재부팅 이후 “어제까지 잘 돌던 서비스가 안 뜬다”는 상황은 흔하지만, 원인은 생각보다 다양합니다. systemctl status만 보고 끝내면 재현이 어려운 타이밍 이슈(네트워크 준비 전 기동, 마운트 지연), 권한/SELinux, 유닛 파일의 잘못된 ExecStart, 환경변수 누락, 또는 enable 자체가 안 된 케이스까지 놓치기 쉽습니다.

이 글은 재부팅 후 systemd 서비스가 올라오지 않을 때 운영자가 실제로 밟아야 할 순서대로 12단계 점검 루틴을 제공합니다. 각 단계는 “무엇을 확인하고 → 어떤 명령을 치며 → 결과를 어떻게 해석하고 → 어떻게 고치는지”까지 연결합니다.

> 참고: 인프라 진단 글을 좋아한다면 EKS 부팅/조인 실패를 다룬 글도 유사한 접근(로그→원인 분해→재현)입니다: EKS 노드가 Join 못할 때 - bootstrap.sh 실패 진단

0) 상황 정의: ‘안 뜬다’의 의미부터 고정

재부팅 후 “서비스가 안 뜬다”는 말은 보통 아래 중 하나입니다.

  • 서비스가 아예 시작되지 않음(inactive/dead)
  • 시작은 했지만 즉시 죽음(failed)
  • 실행 중이지만 정상 동작을 못함(포트 미바인딩, 의존 리소스 미준비)
  • 서비스는 떠 있는데 부팅 시 자동 시작만 실패(수동 시작은 됨)

이 차이를 먼저 분리하면 뒤 단계가 빨라집니다.


1단계) 부팅 단위로 상태 확인: systemctl status + 부팅 ID

재부팅 직후의 상태는 “이번 부팅에서” 무엇이 일어났는지로 봐야 합니다.

systemctl status myapp.service --no-pager
systemctl is-enabled myapp.service
systemctl show -p ActiveState,SubState,Result myapp.service
  • is-enableddisabled/static/masked면 “부팅 후 안 뜸”은 자연스러운 결과일 수 있습니다.
  • Result=exit-code면 대개 ExecStart 또는 환경/권한 문제입니다.

2단계) 이번 부팅 로그만 좁혀 보기: journalctl -b

서비스 로그는 항상 **이번 부팅(-b)**으로 제한해서 노이즈를 줄입니다.

journalctl -b -u myapp.service --no-pager
journalctl -b -u myapp.service -n 200 --no-pager
journalctl -b -p err..alert --no-pager

여기서 자주 보이는 패턴:

  • No such file or directory → 실행 파일 경로/WorkingDirectory/환경파일
  • Permission denied → User/Group/권한/SELinux
  • Address already in use → 포트 충돌(이전 프로세스 잔존, 다른 서비스)
  • Dependency failed → After/Requires/Wants 또는 mount/network 타이밍

3단계) 유닛 파일이 실제로 어디 걸렸는지 확인: systemctl cat

“수정했다고 생각한 유닛”과 “실제로 로드되는 유닛”이 다른 경우가 있습니다(드롭인, 패키지 기본 유닛 등).

systemctl cat myapp.service
systemctl show -p FragmentPath,DropInPaths myapp.service
  • FragmentPath/lib/systemd/system/...라면 패키지 기본 유닛입니다.
  • 운영 수정은 일반적으로 /etc/systemd/system/myapp.service 또는 drop-in을 권장합니다.

유닛 수정 후엔 반드시:

systemctl daemon-reload

4단계) enable이 진짜로 걸렸는지: symlink와 타깃 확인

systemctl enable을 했는데도 안 뜨는 경우는 다음을 확인합니다.

systemctl list-unit-files | grep -E '^myapp\.service'
ls -l /etc/systemd/system/*.wants/myapp.service 2>/dev/null
systemctl get-default
  • 기본 타깃이 multi-user.target인지(서버) graphical.target인지 확인
  • enable이 특정 타깃에만 걸렸거나, 유닛이 WantedBy=를 누락했을 수 있습니다.

유닛에 보통 아래가 있어야 합니다.

[Install]
WantedBy=multi-user.target

5단계) “수동 start는 되는데 부팅 시만 실패” → 타이밍/의존성 의심

이 케이스는 거의 항상 네트워크/마운트/외부 의존성 준비 전에 서비스가 먼저 뜨는 문제입니다.

네트워크 준비 전 기동

After=network.target는 “네트워크 스택 시작” 수준이라 IP/라우팅/DNS가 준비되기 전일 수 있습니다. 네트워크가 필요하면 보통:

[Unit]
Wants=network-online.target
After=network-online.target

그리고 배포판에 따라 systemd-networkd-wait-online.service 또는 NetworkManager-wait-online.service가 활성화돼 있어야 합니다.

systemctl status systemd-networkd-wait-online.service
systemctl status NetworkManager-wait-online.service

마운트 준비 전 기동

데이터 디스크(/data)나 NFS가 늦게 붙으면 WorkingDirectory=/data/app 같은 설정이 부팅 시 실패합니다. 이 경우:

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

6단계) ExecStart 경로/인자/쉘 사용 실수 점검

systemd는 기본적으로 쉘을 거치지 않습니다. ExecStart="cmd arg1 | grep ..." 같은 형태는 실패합니다.

나쁜 예:

ExecStart=/usr/bin/myapp --config=/etc/myapp/config.yml | tee /var/log/myapp.log

좋은 예(파이프가 필요하면 명시적으로 쉘 실행):

ExecStart=/bin/bash -lc '/usr/bin/myapp --config=/etc/myapp/config.yml | tee -a /var/log/myapp.log'

또는 로깅은 systemd/journald에 맡기고 파이프를 제거하는 편이 운영상 더 안전합니다.


7단계) 환경변수/환경파일 누락: EnvironmentFile= 경로와 권한

부팅 후만 실패하는 원인 중 하나가 로그인 쉘에서만 잡히는 환경변수입니다(.bashrc, .profile). systemd 서비스는 이를 읽지 않습니다.

유닛에서 명시:

[Service]
EnvironmentFile=-/etc/myapp/myapp.env
ExecStart=/usr/bin/myapp

확인:

sudo cat /etc/myapp/myapp.env
systemctl show myapp.service -p Environment
  • EnvironmentFile-를 붙이면 파일이 없어도 무시합니다(초기 배포 시 유용).
  • secrets를 env로 둘 경우 권한은 최소화하세요.
sudo chown root:myapp /etc/myapp/myapp.env
sudo chmod 640 /etc/myapp/myapp.env

8단계) User/Group/권한 문제: 파일·디렉터리·Capabilities

재부팅 후 서비스가 root가 아닌 사용자로 뜨면서 실패하는 케이스는 흔합니다.

[Service]
User=myapp
Group=myapp
WorkingDirectory=/var/lib/myapp

점검:

sudo -u myapp ls -la /var/lib/myapp
sudo -u myapp /usr/bin/myapp --version

1024 미만 포트(예: 80)를 바인딩해야 한다면 root로 띄우지 말고 capability를 부여하는 방식이 안전합니다.

sudo setcap 'cap_net_bind_service=+ep' /usr/bin/myapp
getcap /usr/bin/myapp

9단계) 서비스 타입(Type)과 readiness 불일치

프로세스가 포그라운드로 도는지, 데몬 포크를 하는지에 따라 Type=이 맞아야 합니다.

  • 포그라운드 실행(대부분의 현대 앱): Type=simple(기본값)
  • sd_notify 지원 앱: Type=notify
  • 포크 데몬(구형): Type=forking + PIDFile=

잘못된 예(포그라운드 앱인데 forking으로 둠 → systemd가 PID 추적 실패):

Type=forking
ExecStart=/usr/bin/myapp

대부분은 아래로 충분합니다.

[Service]
Type=simple
ExecStart=/usr/bin/myapp
Restart=on-failure
RestartSec=2

10단계) Restart 정책과 StartLimit로 “조용히 포기”하는지 확인

서비스가 부팅 때 여러 번 실패하면 systemd가 StartLimit에 걸려 더 이상 시도하지 않습니다.

systemctl status myapp.service --no-pager
journalctl -b -u myapp.service | grep -E 'Start request repeated too quickly|start-limit'
systemctl show myapp.service -p Restart,RestartSec,StartLimitIntervalUSec,StartLimitBurst

필요 시 유닛에 명시:

[Service]
Restart=on-failure
RestartSec=3

[Unit]
StartLimitIntervalSec=60
StartLimitBurst=10

그리고 실패 카운터 초기화:

systemctl reset-failed myapp.service

11단계) SELinux/AppArmor/보안 샌드박싱으로 차단되는지

Permission denied가 파일 권한만으로 설명되지 않으면 MAC(강제 접근 제어)을 보세요.

SELinux(CentOS/RHEL/Fedora 계열)

getenforce
sudo ausearch -m avc -ts recent | tail -n 50
sudo journalctl -b | grep -i selinux

임시로 원인 확인(운영 적용은 신중):

sudo setenforce 0
# 동작 확인 후 다시
sudo setenforce 1

systemd 샌드박싱 옵션 점검

아래 옵션을 강하게 걸어두면 재부팅 후 경로가 달라졌을 때 실패할 수 있습니다.

ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/myapp /var/log/myapp
NoNewPrivileges=true

로그에서 “Read-only file system”류가 보이면 ReadWritePaths/StateDirectory 등을 재검토하세요.


12단계) 부팅 시퀀스 전체에서 실패 지점 찾기: systemd-analyze

서비스 자체 문제가 아니라 “부팅 타이밍” 문제라면 부팅 그래프를 보는 것이 빠릅니다.

systemd-analyze blame | head -n 30
systemd-analyze critical-chain myapp.service
  • critical-chain에서 myapp.service가 어떤 유닛 이후에 시작되는지 확인
  • 예: 네트워크 온라인이 늦다면 network-online.target 주변이 병목으로 보입니다.

재현 가능한 최소 유닛 예제(템플릿)

아래는 “재부팅 후 자동 기동 + 네트워크 필요 + 환경파일 사용 + 실패 시 재시도”에 무난한 템플릿입니다.

# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp Service
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/var/lib/myapp
EnvironmentFile=-/etc/myapp/myapp.env
ExecStart=/usr/bin/myapp --config /etc/myapp/config.yml
Restart=on-failure
RestartSec=2
StartLimitIntervalSec=60
StartLimitBurst=10

[Install]
WantedBy=multi-user.target

적용:

sudo systemctl daemon-reload
sudo systemctl enable myapp.service
sudo systemctl restart myapp.service
sudo systemctl status myapp.service --no-pager

운영 팁: “원인”을 남기는 습관(로그/관측)

재부팅 후 장애는 다시 재현하기 어려운 경우가 많습니다. 아래를 습관화하면 다음부터는 5분 내로 좁힐 수 있습니다.

  • journalctl -b -u <svc>로 부팅 단위 로그 확인
  • systemctl show로 실제 로드된 유닛 경로/드롭인 확인
  • 타이밍 이슈는 network-online.target, RequiresMountsFor=를 우선 의심
  • 실패 반복은 StartLimit에 막힐 수 있으니 reset-failed를 기억

인증/리다이렉트 같은 문제도 “정상처럼 보이지만 특정 조건에서만 실패”한다는 점에서 접근법이 비슷합니다. 원인 후보를 체크리스트로 쪼개는 방식은 다음 글과도 결이 같습니다: Proxy 뒤 Nginx에서 OAuth 리다이렉트 URI 불일치 해결


빠른 결론: 가장 많이 걸리는 Top 5

마지막으로 현장에서 가장 자주 맞닥뜨리는 원인만 압축하면 다음 순서입니다.

  1. enable 안 됨 / WantedBy 누락
  2. EnvironmentFile/환경변수 누락(로그인 쉘 의존)
  3. 네트워크·마운트 준비 전 기동(network-online.target, RequiresMountsFor)
  4. ExecStart 경로/쉘 파이프 사용 실수
  5. 권한/SELinux/capability 문제

위 12단계를 순서대로 밟으면 “재부팅 후 안 뜸”을 감으로 때려맞히는 대신, 근거 기반으로 원인을 확정하고 유닛을 재발 방지 형태로 정리할 수 있습니다.