Table of contents
- 서버의 프로세스를 누가 감독하는가
- systemd의 큰 그림
- systemctl — 서비스 조작의 표준 명령
- 유닛 파일의 구조
- 커스텀 서비스 — 내가 만든 앱을 systemd에 태운다
- journalctl — 로그는 한 곳에서 본다
- 타이머 — cron을 대체한다
- failed 유닛 다루기
- 컨테이너에서는 systemd가 거의 없다
서버의 프로세스를 누가 감독하는가
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"]
부팅 직후 커널이 systemd를 PID 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
여기서 헷갈리는 게 start와 enable의 차이다. 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
세 섹션이 있다.
[Unit]: 유닛의 메타데이터와 의존성.After=는 “이 유닛 이전에 시작되어야 하는 것”,Requires=는 “함께 올라와야 하는 것”[Service]: 실제 프로세스 기동 방법.Type=은 포크 동작,ExecStart=는 시작 명령,Restart=는 비정상 종료 시 재시작 정책[Install]:enable했을 때 어느 타깃에 연결할지.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
몇 가지 실무 팁이다.
User=를 반드시 지정한다. 기본값은 root다. 앱을 root로 돌리는 건 프로덕션에서 사고의 원인이다. 계정이 없다면sudo useradd -r -s /usr/sbin/nologin myapp로 만든다Restart=on-failure가 기본 추천.always는 정상 종료(exit 0)도 재시작시키니 의도가 분명할 때만 쓴다- 환경변수는
Environment=에 적거나,EnvironmentFile=/etc/myapp.env로 파일에서 읽는다. 시크릿은 파일 쪽이 낫다 ExecStart는 절대 경로를 써야 한다.$PATH가 기대하는 대로 세팅되지 않을 수 있다- 로그는 stdout/stderr로 뽑으면 저절로 journald에 남는다. 따로 파일 로깅 안 해도
journalctl로 읽힌다
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 같은 자주 헷갈리는 부분까지 정리한다.

Loading comments...