Docker Compose command와 entrypoint 차이점 정리
Docker Compose에서 ENTRYPOINT 설정 시 CMD가 초기화되는 원리를 설명합니다. 이미지 내부의 기본값을 확인하는 방법과 4가지 조합별 실행 결과를 통해 컨테이너 시작 명령어를 정확하게 제어하는 방법을 안내합니다.
Docker Compose의 command와 entrypoint, 하나의 규칙으로 정리
Docker Compose에서 entrypoint:은 실행할 프로그램을 설정하고, command:는 해당 프로그램에 전달할 인자를 설정합니다. 컨테이너의 프로세스는 entrypoint 목록 뒤에 command 목록을 덧붙인 형태가 됩니다. 이 페이지에 기술된 다른 모든 동작은 이 문장 하나에서 비롯됩니다.
이 두 키는 Dockerfile의 두 가지 명령어에 대응합니다. entrypoint:은 이미지의 ENTRYPOINT을 대체합니다. command:는 이미지의 CMD을 대체합니다. 이 둘은 독립적이지 않으며, 바로 이 지점에서 사용자들이 혼란을 겪습니다. entrypoint:를 설정하면 이미지의 CMD도 함께 삭제되기 때문입니다. Compose 명세에는 이 내용이 명시되어 있습니다. entrypoint이 null이 아니면, Compose는 이미지에 정의된 기본 command를 무시합니다.
이미지 내부에 선언된 내용 확인하기
무엇인가를 재정의하기 전에, 해당 이미지가 기본적으로 제공하는 내용을 확인해야 합니다.
docker image inspect --format '{{json .Config.Entrypoint}}' postgres:16
docker image inspect --format '{{json .Config.Cmd}}' postgres:16["docker-entrypoint.sh"] 및 ["postgres"]을 확인하면 컨테이너가 docker-entrypoint.sh postgres를 실행한다는 것을 알 수 있습니다. 해당 스크립트는 최초 부팅 시 데이터 디렉터리를 생성하고, POSTGRES_* 변수를 읽으며, postgres 사용자로 권한을 낮춘 뒤, 마지막으로 전달받은 인자를 실행합니다. 변경하려는 부분이 어느 쪽인지 파악하는 것이 결정의 핵심입니다. 데이터베이스에 플래그를 전달하려면 command:를 교체하십시오. 만약 entrypoint:을 교체하면, 앞서 언급한 설정 과정이 전혀 실행되지 않습니다.
작은 이미지에 나타난 네 가지 조합
시작 시 전달받은 인자 목록을 출력하는 것만을 목적으로 하는 이미지를 빌드합니다.
FROM alpine:3.20
ENTRYPOINT ["/bin/echo", "ep"]
CMD ["cmd"]docker build -t argdemo .services:
demo:
image: argdemo각 수정 후 docker compose up를 실행하고 로그에 기록된 한 줄의 내용을 읽습니다.
- 두 키 모두 설정되지 않음. 프로세스는
/bin/echo ep cmd이며 로그에는ep cmd이 표시됩니다. command: ["cmd2"]만 설정됨. 프로세스는/bin/echo ep cmd2입니다. 엔트리포인트는 그대로 유지되고 인자만 변경됩니다.entrypoint: ["/bin/echo", "ep2"]만 설정됨. 프로세스는/bin/echo ep2이며 로그에는ep2이 표시됩니다. 이미지의cmd는 사라지며, 이에 대한 경고는 표시되지 않습니다.- 두 키 모두 설정됨. 프로세스는
/bin/echo ep2 cmd2입니다. 전체 인자 목록을 제어할 수 있는 유일한 경우입니다.
entrypoint를 설정하면 이미지의 CMD가 지워지는 이유
이미지의 CMD는 해당 이미지의 ENTRYPOINT를 위한 기본 인수 목록으로 작성됩니다. entrypoint를 교체하면 기존 인수들은 더 이상 실행되지 않는 프로그램에 귀속되므로, Compose는 이미지 작성자가 의도하지 않은 명령줄이 생성되는 것을 방지하기 위해 해당 인수들을 삭제합니다. docker run --entrypoint도 동일하게 동작하므로, 이는 Compose의 특이 사항이 아닌 Docker의 기본 동작 방식입니다.
그 결과는 명확합니다. nginx:1.27은 ENTRYPOINT ["/docker-entrypoint.sh"]과 CMD ["nginx", "-g", "daemon off;"]를 선언합니다. 이때 entrypoint: /custom-init.sh을 설정하면 스크립트는 빈 인수 목록으로 시작하게 됩니다. 보통 exec "$@"로 끝나는 스크립트는 실행할 대상이 없게 되며, 따라서 exec는 아무런 동작을 하지 않습니다. 스크립트가 마지막 줄에 도달하면 컨테이너는 어떠한 오류 메시지도 남기지 않은 채 코드 0으로 종료됩니다. 인수를 직접 다시 추가하십시오:
services:
web:
image: nginx:1.27
entrypoint: /custom-init.sh
command: ["nginx", "-g", "daemon off;"]기억해야 할 규칙은 다음과 같습니다. entrypoint:을 설정할 때는 언제나 같은 편집 과정에서 command:를 무엇으로 할지 함께 결정하십시오.
Exec 형식과 shell 형식, 그리고 Compose의 차이점
Dockerfile은 두 가지 구문을 허용합니다. CMD ["nginx", "-g", "daemon off;"]는 exec 형식으로, 셸을 거치지 않고 바이너리가 직접 실행됩니다. CMD nginx -g "daemon off;"은 shell 형식으로, Docker가 이를 /bin/sh -c 'nginx -g "daemon off;"'로 재작성하므로 셸이 먼저 실행되고 사용자의 프로그램은 그 자식 프로세스가 됩니다.
Compose는 이 규칙을 따르지 않으며, 이로 인해 혼란이 발생하곤 합니다. command:의 문자열은 인자로 분할되어 직접 실행되며, /bin/sh -c 래퍼를 사용하지 않습니다. Compose 참조 문서에는 이 점이 명시되어 있습니다. command 필드는 이미지에 정의된 SHELL 컨텍스트 내에서 실행되지 않으므로, 셸 기능이 필요하다면 직접 셸을 호출해야 합니다.
이것이 command: echo "hello $$HOSTNAME"가 hello $HOSTNAME이라는 텍스트를 그대로 출력하는 이유입니다. 셸이 해당 문자열을 처리하지 않았기 때문에 아무런 확장도 일어나지 않은 것입니다. 셸 기능이 필요하다면 명시적으로 셸을 요청하십시오.
services:
demo:
image: alpine:3.20
command: /bin/sh -c 'echo "hello $$HOSTNAME"'시그널, PID 1, 그리고 깔끔한 docker compose down
docker compose stop와 docker compose down는 각 컨테이너 내부의 PID 1에 SIGTERM을 보내고 stop_grace_period을 기다린 뒤, SIGKILL을 보냅니다. 기본 유예 기간은 10초입니다.
Linux에서 PID 1은 특별합니다. 커널은 PID 1에 시그널의 기본 동작을 적용하지 않으므로, SIGTERM 핸들러를 설치하지 않은 프로세스는 PID 1로 실행될 때 SIGTERM을 단순히 무시합니다. 해당 프로세스는 유예 기간 내내 유지되다가 강제로 종료되며, 이 과정에서 열려 있는 연결이나 커밋되지 않은 트랜잭션이 끊기게 됩니다.
프로그램 앞에 셸이 있으면 이러한 현상이 발생할 가능성이 커집니다. 셸이 PID 1이 되며 대부분의 셸은 자식 프로세스로 시그널을 전달하지 않기 때문입니다. 일부 셸은 -c 문자열의 마지막 명령어로 자신을 대체하므로, 프로그램이 PID 1이 되는 경우도 있습니다. 이는 셸과 정확한 문자열에 따라 다르므로 추측하지 말고 다음을 확인하십시오.
docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echoPID 1이 프로그램이 아닌 /bin/sh -c ...로 출력된다면 두 가지 해결 방법이 있습니다. 이미지에서 exec 형식을 사용하거나, 셸을 유지하면서 exec을 사용하여 프로세스를 넘겨주는 것입니다.
services:
web:
image: myapp:1.4
command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'exec는 자식 프로세스를 포크(fork)하는 대신 셸 프로세스를 프로그램으로 대체하므로, 프로그램이 PID 1을 상속받아 시그널을 수신하게 됩니다.
일부 프로그램은 자식 프로세스를 생성하고 회수하지 않아 좀비 프로세스를 남기는데, PID 1은 좀비 프로세스를 회수하는 역할도 하기 때문입니다. Compose에는 이를 위한 옵션이 있습니다.
services:
web:
image: myapp:1.4
init: true
stop_grace_period: 30sinit: true는 PID 1로서 작은 init 프로세스를 실행하여 시그널을 프로세스로 전달하고 자식 프로세스를 회수합니다. stop_grace_period은 종료 과정이 느린 경우 더 많은 시간을 제공합니다. 프로그램이 다른 시그널을 기대한다면 stop_signal: SIGQUIT을 사용하여 Compose가 보내는 시그널을 변경할 수 있습니다. 이미지가 이미 요구하는 시그널은 docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27로 확인하십시오.
docker compose down가 서비스당 항상 10초씩 소요된다면, 이는 SIGTERM을 처리하는 프로세스가 없다는 의미입니다. 도구 탓을 하기 전에 이 문제를 먼저 해결하십시오. 각 하위 명령이 무엇을 제거하는지에 대해서는 docker compose down과 stop의 차이를 참조하십시오.
exec와 셸의 구분은 한 곳에서 더 나타납니다. test: ["CMD", "curl", "-f", "http://localhost/"]로 작성된 헬스체크는 바이너리를 직접 실행하는 반면, test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]는 셸을 통해 실행되므로 ||이 의미를 갖게 됩니다. 정확하게 실패하는 Compose 헬스체크 작성하기에서 해당 분야의 나머지 내용을 다룹니다.
공식 이미지에 플래그 추가하기
대부분의 독자가 이 내용을 확인하러 방문했을 것입니다. postgres에 추가 플래그를 하나 설정해야 하며, 초기화 스크립트는 변경하지 않아야 합니다.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
command: postgres -c max_connections=200 -c shared_buffers=256MB
volumes:
pgdata:command:만 변경되었으므로 docker-entrypoint.sh는 여전히 실행되며, 사용자가 지정한 명령을 그대로 수행합니다. 결과를 가정하지 말고 직접 확인하십시오.
docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'출력 결과에 200이 표시되어야 합니다. 여전히 100이 표시된다면 docker compose config을 실행하여 병합된 출력 결과에 예상한 command가 포함되어 있는지 확인하십시오. Compose는 병합 시 command을 추가하는 방식이 아니라 완전히 대체하는 방식으로 오버라이드 파일을 처리합니다. 따라서 command:을 설정하는 두 번째 파일이 있을 경우 해당 설정이 우선 적용됩니다.
위의 ${POSTGRES_PASSWORD}는 컨테이너가 생성되기 전, 호스트의 .env 파일로부터 Compose에 의해 확장됩니다. Compose의 환경 파일 및 비밀값에서 해당 값을 안전하게 관리하는 방법을 다룹니다.
docker compose run을 사용한 일회성 마이그레이션 실행
docker compose run는 동일한 서비스 정의에서 새 컨테이너를 빌드하고, 서비스 이름 뒤에 입력한 명령어로 기존 명령을 대체합니다. 이미지의 entrypoint는 그대로 실행되므로, 컨테이너는 상시 실행되는 컨테이너와 동일한 상태로 준비됩니다.
docker compose run --rm app python manage.py migrate--rm는 명령이 종료되면 컨테이너를 삭제합니다. 이 옵션이 없으면 실행할 때마다 중지된 컨테이너가 남게 되며,docker compose ps -a에서 확인할 수 있습니다.- 포트는 게시되지 않습니다.
run컨테이너는--service-ports를 추가하지 않는 한 서비스의ports:설정을 무시하므로, 이미 실행 중인 서비스와 포트 충돌이 발생하지 않습니다. - 의존 서비스가 먼저 시작됩니다.
depends_on에 정의된 모든 서비스가 명령 실행 전에 올라오며,--no-deps은 이 과정을 건너뜁니다. - 컨테이너에는
myproject-app-run-9f2c1a와 같이 생성된 이름이 부여되므로, 서비스 컨테이너와 이름이 충돌하지 않습니다.
entrypoint까지 교체하려면 다음 플래그를 사용합니다.
docker compose run --rm --entrypoint /bin/sh app -c 'python manage.py migrate'결과적인 인수 목록은 /bin/sh -c 'python manage.py migrate'이 됩니다. 서비스 이름 뒤에 오는 단어들은 여전히 명령어로 취급되기 때문입니다. docker compose exec는 다른 도구이며 작동 방식이 다릅니다. 이미 실행 중인 컨테이너 내부에서 프로세스를 실행하며, entrypoint:와 command:을 완전히 무시합니다. 새로운 컨테이너가 필요한 작업에는 run을 사용하고, 실행 중인 컨테이너 내부를 확인하려면 exec을 사용하십시오. Compose 명령어 요약표에서 나머지 하위 명령어들을 비교할 수 있습니다.
컨테이너가 즉시 종료되는 이유는 무엇입니까?
종료 코드는 원인을 빠르게 좁혀주므로 종료 코드부터 확인하십시오.
docker compose ps -a
docker compose logs app종료 코드 0 및 출력 없음. 명령이 실행되고 완료되었습니다. 가장 흔한 원인은 entrypoint: 재정의로 인해 이미지의 CMD이 함께 사라진 경우입니다. 이로 인해 entrypoint가 빈 인수 목록으로 실행되어 전달할 작업이 없게 됩니다.
permission denied로 끝나는 오류. 스크립트에 실행 권한 비트가 설정되지 않았습니다. 보통 저장소의 파일에 해당 비트가 설정되지 않았기 때문입니다. 빌드 시점에 COPY --chmod=0755 entrypoint.sh /entrypoint.sh를 사용하여 설정하십시오.
이미지에서 분명히 보이는 파일에 대해 no such file or directory 오류가 발생하는 경우. 스크립트에 Windows 줄 바꿈 문자가 포함되어 있습니다. 첫 번째 줄이 #!/bin/sh와 캐리지 리턴 바이트로 읽히기 때문에, 커널은 해당 바이트가 포함된 이름의 인터프리터를 찾으려 하지만 찾지 못합니다. dos2unix entrypoint.sh를 실행한 다음, .gitattributes에 * text eol=lf을 추가하여 재발을 방지하십시오.
executable file not found in $PATH. command:에 지정된 바이너리가 이미지에 없거나, 실제 프로그램만 올 수 있는 곳에 cd과 같은 셸 내장 명령어를 작성한 경우입니다.
Entrypoint가 실패하는 이미지의 셸에 접속하기
Entrypoint가 정보를 확인하기도 전에 종료된다면, 이를 교체하십시오:
docker compose run --rm --entrypoint /bin/sh app이 명령이 executable file not found in $PATH을 반환한다면, 해당 이미지에는 셸이 전혀 없는 것입니다. Distroless 및 scratch 기반 이미지는 셸을 포함하지 않는 경우가 많습니다. 이 경우 Entrypoint를 시작하지 않고도 외부에서 파일 시스템을 읽을 수 있습니다:
docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probe컨테이너를 계속 띄워두고 반복해서 접속해야 한다면, 종료되지 않는 프로세스에 컨테이너를 고정하십시오. 커밋하지 않을 override 파일에 다음 내용을 추가합니다:
services:
app:
entrypoint: ["tail", "-f", "/dev/null"]
command: []entrypoint:를 설정하면 이미지의 CMD가 이미 초기화되므로 command: []은 반드시 필요한 것은 아니지만, 이를 작성해 두면 나중에 파일을 읽는 사람에게 의도를 명확히 전달할 수 있습니다. 컨테이너를 실행하고 내부로 진입하십시오:
docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/sh이제 실제 Entrypoint를 수동으로 실행하여 어디에서 멈추는지 확인하십시오. 이렇게 하면 0.5초 만에 종료된 컨테이너 대신 터미널에서 직접 오류 메시지를 확인할 수 있습니다. 첫 번째 스택을 구성 중이라면, VPS에서의 첫 Compose 스택에서 위 내용이 가정하는 파일 구조를 다루고 있습니다.
FAQ
docker compose up 실행 직후 컨테이너가 즉시 종료되는 이유는 무엇입니까?
docker compose ps -a에서 종료 코드를 확인하십시오. 출력 없이 0으로 종료된다면, 서비스에 entrypoint:을 설정하여 이미지의 CMD이 지워졌을 가능성이 큽니다. 이 경우 entrypoint가 빈 인수 목록으로 실행되어 즉시 종료됩니다. command:를 사용하여 인수를 다시 추가하십시오. permission denied으로 끝나는 오류는 entrypoint 스크립트에 실행 권한이 없음을 의미합니다. 파일이 존재함에도 no such file or directory 오류가 발생한다면, 해당 스크립트가 Windows 줄 바꿈 문자를 사용하고 있어 shebang 라인에 지정된 인터프리터를 찾지 못하는 경우입니다.
Compose에서 entrypoint를 설정하면 이미지의 CMD가 제거됩니까?
네, 그렇습니다. entrypoint가 null이 아니면 Compose는 이미지에 선언된 기본 명령을 무시합니다. 이는 문서화된 동작이며 docker run --entrypoint과 일치합니다. 이미지의 CMD는 해당 이미지의 ENTRYPOINT에 대한 인수로 작성되기 때문에, entrypoint를 교체하면 기존 인수는 더 이상 유효하지 않게 됩니다. 새로운 entrypoint에도 인수가 필요하다면 동일한 서비스 내에 command:을 설정하십시오.
Compose command의 문자열은 셸을 통해 실행됩니까?
아니요. Dockerfile의 CMD과 달리, Compose command:의 문자열은 인수로 분할되어 직접 실행되며 /bin/sh -c 래퍼를 거치지 않습니다. 따라서 $VARIABLE은 컨테이너 내부의 셸에 의해 확장되지 않습니다. 셸이 필요한 경우 command: /bin/sh -c 'echo "hello $$HOSTNAME"'과 같이 직접 셸을 호출하십시오. $$를 두 번 사용하면 달러 기호를 이스케이프하여, Compose가 호스트에서 확장하지 않고 컨테이너로 그대로 전달하게 할 수 있습니다.
docker compose down 실행 시 컨테이너 하나당 10초가 소요되는 이유는 무엇입니까?
Compose는 PID 1에 SIGTERM을 보내고 stop_grace_period(기본값 10초) 동안 대기한 뒤 SIGKILL를 보냅니다. 커널은 PID 1에 기본 신호 동작을 적용하지 않으므로, SIGTERM 핸들러가 없는 프로그램은 신호를 무시하고 항상 전체 대기 시간을 채우게 됩니다. docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' '을 사용하여 실제 PID 1이 무엇인지 확인하십시오. 만약 셸이라면 이미지를 exec 형식으로 전환하거나 셸 문자열 내에 exec을 작성하십시오. 프로세스가 자식 프로세스를 생성하고 이를 회수하지 않는다면 서비스에 init: true를 설정하십시오.