본문으로 건너뛰기
AZ Tools

systemd 서비스 파일 생성기

systemd .service 유닛 파일을 만듭니다. Type, Restart, User, Environment, 샌드박싱 설정과 설치·journalctl 명령까지 함께 출력합니다.

범위:
example.service
[Unit]
Description=Example API server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/example-api --port 8080
WorkingDirectory=/srv/example
User=example
Environment=NODE_ENV=production
Environment=PORT=8080
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

# Sandboxing — remove any line that stops the service working.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
ReadWritePaths=/srv/example

[Install]
WantedBy=multi-user.target
설치 명령
# /etc/systemd/system/example.service
sudo systemctl daemon-reload
sudo systemctl enable --now example.service
sudo systemctl status example.service
sudo journalctl -u example.service -f

사용법

  1. 범위를 고릅니다. 부팅 시 시작하는 /etc/systemd/system의 시스템 유닛이거나, 로그인 시 시작하는 ~/.config/systemd/user의 --user 유닛입니다.
  2. ExecStart에 프로그램의 절대 경로와 인자를 입력합니다. systemd는 $PATH를 참조하지 않으므로 node가 아니라 /usr/bin/node로 적어야 합니다.
  3. Type=과 Restart=를 고릅니다. 포그라운드로 계속 떠 있는 프로그램은 Type=simple, 한 번 실행하고 끝나는 스크립트는 Type=oneshot 이 맞습니다.
  4. 환경 변수는 한 줄에 KEY=value 한 쌍씩 넣고, 비밀값이라면 EnvironmentFile=로 별도 파일을 가리키게 합니다.
  5. 표시된 경로에 유닛 파일을 복사한 뒤, 두 번째 상자의 daemon-reload·enable·journalctl 명령을 실행합니다.

자세한 설명

펼치기

어떤 지시어가 어느 섹션에 들어가는지 외우지 않아도 되도록, 입력 폼에서 systemd 유닛 파일을 만들어 줍니다. 실행할 명령과 실행 계정, 재시작 정책을 채우면 [Unit] / [Service] / [Install] 세 섹션이 완성된 파일과 함께 설치·활성화·로그 확인 명령이 나옵니다. 또한 systemd가 오류를 내지 않고 조용히 다르게 동작하는 네 가지 실수를 표시합니다. ExecStart가 절대 경로가 아닌 경우(systemd는 $PATH를 찾지 않으므로 status=203/EXEC로 실패합니다), 명령에 파이프나 리다이렉션, && 가 들어간 경우(셸을 거치지 않으므로 그대로 인자로 전달됩니다), --user 유닛에 User=를 쓴 경우(사용자 관리자는 이미 본인 권한으로 실행되므로 무시됩니다), Type=oneshot에 Restart=always를 건 경우(끝나야 할 프로세스를 무한히 재시작합니다)입니다. 샌드박싱 블록은 선택 사항이며 의도적으로 보수적입니다. ProtectSystem=strict 같은 설정은 작업 디렉터리 밖에 쓰는 서비스를 망가뜨리므로, 생성된 파일에 그 사실을 주석으로 남깁니다. 파일은 브라우저 안에서만 조립되며 업로드되지 않습니다.

자주 묻는 질문

서비스가 status=203/EXEC로 실패합니다.

대부분 ExecStart가 절대 경로가 아니기 때문입니다. systemd는 셸을 거치지 않고 실행 파일을 직접 실행하므로 $PATH를 전혀 찾지 않습니다. 그냥 node server.js 라고 쓰면 파일 시스템 루트에 있는 node 라는 파일을 실행하라는 뜻이 됩니다. which node 결과를 그대로 넣으십시오. 경로가 이미 절대 경로인데도 같은 코드가 나온다면 실행 권한(chmod +x)과 셔뱅 줄이 가리키는 인터프리터가 실제로 존재하는지 확인하십시오.

ExecStart에 파이프나 리다이렉션, && 를 쓸 수 있나요?

쓸 수 없고, systemd는 경고도 하지 않습니다. 그냥 프로그램의 인자로 넘겨 버리므로 myapp > /var/log/app.log는 인자 세 개를 받은 myapp 이 됩니다. 셸 문법이 꼭 필요하다면 ExecStart=/bin/sh -c 'myapp | tee /var/log/app.log' 처럼 셸을 명시적으로 호출하십시오. 다만 대개는 필요 없습니다. 표준 출력은 이미 저널로 가므로 파일로 리다이렉션하는 대신 journalctl -u yourapp으로 읽으면 됩니다.

After=와 Wants=는 무엇이 다릅니까?

After=는 순서만 지정합니다. 두 유닛이 같은 트랜잭션에서 시작될 때 내 유닛을 나중에 시작하라는 뜻일 뿐, 상대 유닛을 끌어오지는 않습니다. 그래서 아무도 network-online.target을 원하지 않는 머신에서 After=network-online.target만 쓰면 그 타깃은 아예 활성화되지 않고, 기다리는 의미도 사라집니다. 실제로 끌어오는 의존 관계는 Wants=입니다. 이 생성기가 After= 옆에 Wants=network-online.target을 함께 넣는 이유이며, 서비스가 네트워크보다 먼저 뜨는 문제의 가장 흔한 원인이기도 합니다.

시스템 유닛과 --user 유닛 중 무엇을 써야 합니까?

로그인 전에도, 로그인 없이도 떠 있어야 하는 것(웹 서버, 데이터베이스, 정기 작업)은 시스템 유닛입니다. 세션에 묶인 것(동기화 에이전트, 개인용 봇)은 --user 유닛이 맞습니다. 본인 권한으로 실행되고 sudo가 필요 없으며 ~/.config/systemd/user에 놓입니다. 다만 사용자 관리자는 마지막 세션이 끝나면 보통 함께 종료되므로, 로그아웃 후에도 살아 있어야 하는 서비스에는 loginctl enable-linger가 필요하고 생성된 명령에 포함되어 있습니다. 또 사용자 유닛에서는 User=와 Group=를 지정할 수 없고, network-online.target 같은 시스템 타깃도 존재하지 않습니다.

샌드박싱 블록은 그대로 둬도 됩니까?

시도해 볼 가치는 있지만 당연히 된다고 가정하면 안 됩니다. ProtectSystem=strict는 /dev, /proc, /sys를 뺀 파일 시스템 전체를 읽기 전용으로 만들므로, 작업 디렉터리 밖에 PID 파일이나 SQLite 데이터베이스, 로그를 쓰는 서비스는 활성화하는 순간 실패합니다. 그래서 생성기는 작업 디렉터리에 대해 ReadWritePaths를 함께 넣어 가장 흔한 경우는 그대로 동작하게 합니다. 그 밖에 쓰는 경로가 있다면 같은 줄에 추가해야 합니다. 블록을 넣자마자 서비스가 죽는다면 전부 지우지 말고 한 줄씩 빼면서 확인하십시오. systemd-analyze security yourapp.service가 각 지시어의 효과를 점수로 알려 줍니다.

유닛 파일을 수정했는데 반영되지 않습니다.

systemd는 유닛 파일을 캐시하므로 디스크의 수정은 systemctl daemon-reload(사용자 유닛이면 systemctl --user daemon-reload)를 실행하기 전까지 보이지 않습니다. 그리고 reload 이후에도 이미 실행 중인 서비스가 새 설정을 반영하려면 systemctl restart yourapp 이 따로 필요합니다. daemon-reload는 systemd가 가진 파일의 사본만 갱신하기 때문입니다. 그렇게 했는데도 Environment= 변경이 무시된다면 EnvironmentFile= 이 같은 변수를 설정하고 있는지 확인하십시오. 파일은 인라인 지시어보다 나중에 읽히므로 파일 쪽이 이깁니다.

관련 도구