- Published on
systemd 서비스 무한 재시작 - Exit code 203 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 배포 직후 systemctl status를 보면 서비스가 계속 재시작되고, 로그에는 Main process exited, code=exited, status=203/EXEC 같은 문구가 찍히는 경우가 있습니다. 이때 많은 분들이 애플리케이션 내부 오류(예: 예외, 설정 파일 누락)를 먼저 의심하지만, **203/EXEC는 대개 “프로세스를 실행(execute)하는 단계에서 실패”**했다는 뜻이라 앱 로직까지 도달하지 못한 경우가 많습니다.
이 글에서는 Exit code 203의 의미를 systemd 관점에서 해부하고, 무한 재시작 루프를 끊고 원인을 빠르게 찾는 체크리스트를 제공합니다. (서비스가 계속 재시작되면 로그가 쏟아지며 장애가 커지므로, 먼저 “진단 모드”로 전환하는 게 핵심입니다.)
1) 203/EXEC가 의미하는 것: 앱이 아니라 “execve()” 단계 문제
systemd의 status=203/EXEC는 대략 다음 중 하나를 의미합니다.
ExecStart=에 지정한 경로가 없음- 실행 파일이지만 권한(x bit) 이 없음
- 스크립트인데 shebang(#!) 누락/오류
- 바이너리인데 동적 로더/라이브러리 미존재로 실행 불가
User=/Group=/WorkingDirectory=등 유닛 설정 때문에 접근 불가- Windows CRLF, 잘못된 인코딩 등으로 인터프리터 경로 파싱 실패
즉, 애플리케이션 로그가 아니라 systemd/커널 레벨에서 실행 자체가 실패한 것입니다.
2) 무한 재시작 루프 먼저 끊기: 진단을 위한 안전장치
서비스가 Restart=always 또는 Restart=on-failure로 설정되어 있으면, 실행 실패가 발생할 때마다 계속 재시작합니다. 우선 루프를 끊어야 원인 파악이 쉬워집니다.
2.1 즉시 중지 및 재시작 카운터 리셋
sudo systemctl stop myapp.service
sudo systemctl reset-failed myapp.service
2.2 임시로 Restart 비활성화(드롭인 override)
sudo systemctl edit myapp.service
아래를 입력합니다.
[Service]
Restart=no
적용:
sudo systemctl daemon-reload
sudo systemctl start myapp.service
sudo systemctl status myapp.service -l
이제 한 번 실패하고 멈추므로 203/EXEC 원인을 안정적으로 확인할 수 있습니다.
3) 1분 안에 하는 1차 진단: status + journal + ExecStart 확인
3.1 상태와 마지막 로그를 길게 출력
systemctl status myapp.service -l --no-pager
journalctl -u myapp.service -b --no-pager -n 200
여기서 특히 봐야 할 포인트:
ExecStart=에 찍히는 실제 커맨드(경로/인자)No such file or directory,Permission denied같은 커널 에러Failed at step EXEC spawning ...문구
3.2 유닛 파일의 실제 내용 확인
드롭인/override가 섞여 있으면 cat이 가장 확실합니다.
systemctl cat myapp.service
systemctl show myapp.service -p ExecStart -p User -p Group -p WorkingDirectory
4) Exit 203의 대표 원인 7가지와 해결법
4.1 ExecStart 경로 오타 / 파일이 없음
가장 흔합니다.
ls -al /opt/myapp/bin/myapp
- 파일이 없다면 배포 경로를 수정하거나, 유닛의
ExecStart=를 올바른 위치로 변경 - 심볼릭 링크를 쓴다면 링크가 깨지지 않았는지 확인
팁: systemd는 PATH를 셸처럼 확장하지 않습니다. ExecStart=myapp처럼 쓰면 실패할 수 있으니 절대경로를 권장합니다.
4.2 실행 권한(x bit) 없음
stat /opt/myapp/bin/myapp
# 또는
ls -l /opt/myapp/bin/myapp
권한 부여:
sudo chmod +x /opt/myapp/bin/myapp
4.3 스크립트인데 shebang(#!)이 없거나 인터프리터 경로가 잘못됨
예: #!/usr/bin/env bash 또는 #!/bin/bash가 필요합니다.
head -n 1 /opt/myapp/bin/start.sh
file -b /opt/myapp/bin/start.sh
수정 예:
sudo sed -i '1s|^|#!/usr/bin/env bash\n|' /opt/myapp/bin/start.sh
sudo chmod +x /opt/myapp/bin/start.sh
또 다른 함정:
#!/bin/bash^M(CRLF) → 인터프리터를 못 찾아No such file or directory가 날 수 있음
CRLF 제거:
sudo sed -i 's/\r$//' /opt/myapp/bin/start.sh
4.4 바이너리/스크립트가 의존하는 동적 로더/라이브러리가 없음
특히 다른 배포판에서 빌드한 바이너리를 가져오면 발생합니다.
file /opt/myapp/bin/myapp
ldd /opt/myapp/bin/myapp
not found가 보이면 해당 라이브러리를 설치하거나- 정적 빌드로 전환하거나
- 컨테이너/패키징 방식(예: deb/rpm)으로 의존성을 명시하세요.
4.5 WorkingDirectory가 없거나 권한이 없어 chdir 실패
WorkingDirectory=/var/lib/myapp 같은 설정이 있고, 해당 디렉터리가 없으면 실행 단계에서 실패할 수 있습니다.
systemctl show myapp.service -p WorkingDirectory
ls -ald /var/lib/myapp
해결:
sudo mkdir -p /var/lib/myapp
sudo chown -R myapp:myapp /var/lib/myapp
4.6 User/Group 설정으로 인해 실행 파일 접근 불가
User=myapp로 실행하는데 바이너리가 root만 읽을 수 있으면 Permission denied가 납니다.
systemctl show myapp.service -p User -p Group
namei -l /opt/myapp/bin/myapp
- 상위 디렉터리까지 실행 권한(
x)이 있는지 확인 - 필요한 경우 소유권/권한 조정
4.7 ExecStart에 셸 문법을 그대로 넣음(리다이렉션/파이프 등)
systemd는 기본적으로 셸을 거치지 않으므로 아래처럼 쓰면 의도대로 동작하지 않습니다.
ExecStart=/opt/myapp/bin/myapp >> /var/log/myapp.log 2>&1
해결 방법은 2가지입니다.
- 표준 출력은 systemd/journald로 보내고
journalctl로 조회 (권장) - 꼭 셸이 필요하면 명시적으로
/bin/bash -lc사용
ExecStart=/bin/bash -lc '/opt/myapp/bin/myapp >>/var/log/myapp.log 2>&1'
다만 셸을 끼우면 quoting 문제가 생기고, 장애 시 원인 파악이 어려워질 수 있어 신중히 선택하세요.
5) 재현 가능한 디버깅: systemd가 실행하려는 것을 그대로 실행해보기
유닛의 User=를 반영해서 직접 실행해보면 원인이 빠르게 드러납니다.
# myapp 유저로 실행해보기
sudo -u myapp -H /opt/myapp/bin/myapp
# WorkingDirectory가 중요하다면
sudo -u myapp -H bash -lc 'cd /var/lib/myapp && /opt/myapp/bin/myapp'
여기서도 실행이 안 된다면 systemd 문제가 아니라 실행 파일/권한/의존성 문제입니다.
6) “무한 재시작”을 예방하는 유닛 설정 패턴
203/EXEC 같은 즉시 실패는 재시작해도 해결되지 않는 경우가 대부분입니다. 아래 설정으로 장애 증폭을 막을 수 있습니다.
6.1 Restart 정책과 백오프 설정
[Service]
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=3
- 1분 동안 3번 이상 실패하면 더 이상 시도하지 않게 만들어 알람/조치를 유도합니다.
6.2 실행 전 조건 체크(Preflight)
실행 파일 존재/권한/필수 파일을 미리 확인해 더 명확한 로그를 남길 수 있습니다.
[Service]
ExecStartPre=/usr/bin/test -x /opt/myapp/bin/myapp
ExecStartPre=/usr/bin/test -d /var/lib/myapp
ExecStart=/opt/myapp/bin/myapp
6.3 로그는 journald 중심으로
파일 리다이렉션 대신:
[Service]
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
이러면:
journalctl -u myapp.service -f
로 즉시 추적 가능합니다.
7) 실전 예시: 203/EXEC 문제 유닛을 올바르게 고치기
문제 유닛(예시):
[Unit]
Description=MyApp
After=network.target
[Service]
User=myapp
WorkingDirectory=/var/lib/myapp
ExecStart=/opt/myapp/bin/start.sh
Restart=always
[Install]
WantedBy=multi-user.target
원인: start.sh가 CRLF + shebang 누락 + 실행 권한 없음.
해결:
sudo sed -i 's/\r$//' /opt/myapp/bin/start.sh
sudo bash -lc "printf '%s\n' '#!/usr/bin/env bash' | cat - /opt/myapp/bin/start.sh > /tmp/start.sh && mv /tmp/start.sh /opt/myapp/bin/start.sh"
sudo chmod +x /opt/myapp/bin/start.sh
sudo chown myapp:myapp /opt/myapp/bin/start.sh
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl status myapp.service -l
추가로 재시작 폭주 방지 설정을 반영:
sudo systemctl edit myapp.service
[Service]
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=3
8) 운영 관점 체크리스트(장애 대응용)
systemctl status -l에서 status=203/EXEC 확인systemctl cat으로 실제ExecStart/User/WorkingDirectory확인ls -al/stat로 실행 파일 존재/권한 확인- 스크립트면
head -n1로 shebang 확인,sed -i 's/\r$//'로 CRLF 제거 - 바이너리면
ldd로not found확인 sudo -u <User>로 동일 조건 실행 테스트Restart/StartLimit*로 재시작 폭주 방지
9) 비슷한 “원인 추적형 장애” 접근법 참고
이 문제는 겉으로는 “서비스가 죽는다”지만, 실제로는 실행 단계(환경/권한/의존성)에서 실패하는 전형적인 케이스입니다. 비슷한 방식으로 원인을 계층적으로 좁혀가는 접근은 아래 글들도 도움이 됩니다.
- OpenAI Responses API 415 Unsupported Media Type 해결
- Python uvloop 도입 후 Event loop is closed 해결 가이드
- EKS STS 엔드포인트 타임아웃 - VPC·NAT·DNS 해결
마무리
Exit code 203/EXEC는 “앱이 실행되다가 죽었다”가 아니라, 애초에 실행이 성립하지 않았다는 신호입니다. 따라서 해결의 핵심은 애플리케이션 로직 디버깅이 아니라, ExecStart 경로/권한/shebang/CRLF/동적 라이브러리/유닛의 User·WorkingDirectory 같은 실행 조건을 빠르게 검증하는 것입니다.
무한 재시작 루프는 장애를 증폭시키므로, 먼저 Restart를 잠시 끄거나 StartLimit*로 제한을 걸고, 위 체크리스트 순서대로 확인하면 대부분 10~20분 내에 원인을 찾을 수 있습니다.