터미널에서 실행한 프로그램을 SSH 접속이 끊긴 뒤에도 운영하려면 실행 주체와 재시작·로그 정책을 함께 정해야 합니다. systemd 서비스로 등록하면 시작 명령, 실행 계정, 실패했을 때의 처리와 부팅 시 동작을 한곳에서 관리할 수 있어요.
여기서는 외부에 포트를 열지 않고 주기적으로 로그만 남기는 maru-heartbeat.service를 만듭니다. 대상은 Rocky Linux 9, Ubuntu 24.04·26.04의 시스템 서비스입니다. 사용자 세션의 systemctl --user와는 관리 범위가 다릅니다.
함께 보는 Linux 운영 글 · Rocky 9 고정 IP · Ubuntu 24.04 Netplan · Ubuntu 26.04·nmtui · systemd 서비스·로그 · LVM 루트 확장 · 일반 파티션 루트 확장
이 글의 목차
1. start와 enable은 서로 다른 설정이다
| 명령 | 하는 일 | 기억할 점 |
|---|---|---|
| start / stop | 지금 프로세스를 시작·중지 | 부팅 시 자동 시작 설정과는 별개 |
| enable / disable | 부팅 등의 의존 관계에 등록·해제 | –now를 붙이지 않으면 현재 실행 상태를 바꾸지 않음 |
| restart | 중지한 뒤 다시 시작 | 짧은 중단이 생길 수 있음 |
| reload | 앱에 설정 다시 읽기를 요청 | 서비스가 reload를 지원해야 함 |
| daemon-reload | systemd가 unit 정의를 다시 읽음 | 실행 중인 앱이 자동 재시작되는 명령은 아님 |
disable은 실행 금지 기능이 아닙니다. 수동 시작이나 다른 unit의 의존 관계로 다시 실행될 수 있어요. 시작 자체를 막는 mask는 영향 범위가 더 크므로 목적을 확인하고 사용합니다. 자세한 동작은 systemctl 매뉴얼을 기준으로 확인할 수 있습니다.
systemctl --version
systemctl list-units --type=service --state=running
systemctl list-unit-files --type=service
systemctl --failed
2. 실행 계정과 프로그램을 준비한다
아래 이름이 이미 있는지 먼저 확인합니다. 같은 계정이나 서비스가 이미 있다면 다른 실습 이름을 정하고 이후 경로도 함께 바꿉니다. 계정과 실행 파일은 관리자만 변경하고, 서비스는 권한이 제한된 계정으로 실행합니다.
getent passwd maruops-demo
systemctl cat maru-heartbeat.service
아직 만들지 않은 상태라면 위 명령에서 계정·unit을 찾지 못하는 것이 예상됩니다. 새로 생성하는 경우에만 다음을 진행합니다.
sudo useradd --system --user-group --no-create-home \
--shell /usr/sbin/nologin maruops-demo
sudoedit /usr/local/bin/maru-heartbeat
#!/usr/bin/bash
set -eu
interval="${HEARTBEAT_INTERVAL:-15}"
case "$interval" in
''|*[!0-9]*)
printf 'HEARTBEAT_INTERVAL must be a positive integer\n' >&2
exit 2
;;
esac
if [ "$interval" -lt 1 ]; then
printf 'HEARTBEAT_INTERVAL must be at least 1\n' >&2
exit 2
fi
trap 'printf "heartbeat stopping\n"; exit 0' TERM INT
while :; do
printf 'heartbeat time=%s interval=%s\n' "$(date --iso-8601=seconds)" "$interval"
sleep "$interval" &
wait "$!"
done
스크립트는 표준 출력에 시간을 남기고, 종료 신호가 오면 정상 종료합니다. &는 sleep을 기다리는 동안 Bash가 신호를 처리하도록 사용했습니다. 실제 서비스의 주 프로세스를 백그라운드로 띄워 systemd를 빠져나가는 구성과는 구분합니다.
sudo chown root:root /usr/local/bin/maru-heartbeat
sudo chmod 755 /usr/local/bin/maru-heartbeat
sudo bash -n /usr/local/bin/maru-heartbeat
sudoedit /etc/maru-heartbeat.env
HEARTBEAT_INTERVAL=15
sudo chown root:root /etc/maru-heartbeat.env
sudo chmod 644 /etc/maru-heartbeat.env
이 환경 파일에는 공개해도 되는 간격 값만 넣었습니다. EnvironmentFile은 일반 셸 스크립트처럼 source하는 파일이 아니므로 export나 임의 셸 명령을 적지 않습니다. 비밀번호·토큰 같은 비밀은 환경 변수만으로 안전하게 감춰진다고 가정하지 말고 애플리케이션의 비밀 관리 방식을 따릅니다. systemd.exec의 환경 설정 설명을 참고하세요.
3. unit 파일을 작성하고 등록한다
sudoedit /etc/systemd/system/maru-heartbeat.service
[Unit]
Description=MaruOps heartbeat demonstration
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=exec
User=maruops-demo
Group=maruops-demo
EnvironmentFile=/etc/maru-heartbeat.env
ExecStart=/usr/local/bin/maru-heartbeat
Restart=on-failure
RestartSec=3s
TimeoutStopSec=10s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=maru-heartbeat
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
| 설정 | 선택한 이유 |
|---|---|
| Type=exec | 지정 실행 파일을 실행하는 단계의 실패를 시작 오류로 다루기 위해 사용 |
| User / Group | 서비스에 불필요한 root 권한을 주지 않음 |
| Restart=on-failure | 비정상 종료 시 재시작. 관리자의 stop은 자동 재시작 대상이 아님 |
| StartLimitIntervalSec / Burst | 짧은 시간의 무한 재시작을 제한 |
| StandardOutput / Error | 앱 출력을 journal에서 조회 |
| ProtectSystem / ProtectHome | 이 로그 전용 데모가 파일시스템·홈을 수정할 필요가 없어 접근을 제한 |
Type=exec로 시작되었다고 앱의 API나 DB 연결까지 준비됐다는 뜻은 아닙니다. 준비 완료를 정교하게 통지하려면 프로그램이 지원하는 notify 등의 방식을 검토합니다. 파일을 쓰는 실제 앱에서는 StateDirectory나 쓰기 허용 경로를 설계해야 하므로 이 데모의 보안 설정을 그대로 복사해 적용하지 않습니다. 서비스 유형·재시작 정책과 unit 공통 설정이 기준 문서입니다.
이 프로그램은 외부 네트워크를 사용하지 않으므로 network-online.target 의존성을 넣지 않았습니다. After는 시작 순서이고 상대 unit을 끌어오는 기능은 아니며, 네트워크 온라인 판정도 특정 DB·인터넷 서비스의 응답을 보장하지 않습니다.
sudo chmod 644 /etc/systemd/system/maru-heartbeat.service
sudo systemd-analyze verify /etc/systemd/system/maru-heartbeat.service
sudo systemctl daemon-reload
sudo systemctl enable --now maru-heartbeat.service
systemctl is-enabled maru-heartbeat.service
systemctl is-active maru-heartbeat.service
sudo systemctl status maru-heartbeat.service --no-pager
Rocky에서 실행 권한이 맞는데도 거부되면 SELinux 컨텍스트를 확인합니다. 기본 경로에 맞게 sudo restorecon -v /usr/local/bin/maru-heartbeat로 복원할 수 있습니다. 거부 원인을 확인하지 않고 SELinux를 끄는 방식으로 해결하지 않습니다.
4. 변경한 대상에 따라 반영 방법을 고른다
| 변경한 것 | 반영 순서 |
|---|---|
| 앱 스크립트·환경 파일 내용 | 해당 서비스 restart |
| unit 본문·drop-in | daemon-reload 후 해당 서비스 restart |
| 앱 자체의 설정 | 앱이 reload를 지원하면 reload, 아니면 restart |
| 부팅 시 실행 여부 | enable / disable; 지금도 바꾸려면 –now |
로그 간격을 바꾸려면 /etc/maru-heartbeat.env의 값을 5로 바꾸고 재시작합니다. unit의 경로나 정의가 바뀐 것이 아니므로 이 경우 daemon-reload는 필요하지 않습니다. 이 데모에는 reload 동작을 구현하지 않았습니다.
sudoedit /etc/maru-heartbeat.env
sudo systemctl restart maru-heartbeat.service
sudo journalctl -u maru-heartbeat.service -n 20 --no-pager
drop-in으로 일부 설정을 덮어쓴다
배포판이나 패키지가 제공하는 unit을 바꿀 때는 원본을 직접 덮어쓰기보다 drop-in을 씁니다. 데모에서도 같은 방법을 연습할 수 있어요. 아래 명령의 편집 영역에 다음 두 줄을 넣고 저장합니다. 편집기에 안내된 “저장되는 영역” 밖에 적으면 반영되지 않을 수 있습니다.
sudo systemctl edit maru-heartbeat.service
[Service]
RestartSec=10s
sudo systemctl daemon-reload
sudo systemctl restart maru-heartbeat.service
systemctl cat maru-heartbeat.service
systemctl show maru-heartbeat.service \
-p FragmentPath -p DropInPaths -p RestartUSec
일반적인 저장 경로는 /etc/systemd/system/maru-heartbeat.service.d/override.conf입니다. ExecStart를 drop-in에서 바꿀 때는 기존 목록을 비우는 ExecStart= 줄을 먼저 두고 새 ExecStart를 지정해야 합니다. 직접 만든 데모 unit도 삭제 전에 cat·FragmentPath로 실제 경로를 다시 확인합니다.
5. journalctl에서 필요한 로그만 본다
sudo journalctl -u maru-heartbeat.service -n 50 --no-pager
sudo journalctl -u maru-heartbeat.service -f
-f는 새 로그를 계속 기다립니다. Ctrl+C는 로그 조회를 끝내며 서비스는 계속 실행됩니다. 다음 명령은 각각 필요할 때 따로 실행합니다.
sudo journalctl -u maru-heartbeat.service --since today --no-pager
sudo journalctl -u maru-heartbeat.service --since '1 hour ago' -o short-iso --no-pager
sudo journalctl -u maru-heartbeat.service -b --no-pager
sudo journalctl --list-boots
sudo journalctl -u maru-heartbeat.service -b -1 --no-pager
sudo journalctl -k -b -p warning --no-pager
-b -1은 보관된 이전 부팅의 로그를 읽습니다. 이전 로그가 남아 있지 않으면 조회할 수 없습니다. -p warning은 journal에 기록된 심각도로 필터링하므로 앱의 출력 문장에 ERROR라고 적힌 것만으로 오류 등급이 되는 것은 아닙니다. journalctl 매뉴얼에서 필터의 의미를 확인할 수 있습니다.
로그를 재부팅 뒤에도 보관하려면
sudo journalctl --disk-usage
sudo install -d -m 755 /etc/systemd/journald.conf.d
sudoedit /etc/systemd/journald.conf.d/60-maruops-retention.conf
[Journal]
Storage=persistent
SystemMaxUse=200M
SystemKeepFree=1G
MaxRetentionSec=14day
sudo systemctl restart systemd-journald.service
sudo journalctl --flush
sudo journalctl --disk-usage
이 설정은 데모 한 개가 아니라 시스템 journal 전체에 적용되는 예시입니다. 용량과 기간은 운영 정책에 맞춰 바꿉니다. 설정된 기간보다 빨리 용량 한계에 닿으면 더 오래 보관된 로그가 먼저 정리될 수 있어요. 기본 Storage 동작은 배포판 설정도 확인합니다. journald 저장·용량 제한 문서를 기준으로 현재 보존 요구와 디스크 여유를 함께 판단합니다.
기존 로그를 정리해야 한다면 필요한 기록을 외부에 보관한 뒤 sudo journalctl --rotate --vacuum-time=14d처럼 정책에 맞춰 실행합니다. 이 작업은 오래된 아카이브 journal을 삭제하므로 되돌릴 수 없습니다. 서비스 하나만의 로그를 골라 지우는 명령도 아닙니다.
6. 실패 상태에서 볼 곳
sudo systemctl status maru-heartbeat.service --no-pager -l
sudo journalctl -u maru-heartbeat.service -b -n 100 --no-pager
systemctl show maru-heartbeat.service \
-p Result -p ExecMainStatus -p NRestarts
| 표시 | 먼저 확인할 항목 |
|---|---|
| 203/EXEC | 실행 경로·권한·shebang·SELinux·줄바꿈 형식 |
| 217/USER | User·Group 계정이 존재하는지 |
| 200/CHDIR | WorkingDirectory 경로와 접근 권한 |
| start-limit-hit | 반복 실패 원인과 재시작 횟수 제한 |
원인을 고친 뒤 시작 제한에 걸려 있으면 sudo systemctl reset-failed maru-heartbeat.service로 해당 unit의 실패 상태·카운터를 초기화하고 다시 start합니다. reset-failed가 실행 파일이나 잘못된 설정을 고치는 것은 아닙니다.
7. 직접 만든 서비스를 삭제한다
다음은 이 글에서 직접 만든 파일만 제거하는 절차입니다. 패키지가 설치한 서비스는 패키지의 제거·중지 정책을 따릅니다. 파일을 지우기 전에 실행과 자동 시작을 먼저 멈춥니다.
sudo systemctl disable --now maru-heartbeat.service
sudo rm -f /etc/systemd/system/maru-heartbeat.service.d/override.conf
sudo rmdir --ignore-fail-on-non-empty /etc/systemd/system/maru-heartbeat.service.d
sudo rm -f /etc/systemd/system/maru-heartbeat.service
sudo systemctl daemon-reload
systemctl list-unit-files maru-heartbeat.service
drop-in을 만들지 않았다면 해당 디렉터리가 없어 rmdir이 오류를 출력할 수 있습니다. 다른 파일이 들어 있는 디렉터리는 강제로 통째로 지우지 않습니다. systemctl status에서는 unit을 찾지 못하는 것이 예상됩니다. unit 파일을 삭제해도 과거 journal이나 앱 데이터는 자동 삭제되지 않습니다.
이 데모만을 위해 만든 실행 파일·환경 파일은 필요 없을 때 각각 제거합니다. 계정까지 지우려면 다른 프로세스·파일이 그 계정을 사용하지 않는지 확인합니다. 실제 애플리케이션은 실행 파일, 설정, 데이터, 로그의 수명이 서로 다르므로 서비스 등록 해제와 데이터 삭제를 구분해 두는 편이 좋습니다.
확인 범위 · 2026-10-18: unit 구문은 Rocky Linux 9.8의 systemd 252와 Ubuntu 26.04.1의 systemd 259에서 확인했습니다. 서비스 생성·실행, 환경 파일·drop-in 반영, 비정상 종료 뒤 자동 재시작, journal 조회·영구 저장은 Ubuntu 24.04.5 VM의 systemd 255에서 실행했습니다. 데모의 권한 제한이 모든 앱의 파일 접근 요구에 맞는다는 뜻은 아닙니다.
네트워크·systemd 예제 파일 내려받기 · Netplan YAML, 서비스·환경 파일과 스토리지 구조를 읽는 스크립트를 묶었습니다. 주소와 장치 이름은 실제 환경에 맞춰 바꿉니다. 디스크를 자동 확장하는 스크립트는 포함하지 않습니다.




답글 남기기