SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-21

systemd 의존성 설정과 실행 순서 완벽 정리

Requires와 After의 차이점부터 부팅 시 서비스가 실패하는 원인을 분석합니다. Wants, Requisite, Condition 등 각 설정이 수행하는 역할을 명확히 구분하고, 서비스가 정상적으로 실행되도록 의존성을 올바르게 구성하는 방법을 설명합니다.

Requires는 After를 의미하지 않습니다

systemd의 의존성과 조건은 대부분의 unit 파일에서 하나인 것처럼 사용되지만, 실제로는 네 가지 별개의 메커니즘입니다. Requires=Wants=은 어떤 다른 unit을 함께 불러올지 결정합니다. After=Before=은 unit이 시작되는 순서를 결정합니다. ExecStartPre=은 unit을 실패하게 만들 수 있는 검사를 수행합니다. ConditionAssert 계열은 unit을 실행할지 여부를 결정합니다. 각 메커니즘은 서로 독립적이므로, 한 unit이 다른 unit을 필요로(require) 하더라도 동시에 시작될 수 있습니다.

마지막 문장은 "수동으로 시작하면 잘 되는데 부팅 시에는 실패한다"는 거의 모든 보고서의 원인이 되는 버그입니다.

[Unit]
Description=Inventory API
Requires=postgresql.service

[Service]
ExecStartPre=/usr/bin/pg_isready -h 127.0.0.1 -t 5
ExecStart=/usr/local/bin/inventory-api

Requires=postgresql.service는 PostgreSQL을 동일한 시작 트랜잭션으로 불러옵니다. 하지만 PostgreSQL이 준비될 때까지 기다리지는 않습니다. systemd는 두 작업을 병렬로 시작하므로, PostgreSQL이 데이터 디렉터리를 여는 동안 pg_isready가 실행됩니다. 아직 수신 대기 중인 프로세스가 없으므로 2번 코드로 종료되며, ExecStart에 도달하기도 전에 unit이 실패합니다. 한 시간 뒤에 sudo systemctl start inventory-api을 실행하면 성공하는데, 그 시점에는 이미 PostgreSQL이 실행 중이기 때문입니다. unit 파일의 내용은 바뀐 것이 없으므로 파일은 정상적으로 보입니다.

해결책은 한 줄을 추가하는 것입니다.

[Unit]
Requires=postgresql.service
After=postgresql.service

같은 부분에 더 중요한 세부 사항이 숨어 있습니다. 실패하는 Requires= 의존성은 After=를 함께 설정했을 때만 unit이 시작되는 것을 막습니다. 순서 설정이 없으면 systemd는 이미 unit을 시작해 버린 상태이므로, 다른 unit이 실패하더라도 취소할 대상이 없습니다. Requires= 단독으로는 사람들이 기대하는 보호 효과를 얻을 수 없습니다. 특별한 이유가 없다면 모든 Requires=Wants= 옆에 After=을 함께 작성하십시오.

Requires, Wants, Requisite 및 BindsTo의 역할

이 설정들은 모두 의존성 관련 설정입니다. 이 중 어느 것도 실행 순서를 보장하지는 않습니다.

  • Wants=: 다른 유닛을 함께 실행합니다. 해당 유닛이 실패하거나 존재하지 않더라도 이 유닛은 시작됩니다. systemctl enable 명령이 .wants/ 디렉터리 내부에 심볼릭 링크를 생성하는 방식이 바로 이것입니다.
  • Requires=: 다른 유닛을 함께 실행합니다. 만약 해당 유닛이 실패하고 After=로 순서까지 지정했다면, 이 유닛은 시작되지 않습니다. 다른 유닛이 명시적으로 중지되면 이 유닛도 함께 중지됩니다.
  • Requisite=: 다른 유닛을 자동으로 실행하지 않습니다. 해당 유닛이 이미 활성화된 상태가 아니라면 이 유닛은 즉시 실패합니다.
  • BindsTo=: Requires=와 유사하며, 하드웨어가 사라지는 경우를 포함하여 다른 유닛이 어떤 이유로든 중지되면 이 유닛도 함께 중지됩니다.
  • PartOf=: 중지 및 재시작 명령이 다른 유닛에서 이 유닛으로 전파됩니다. 시작 명령은 전파되지 않습니다.
  • Conflicts=: 이 유닛을 시작하면 다른 유닛은 중지됩니다.

다른 데몬과 통신하는 데몬의 경우, 보통 Wants=After=를 조합하는 것이 적절합니다. Requires=은 두 유닛의 생명 주기를 결합합니다. 즉, 유지보수를 위해 데이터베이스를 중지하면 애플리케이션도 함께 종료되며, 데이터베이스가 다시 시작되어도 애플리케이션은 자동으로 돌아오지 않습니다. Wants=After=을 조합하면 생명 주기를 결합하지 않고도 부팅 순서를 제어할 수 있으며, 의존 서비스가 나중에 사라지는 경우 재시작 정책(restart policy)을 통해 대응할 수 있습니다.

작성하지 않은 의존성을 상속받는 경우도 있습니다. 기본값인 DefaultDependencies=yes를 사용하는 일반적인 서비스는 Requires=sysinit.target, After=sysinit.target basic.target, Conflicts=shutdown.target를 자동으로 부여받습니다. 이것이 바로 [Unit] 섹션이 거의 비어 있는 서비스라도 부팅 후반부에 시작되고 종료 시 깔끔하게 중지되는 이유입니다.

After와 Before는 트랜잭션의 순서만 지정할 뿐입니다

After=Before=는 순수하게 순서만을 지정합니다. 이들은 어떠한 의존성 요구 사항도 포함하지 않습니다. 다른 유닛이 Redis를 호출하지 않는 상황에서 After=redis.service을 설정하는 것은 아무런 동작을 하지 않는 것과 같습니다. 만약 redis.service이 트랜잭션에 포함되어 있지 않다면 대기할 대상이 없으므로, 해당 유닛은 즉시 시작됩니다.

이 점은 두 번 강조할 가치가 있습니다. 아래에서 다룰 network-online.target 실수의 정확한 원인이기 때문입니다. 순서 지정은 동일한 트랜잭션 내에서 이미 시작되고 있는 유닛만을 기다립니다.

이 쌍은 대칭적입니다. a.service에 작성된 After=b.serviceb.service에 작성된 Before=a.service과 동일한 의미를 갖습니다. 따라서 둘 중 하나를 선택하여 본인이 관리하는 유닛에 작성하십시오. 종료 시에는 순서가 자동으로 역전되므로, After=b.service은 귀하의 유닛이 b.service보다 먼저 정지됨을 의미하기도 합니다.

After=는 "시작됨"을 기다리며, Type=은 그 의미를 정의합니다

After=는 다른 유닛이 시작을 완료할 때까지 기다립니다. "시작 완료"의 의미는 전적으로 해당 유닛의 Type=에 의해 결정됩니다.

  • Type=simple: systemd가 프로세스를 포크(fork)하자마자 시작된 것으로 간주합니다. 프로그램이 설정을 파싱하지 않았거나 소켓을 열지 않았을 수도 있습니다.
  • Type=exec: execve()가 성공하자마자 시작된 것으로 간주합니다. 앞선 방식보다 약간 더 강력하지만, 여전히 준비 상태에 대해서는 아무것도 보장하지 않습니다.
  • Type=forking: 원래의 부모 프로세스가 종료될 때 시작된 것으로 간주합니다.
  • Type=oneshot: 프로세스가 종료될 때 시작된 것으로 간주합니다. 여기서 "시작됨"은 작업이 실제로 완료되었음을 의미합니다.
  • Type=notify: 서비스가 알림 소켓으로 READY=1을 보낼 때 시작된 것으로 간주합니다. 진정한 준비 상태를 보고하는 유일한 유형입니다.

따라서 Type=simple 데몬에서의 After=는 약한 약속이며, 이것이 첫 번째 예시에서 발생하는 레이스 컨디션의 두 번째 원인입니다. 의존하는 유닛이 Type=simple으로 배포된다면, 해당 유닛 뒤에 순서를 배치하더라도 실제로 연결을 수락하고 있다는 의미는 아닙니다. 두 가지 확실한 해결책이 있습니다. 대신 해당 유닛의 소켓 유닛 뒤에 순서를 배치하여 데몬이 시작되는 동안 커널이 들어오는 연결을 큐에 대기하게 하거나, 서비스가 재시도하도록 구성하고 재시작 정책(restart policy)에 맡기는 것입니다. 유닛이 어떤 유형을 사용하는지는 systemctl cat에서 확인할 수 있으며, Type= 설정과 각 값이 systemd에 전달하는 의미에 관한 문서는 의존성을 설정하기 전에 읽어볼 가치가 있습니다.

ExecStartPre는 유닛 실행을 중단시킬 수 있는 관문입니다

ExecStartPre=ExecStart= 이전에 실행됩니다. 만약 이 과정에서 0이 아닌 종료 코드가 반환되면, 활성화 작업은 즉시 중단되고 유닛은 failed 상태가 됩니다. 따라서 ExecStart=은 실행되지 않습니다. 실제 프로그램이 시작조차 되지 않았기 때문에 프로그램으로부터 아무런 메시지도 출력되지 않은 채 유닛이 실패하는 경우의 상당수가 바로 이 메커니즘 때문입니다.

주의해야 할 사실은 다음과 같습니다:

  • 셸이 아닙니다. 파이프, 리다이렉션, 와일드카드(glob), &&를 사용할 수 없습니다. 첫 번째 토큰은 반드시 절대 경로여야 합니다. 셸 문법이 필요하다면 해당 줄을 /bin/sh -c '...'으로 감싸야 합니다.
  • - 접두사를 붙이면 0이 아닌 종료 코드가 반환되어도 치명적인 오류로 간주하지 않습니다: ExecStartPre=-/usr/bin/optional-check.
  • 모든 ExecStartPre=은 다음 항목이 실행되기 전에 반드시 종료되어야 합니다. 따라서 오래 실행되는 프로세스를 시작할 수 없습니다.
  • 모든 ExecStartPre= 줄은 ExecStart=와 동일한 TimeoutStartSec=을 공유합니다. 데이터베이스를 기다리며 루프를 도는 사전 검사(pre-check)는 시작 제한 시간(timeout)을 소모하며, 저널에 start operation timed out. Terminating.이 기록된 후 유닛은 Result: timeout 오류와 함께 실패합니다.

실패 메시지에는 메인 프로세스가 아닌 제어 프로세스의 이름이 표시됩니다:

inventory-api.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
inventory-api.service: Failed with result 'exit-code'.

해당 기호 이름을 주의 깊게 읽어야 합니다. systemd는 작은 종료 코드들을 고정된 테이블에 매핑하므로, 프로그램이 어떤 의미로 종료했든 2는 항상 INVALIDARGUMENT으로 출력됩니다. status=203/EXEC는 실제 정보를 담고 있는 코드인데, 이는 경로가 잘못되었거나 파일에 실행 권한이 없어 systemd가 바이너리를 전혀 실행할 수 없었음을 의미합니다.

디렉터리를 생성할 때 ExecStartPre=를 사용하지 마십시오. RuntimeDirectory=, StateDirectory=, LogsDirectory=, CacheDirectory=를 사용하면 올바른 소유자와 모드로 디렉터리가 생성되며, RuntimeDirectory=은 서비스가 중지될 때 정리됩니다. 또한 이들은 DynamicUser= 환경에서도 올바르게 동작하지만, 직접 작성한 mkdir는 그렇지 못합니다.

Condition은 조용히 실패하고, Assert는 요란하게 실패합니다.

ConditionAssert 계열은 동일한 테스트를 수행합니다. 테스트 실패 시 동작 방식만 다릅니다.

Condition...=가 실패하면 해당 유닛은 건너뜁니다. 시작 작업은 성공으로 보고됩니다. 유닛은 inactive (dead) 상태로 유지되며, 실패로 표시되지 않고, 경고도 발생하지 않으며, 저널에는 다음과 같은 한 줄만 기록됩니다.

Condition check resulted in Inventory API being skipped.

systemd 250 버전 이상에서는 systemctl status이 실패 원인을 직접 출력합니다.

     Active: inactive (dead)
  Condition: start condition unmet at Thu 2026-08-20 09:14:02 UTC; 2min ago

그 아래 들여쓰기 된 줄은 실패한 정확한 지시어(예: ConditionPathExists=/etc/inventory/api.conf was not met)를 명시합니다.

Assert...=가 실패하면 유닛은 실패 처리됩니다. 저널에는 Assertion failed for Inventory API.이 기록되고 유닛은 failed (Result: assert) 상태로 종료되므로, 모니터링 시스템이 감지할 수 있을 만큼 명확하게 드러납니다.

테스트 실패가 무엇을 의미하는지 자문하여 둘 중 하나를 선택하십시오. Condition는 "이 유닛은 이 머신에 적용되지 않음"을 의미합니다. Assert은 "이 조건은 반드시 참이어야 하며, 그렇지 않다면 누군가에게 알려야 함"을 의미합니다. 대부분의 유닛은 Condition를 사용해야 합니다. Assert는 아무것도 하지 않고 넘어가는 것이 유닛 실패보다 더 나쁜 경우에만 사용하십시오.

Condition 계열을 사용할 때는 두 가지 함정을 주의해야 합니다.

첫째, 조건 실패는 해당 유닛에 의존하는 다른 유닛들을 실패하게 만들지 않습니다. 만약 a.serviceRequires=b.service을 가지고 있고 b.service가 조건에 의해 건너뛰어지더라도, b.service에 대한 시작 작업은 완료된 것으로 간주됩니다. 따라서 a.serviceb가 실행되지 않는 환경에서 정상적으로 시작됩니다. 조건은 해당 조건이 작성된 유닛만 보호합니다.

둘째, 조건은 유닛이 시작될 때마다, 즉 작업이 실행되는 순간에 평가됩니다. VPS의 systemd 타이머에 의해 트리거되는 유닛은 백 번 연속으로 건너뛰어질 수 있으며, 단 한 번도 실패한 것처럼 보이지 않을 수 있습니다. 이는 실행은 되지만 아무것도 하지 않는 cron 작업과 같은 부류의 조용한 무동작(no-op)이며, 유닛의 종료 상태를 신뢰하는 대신 저널을 읽음으로써 동일한 방식으로 찾아낼 수 있습니다.

서버에서 알아둘 가치가 있는 조건들은 다음과 같습니다.

  • ConditionPathExists=/etc/inventory/api.conf 및 그 부정형인 ConditionPathExists=!/etc/inventory/api.conf.
  • 패키지가 생성했지만 비어 있는 설정 파일이나 데이터 디렉터리를 위한 ConditionFileNotEmpty=ConditionDirectoryNotEmpty=.
  • 실제 커널 인터페이스가 필요한 유닛이 ConditionVirtualization=!container을 사용할 수 있게 하는 ConditionVirtualization=. systemd-detect-virt를 사용하여 현재 서버가 무엇을 보고하는지 확인하십시오.
  • 호스트 이름이나 머신 ID와 일치하는 ConditionHost=. 이를 통해 하나의 공유 유닛 파일을 두 서버에서 다르게 동작하게 할 수 있습니다.
  • 부팅 매개변수나 최소 커널 버전에 연결된 유닛을 위한 ConditionKernelCommandLine=ConditionKernelVersion=.

빈 값을 할당하면 목록이 초기화됩니다. 이는 패키지가 제공한 조건을 드롭인(drop-in) 파일에서 제거하는 방법입니다.

[Unit]
ConditionPathExists=
ConditionPathExists=/srv/inventory/api.conf

network.target이 네트워크 연결을 보장하지 않는 이유

network.target은 동기화 지점일 뿐 상태를 의미하지 않습니다. 부팅 시 이 뒤에 순서를 배치한다는 것은 네트워크 관리 소프트웨어가 시작되었다는 뜻입니다. 인터페이스에 주소가 할당되었거나 인터넷으로 가는 경로가 존재한다는 의미는 아닙니다. 이 타겟은 주로 반대 방향을 위해 존재합니다. 즉, After=network.target로 순서가 지정된 유닛은 시스템 종료 시 네트워크가 해제되기 전에 먼저 중지됩니다.

network-online.target는 실제로 대기하는 타겟입니다. 이 타겟은 사용 중인 네트워크 관리자에 따라 적절한 wait-online 서비스가 지원합니다.

  • systemd-networkd-wait-online.service: systemd-networkd가 링크를 관리할 때 사용하며, netplan으로 설정된 Ubuntu 서버의 일반적인 경우입니다.
  • NetworkManager-wait-online.service: NetworkManager를 사용할 때 사용합니다.

이전의 ifupdown 설정에서는 networking.service을 사용하여 동일한 효과를 얻습니다. 어떤 방식을 사용하든 타겟을 올바르게 사용하려면 한 줄이 아닌 두 줄의 설정이 필요합니다.

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

network-online.target는 기본 부팅 트랜잭션의 일부가 아니며, 아무것도 이를 자동으로 호출하지 않습니다. After=만 작성하면 큐에 추가되지 않은 유닛에 대해 순서를 지정하는 꼴이 되므로, 해당 설정은 아무런 동작을 하지 않습니다. 이것이 앞서 설명한 비용이 많이 드는 무동작(no-op) 상태입니다. Wants= 줄이 타겟을 트랜잭션에 포함시켜야 After= 줄이 기다릴 대상이 생깁니다.

두 번째로 알아야 할 점은 "online"의 정의가 systemd가 아닌 wait-online 구현체에 의해 결정된다는 것입니다. systemd-networkd-wait-online은 관리하는 링크가 설정된 상태에 도달하면 즉시 반환됩니다. DNS가 해결되는지 확인하지 않으며, 원격 호스트에 도달할 수 있는지도 확인하지 않습니다.

이러한 정의 때문에 VPS에서 흔한 장애가 발생합니다. netplan에는 선언되었지만 주소가 할당되지 않은 사설 네트워크용 두 번째 인터페이스가 있으면, 대기 서비스는 타임아웃이 될 때까지 기다리게 됩니다.

systemd-networkd-wait-online[612]: Timeout occurred while waiting for network connectivity.
systemd-networkd-wait-online.service: Failed with result 'exit-code'.

기본 타임아웃이 120초이므로 부팅 시간이 2분 더 소요됩니다. 해결 방법은 두 가지입니다. netplan 파일에서 사용하지 않는 인터페이스를 optional: true로 표시하여 networkd가 기다리지 않게 하거나, wait 서비스에 드롭인 파일을 추가하여 --interface=로 필요한 링크만 지정하거나, --any을 전달하여 링크 하나만 활성화되어도 즉시 반환되도록 설정합니다.

더 좋은 방법은 타겟에 의존하지 않는 것입니다. 많은 서비스가 network-online.target 이후에 실행되도록 설정되는 이유는 단순히 특정 주소에 바인딩을 시도하다가 부팅 시 다음과 같은 오류로 실패하기 때문입니다.

nginx: [emerg] bind() to 203.0.113.10:443 failed (99: Cannot assign requested address)

커널은 해당 주소가 아직 활성화되지 않았기 때문에 바인딩을 거부합니다. net.ipv4.ip_nonlocal_bind=1을 설정하면 프로세스가 아직 보유하지 않은 주소에도 바인딩할 수 있으며, 재시작 정책(restart policy)이 나머지 문제를 해결합니다. 네트워크 준비 상태에 맞춰 전체 부팅을 지연시키는 것은 소켓 하나 때문에 발생하는 문제치고는 너무 과한 대응입니다.

실행 중인 시스템에서 실제 systemd 의존 관계를 확인하는 방법

단위 파일(unit file)만 보고 판단해서는 안 됩니다. 드롭인(drop-in), .wants/ 심볼릭 링크, 암시적 기본 의존 관계 모두 파일에는 나타나지 않는 연결 고리를 추가하기 때문입니다.

systemctl cat inventory-api.service

이 명령은 단위 파일과 모든 드롭인을 적용 순서대로 출력하며, 각 블록 위에 원본 경로를 표시합니다. 가장 먼저 실행하십시오. /etc/systemd/system/inventory-api.service.d/에 있는 5줄짜리 재정의(override) 설정은 패키지 파일을 덮어쓰며, 그렇지 않으면 눈에 보이지 않습니다.

systemctl show inventory-api.service -p Requires -p Wants -p After -p Before -p ConditionResult -p AssertResult

이 명령은 드롭인과 systemd가 추가한 암시적 의존 관계가 모두 반영된 최종 값을 출력합니다. ConditionResult=no은 "단위가 성공했다고 보고했지만 아무 일도 일어나지 않은" 상황에 대한 직접적인 해답입니다.

systemctl list-dependencies inventory-api.service
systemctl list-dependencies --reverse inventory-api.service
systemctl list-dependencies --after inventory-api.service
systemctl list-dependencies --before inventory-api.service

기본 형식은 Requires=Wants=을 하향식으로 탐색합니다. --reverse는 어떤 단위가 해당 단위를 호출하는지 보여주며, 이를 통해 부팅 시 서비스를 시작하게 만드는 대상(target)을 찾을 수 있습니다. --after--before은 순서를 보여주며, 특정 작업이 실제로 대기했는지 확인해야 할 때 이 두 명령을 함께 읽어야 합니다.

journalctl -b -u inventory-api.service --no-pager
journalctl -b -o short-precise -u inventory-api.service -u postgresql.service

두 번째 명령은 두 단위의 로그를 밀리초 단위 타임스탬프와 함께 교차 출력합니다. 이는 추측하는 대신 순서 경쟁(ordering race)을 증명하는 방법입니다. pg_isready 실패가 PostgreSQL 로그 database system is ready to accept connections보다 먼저 발생하며, 그 사이의 시간 간격이 출력에 그대로 나타납니다.

systemd-analyze verify /etc/systemd/system/inventory-api.service
systemd-analyze critical-chain inventory-api.service

verify는 systemd가 단위를 로드하는 방식으로 파일을 읽어 들여 알 수 없는 지시어, 존재하지 않는 단위에 대한 의존성, 순서 순환, 구문 오류를 보고합니다. 시스템에는 아무런 변경도 가하지 않습니다. critical-chain은 단위를 지연시킨 순서 체인을 각 단계가 활성화된 시간과 함께 출력하며, 현재 부팅 세션 중에 시작된 단위에 대해서만 작동합니다.

단위 파일을 수정한 후에는 반드시 sudo systemctl daemon-reload을 실행하십시오. 패키지된 단위를 변경하려면 sudo systemctl edit inventory-api.service를 사용하십시오. 이 명령은 자동으로 드롭인을 생성해 줍니다. /usr/lib/systemd/system/ 아래의 벤더 파일을 직접 수정하면 다음 패키지 업그레이드 시 파일이 교체되어 수정 사항이 사라집니다. 동일한 드롭인 메커니즘을 사용하여 패키지가 소유한 파일을 건드리지 않고도 서비스에 메모리 및 CPU 제한을 설정할 수 있습니다.

순서 지정 순환과 저널에 남는 기록

양방향으로 순서를 지정하면 systemd는 작업 중 하나를 삭제하여 루프를 끊습니다.

systemd[1]: Found ordering cycle on inventory-api.service/start
systemd[1]: Job postgresql.service/start deleted to break ordering cycle starting with inventory-api.service/start

systemd는 삭제할 작업을 임의로 선택하며, 사용자가 의도한 것과 다를 수 있습니다. 그 결과 특정 재부팅 시에는 서비스가 누락되고 다른 때는 정상적으로 나타나는 현상이 발생하며, 외부에서 디버깅하기 매우 어렵습니다. 대부분의 순환 문제는 DefaultDependencies=no를 설정한 유닛이 다시 basic.target를 기준으로 순서를 지정하거나, 이미 자신을 가리키는 After=이 있는 유닛에 Before=을 추가할 때 발생합니다. systemd-analyze verify을 사용하면 재부팅 없이도 이러한 문제를 찾을 수 있습니다.

고정된 유닛

[Unit]
Description=Inventory API
Wants=postgresql.service network-online.target
After=postgresql.service network-online.target
ConditionPathExists=/etc/inventory/api.conf

[Service]
Type=notify
StateDirectory=inventory
ExecStart=/usr/local/bin/inventory-api
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

각 줄은 하나의 작업만 수행합니다. Wants=는 이 유닛의 수명과 관계없이 두 의존성을 트랜잭션에 포함합니다. After=은 대기 작업을 수행하며, 의존성과 순서 설정이 별개이므로 두 이름을 모두 반복해야 합니다. ConditionPathExists=은 패키지는 설치되었으나 설정이 없는 경우 경고 없이 유닛을 건너뛰게 합니다. 이는 설정 기반 서비스에 적합한 동작입니다. Type=notify는 이 유닛 뒤에 순서가 지정된 모든 항목이 프로세스 포크가 아닌 실제 준비 완료 상태를 기다리게 합니다. Restart=on-failure은 부팅 후 한참 뒤에 데이터베이스가 사라지는 경우를 대비합니다. 순서 설정은 최초 시작 시에만 적용되기 때문입니다. 재시도 강도는 Restart= 및 RestartSec= 설정으로 제어합니다.

신뢰하기 전에 다음을 확인하십시오.

sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemctl list-dependencies --after inventory-api.service
sudo systemctl start inventory-api.service
systemctl show inventory-api.service -p ConditionResult -p ActiveState -p Result

정상적인 유닛은 ActiveState=active를 사용하여 ConditionResult=yes를 읽으며, Result=success은 마지막 실행에서 실패한 항목이 없음을 확인합니다. ActiveState=inactive과 함께 ConditionResult=no이 나타나면 해당 유닛이 건너뛰어졌음을 의미하며, 저널 로그에서 조건을 명시한 줄을 확인하면 어떤 테스트가 실패했는지 알 수 있습니다.

FAQ

Does Requires= wait for the other unit to start?

No. Requires= and After= are separate settings. Requires= pulls the other unit into the same transaction, then systemd starts both jobs in parallel. To wait, add After= naming the same unit. There is a second reason to add it: a Requires= dependency that fails only prevents your unit from starting when After= is also set, because without ordering your unit has already been started by the time the other one fails.

Should I order after network.target or network-online.target?

At boot, network.target only means the network management software was started, so it promises nothing about addresses or routes. Use network-online.target when your service needs a working address at start, and write both Wants=network-online.target and After=network-online.target, because the target is not in the default boot transaction and After= alone waits for a unit nobody queued. If the service fails only because it binds one specific IP, net.ipv4.ip_nonlocal_bind=1 with Restart=on-failure is lighter than delaying boot.

Why does my unit report success but never run?

A failed Condition...= test skips the unit and reports the start job as successful, so nothing is ever marked failed. Run systemctl show <unit> -p ConditionResult, and ConditionResult=no confirms it. Then read journalctl -b -u <unit> for the line Condition check resulted in <description> being skipped. On systemd 250 and newer, systemctl status <unit> also names the exact directive that was not met.

What is the difference between Condition and Assert?

They run identical tests. A failed Condition skips the unit quietly and the job still succeeds. A failed Assert fails the unit, logs Assertion failed for <description>. and leaves it in failed (Result: assert). Use Condition for "this unit does not apply on this machine", which covers almost every real case. Use Assert only when a missing precondition has to be visible to whoever watches failed units.

Why does ExecStartPre fail with status=203/EXEC?

203/EXEC means systemd could not execute the command at all. The usual causes are a path that is not absolute, a binary that does not exist on that machine, a file without the execute bit, or a script whose #! line points at a missing interpreter. systemd's other small codes come from a fixed table, so status=2/INVALIDARGUMENT just means the command exited 2 and says nothing about arguments. Remember that ExecStartPre= is not run through a shell, so pipes and globs need /bin/sh -c '...'.

#systemd#units#dependencies#ordering#troubleshooting