블로그로 돌아가기

systemd 서비스가 시작되지 않을 때: 고치는 법

2026년 9월 14일

시작되지 않는 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 줄바꿈이거나 잘못된 shebangdos2unix 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가 아무것도 보여주지 않는다면 세 가지를 순서대로 확인하세요:

  1. 유닛의 StandardOutput=StandardError=journal(기본값) 또는 journal+console이어야 합니다. 누군가 null로 설정했을 수 있습니다.
  2. 앱이 stdout 대신 파일에 씁니다. Systemd는 stdout/stderr만 포착합니다; 로그 파일에 쓰는 앱은 저널을 완전히 우회합니다. 앱을 stdout으로 향하게 하거나 파일을 직접 읽으세요.
  3. 저장소 한도가 오래된 줄을 버렸습니다: journalctl --disk-usage, 저널이 숨을 쪼르고 있다면 /etc/systemd/journald.confSystemMaxUse=.

시작 자체를 디버깅하는 데는 부트 타임 컨텍스트에 셸을 떨어뜨리는 것만 한 게 없습니다:

[Service]
ExecStart=/bin/bash -c 'exec /opt/app/bin/server 2>&1'

아니면, 진짜 미스터리에는, 유닛의 사용자로 셸에서 정확한 ExecStart= 명령을 돌려보세요 — 대부분의 환경 차이는 처음 10초 안에 표면화됩니다.

크래시 뒤 자동 재시작은 어떻게 만들까?

기본 정책은 Restart=no입니다: 크래시된 서비스는 죽은 채 머물고, 당신은 사용자에게서 소식을 듣습니다. 서비스별로 고치세요:

[Service]
Restart=on-failure
RestartSec=5
설정재시작하는 경우
no (기본값)절대 없음
on-failure0이 아닌 종료, 시그널, 타임아웃
always어떤 종료든, 깨끗한 종료 포함
on-watchdog워치독 타임아웃만

Restart=on-failure를 시작 속도 가드와 짝지어 크래시 루프가 박스를 두드리지 않게 하세요: [Unit] 섹션의 StartLimitIntervalSec=StartLimitBurst=. 60초에 실패 다섯 번은 CPU를 돌릴 일이 아니라 사람을 호출해야 합니다.

이걸 데몬이 아니라 예약 작업으로 엮는 중이라면, 먼저 cron vs systemd timer를 저울질하세요 — 타이머는 저널 로깅과 의존성 순서를 공짜로 줍니다. 이 글이 계속 손을 뻗는 것이 정확히 그겁니다.

60초 체크리스트

  1. systemctl status <unit> — 상태와 마지막 줄을 읽는다.
  2. journalctl -u <unit> -n 50 --no-pager — 진짜 에러를 찾는다.
  3. 203/EXEC? 경로, shebang, 권한을 고친다. Permission denied? 사용자와 파일 소유권을 고친다.
  4. 부트 때만 실패? After=/Wants=network-online.targetExecStartPre= 가드를 추가한다.
  5. 유닛 파일을 고쳤다? systemctl daemon-reload && systemctl restart <unit>.
  6. Restart=on-failure를 추가해 다음 크래시가 숨는 대신 스스로 알리게 한다.

대부분의 “systemd는 복잡하다”는 순간은 에러가 처음부터 저널에 있었다로 귀결됩니다. 유닛 파일을 고치기 전에 읽으세요 — 고친 뒤가 아니라.

— mrsaynothing

Get the next one by email

One email per post. No spam, no algorithms.

self-hosted · no third parties · one-click unsubscribe

what is this?

GGUF 양자화: 어떤 단계를 써야 할까?

글이 마음에 드셨나요? 제 본업이 바로 이런 일입니다. 저를 고용하세요