- Published on
systemd 서비스 등록과 배포: 유닛 파일부터 재시작까지
- Authors

- Name
- Indo Yoon
백엔드 서비스를 서버에 올릴 때 재현성이 가장 높은 방법은 systemd 유닛 파일로 프로세스를 등록하고 systemctl로 관리하는 것입니다. 실행 경로와 계정, 환경 변수, 재시작 정책을 유닛 파일에 적어 두면 서버가 재부팅되거나 프로세스가 예기치 않게 종료되어도 같은 방식으로 다시 올라옵니다. 이 글은 유닛 파일 작성부터 데몬 리로드, 자동 시작 등록, 상태와 로그 확인까지 배포 순서대로 정리해요.
한눈에 보기
- 직접 만든 유닛 파일은
/etc/systemd/system아래에 둡니다. 배포 스크립트가 이 경로에 파일을 밀어 넣는 구조로 만들면 관리가 단순해져요. - 유닛 파일을 새로 만들거나 수정한 뒤에는
systemctl daemon-reload를 실행해야 systemd가 내용을 다시 읽습니다. 이 단계를 건너뛰면 이전 설정이 그대로 적용됩니다. - 부팅 시 자동 시작은
systemctl enable로 등록하고, 지금 바로 시작까지 하려면systemctl enable --now를 씁니다. - 프로세스 상태는
systemctl status, 로그는journalctl -u 유닛이름으로 확인합니다. 애플리케이션이 표준 출력과 표준 에러로 남긴 내용이 그대로 모입니다. - 실행 계정과 재시작 정책은
[Service]섹션에서 지정하며, 배포 후 장애의 상당 부분이 이 두 항목에서 나옵니다. - 배포 스크립트는 "파일 복사 → 데몬 리로드 → 재시작 → 상태 확인" 순서를 한 덩어리로 묶어 두는 것이 좋아요.
유닛 파일의 섹션 구조
유닛 파일은 섹션과 지시자로 이루어진 단순한 텍스트 파일입니다. 어떤 섹션에 무엇을 적는지 먼저 익히면 이후 단계가 훨씬 빨라져요.
| 섹션 | 하는 일 | 자주 쓰는 지시자 | 놓치기 쉬운 점 |
|---|---|---|---|
[Unit] | 유닛 설명과 다른 유닛과의 순서·의존 관계 정의 | Description, After, Wants, Requires | After는 순서만 정할 뿐 상대 유닛의 존재를 보장하지 않으므로 필요하면 Requires를 함께 씁니다 |
[Service] | 프로세스를 어떻게 실행하고 감시할지 정의 | Type, ExecStart, User, Restart, EnvironmentFile | ExecStart는 셸을 거치지 않고 실행되므로 파이프나 리다이렉션을 그대로 쓸 수 없습니다 |
[Install] | enable 할 때 어느 타깃에 연결할지 정의 | WantedBy | 이 섹션이 없으면 자동 시작 등록이 되지 않아 재부팅 후 서비스가 뜨지 않습니다 |
배포 방식 비교
같은 서비스를 올리더라도 운영 규모와 팀 상황에 따라 선택지가 달라집니다. 아래 표는 서버에서 직접 운영할 때 자주 비교하는 방식들을 정리한 것이에요.
| 방식 | 적합한 경우 | 장점 | 고려할 점 |
|---|---|---|---|
| systemd 유닛 | 단일 서버에 바이너리를 직접 두고 운영 | 추가 도구 없이 부팅 시 시작과 비정상 종료 복구를 관리 | 무중단 전환은 배포 스크립트와 리버스 프록시 쪽에서 직접 설계해야 함 |
| 컨테이너 오케스트레이션 | 여러 노드에 걸친 확장과 롤링 배포 | 선언형 배포와 상태 점검이 기본 제공 | 클러스터 자체의 운영 비용과 학습 부담이 큼 |
| 별도 프로세스 매니저 | 소규모 구성이나 임시 운영 | 설정이 단순하고 도입이 빠름 | 부팅 연동과 로그 수집을 따로 붙여야 함 |
단계별로 서비스 등록하기
1. 실행 파일과 작업 디렉터리 정리
빌드 산출물을 /srv/app처럼 변하지 않는 경로에 두고, 실행 파일에 실행 권한이 있는지 확인해요. ExecStart에는 절대 경로를 적어야 하며 상대 경로나 셸 변수는 systemd가 해석하지 않습니다. 로그나 업로드 파일처럼 쓰기가 필요한 디렉터리는 실행 계정이 접근할 수 있도록 미리 권한을 잡아 두세요.
2. 전용 실행 계정 만들기
서비스를 root로 돌리지 않도록 전용 계정을 만드는 것이 기본입니다. 로그인 셸이 필요 없는 시스템 계정으로 만들고, 유닛 파일의 User와 Group에 그 계정을 지정해요. 계정을 바꾼 뒤에는 애플리케이션 디렉터리 소유권도 함께 맞춰야 권한 오류가 나지 않습니다.
3. 환경 변수 파일 분리
DB 접속 정보나 비밀 키는 유닛 파일에 직접 쓰지 말고 EnvironmentFile로 분리해요. 파일은 KEY=VALUE 형태로 한 줄에 하나씩 적고, 값에 공백이 있으면 따옴표로 감쌉니다. 이 파일은 실행 계정만 읽을 수 있게 권한을 좁혀 두는 것이 안전해요.
4. 유닛 파일 작성
/etc/systemd/system/app.service 파일을 만들고 아래와 같은 형태로 채웁니다. 값은 운영 환경에 맞게 바꿔야 하고, 경로와 계정 이름은 실제 구성과 일치해야 해요.
[Unit]
Description=Backend API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/srv/app
EnvironmentFile=/etc/app/app.env
ExecStart=/srv/app/bin/api
Restart=on-failure
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
Type=simple은 ExecStart로 띄운 프로세스가 곧 서비스의 주 프로세스라는 뜻입니다. 스스로 백그라운드로 빠지는 스크립트라면 Type=forking이나 Type=exec 쪽을 검토해야 하고, 짧게 끝나는 작업이라면 Type=oneshot을 씁니다.
5. 데몬 리로드와 자동 시작 등록
파일을 저장한 뒤 systemctl daemon-reload를 실행해요. 이어서 systemctl enable --now app.service를 실행하면 부팅 시 자동 시작이 등록되면서 지금 즉시 시작됩니다. enable과 start는 하는 일이 다르므로, 자동 시작 없이 한 번만 띄우려면 start만 쓰면 됩니다.
6. 상태와 로그 확인
systemctl status app.service로 활성 상태와 최근 로그 몇 줄을 확인해요. 더 자세히 보려면 journalctl -u app.service -f로 실시간 로그를 따라가면 됩니다. 배포 직후에는 상태 확인과 로그 확인을 한 세트로 묶어 두는 것이 좋아요.
상황별 선택
서비스 형태에 따라 유닛 파일 구성이 달라집니다. 아래 경우를 미리 알아 두면 배포 스크립트를 설계할 때 판단이 빨라져요.
- 같은 애플리케이션을 여러 인스턴스로 띄워야 한다면 템플릿 유닛을 씁니다.
app@.service형태로 파일을 만들고systemctl start app@인스턴스명으로 각각 실행하며, 인스턴스 이름은%i로 유닛 안에서 참조할 수 있어요. - 주기적으로 도는 배치 작업은 서비스 유닛과 타이머 유닛을 나눠 구성합니다.
Type=oneshot으로 작업을 정의하고.timer유닛이 그 서비스를 깨우는 구조예요. - 낮은 번호의 포트를 직접 바인딩해야 한다면 권한 부여 지시자를 쓰거나, 앞단에 리버스 프록시를 두고 애플리케이션은 높은 번호의 포트를 쓰게 하는 편이 안전합니다.
- 무중단 배포가 필요하다면 유닛 파일만으로는 부족합니다. 새 버전을 다른 포트에 먼저 띄우고 상태 점검이 통과한 뒤 프록시 설정을 바꾸는 식으로 절차를 직접 설계해야 해요.
- 설정 파일만 바꾸는 배포라면 재시작 대신 리로드를 지원하는지 먼저 확인하세요. 애플리케이션이 신호를 처리해 설정을 다시 읽는 구조라면
ExecReload를 정의해 두는 편이 좋습니다.
주의할 점
- 유닛 파일을 고치고
daemon-reload를 잊는 실수가 가장 흔합니다. 배포 스크립트에 반드시 포함하세요. ExecStart는 셸을 거치지 않습니다. 파이프나 리다이렉션이 필요하면 셸을 명시적으로 호출하는 형태로 감싸야 해요.- 즉시 실패하는 프로세스에 무조건 재시작을 걸어 두면 재시작이 반복됩니다. 실패 횟수를 제한하는 지시자를 함께 두고, 반복 실패 시 완전히 멈추도록 설계하세요.
WorkingDirectory를 지정하지 않으면 상대 경로로 접근하는 파일을 찾지 못합니다. 설정 파일 경로는 절대 경로로 통일하는 편이 안전해요.- 애플리케이션이 자체적으로 파일 로그를 남기면서 표준 출력도 쓰면 로그가 두 곳에 쌓입니다. 수집 경로를 하나로 정리하세요.
- 유닛 이름과 파일 이름은 일치해야 합니다. 이름이 어긋나면
enable이나status명령이 엉뚱한 유닛을 가리켜요. - 전용 계정으로 전환한 뒤 파일 소유권을 그대로 두면 시작 단계에서 바로 실패합니다. 배포 절차에 소유권 정리를 넣어 두세요.
자주 묻는 질문
systemctl restart와 daemon-reload는 각각 언제 필요한가요?
daemon-reload는 유닛 파일 내용 자체를 바꿨을 때 systemd가 그 파일을 다시 읽도록 하는 명령입니다. 반면 restart는 이미 읽어 둔 설정에 따라 프로세스를 다시 띄우는 동작이에요. 그래서 유닛 파일을 수정한 배포에서는 두 명령을 순서대로 실행해야 변경이 반영됩니다.
유닛 파일을 고쳤는데 이전 설정대로 동작합니다.
대부분 daemon-reload를 건너뛴 경우입니다. systemctl cat 유닛이름으로 systemd가 실제로 읽고 있는 내용을 확인해 보세요. 드롭인 설정이 함께 적용되어 예상과 다른 값이 보일 수도 있으니, 출력에 함께 표시되는 경로도 같이 확인하는 것이 좋아요.
환경 변수 파일을 만들었는데 값이 비어 있습니다.
EnvironmentFile 경로가 실제 파일과 다르거나, 실행 계정에 읽기 권한이 없을 때 자주 발생합니다. 파일 형식이 KEY=VALUE 한 줄 구조인지, 값에 불필요한 공백이나 따옴표가 섞이지 않았는지도 함께 확인해 보세요.
서비스가 계속 재시작을 반복합니다.
ExecStart 경로나 실행 권한, 작업 디렉터리 문제로 프로세스가 즉시 종료되는 경우가 많습니다. journalctl -u 유닛이름으로 종료 직전 메시지를 먼저 확인하고, 실패 횟수를 제한하는 지시자로 반복을 멈춘 뒤 원인을 고치는 순서가 안전해요.
로그를 파일로 따로 남기고 싶습니다.
표준 출력을 그대로 두면 journald가 수집하므로 별도 설정 없이도 조회가 가능합니다. 파일로도 남겨야 한다면 애플리케이션 쪽에서 로그 경로를 지정하고, 수집 도구와 중복되지 않도록 정리하세요. journald 보관 정책은 별도 설정으로 조정할 수 있습니다.
출처
- systemd.service 매뉴얼: https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
- systemctl 매뉴얼: https://www.freedesktop.org/software/systemd/man/latest/systemctl.html
- systemd.unit 매뉴얼: https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html
- systemd.exec 매뉴얼: https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
배포 후 로그 확인과 보관 정책까지 함께 정리하고 싶다면 journald로 서비스 로그 관리하기를 이어서 읽어 보세요. 유닛 파일로 서비스를 등록한 뒤에는 로그 수집 경로를 하나로 정리하는 것이 운영 부담을 가장 많이 줄여 줍니다.