- Published on
systemd 서비스가 재부팅 후 안 뜰 때 12단계 점검
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 재부팅 이후 “어제까지 잘 돌던 서비스가 안 뜬다”는 상황은 흔하지만, 원인은 생각보다 다양합니다. 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-enabled가disabled/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/권한/SELinuxAddress 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
마지막으로 현장에서 가장 자주 맞닥뜨리는 원인만 압축하면 다음 순서입니다.
enable안 됨 /WantedBy누락EnvironmentFile/환경변수 누락(로그인 쉘 의존)- 네트워크·마운트 준비 전 기동(
network-online.target,RequiresMountsFor) ExecStart경로/쉘 파이프 사용 실수- 권한/SELinux/capability 문제
위 12단계를 순서대로 밟으면 “재부팅 후 안 뜸”을 감으로 때려맞히는 대신, 근거 기반으로 원인을 확정하고 유닛을 재발 방지 형태로 정리할 수 있습니다.