Skip to content
ioob.dev
Go back

Linux 기초 6편 — Systemd와 서비스 관리

· 8분 읽기
Linux 시리즈 (6/8)
  1. Linux 기초 1편 — 셸과 파일시스템 구조
  2. Linux 기초 2편 — 파일 권한과 사용자/그룹
  3. Linux 기초 3편 — 프로세스와 시그널
  4. Linux 기초 4편 — 텍스트 처리와 파이프
  5. Linux 기초 5편 — 네트워크 도구
  6. Linux 기초 6편 — Systemd와 서비스 관리
  7. Linux 기초 7편 — 패키지 관리
  8. Linux 기초 8편 — Bash 스크립팅 기초
Table of contents

Table of contents

서버의 프로세스를 누가 감독하는가

SSH로 서버에 올라가서 Nginx를 켜고, 백엔드 앱을 올리고, 백업 스크립트를 도는 시점에 매일 아침 5시로 잡는다. 이 모든 일의 공통점은 “프로세스를 언제·어떻게·어떤 순서로 띄우고 내릴지”다.

예전 리눅스에서는 이 일을 SysV init이라는 방식이 맡았다. /etc/init.d/ 아래의 쉘 스크립트 묶음이 부팅 순서를 결정했다. 쉘 스크립트 기반이라 유연했지만, 병렬 실행도 어렵고 의존성 표현도 빈약하고, 로깅은 각자 알아서. “이 서비스가 왜 안 떴지?”를 조사하려면 스크립트를 한 줄씩 읽어야 했다.

systemd는 이 세계를 다시 그렸다. 대부분의 현대 리눅스 배포판(Ubuntu 16 이후, Debian 8 이후, RHEL/CentOS 7 이후, 거의 모든 컨테이너 베이스 이미지 제외)이 systemd를 기본 init 시스템으로 채택하고 있다. 선언적 유닛 파일, 병렬 부팅, 중앙집중 로그, 의존성 그래프 — 필요한 도구가 한 세트로 묶여 있다.

systemd의 큰 그림

systemd가 “init 시스템”이라고 부르긴 하지만, 실제로는 리눅스 부팅 이후의 거의 모든 것을 아우르는 제품군이다. 핵심 컴포넌트를 한 장으로 정리하면 이렇다.

flowchart TB
    KERNEL["Linux Kernel"] --> SYSD["systemd (PID 1)"]
    SYSD --> UNITS["Units<br/>(.service / .socket / .timer / .target ...)"]
    UNITS --> SVC[".service<br/>데몬·프로세스"]
    UNITS --> SOC[".socket<br/>소켓 기반 활성화"]
    UNITS --> TMR[".timer<br/>cron 대체"]
    UNITS --> TGT[".target<br/>runlevel 대체"]
    SYSD --> JOURNALD["systemd-journald<br/>중앙 로그"]
    SYSD --> LOGIND["systemd-logind<br/>로그인 세션"]
    SYSD --> RESOLVED["systemd-resolved<br/>DNS 캐시"]
    SYSD --> TIMESYNCD["systemd-timesyncd<br/>NTP"]

부팅 직후 커널이 systemdPID 1로 띄우고, 이후 모든 사용자 영역 프로세스는 systemd의 자식으로 시작된다. 여러 서브시스템(journald, logind, resolved, timesyncd)이 각자 역할을 나눠 맡는다.

이 글에서는 실무에서 가장 많이 쓰는 .service, systemctl, journalctl, 그리고 타이머까지만 파고든다. 나머지는 필요할 때 공식 문서를 찾아봐도 된다.

systemctl — 서비스 조작의 표준 명령

systemctl은 유닛을 다루는 진입점이다. 시작, 중지, 활성화, 상태 확인이 전부 여기를 통한다.

가장 자주 쓰는 명령은 다음 다섯 가지다.

# 상태 확인
sudo systemctl status nginx

# 즉시 시작·중지·재시작
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx

# 설정만 다시 읽기 (프로세스는 유지)
sudo systemctl reload nginx

# 부팅 시 자동 시작 등록·해제
sudo systemctl enable nginx
sudo systemctl disable nginx

# 활성화 + 즉시 시작을 한 번에
sudo systemctl enable --now nginx

여기서 헷갈리는 게 startenable의 차이다. start지금 이 순간 서비스를 띄우는 것이고, enable다음 부팅 때도 자동으로 뜨도록 등록하는 것이다. 실무에서는 enable --now로 두 가지를 한 번에 하는 경우가 많다.

status의 출력이 꽤 풍부하니 한 번 짚어두자.

sudo systemctl status nginx
# ● nginx.service - A high performance web server
#      Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
#      Active: active (running) since Mon 2026-04-20 10:00:12 UTC; 2h 15min ago
#        Docs: man:nginx(8)
#    Main PID: 12345 (nginx)
#       Tasks: 5 (limit: 4915)
#      Memory: 12.8M
#         CPU: 234ms
#      CGroup: /system.slice/nginx.service
#              ├─12345 "nginx: master process"
#              ├─12346 "nginx: worker process"
#              └─12347 "nginx: worker process"
#
# Apr 20 10:00:12 host systemd[1]: Started A high performance web server.
# Apr 20 10:00:12 host nginx[12345]: nginx: [warn] conflicting server name ...

이 출력에는 쓸 만한 정보가 빼곡하다. 프로세스 트리(CGroup), 메모리·CPU 사용량, 최근 로그 몇 줄까지. 트러블슈팅 초반에 “우선 status부터”는 거의 반사적인 습관으로 잡아두면 좋다.

유닛 파일의 구조

.service 유닛 파일은 단순한 INI 포맷이다. 시스템이 관리하는 파일과 사용자가 고치는 파일의 위치가 나뉘어 있는 게 핵심이다.

flowchart LR
    A["/lib/systemd/system/<br/>배포판 기본 유닛"]
    B["/etc/systemd/system/<br/>관리자 커스텀"]
    C["/run/systemd/system/<br/>런타임 동적 유닛"]

    A -.->|"override"| B
    C -.->|"override"| B
    B -->|"우선"| FINAL["최종 적용 유닛"]

같은 이름의 유닛이 여러 디렉토리에 있으면 /etc/systemd/system/이 최우선이다. 그래서 배포판이 제공하는 원본을 건드리지 않고, 내 커스텀은 /etc/systemd/system/에 두는 게 관례다.

한 예로, Nginx의 유닛 파일을 보자.

cat /lib/systemd/system/nginx.service
[Unit]
Description=A high performance web server and a reverse proxy server
After=network.target
Documentation=man:nginx(8)

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
Restart=on-failure

[Install]
WantedBy=multi-user.target

세 섹션이 있다.

Type=의 값은 simple(기본·포크 안 함), forking(데몬 스타일), oneshot(한 번 실행 후 종료), notify(sd_notify로 준비 완료 알림), exec(포크만 기다림) 등이 있다. 대부분의 최신 앱은 simple 또는 exec가 적절하다.

override.conf — 원본을 건드리지 않고 일부만 수정

배포판의 원본 유닛을 수정하고 싶을 때, 파일 자체를 편집하지 말고 drop-in 디렉토리를 쓴다.

sudo systemctl edit nginx

이 명령이 /etc/systemd/system/nginx.service.d/override.conf를 에디터로 열어준다. 거기에 덮어쓰고 싶은 지시자만 적으면 된다.

[Service]
# 원본 환경변수를 지우고 새로 지정
Environment=
Environment="NGINX_WORKER_CONNECTIONS=2048"
Restart=always
RestartSec=5s

Environment= 같이 목록형 지시자는 빈 값으로 한 번 리셋한 뒤 새로 설정하는 패턴을 기억해두면 좋다. 그렇지 않으면 원본과 오버라이드가 합쳐져서 엉뚱한 값이 남는다.

편집 후에는 데몬 리로드가 필요하다. systemctl edit은 자동으로 하지만, 파일을 직접 편집했다면 아래를 친다.

sudo systemctl daemon-reload
sudo systemctl restart nginx

커스텀 서비스 — 내가 만든 앱을 systemd에 태운다

이제 스스로 만든 앱을 systemd 서비스로 등록해본다. 예를 들어 Node.js로 만든 간단한 API 서버를 myapp이라는 서비스로 등록한다고 치자.

# 앱이 배포된 경로
/opt/myapp/server.js

유닛 파일을 만든다.

sudo tee /etc/systemd/system/myapp.service <<'EOF'
[Unit]
Description=My Node.js API
After=network.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node /opt/myapp/server.js
Restart=on-failure
RestartSec=3s
Environment=NODE_ENV=production
Environment=PORT=8080
# 리소스 제한 예시
LimitNOFILE=65535
MemoryMax=512M

[Install]
WantedBy=multi-user.target
EOF

그리고 등록·시작·자동 시작을 한 번에.

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp

몇 가지 실무 팁이다.

journalctl — 로그는 한 곳에서 본다

앞서 나온 “stdout/stderr가 저절로 journald에 남는다”는 이야기가 systemd의 강력한 포인트다. 예전에는 서비스마다 /var/log/nginx/, /var/log/mysql/ 하는 식으로 로그가 흩어져 있었다. journalctl은 이걸 한 창구로 모은다.

가장 자주 쓰는 조합이다.

# 최근 로그를 이어서 보기 (tail -f 느낌)
sudo journalctl -u nginx -f

# 최근 50줄만
sudo journalctl -u nginx -n 50

# 특정 기간
sudo journalctl -u nginx --since "1 hour ago"
sudo journalctl -u nginx --since "2026-04-20 09:00" --until "2026-04-20 10:00"

# 특정 서비스 + 우선순위 (err 이상)
sudo journalctl -u myapp -p err

# 부팅 단위 (이번 부팅)
sudo journalctl -b

# 이전 부팅
sudo journalctl -b -1

# 커널 메시지만
sudo journalctl -k

-u <유닛>, -f(follow), --since / --until 세 개만 외워두면 대부분 커버된다.

로그가 너무 커지는 걸 걱정한다면 /etc/systemd/journald.conf에서 로테이션 정책을 잡는다. 기본은 /var/log/journal이 생겨있는 배포판과 그렇지 않은 배포판이 다르다.

# /etc/systemd/journald.conf
[Journal]
Storage=persistent
SystemMaxUse=1G
SystemMaxFileSize=100M
MaxRetentionSec=2week

설정을 바꾸면 journald를 다시 시작한다.

sudo systemctl restart systemd-journald

타이머 — cron을 대체한다

cron은 오래된 친구지만, 한계도 많다. 실패 시 재시도 정책을 넣기 어렵고, 환경변수·로그·권한 제어가 빈약하다. systemd 타이머는 이런 부분을 .service + .timer 페어로 해결한다.

예시로, 매일 새벽 3시에 백업 스크립트를 돌리는 경우를 만들어보자.

/etc/systemd/system/backup.service무엇을 할지

[Unit]
Description=Daily database backup
After=postgresql.service

[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup.sh

/etc/systemd/system/backup.timer언제 할지

[Unit]
Description=Run daily database backup at 03:00

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=5m

[Install]
WantedBy=timers.target

그다음 활성화한다.

sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

# 타이머 상태 조회
systemctl list-timers

list-timers를 치면 다음 실행 시각이 표에 나온다. Persistent=true는 “머신이 꺼져 있어서 타이머가 놓친 실행이 있으면 부팅 후 따라잡아 실행”하는 옵션이다. cron에 없는 기능이다. RandomizedDelaySec은 정확히 정각이 아니라 0~5분 범위에서 무작위로 지연시켜 같은 시각에 타이머가 몰리는 걸 방지한다.

OnCalendar는 cron과 유사한 표기가 가능하다. 자주 쓰는 몇 가지를 적어두면 편하다.

*-*-* 03:00:00        매일 03:00
Mon..Fri 09:00        평일 오전 9시
*-*-1 00:00:00        매월 1일 자정
hourly                매시 정각
daily                 매일 자정
weekly                매주 월요일 자정

failed 유닛 다루기

운영 중에 “어떤 서비스가 실패 상태인지”를 훑어보는 건 흔한 작업이다.

systemctl --failed
# UNIT           LOAD   ACTIVE SUB    DESCRIPTION
# myapp.service  loaded failed failed My Node.js API

실패한 유닛이 있다면 원인은 거의 항상 로그에서 나온다.

sudo systemctl status myapp
sudo journalctl -u myapp --since "10 minutes ago"

특히 부팅 실패의 경우, systemd-analyze blame으로 “어떤 유닛이 부팅을 느리게 했는지” 볼 수 있다.

systemd-analyze blame | head
# 45.200s docker.service
# 12.400s snapd.service
#  6.200s systemd-networkd-wait-online.service

critical-chain을 쓰면 부팅 경로의 병목 체인을 그려준다.

systemd-analyze critical-chain

대규모 서버 튜닝의 첫 단계로 유용하다.

컨테이너에서는 systemd가 거의 없다

헷갈리기 쉬운 지점 하나만 짚고 가자. 컨테이너 안에는 대부분 systemd가 없다. docker run ubuntu로 들어가면 init 시스템도, journald도 없고, PID 1이 그냥 앱 프로세스다. 이건 컨테이너 설계 철학(한 컨테이너 = 한 프로세스)과 맞닿아 있다.

그래서 systemd는 호스트(VM·베어메탈)의 프로세스 관리에 쓰는 도구라는 선이 분명하다. 컨테이너 오케스트레이션은 Kubernetes나 Docker Compose가 맡는다. 역할이 다르다.

예외도 있다. systemd-nspawn이나 일부 OS 컨테이너(LXC)는 안에 systemd를 그대로 돌린다. 하지만 “Docker 베이스 이미지에서 systemctl 치면 왜 안 되지?”는 질문의 답은 “거긴 systemd가 없다”다.


다음 편에서는 리눅스 패키지 관리를 본다. apt(Debian/Ubuntu 계열)와 dnf/yum(RHEL 계열)의 사용법, 저장소와 의존성의 개념, 그리고 apt-get vs apt 같은 자주 헷갈리는 부분까지 정리한다.

7편: 패키지 관리


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Linux 기초 5편 — 네트워크 도구
Next Post
Linux 기초 7편 — 패키지 관리