시작되지 않는 systemd 서비스는 거의 신비롭지 않습니다. systemctl status <unit>을 실행한 뒤 journalctl -u <unit> -n 50 --no-pager로 그 유닛의 마지막 저널 50줄을 읽으세요 — 이 둘 사이에서 다섯 원인 가운데 하나가 보통 그냥 이름을 대줍니다: 잘못된 경로, 없는 바이너리, 잘못된 권한, SELinux/AppArmor 거부, 또는 유닛 파일 문법 오류. 실패 이유는 로그에 있습니다; 아래의 수정은 그저 로그에 대한 패턴 매칭일 뿐입니다.
systemd 서비스가 실패한 이유는 어떻게 볼까?
상태 먼저, 저널 다음:
systemctl status myapp.service
journalctl -u myapp.service -n 50 --no-pager status는 상태(inactive (dead), failed (exit-code), activating (auto-restart))와 마지막 로그 몇 줄을 줍니다. 저널은 전체 이야기를 줍니다: stdout, stderr, 그리고 유닛에 대한 systemd 자체의 불평.
유닛이 이전에 실패했고 그 실행의 기록이 필요하다면, 현재 부트는 -b, 오늘 기준은 --since today를 더하세요:
journalctl -u myapp.service -b --no-pager 전체 도구 상자 — 부트, 우선순위, 출력 실시간 따라가기 — 는 journalctl cheat sheet에 있습니다. 같은 근육 기억입니다.
알아둘 한 쌍 더:
systemctl list-units --failed
systemctl reset-failed myapp.service 첫 번째는 이 박스의 모든 빨간 유닛을 나열합니다. 두 번째는 고친 뒤 failed 상태를 지웁니다 — 외관상 문제지만, 상태 페이지가 늑대인지를 그만 외치게 합니다.
가장 흔한 원인은 무엇일까?
로그가 증상을 지목한 뒤에는, 원인이 거의 항상 이 다섯 중 하나입니다:
| 로그의 말 | 유력한 원인 | 수정 |
|---|---|---|
status=203/EXEC | 잘못된 ExecStart= 경로 또는 없는 인터프리터 | 절대 경로, chmod +x, shebang 확인 |
스크립트에서 status=203/EXEC | 스크립트가 CRLF 줄바꿈이거나 잘못된 shebang | dos2unix script.sh, 첫 줄 수정 |
status=1/FAILURE, 앱 로그 없음 | 작업 디렉터리 또는 환경 변수 누락 | WorkingDirectory= 설정, Environment= 추가 |
Permission denied | 사용자가 파일을 읽지 못하거나 포트를 바인드하지 못함 | 소유권 수정; 1024 미만 포트는 AmbientCapabilities=CAP_NET_BIND_SERVICE 또는 root |
Unit is masked | 누군가 systemctl mask를 실행함 | systemctl unmask myapp.service |
| 유닛 파일 수정 “아무 효과 없음” | 데몬이 리로드되지 않음 | systemctl daemon-reload |
203/EXEC 계열은 별도 언급이 값어치 있습니다 — 오후를 통째로 삼키는 녀석이니까요. Systemd는 ExecStart=를 실행할 때 당신의 셸을 쓰지 않습니다. 즉:
# 틀림 — 셸 없음, ~는 절대 확장 안 됨, PATH 검색 없음
ExecStart=~/app/run.sh
# 맞음
ExecStart=/opt/app/run.sh 그리고 스크립트 자체가 실행 가능해야 하고 진짜 shebang(#!/bin/bash 또는 #!/usr/bin/env bash)으로 시작해야 합니다. 터미널에서는 잘 도는데 systemd 아래에서 203으로 실패하는 스크립트는 거의 항상 다음 중 하나입니다: 실행 불가, CRLF 줄바꿈, 어디로도 가지 않는 shebang, 또는 상대 경로.
수동으로는 되는데 부팅 때는 안 되는 이유는?
고전적인 순서 버그입니다. 로그가 부트 직후의 실패를 보여주는데 손으로 systemctl start하면 잘 시작된다면, 서비스가 경쟁에서 지고 있는 겁니다 — 네트워크, 마운트된 디스크, 데이터베이스가 존재하기 전에 손을 뻗는 겁니다.
수정은 바라는 대신 의존성을 선언하는 것입니다:
[Unit]
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
ExecStartPre=/usr/bin/test -f /opt/app/config.toml After=가 시작 순서를 정하고; Wants=가 systemd가 실제로 의존성을 끌어올리게 합니다. network-online.target은 배포판에서 network-wait 서비스가 활성화되어 있어야 동작하므로, systemctl is-enabled NetworkManager-wait-online.service(또는 systemd-networkd 등가물)를 확인하세요. ExecStartPre= 가드는 스택 트레이스 대신 읽을 수 있는 메시지로 요란하게 실패하게 만드는 싸고 정직한 방법입니다.
두 번째 변형: 부트 때는 시작되지만 곧바로 죽습니다. 대화형 환경에는 있었는데 부트에는 없는 것을 찾으세요 — PATH 차이, virtualenv, HOME. 필요한 것은 명시적으로 설정합니다:
[Service]
Environment="PATH=/opt/app/venv/bin:/usr/bin"
User=appuser
WorkingDirectory=/opt/app 서비스가 저널에 로그를 남기지 않는 이유는?
journalctl -u가 아무것도 보여주지 않는다면 세 가지를 순서대로 확인하세요:
- 유닛의
StandardOutput=과StandardError=—journal(기본값) 또는journal+console이어야 합니다. 누군가null로 설정했을 수 있습니다. - 앱이 stdout 대신 파일에 씁니다. Systemd는 stdout/stderr만 포착합니다; 로그 파일에 쓰는 앱은 저널을 완전히 우회합니다. 앱을 stdout으로 향하게 하거나 파일을 직접 읽으세요.
- 저장소 한도가 오래된 줄을 버렸습니다:
journalctl --disk-usage, 저널이 숨을 쪼르고 있다면/etc/systemd/journald.conf의SystemMaxUse=.
시작 자체를 디버깅하는 데는 부트 타임 컨텍스트에 셸을 떨어뜨리는 것만 한 게 없습니다:
[Service]
ExecStart=/bin/bash -c 'exec /opt/app/bin/server 2>&1' 아니면, 진짜 미스터리에는, 유닛의 사용자로 셸에서 정확한 ExecStart= 명령을 돌려보세요 — 대부분의 환경 차이는 처음 10초 안에 표면화됩니다.
크래시 뒤 자동 재시작은 어떻게 만들까?
기본 정책은 Restart=no입니다: 크래시된 서비스는 죽은 채 머물고, 당신은 사용자에게서 소식을 듣습니다. 서비스별로 고치세요:
[Service]
Restart=on-failure
RestartSec=5 | 설정 | 재시작하는 경우 |
|---|---|
no (기본값) | 절대 없음 |
on-failure | 0이 아닌 종료, 시그널, 타임아웃 |
always | 어떤 종료든, 깨끗한 종료 포함 |
on-watchdog | 워치독 타임아웃만 |
Restart=on-failure를 시작 속도 가드와 짝지어 크래시 루프가 박스를 두드리지 않게 하세요: [Unit] 섹션의 StartLimitIntervalSec=와 StartLimitBurst=. 60초에 실패 다섯 번은 CPU를 돌릴 일이 아니라 사람을 호출해야 합니다.
이걸 데몬이 아니라 예약 작업으로 엮는 중이라면, 먼저 cron vs systemd timer를 저울질하세요 — 타이머는 저널 로깅과 의존성 순서를 공짜로 줍니다. 이 글이 계속 손을 뻗는 것이 정확히 그겁니다.
60초 체크리스트
systemctl status <unit>— 상태와 마지막 줄을 읽는다.journalctl -u <unit> -n 50 --no-pager— 진짜 에러를 찾는다.203/EXEC? 경로, shebang, 권한을 고친다.Permission denied? 사용자와 파일 소유권을 고친다.- 부트 때만 실패?
After=/Wants=network-online.target과ExecStartPre=가드를 추가한다. - 유닛 파일을 고쳤다?
systemctl daemon-reload && systemctl restart <unit>. Restart=on-failure를 추가해 다음 크래시가 숨는 대신 스스로 알리게 한다.
대부분의 “systemd는 복잡하다”는 순간은 에러가 처음부터 저널에 있었다로 귀결됩니다. 유닛 파일을 고치기 전에 읽으세요 — 고친 뒤가 아니라.
— mrsaynothing
Get the next one by email
One email per post. No spam, no algorithms.
what is this?글이 마음에 드셨나요? 제 본업이 바로 이런 일입니다. 저를 고용하세요