docker compose exec 대화형 셸 실행 방법
실행 중인 컨테이너에 docker compose exec를 사용하여 셸을 접속하는 방법을 설명합니다. 서비스가 중지되었거나 별도 프로세스가 필요한 경우 docker compose run --rm을 활용하는 차이점과 -it 플래그 사용법을 상세히 정리했습니다.
docker compose exec를 사용하여 대화형 셸 실행하기
docker compose exec web bash은 이미 실행 중인 web 서비스 컨테이너 내부에서 대화형 셸을 엽니다. exec 뒤에 오는 이름은 컨테이너 이름이 아니라 compose.yaml에 정의된 서비스 이름입니다. 이미지에 bash가 없다면 대신 sh을 요청하십시오.
docker compose ps
docker compose exec web bash먼저 docker compose ps을 실행하십시오. 상태가 running인 web 목록이 출력되어야 합니다. 그 다음 두 번째 명령어를 입력하면 컨테이너 내부 프롬프트로 진입하며, exit 또는 Ctrl-D를 누르면 호스트로 돌아옵니다. exec는 메인 프로세스 외에 두 번째 프로세스를 시작한 것이므로, 셸을 종료해도 서비스는 계속 실행됩니다. 셸을 닫는 행위는 컨테이너가 실행하도록 설계된 PID 1(프로세스 ID 1) 프로세스에 영향을 주지 않습니다.
이것이 컨테이너에 접속하는 두 가지 방법 중 하나입니다. exec는 이미 존재하는 컨테이너에 합류합니다. docker compose run는 동일한 서비스 정의를 사용하여 새로운 컨테이너를 생성합니다. 이 가이드의 거의 모든 내용은 이 단 하나의 차이점에서 비롯됩니다.
왜 Compose에서는 -it이 선택 사항이지만 일반 docker에서는 필수인가
세션의 대화형 기능을 제어하는 플래그는 두 가지입니다. -i은 표준 입력(stdin)을 열어두어 사용자가 입력한 내용이 프로세스에 전달되도록 합니다. -t은 TTY라고 불리는 가상 터미널을 할당하여 셸이 프롬프트를 출력하고 화살표 키를 처리할 수 있게 합니다. 일반 docker exec은 기본적으로 이 두 가지를 모두 비활성화하므로, 지금까지 본 모든 예제에서 docker exec -it를 작성하는 것입니다. docker compose exec은 이 두 가지를 모두 활성화하므로, docker compose exec -it web bash과 docker compose exec web bash는 동일한 동작을 수행합니다. Compose는 여전히 -it을 허용하므로 기존의 습관대로 명령어를 사용해도 문제없습니다.
TTY가 누락되면 몇 초 안에 바로 알 수 있습니다. 셸은 실행되지만 프롬프트가 출력되지 않으며, Ctrl-C를 눌러도 프로세스에 전달되지 않습니다. 반대로 Compose가 TTY를 할당하지 않도록 설정해야 하는 경우에는 별도의 플래그가 필요하며, 이에 대해서는 아래 섹션에서 자세히 다룹니다.
이미지에 bash가 없을 때 대처 방법
Alpine 기반 이미지에 bash를 요청하면 다음과 같이 exec 실행이 실패합니다.
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown이 메시지는 exec 자체의 문제가 아닙니다. 요청한 바이너리가 이미지 내에 존재하지 않는다는 뜻입니다. Alpine은 BusyBox를 포함하고 있으며, 이는 ash를 /bin/sh로 제공할 뿐 bash는 전혀 포함하지 않습니다. 따라서 sh을 요청하십시오.
docker compose exec web sh-slim 태그를 포함한 Debian 및 Ubuntu 기반 이미지는 bash를 기본적으로 포함하며, bash를 사용하면 명령어 기록과 더 나은 자동 완성을 이용할 수 있습니다. 따라서 먼저 bash를 시도하고 실패할 경우 sh로 대체하십시오. sh는 거의 모든 범용 이미지에 존재합니다.
일부 이미지는 셸을 전혀 포함하지 않습니다. Distroless 이미지나 FROM scratch으로 빌드된 이미지는 의도적으로 애플리케이션 바이너리와 그 라이브러리 외에는 아무것도 포함하지 않습니다. 존재하지 않는 셸은 공격에 악용될 수 없기 때문입니다. 이러한 이미지에서 sh을 실행하면 동일한 메시지와 함께 실패하며, 더 이상 시도할 방법이 없습니다. 이때는 두 가지 접근 방식을 사용할 수 있습니다. Google의 distroless 이미지는 BusyBox 셸이 추가된 :debug 태그를 제공하므로, 태그를 일시적으로 변경하여 접속할 수 있습니다. 또는 대상의 네임스페이스 내에서 별도의 컨테이너를 시작하십시오.
CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot이제 netshoot의 도구들이 애플리케이션의 네트워크를 가리키게 되므로, curl localhost:8080 및 ss -lntp는 마치 해당 컨테이너 내부에 있는 것처럼 동작합니다. 이때 보이는 파일 시스템은 애플리케이션이 아닌 netshoot의 것입니다. 프로세스 네임스페이스가 공유되므로, root 권한이 있다면 ls /proc/1/root/를 통해 대상의 파일에 접근할 수 있습니다.
서비스가 실행 중이 아닐 때는 docker compose run --rm을 사용하십시오
exec를 사용하려면 실행 중인 컨테이너가 필요합니다. 중지된 서비스에 exec를 지정하면 거부됩니다:
service "web" is not runningexec는 아무것도 시작해주지 않습니다. docker compose run은 다음과 같이 작동합니다:
docker compose run --rm web bashrun은 web 서비스 정의를 바탕으로 동일한 이미지, 환경 변수, 볼륨, 네트워크를 사용하여 새 컨테이너를 생성하며, 서비스의 기본 명령어를 입력한 명령어로 대체합니다. --rm은 종료 시 해당 컨테이너를 삭제합니다. --rm를 생략하면 myproject-web-run-4f1c2b과 같은 이름으로 잔여 컨테이너가 쌓이며, 이는 docker compose ps -a로 확인할 수 있고 다른 어떤 도구도 이를 자동으로 정리하지 않습니다.
run의 두 가지 동작 방식은 사용자들을 당황하게 합니다. 첫째, --service-ports를 추가하지 않으면 서비스의 포트를 게시하지 않는데, 이는 의도된 설계입니다. 첫 번째 컨테이너가 이미 8080 포트를 점유한 상태에서 두 번째 컨테이너가 같은 호스트 포트에 바인딩을 시도하면 bind: address already in use 오류가 발생하기 때문입니다. 둘째, 셸이 나타나기 전에 서비스가 depends_on에 나열한 모든 항목을 시작하므로, 내부를 잠시 확인하려다가 데이터베이스나 캐시가 시작될 수 있습니다. --no-deps를 사용하면 이를 건너뛸 수 있습니다.
run은 이미지의 ENTRYPOINT를 거치지만, exec는 그렇지 않습니다. exec는 기존 컨테이너에서 명령어를 직접 시작하므로 entrypoint 스크립트는 해당 명령어를 인식하지 못합니다. run을 사용하면 bash이 해당 스크립트의 인자로 전달됩니다. 많은 공식 이미지는 entrypoint의 마지막에 exec "$@"을 포함하므로 인자가 그대로 전달되어 셸을 얻을 수 있습니다. 하지만 자체적으로 인자를 해석하는 스크립트라면 다르게 동작할 수 있으며, 이 경우 해당 실행에 대해서만 entrypoint를 교체해야 합니다:
docker compose run --rm --entrypoint sh web이것이 exec에서는 작동하던 명령어가 run에서는 다르게 동작하는 가장 흔한 이유이며, command와 entrypoint의 구분에서 매번 이미지 설정의 어느 부분을 교체하고 있는지 설명합니다.
exec와 run: 선택 기준
- exec는 실행 중인 컨테이너가 필요합니다. run은 그렇지 않으며, 의존성 서비스를 시작할 수도 있습니다.
- exec는 현재 실행 중인 프로세스 목록과 애플리케이션이 시작된 이후 기록한 모든 파일을 포함한 현재 상태를 확인합니다. run은 이미지의 깨끗한 복사본을 가져오므로 이전의 변경 사항은 포함되지 않습니다.
- exec는 entrypoint를 건너뜁니다. run은 entrypoint를 실행합니다.
- run은
--rm옵션을 사용하지 않으면 컨테이너를 남깁니다.
실제로 어떤 일이 벌어지고 있는지 확인하려면 exec를 사용하십시오. 동일한 환경의 일회성 복사본이 필요하거나, 일회성 마이그레이션 명령을 실행할 때, 또는 실제 서비스가 exec를 수행할 만큼 충분히 오래 유지되지 않을 때는 run --rm를 사용하십시오.
유용한 exec 플래그: 사용자, 작업 디렉터리 및 복제본
대부분의 이미지는 root가 아닌 사용자로 실행되므로, exec 셸 내부에서 진단 도구를 설치하려는 시도는 여기서 막히게 됩니다.
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)-u root을 사용하면 동일한 컨테이너 내에서 root 권한의 셸을 얻을 수 있습니다.
docker compose exec -u root web sh-w /srv/app은 해당 명령에 대해서만 작업 디렉터리를 설정합니다. -e KEY=value는 서비스가 아닌 현재 세션에만 환경 변수를 추가합니다. 서비스가 여러 개의 복제본으로 실행 중일 때, --index 2은 접속할 컨테이너를 결정합니다. 마운트된 디렉터리의 파일 소유권 문제를 해결하려는 경우, 컨테이너 이미지의 PUID 및 PGID에서 사용자 이름이 아닌 숫자 ID가 쓰기 권한을 결정하는 이유를 다룹니다.
데이터베이스 컨테이너 내부에서 psql 또는 mysql 셸 실행하기
클라이언트가 이미 데이터베이스 이미지 내부에 포함되어 있으므로 호스트에 별도의 클라이언트를 설치하거나 포트를 외부에 노출할 필요가 없습니다.
docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -pPostgres 이미지에는 psql가, MySQL 이미지에는 mysql가, MariaDB 이미지에는 mariadb가 포함되어 있습니다. 컨테이너 내부에서 연결이 이루어지므로, compose 파일에서 데이터베이스 포트를 전혀 노출하지 않은 경우에도 이 방법은 정상적으로 작동합니다. 포트를 노출하지 않는 것이 더 안전한 구성 방식입니다. 포트를 노출하지 않으면 인터넷상의 그 누구도 해당 포트에 접근할 수 없기 때문입니다.
많은 사용자가 흔히 빠지는 함정이 하나 있습니다. Docker가 명령어를 인식하기 전에 호스트의 셸이 변수를 먼저 확장해 버리는 경우입니다. 따라서 해당 변수가 컨테이너 내부에만 존재할 때 -U "$POSTGRES_USER"을 사용하면 빈 문자열이 전달됩니다. 작은따옴표를 사용하고 컨테이너 내부의 셸에서 실행하면 올바른 위치에서 변수가 확장됩니다.
docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'명령어 없이 docker compose run --rm db을 사용하지 마십시오. 이는 동일한 데이터 볼륨을 대상으로 두 번째 Postgres 서버를 시작하려는 시도이며, 서버는 시작을 거부하게 됩니다.
FATAL: lock file "postmaster.pid" already exists잠금 파일(lock file)이 제 역할을 수행하고 있는 것입니다. 두 개의 서버가 하나의 데이터 디렉터리에 동시에 쓰기 작업을 수행하면 데이터가 손상될 수 있기 때문입니다. 데이터베이스가 실행 중일 때는 실행 중인 컨테이너에 exec로 접속하십시오. 데이터베이스를 Compose에 포함할지 여부는 별개의 결정 사항이며, Docker에서 데이터베이스를 실행할지 호스트에서 실행할지에 대한 내용은 해당 문서를 참조하십시오.
시작 시 콘솔이 필요한 서비스: stdin_open 및 tty
exec와 run은 수동으로 여는 셸을 다룹니다. 본질적으로 대화형인 메인 프로세스를 가진 서비스는 compose 파일에 두 가지 키가 필요합니다.
services:
console:
image: python:3.12-slim
command: python
stdin_open: true
tty: truestdin_open: true는 docker run -i이고 tty: true은 docker run -t입니다. 이 설정이 없으면 컨테이너는 시작하자마자 코드 0으로 즉시 종료되며, docker compose ps -a은 Exited (0)를 표시합니다. 충돌한 것이 아닙니다. stdin에 터미널이 없는 python는 즉시 파일 끝(EOF)을 읽고 정상 종료되는데, 이는 아무도 입력하지 않는 프로그램의 올바른 동작 방식입니다.
두 키를 모두 설정한 상태에서 실행 중인 프로세스에 연결하려면 다음을 사용합니다.
docker attach $(docker compose ps -q console)Ctrl-P를 누른 뒤 Ctrl-Q를 누르면 프로세스를 계속 실행한 채로 분리(detach)할 수 있습니다. 이 시퀀스는 컨테이너에 TTY와 stdin이 모두 열려 있을 때만 작동합니다. 반면 Ctrl-C는 PID 1에 인터럽트를 보내 서비스를 중지시킵니다.
일반적인 서비스에는 두 키를 모두 끄십시오. 웹 서버는 stdin을 읽지 않으며, tty: true은 많은 프로그램이 사람이 보고 있다고 판단하여 컬러 출력 및 라인 버퍼링을 활성화하게 만듭니다. 이로 인해 docker compose logs이 이스케이프 코드로 가득 차게 됩니다.
cron과 CI에서 스크립트 실행이 실패하는 이유: -T 플래그
터미널에서는 정상적으로 작동하는 exec 명령이 cron 작업이나 CI(Continuous Integration) 러너 내부에서는 실패하는 경우가 있습니다.
the input device is not a TTYCompose는 기본적으로 의사 터미널(pseudo terminal)을 요청하지만, cron은 작업에 터미널을 할당하지 않으므로 명령이 실행되기도 전에 요청이 실패합니다. -T 옵션은 이 요청을 비활성화합니다.
0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump-T가 중요한 이유는 한 가지 더 있습니다. TTY는 출력되는 바이트 스트림을 재작성하기 때문에, TTY를 거친 압축 덤프 파일은 손상된 상태로 전달됩니다. 리다이렉트나 파이프를 사용하는 모든 출력에는 -T이 필요합니다.
cron과 관련된 두 가지 세부 사항이 더 있습니다. cron은 홈 디렉터리에서 작업을 실행하는데 이곳에는 compose 파일이 없으므로, -f과 함께 절대 경로를 전달해야 합니다. 그렇지 않으면 Compose는 no configuration file provided: not found 오류와 함께 중단됩니다. 또한 exec는 실행한 명령의 종료 코드를 반환하므로, 실패한 pg_dump은 빈 백업을 생성하고 성공으로 보고하는 대신 set -e 설정 하에서 스크립트를 실패하게 만듭니다. 그 외 일상적인 명령들은 스크립트 옆에 두고 참고하기 좋은 Compose 명령 치트 시트에 정리되어 있습니다.
컨테이너 내부에서 변경한 내용이 사라지는 이유
exec를 사용하여 도구를 설치하거나, 설정 파일을 수정하여 문제를 해결한 뒤 일주일이 지나면 해당 수정 사항이 사라집니다. 이는 컨테이너의 쓰기 가능 레이어(writable layer)가 설계된 대로 동작하기 때문입니다. 이미지 태그나 서비스 정의가 변경된 후 docker compose up -d를 실행하면 기존 컨테이너는 삭제되고 이미지로부터 새로운 컨테이너가 생성되므로, 수동으로 적용한 모든 변경 사항은 이전 컨테이너와 함께 사라집니다.
docker compose restart은 다르게 동작합니다. 동일한 컨테이너를 중지했다가 다시 시작하기 때문에 수동으로 적용한 수정 사항이 유지됩니다. 이것이 바로 수동으로 적용한 수정 사항이 몇 주 동안은 유지되는 것처럼 보이다가, 무관한 업데이트 과정에서 갑자기 사라지는 이유입니다. 네임드 볼륨(named volumes)과 바인드 마운트(bind mounts)는 데이터가 컨테이너 외부에 존재하므로 두 작업 모두에서 살아남습니다. 바인드 마운트와 네임드 볼륨 섹션에서 보존해야 할 데이터를 위해 어떤 방식을 선택해야 하는지 다룹니다.
따라서 exec 셸은 내용을 확인하고 테스트하는 용도로만 사용하십시오. 수정 방법을 알아냈다면, 영구적으로 유지될 수 있는 곳에 기록해야 합니다. 패키지는 Dockerfile에, 설정은 compose 파일에 작성하십시오. 그 후 docker compose up -d을 실행하여 변경 사항을 적용하고, 다시 exec를 사용하여 새로운 컨테이너에 수정 사항이 제대로 반영되었는지 확인하십시오.
FAQ
docker compose exec와 docker compose run의 차이점은 무엇입니까?
exec는 이미 실행 중인 컨테이너 내부에서 메인 프로세스와 별개로 명령을 실행하며, 이미지의 entrypoint를 무시합니다. run은 동일한 서비스 정의를 바탕으로 이미지, 환경 변수, 볼륨, 네트워크를 사용하여 새로운 컨테이너를 생성합니다. 또한 명령을 entrypoint에 전달하며, 필요한 경우 depends_on 서비스를 먼저 시작합니다. run은 --service-ports를 추가하지 않는 한 서비스의 포트를 외부에 노출하지 않습니다. 실행 중인 서비스를 점검할 때는 exec를 사용하십시오. 서비스가 중지되었거나 서비스에 영향을 주지 않아야 할 때는 run --rm을 사용하십시오.
docker compose exec 실행 시 서비스가 실행 중이 아니라는 오류가 발생하는 이유는 무엇입니까?
exec는 기존 컨테이너에 접속하는 방식이므로 컨테이너를 새로 생성할 수 없습니다. 따라서 서비스가 중지되었거나 비정상 종료된 경우 service "web" is not running 오류가 발생합니다. Exited (1)과 같은 상태로 종료된 컨테이너를 나열하는 docker compose ps -a를 확인하고, docker compose logs web를 읽어 중지된 원인을 파악하십시오. 그럼에도 셸에 접속해야 한다면 docker compose run --rm --entrypoint sh web를 실행하십시오. 이는 고장 난 시작 명령을 실행하지 않고 동일한 서비스 정의를 사용하여 새로운 컨테이너를 생성합니다.
이미지에 bash가 없을 때 셸을 여는 방법은 무엇입니까?
docker compose exec web bash 명령이 exec: "bash": executable file not found in $PATH 오류와 함께 실패한다면 이미지에 bash가 없는 것이며, 이는 Alpine 기반 이미지에서 흔히 나타나는 현상입니다. BusyBox가 /bin/sh를 제공하므로 docker compose exec web sh을 사용하십시오. Distroless 및 scratch 이미지에는 셸이 전혀 포함되어 있지 않으므로 어떠한 exec 명령도 작동하지 않습니다. 배포자가 제공한다면 이미지의 :debug 태그로 변경하거나, docker compose ps -q web에서 가져온 $CID를 사용하여 대상의 네임스페이스 내에서 디버그 컨테이너를 시작하십시오. docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot 명령을 사용합니다.
cron에서 exec 명령을 실행할 때 "the input device is not a TTY" 오류가 발생하는 이유는 무엇입니까?
docker compose exec는 기본적으로 의사 터미널(pseudo terminal)을 요청하는데, cron은 이를 제공하지 않으므로 명령이 실행되기 전에 요청이 실패합니다. -T를 추가하여 이를 비활성화하십시오: docker compose exec -T db pg_dump -U postgres app. TTY는 바이트 스트림을 변경하여 바이너리 덤프를 손상시킬 수 있으므로, 출력을 리다이렉트하거나 파이프할 때도 -T을 사용하십시오. cron에서는 compose 파일의 절대 경로와 함께 -f을 전달해야 합니다. 그렇지 않으면 Compose가 no configuration file provided: not found 오류와 함께 종료됩니다.
exec를 사용하여 컨테이너 내부에서 변경한 내용은 재시작 후에도 유지됩니까?
동일한 컨테이너를 재사용하는 docker compose restart의 경우에는 변경 사항이 유지됩니다. 하지만 이미지나 설정이 변경된 후 docker compose up -d을 수행하면 컨테이너가 이미지로부터 다시 생성되고 쓰기 가능한 레이어가 삭제되므로 변경 사항은 사라집니다. 이름이 지정된 볼륨이나 바인드 마운트에 기록된 데이터는 컨테이너 외부의 영역에 존재하므로 두 경우 모두 유지됩니다. 진단을 위한 변경은 exec로 수행하되, 영구적인 변경 사항은 Dockerfile이나 compose 파일에 반영하십시오.