Dockerfile에서 ENTRYPOINT나 CMD를 쓸 때, 어떤 글에서는 ["nginx", "-g", "daemon off;"]처럼 대괄호로 쓰고 어떤 글에서는 nginx -g "daemon off;"처럼 그냥 씁니다. 이 둘은 단순한 취향 차이가 아니라 컨테이너가 시그널을 받는 방식까지 바꾸는 서로 다른 형식입니다. exec form과 shell form의 차이를 알면 docker stop이 왜 느린지, 변수가 왜 안 풀리는지가 설명됩니다.
두 가지 형식
| 구분 | exec form | shell form |
|---|---|---|
| 표기 | ["실행파일", "인자"] (JSON 배열) | 명령어 인자 (문자열) |
| 실행 방식 | 프로세스를 직접 실행 | /bin/sh -c "..."로 감싸 실행 |
| PID 1 | 내 프로세스가 PID 1 | sh가 PID 1 |
| 시그널 전달 | 프로세스가 직접 받음 | sh가 받고 자식엔 전달 안 될 수 있음 |
변수 전개($VAR) | 안 됨 | 셸이 처리해서 됨 |
exec form은 대괄호 JSON 배열이고, shell form은 따옴표 없는 문자열입니다. shell form은 도커가 내부적으로 /bin/sh -c로 감싸 실행한다는 점이 모든 차이의 출발점입니다.
왜 시그널이 문제인가
컨테이너의 첫 프로세스는 PID 1이 되는데, PID 1은 시그널 처리가 특별합니다. docker stop은 PID 1에게 SIGTERM을 보내 정상 종료를 유도하고, 10초 안에 안 끝나면 SIGKILL로 강제 종료합니다.
# shell form — sh가 PID 1, nginx는 자식
ENTRYPOINT nginx -g "daemon off;"
# exec form — nginx가 직접 PID 1
ENTRYPOINT ["nginx", "-g", "daemon off;"]
shell form이면 sh가 PID 1이 되고 nginx는 그 자식입니다. docker stop이 보낸 SIGTERM을 sh가 받지만 자식에게 전달하지 않는 경우가 많아, nginx는 신호를 못 받고 결국 10초 뒤 SIGKILL로 강제 종료됩니다. 정상 종료(graceful shutdown)가 깨지고 stop이 매번 10초씩 걸리는 것입니다. exec form이면 nginx가 직접 PID 1이라 SIGTERM을 바로 받아 깔끔하게 종료됩니다.
변수가 필요할 때
반대로 shell form만 되는 것도 있습니다. 환경변수 전개입니다.
# 변수가 안 풀림 — 문자열 "$PORT" 그대로 전달
ENTRYPOINT ["sh", "-c", "echo literal"]
# 변수가 풀림 — 셸이 $PORT를 치환
ENTRYPOINT echo "port is $PORT"
exec form은 셸을 안 거치므로 $PORT 같은 변수가 치환되지 않습니다. 변수 전개가 필요하면서도 시그널을 제대로 받고 싶다면, exec form 안에서 명시적으로 셸을 호출하는 절충안을 씁니다.
ENTRYPOINT ["sh", "-c", "exec nginx -g \"daemon off;\""]
exec를 앞에 붙이면 셸이 nginx로 대체되어 nginx가 PID 1이 되므로 시그널 문제도 같이 해결됩니다.
요점 정리
- exec form은 JSON 배열, shell form은 따옴표 없는 문자열이다.
- shell form은
/bin/sh -c로 감싸 실행되어 셸이 PID 1이 된다. - 시그널 전달과 graceful shutdown을 위해 exec form이 기본 권장이다.
- 변수 전개가 필요하면
["sh", "-c", "exec ..."]패턴으로 둘을 모두 만족시킨다.
ENTRYPOINT 형식을 바꿔 가며 시그널 동작을 직접 관찰하는 실습은 도커 트랙에서 할 수 있습니다 — 회원가입 없이 무료로.