환경 systemd 유닛 · 리눅스 서버 기준 · 2026-08-28 확인
지금 도는 것과 다음 부팅에 뜨는 건 다른 상태입니다. enable 부터 Type 까지 차례로 짚어 보면 서버를 내리기 전에 알 수 있어요.
이 목록을 꺼내야 하는 때
새 유닛 파일을 올린 날, 도는 방식을 바꾼 날, 서버를 새로 찍어 같은 구성을 옮긴 날. 이 세 경우에 꺼내면 됩니다. 서비스를 손으로 띄워 놓고 그대로 넘어간 서버가 제일 위험해요.
systemd 에서 '지금 도는 것'과 '다음 부팅에 뜨는 것'은 서로 다른 상태입니다. 하나는 프로세스가 살아 있느냐고, 다른 하나는 부팅 시점에 시작하라는 링크가 걸려 있느냐죠. 두 값은 따로 놉니다. 지금 active 인데 다음 부팅에는 안 뜨는 조합이 얼마든지 가능합니다.
그런데 평소에 보이는 건 앞쪽뿐입니다. status 를 열어 초록 글씨를 보고 다 됐다고 넘어가면, 몇 달 뒤 정기 점검으로 서버를 내렸다 올리는 날 드러납니다. 하필 그날은 밤이고요.
그러니 물어볼 질문을 바꿔야 합니다. 지금 도느냐가 아니라 다음 부팅에 뜨느냐로요. 답은 명령 한 줄에 나옵니다.
systemctl is-enabled myapp.service
# enabled ← 다음 부팅에 뜬다
# disabled ← 지금 돌고 있어도 재부팅하면 사라진다
# static ← [Install] 이 없어 enable 이 걸리지 않는 유닛
systemctl is-active myapp.service
# active ← 이건 '지금'에 대한 답일 뿐이다
is-active 와 is-enabled 는 서로 다른 질문에 답합니다
지금 active 인 것과 다음 부팅에 뜨는 것은 별개입니다. 한쪽만 보고 넘어가는 데서 사고가 납니다.
start 는 지금까지고, enable 이 다음 부팅을 정합니다
start 는 유닛을 그 자리에서 띄웁니다. 그게 전부입니다. 부팅 목록에는 아무것도 남기지 않아요.
enable 은 반대입니다. 유닛 파일의 [Install] 섹션에 적힌 대로 심볼릭 링크를 만들어 둡니다. 다음 부팅에 그 링크를 보고 시작하죠. 대신 지금 띄우는 일은 하지 않습니다. 문서에도 못을 박아 뒀습니다. enable 은 대상 유닛을 시작하지 않는다고요.
그래서 배포 스크립트가 start 만 하고 끝나면 서비스는 재부팅 전까지만 살아 있습니다. 두 가지를 한 번에 하려면 --now 를 붙입니다.
확인은 링크를 직접 보는 게 확실합니다. WantedBy 에 적은 타깃의 .wants 디렉터리에 내 유닛 이름이 있어야 합니다.
systemctl start myapp.service # 지금만 띄운다
systemctl enable myapp.service # 다음 부팅부터 뜬다 (지금 띄우지는 않는다)
systemctl enable --now myapp.service # 둘 다
ls -l /etc/systemd/system/multi-user.target.wants/ | grep myapp
# myapp.service -> /etc/systemd/system/myapp.service ← 이 링크가 부팅 때 쓰인다
enable 이 만드는 건 결국 이 심볼릭 링크입니다
[Install] 이 없는 유닛은 enable 이 그냥 지나갑니다
enable 이 만드는 링크의 위치는 유닛 파일의 [Install] 섹션이 정합니다. WantedBy=multi-user.target 같은 줄이죠. 이 섹션이 통째로 빠져 있으면 만들 링크가 없습니다.
이때 명령이 요란하게 실패하지는 않습니다. 설치 정보가 없다는 경고를 한 줄 남기고 끝납니다. 터미널을 대충 훑으면 성공한 것처럼 보이죠. 스크립트로 돌렸다면 그 경고는 로그 어딘가에 파묻힙니다.
이런 유닛의 상태값은 static 입니다. enabled 도 disabled 도 아닙니다. 다른 유닛이 끌어당길 때만 뜨고, 혼자서는 부팅 목록에 오르지 않습니다.
그러니 enable 을 한 다음에는 반드시 is-enabled 로 되받아 봅니다. static 이 나오면 [Install] 섹션을 넣고 다시 enable 합니다.
# /etc/systemd/system/myapp.service
[Unit]
Description=myapp API server
[Service]
ExecStart=/usr/local/bin/myapp
[Install]
WantedBy=multi-user.target ← 이 섹션이 없으면 enable 이 할 일이 없다
부팅에 뜨게 하는 건 [Service] 가 아니라 [Install] 입니다
is-enabled 가 static 을 돌려주면 enable 을 아무리 여러 번 해도 결과는 같습니다.
network.target 은 네트워크가 준비됐다는 뜻이 아닙니다
부팅 직후에만 실패하고 손으로 재시작하면 멀쩡한 서비스가 있습니다. 대개 뜰 때 주소를 잡거나 DB 에 붙는 쪽입니다. After=network.target 을 적어 뒀는데도 그럽니다.
이름이 오해를 부릅니다. 이 타깃에 도달했다고 해서 장치가 다 올라왔거나 IP 설정이 끝났다는 보장이 없습니다. 문서가 직접 그렇게 적어 뒀습니다.
쓸 수 있는 네트워크를 기다리려면 network-online.target 을 Wants= 로 끌어당기고 After= 로 순서를 잡아야 합니다. 둘 다 있어야 합니다. Wants= 없이 After= 만 적으면 그 타깃이 부팅에 아예 안 들어와서 기다릴 대상이 없습니다.
그리고 실제로 기다리는 일은 타깃이 아니라 그 뒤에 붙은 wait-online 서비스가 합니다. 네트워크 관리자마다 이름이 다르니, 쓰는 쪽 것이 켜져 있는지 같이 봅니다.
[Unit]
Description=myapp API server
Wants=network-online.target
After=network-online.target
# 확인
# systemctl is-enabled NetworkManager-wait-online.service
# systemctl is-enabled systemd-networkd-wait-online.service
# → 네트워크 관리자에 맞는 쪽이 enabled 여야 실제로 기다린다
Wants= 와 After= 는 짝으로 넣습니다
죽은 다음에 다시 뜨는지까지 적어 둡니다
부팅에 뜨는 것과 죽었을 때 되살아나는 건 또 다른 이야기입니다. Restart= 를 안 적으면 기본은 no 입니다. 프로세스가 한 번 죽으면 그걸로 끝이죠.
on-failure 는 0 이 아닌 종료 코드, 시그널로 끊긴 경우, 타임아웃, 워치독까지 잡아 줍니다. always 는 정상 종료까지 포함해 무조건 다시 띄웁니다. 스스로 끝나는 게 정상인 프로그램에 always 를 걸면 종료할 때마다 다시 뜹니다.
여기에 시작 횟수 제한이 겹칩니다. 기본값은 10초에 5회입니다. 그 안에 그보다 자주 시작되면 systemd 가 더 이상 띄우지 않습니다. 그래서 설정이 틀린 서비스는 부팅 직후 몇 초 동안 재시작을 반복하다가 조용히 죽어 있습니다.
이 상태는 카운터를 비워야 풀립니다. reset-failed 로 지우고, 그전에 RestartSec 을 늘려 재시작 간격을 벌려 두면 제한에 덜 걸립니다.
[Unit]
StartLimitIntervalSec=30s
StartLimitBurst=5 # 기본은 10초에 5회
[Service]
Restart=on-failure
RestartSec=5 # 바로 다시 띄우지 말고 5초 쉰다
# 제한에 걸려 멈춘 유닛은 카운터부터 비운다
# systemctl reset-failed myapp.service
재시작 정책과 시작 제한은 짝으로 봅니다
재시작을 너무 자주 하면 systemd 가 손을 뗍니다. 로그에는 실패만 남고 이유는 정책 쪽에 있습니다.
Type 이 실제 동작과 다르면 순서가 어긋납니다
systemd 는 유닛이 '떴다'고 판단하는 시점을 Type= 으로 정합니다. 이 판단이 틀리면 뒤에 오는 유닛이 먼저 올라갑니다. 준비도 안 된 API 에 대고 마이그레이션을 돌리는 식이죠.
스스로 백그라운드로 넘어가는 데몬은 forking 입니다. 포크하고 나온 원래 프로세스가 끝나는 순간을 시작 완료로 봅니다. 이때는 PIDFile= 을 같이 적어 줘야 systemd 가 진짜 프로세스를 붙잡습니다.
준비 완료를 스스로 알리는 프로그램은 notify 가 정확합니다. READY=1 을 받고 나서야 다음 유닛으로 넘어갑니다.
한 번 실행하고 끝나는 작업은 좀 다릅니다. 프로세스가 다 빠지면 기본적으로 inactive 로 떨어지니, 끝난 상태를 active 로 남겨 두고 싶으면 RemainAfterExit=yes 를 넣습니다. 마운트나 초기화처럼 '한 번 해 두면 되는' 유닛에 씁니다.
systemctl show -p Type -p MainPID -p RemainAfterExit myapp.service
# Type=simple
# MainPID=0 ← simple 이라고 적었는데 붙잡은 프로세스가 없다
# RemainAfterExit=no
# 데몬이 스스로 포크한다면
# Type=forking
# PIDFile=/run/myapp.pid
MainPID 가 0 이면 systemd 가 실제 프로세스를 놓친 겁니다
내 계정으로 돌리는 유닛은 로그아웃과 함께 내려갑니다
systemctl --user 로 등록한 유닛은 편합니다. sudo 도 필요 없고 홈 디렉터리 권한 그대로 돌아가죠. 그래서 배치나 동기화 스크립트를 여기에 걸어 두는 경우가 많습니다.
문제는 이 유닛들이 로그인 세션에 매여 있다는 점입니다. 사용자 매니저는 로그인할 때 뜨고 로그아웃하면 내려갑니다. 그러니 아무도 접속하지 않은 서버에서는 부팅해도 그 유닛이 시작되지 않습니다.
떼어 놓으려면 linger 를 켜야 합니다. 켜 두면 부팅 시점에 그 사용자의 매니저가 뜨고, 로그아웃한 뒤에도 남습니다.
그러니 --user 유닛은 확인할 곳이 두 군데입니다. 유닛이 enabled 인지, 그리고 그 계정에 linger 가 걸려 있는지.
systemctl --user is-enabled myapp.service
# enabled
loginctl show-user "$USER" -p Linger
# Linger=no ← 로그아웃하면 내려가고, 부팅해도 안 뜬다
sudo loginctl enable-linger "$USER"
유닛만 enable 하고 linger 를 빼먹는 실수가 흔합니다
고친 파일은 반영하고, 반영 전에 검사합니다
유닛 파일을 편집기로 고쳐도 systemd 는 모릅니다. daemon-reload 를 해야 다시 읽습니다. enable 이나 disable 은 안에서 알아서 다시 읽지만, 손으로 파일만 고친 경우는 직접 해 줘야 합니다.
reload 만 하면 설정이 새로 읽힐 뿐 도는 프로세스는 예전 설정 그대로입니다. 바뀐 내용을 지금 적용하려면 restart 까지 해야 합니다.
올리기 전에 문법 검사를 한 번 돌리면 오타를 미리 걸러냅니다. 이 명령은 유닛을 설치하지 않고 읽어서 문제만 알려 줍니다. 모르는 지시어, 없는 의존 유닛, 실행할 수 없는 ExecStart 경로 같은 것들이 잡힙니다.
출력이 없으면 통과입니다. 짧은 명령 하나로 오타 때문에 부팅에 실패하는 경우를 대부분 막을 수 있습니다.
systemd-analyze verify /etc/systemd/system/myapp.service
# 출력이 없으면 통과
systemctl daemon-reload # 파일을 손으로 고쳤으면 필수
systemctl restart myapp.service # 바뀐 설정으로 다시 띄운다
verify → daemon-reload → restart 순서로 갑니다
어디까지 맞으면 통과인가
다섯 줄이면 판정이 납니다. is-enabled 가 enabled, is-active 가 active, verify 는 무출력. 사용자 유닛이면 Linger=yes 까지. 네트워크를 타는 서비스라면 유닛 파일에 Wants= 와 After= 가 둘 다 있어야 합니다.
하나라도 어긋나면 위에서 해당하는 자리로 돌아갑니다. static 이면 [Install] 섹션, disabled 면 enable, 부팅 직후에만 실패하면 네트워크 순서, 몇 초 만에 죽어 있으면 재시작 정책입니다.
다 맞았다면 마지막으로 실제 재부팅 한 번이 남습니다. 점검 목록은 확률을 올려 줄 뿐이고, 진짜 답은 서버를 내렸다 올려야 나옵니다. 정기 점검 창이 있다면 그때 확인해 두면 됩니다.
재부팅을 당장 못 하는 서버라면 차선책이 있습니다. 부팅 순서를 그대로 재연해 주지는 않지만, 유닛이 혼자 힘으로 올라오는지는 볼 수 있습니다.
u=myapp.service
systemctl is-enabled "$u" # enabled
systemctl is-active "$u" # active
systemd-analyze verify /etc/systemd/system/"$u" # 무출력
# 재부팅 전 차선책: 완전히 내렸다가 부팅 때처럼 다시 올려 본다
systemctl stop "$u" && systemctl start "$u" && systemctl status "$u" --no-pager
세 줄이 다 맞으면 재부팅 전에 할 수 있는 건 끝입니다
점검 목록의 마지막 항목은 언제나 재부팅입니다. 확인하지 않은 자동 시작은 아직 자동이 아닙니다.
조치 후 확인할 것
- is-active 말고 is-enabled 를 본다. enabled 가 아니면 지금 잘 돌고 있어도 재부팅하면 사라진다.
- enable 한 뒤 is-enabled 로 되받는다. static 이 나오면 [Install] 섹션이 없다는 뜻이고, enable 은 경고만 남기고 지나간다.
- 네트워크가 필요한 서비스는 network-online.target 을 Wants= 와 After= 에 둘 다 적고, 쓰는 네트워크 관리자의 wait-online 서비스가 켜져 있는지 본다.
- Restart= 를 적었으면 시작 제한도 같이 본다. 기본 10초 5회를 넘기면 더 이상 안 올라오고, reset-failed 로 카운터를 비워야 풀린다.
- systemctl --user 유닛은 loginctl show-user 로 Linger 까지 확인한다. 유닛만 enable 해 두면 아무도 로그인하지 않은 서버에서는 뜨지 않는다.
부팅에 안 뜨는 서비스는 평소에 아무 신호도 주지 않습니다. 화면은 초록색이고 로그도 깨끗하죠. 그래서 몇 달 지나 서버를 내리는 날 처음 드러납니다. 위 다섯 줄을 찍어 보는 데는 일 분도 안 걸립니다. 새 유닛을 올리는 날마다 한 번씩만 해 두면, 재부팅이 무서운 서버가 하나씩 줄어듭니다.
'복사금지 블로그 짜증나서 만든 개발문서' 카테고리의 다른 글
| 리눅스 load average 정리 — CPU 사용률이 아니라 줄 선 프로세스 수입니다 (0) | 2026.09.05 |
|---|---|
| docker compose down을 stop 대신 쓰면 무엇이 사라질까 (0) | 2026.08.14 |
| npm 배포 전에 어떤 파일이 올라가는지 미리 확인하는 방법 (0) | 2026.08.07 |
| 맥에서 파일명 대소문자만 바꿨더니 git이 못 잡을 때 (0) | 2026.08.05 |
| docker stop이 10초씩 걸리고 종료 코드 137이 뜰 때 확인하는 순서 (0) | 2026.08.03 |