launchctl bootstrap으로 LaunchAgent 등록하기 — KeepAlive가 크래시를 75초 만에 살린 plist

launchdlaunchctlmacOSMLX운영

터미널에서 띄운 서버는 창을 닫으면 죽고 재부팅하면 없습니다. launchd에 등록하면 둘 다 해결되는데, 요즘 macOS에서 그 명령은 load가 아니라 bootstrap입니다.

plist 한 파일을 ~/Library/LaunchAgents/에 두고 세 줄입니다.

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.marketriskradar.mlxserver.plist
launchctl print gui/$(id -u)/com.marketriskradar.mlxserver | grep -E 'state|pid|runs'
launchctl list | grep mlxserver

plist에서 빠지면 안 되는 두 줄이 RunAtLoad와 KeepAlive입니다. KeepAlive가 실제 크래시에서 한 일은 아래에 시각까지 적었습니다.

환경

  • Mac mini, Apple M4 Pro, 통합 메모리 64GB, macOS 26.5
  • mlx_lm.server 0.31.1, 모델 EXAONE-3.5-32B 4bit, 포트 8080
  • 2026년 3월 30일부터 9월 18일까지 149일 동안 처리 기록이 있는 서버입니다. 캐시 상한과 메모리 측정은 맥미니 로컬 LLM 상시 운영 설정 글에 있고, 여기는 등록과 KeepAlive만 다룹니다

load 대신 bootstrap인 이유는?

man page가 load와 unload를 레거시 하위 명령으로 묶어 두고 bootstrap과 bootout을 대신 권합니다.

둘 다 동작합니다.

새 이름을 쓰는 이유는 하나 더 있는데, plist를 고쳤을 때 kickstart가 아니라 bootout 뒤 bootstrap이어야 한다는 함정을 설명하려면 새 이름으로 불러야 합니다. 그 이야기는 launchctl kickstart로 재시작했는데 설정이 그대로일 때에 따로 적었습니다.

등록에 필요한 부분만 추리면 이렇습니다.

<key>Label</key>
<string>com.marketriskradar.mlxserver</string>
<key>ProgramArguments</key>
<array>
  <string>/opt/homebrew/bin/mlx_lm.server</string>
  <string>--model</string>  <string>/Users/you/models/exaone-3.5-32b-4bit</string>
  <string>--port</string>   <string>8080</string>
</array>
<key>EnvironmentVariables</key>
<dict><key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string></dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/Users/you/logs/mlx-server.log</string>
<key>StandardErrorPath</key><string>/Users/you/logs/mlx-server.error.log</string>

⚠️ launchd는 ~를 풀지 않습니다. 파일 안의 경로는 전부 절대경로입니다. 명령줄의 ~/Library/...는 셸이 풀어 주는 것이고 plist 안에서는 아무도 안 풀어 줍니다.

PATH를 직접 적는 이유는 launchd가 로그인 셸을 거치지 않아서입니다. 이 서버는 절대경로로 부르니 여기서는 안 걸렸고, 같은 방식으로 등록한 다른 잡에서 걸렸습니다. launchd cron이 command not found로 죽을 때가 그 글입니다.

로그는 어디에 쌓이나?

경로를 둘로 나눠 놨는데 stdout 쪽은 0바이트이고 stderr 쪽에 11.5MB가 다 쌓였습니다.

파이썬 logging 모듈이 stderr에 쓰기 때문입니다.

요청 로그도 경고도 전부 그쪽.

두 경로를 한 파일로 모아도 됩니다.

나눠 뒀다면 stdout 파일이 비어 있는 것이 정상입니다.

KeepAlive는 무엇을 했나?

8월 20일 08시 09분 36초, 프롬프트 여섯 개를 동시에 처리하던 서버가 이 한 줄을 남기고 죽었습니다.

libc++abi: terminating due to uncaught exception of type std::runtime_error:
[METAL] Command buffer execution failed: Insufficient Memory

launchd는 08시 10분 51초에 다시 띄웠습니다.

75초 뒤.

모델을 다 읽어 포트가 열린 것이 08시 16분 48초, 첫 요청이 200으로 돌아온 것이 08시 20분 05초입니다.

배치는 11분 비고 끝났습니다.

KeepAlive가 없었다면 그날 배치는 통째로 실패했습니다.

그 뒤로 30일, 요청 6,862건이 전부 200입니다(2026-09-20 기준).

살아난 것을 어떻게 알까?

launchd는 다시 띄워 주지만 알려 주지는 않습니다.

$ launchctl list | grep mlxserver
86000   -6    com.marketriskradar.mlxserver      # PID, 마지막 종료 상태

두 번째 열이 마지막 종료 상태입니다.

-6은 SIGABRT. 살아난 지 한 달이 지난 지금도 그대로 남아 있습니다.

저는 며칠 뒤 로그를 열고서야 알았습니다.

그 며칠을 줄이는 가장 싼 방법이 이 열을 보는 습관입니다.

크래시 경보는 아직 없습니다.

남는 것

등록은 세 줄입니다. bootstrap으로 올리고, print로 상태를 보고, list로 종료 코드를 봅니다. load도 되지만 새 이름을 쓰면 다음 함정을 설명할 수 있습니다.

plist 안에서 ~는 풀리지 않습니다. 절대경로만 적습니다. PATH도 마찬가지로 직접 적습니다.

KeepAlive는 살리지만 알리지 않습니다. 75초 만에 살아났고 그것을 안 것은 며칠 뒤였습니다. launchctl list의 두 번째 열이 그 며칠을 줄입니다.


이 글의 시각과 요청 수는 다음 명령으로 직접 확인한 값입니다. 149일과 첫 처리일은 비공개 DB의 llm_run 테이블에서 센 값이라 재현되지 않습니다.

# 서비스 상태와 마지막 종료 코드
launchctl print gui/$(id -u)/com.marketriskradar.mlxserver | grep -E 'state|pid|runs'
launchctl list | grep mlxserver
# 로그 파일 둘의 크기, stdout 쪽이 0인 것이 정상
ls -l ~/logs/mlx-server.log ~/logs/mlx-server.error.log
# 크래시와 재기동 시각
grep -n 'Insufficient Memory' ~/logs/mlx-server.error.log | tail -1
grep 'Starting httpd' ~/logs/mlx-server.error.log | tail -1
# 재기동 뒤 200 응답 수
grep -c 'POST /v1/chat/completions HTTP/1.1" 200' ~/logs/mlx-server.error.log

자주 묻는 질문

Q. launchctl bootstrap과 load는 뭐가 다른가요?

하는 일은 같습니다.

plist를 읽어 서비스를 등록하고, RunAtLoad가 있으면 바로 띄웁니다. load는 man page에서 레거시로 묶여 있고, bootstrap은 대상 도메인(gui/사용자ID 또는 system)을 명시합니다. 내리는 짝은 각각 unload와 bootout입니다.

Q. KeepAlive를 true로 두면 언제 다시 띄우나요?

종료 상태와 무관하게 프로세스가 사라지면 다시 띄웁니다. 이 서버는 SIGABRT로 죽은 뒤 75초 만에 다시 떴습니다. 정상 종료 뒤에만, 또는 비정상 종료 뒤에만 살리고 싶으면 KeepAlive를 불리언 대신 SuccessfulExit 키가 든 딕셔너리로 적습니다.

참고 자료

  • launchctl man page 는 load와 unload가 레거시로 묶여 있고 bootstrap과 bootout이 권장된다는 것의 근거입니다.
  • launchd.plist man page 는 KeepAlive가 불리언일 때와 SuccessfulExit 딕셔너리일 때 언제 다시 띄우는지의 근거입니다.
  • 맥미니 로컬 LLM 상시 운영 설정 은 이 서버의 plist 전체와 캐시 상한, 메모리 측정이 있는 허브 글입니다.

이 사이트의 수치는 주 1회 DB와 다시 대조하고, 판단이 바뀌면 지우지 않고 글 안에 덧붙입니다. 갱신은 RSS로 받을 수 있습니다.