SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor

Docker Compose exec: abrir shell interativo no serviço

Aprenda a usar docker compose exec para abrir um shell em um servico em execucao e run --rm quando o servico estiver parado ou nao puder ser alterado.

Obter um shell interativo com docker compose exec

docker compose exec web bash abre um shell interativo dentro do contentor que já está em execução como o serviço web. O nome depois de exec é o nome do serviço definido no seu compose.yaml, não o nome do contentor. Se a imagem não tiver bash, peça sh.

docker compose ps
docker compose exec web bash

Execute docker compose ps primeiro. O comando deve listar web com o estado running. Depois, o segundo comando coloca-o numa linha de comandos dentro do contentor. exit ou Ctrl-D devolve-o ao host. O serviço continua em execução depois de sair, porque exec iniciou um segundo processo juntamente com o processo principal. Fechar o shell não afeta o PID 1 (processo com ID 1), que é o processo para o qual o contentor foi criado.

Esta é uma das duas formas de entrar. exec liga-se a um contentor que já existe. docker compose run cria um novo contentor a partir da mesma definição de serviço. Quase tudo o resto neste guia resulta desta única diferença.

Por que -it é opcional no Compose, mas obrigatório com o docker simples

Duas flags controlam a parte interativa de uma sessão. -i mantém a entrada padrão aberta, para que o que escrever seja recebido pelo processo. -t aloca um terminal pseudo, chamado TTY, para que a shell mostre um prompt e processe as teclas de seta. O docker exec simples deixa ambas desativadas por padrão. Por isso, todos os exemplos apresentados escrevem docker exec -it. O docker compose exec ativa ambas por si só. Assim, docker compose exec -it web bash e docker compose exec web bash fazem a mesma coisa. O Compose também aceita -it, para manter compatibilidade com hábitos antigos.

É possível detetar a ausência de um TTY em poucos segundos. A shell é executada, mas não mostra nenhum prompt, e Ctrl-C nunca chega ao processo. O caso oposto, em que é necessário pedir ao Compose para não alocar um TTY, tem a sua própria flag e a sua própria secção mais abaixo.

O que fazer quando a imagem não tem bash

Peça bash a uma imagem baseada em Alpine e o comando exec falhará assim:

OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown

Essa mensagem não indica um problema do exec. Ela informa que o binário solicitado não existe na imagem. O Alpine inclui o BusyBox, que fornece ash como /bin/sh e não inclui bash. Por isso, peça sh:

docker compose exec web sh

As imagens baseadas em Debian e Ubuntu, incluindo as tags -slim, incluem bash. O bash fornece histórico de comandos e preenchimento automático mais completo. Portanto, tente primeiro o bash e, se falhar, use sh. O sh existe em quase todas as imagens de uso geral.

Algumas imagens não têm shell. As imagens Distroless e as imagens compiladas FROM scratch contêm apenas o binário da aplicação e as respetivas bibliotecas. Isto é intencional. Um shell que não está presente não pode ser usado contra a aplicação. Nessas imagens, sh falha com a mesma mensagem e não há outra opção para tentar. Existem duas abordagens. As imagens Distroless da Google publicam tags :debug que adicionam um shell BusyBox. Assim, pode alterar temporariamente a tag para obter acesso. Outra opção é iniciar um contentor separado dentro dos namespaces do destino:

CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot

Agora tem as ferramentas do netshoot apontadas para a rede da aplicação. Por isso, curl localhost:8080 e ss -lntp comportam-se como se estivesse dentro dela. O sistema de ficheiros que vê pertence ao netshoot, não à aplicação. Como o namespace de processos é partilhado, ls /proc/1/root/ acede aos ficheiros do próprio destino quando é root.

Quando o serviço não está em execução, use docker compose run --rm

exec exige um container em execução. Se você o apontar para um serviço parado, o comando falhará:

service "web" is not running

Ele não iniciará nada por você. docker compose run fará isso:

docker compose run --rm web bash

run cria um novo container a partir da definição do serviço web, usando a mesma imagem, variáveis de ambiente, volumes e redes, e substitui o comando do serviço pelo comando que você digitou. --rm exclui esse container quando você sair. Se você omitir --rm, os containers restantes se acumulam com nomes como myproject-web-run-4f1c2b. docker compose ps -a os exibirá, mas nenhum outro comando os removerá.

Dois comportamentos de run surpreendem muitos usuários. Ele não publica as portas do serviço, a menos que você adicione --service-ports. Isso é intencional: um segundo container tentando vincular a porta 8080 do host enquanto o primeiro ainda a utiliza falharia com bind: address already in use. O comando também inicia tudo o que o serviço lista em depends_on antes de abrir o seu shell. Uma verificação rápida pode iniciar um banco de dados e um cache. --no-deps desativa esse comportamento.

run executa o ENTRYPOINT da imagem; exec não. exec inicia o seu comando diretamente no container existente, portanto o script de entrypoint nunca o recebe. Com run, o seu bash chega como argumento para esse script. Muitas imagens oficiais terminam o entrypoint com exec "$@". Assim, o comando é repassado diretamente e você obtém o seu shell. Um script que interpreta os próprios argumentos fará algo diferente com eles. Nesse caso, você pode substituir o entrypoint nessa execução:

docker compose run --rm --entrypoint sh web

Essa é a razão mais comum para um comando que funciona com exec se comportar de forma diferente com run. A distinção entre command e entrypoint explica qual parte da configuração da imagem você está substituindo em cada execução.

exec ou run: como escolher

  • exec precisa de um container em execução. run não precisa e pode iniciar dependências.
  • exec vê a lista de processos ativa e os ficheiros no estado atual, incluindo tudo o que a aplicação escreveu desde que arrancou. run obtém uma cópia limpa da imagem, por isso nada disso está presente.
  • exec ignora o entrypoint. run executa-o.
  • run deixa um container para trás, a menos que passe --rm.

Use exec para verificar o que está realmente a acontecer. Use run --rm para uma cópia descartável do mesmo ambiente, para executar um comando de migração pontual ou quando o serviço real não permanece ativo tempo suficiente para permitir o uso de exec.

Sinalizadores úteis de exec: utilizador, diretório de trabalho e réplicas

A maioria das imagens muda para um utilizador que não é root. Por isso, a instalação de uma ferramenta de diagnóstico dentro da shell de execução para neste ponto:

E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)

-u root fornece uma shell root no mesmo contentor:

docker compose exec -u root web sh

-w /srv/app define o diretório de trabalho apenas para esse comando. -e KEY=value adiciona uma variável de ambiente à sua sessão, mas não ao serviço. Quando um serviço tem mais do que uma réplica, --index 2 determina em que contentor entra. Se estiver a investigar a propriedade dos ficheiros num diretório montado, PUID e PGID em imagens de contentores explica por que motivo são os IDs numéricos, e não os nomes dos utilizadores, que determinam quem pode escrever nesse diretório.

Obter uma shell psql ou mysql dentro do contentor da base de dados

O cliente já está incluído na imagem da base de dados. Por isso, não precisa de o instalar no host nem de publicar a porta:

docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -p

As imagens Postgres incluem psql, as imagens MySQL incluem mysql e as imagens MariaDB incluem mariadb. A ligação é feita a partir de dentro do contentor. Por isso, funciona mesmo quando o ficheiro Compose não publica nenhuma porta da base de dados. Essa é a configuração mais segura: nada na Internet consegue aceder a uma porta que nunca publicou.

Há uma armadilha que pode consumir uma tarde. A shell expande as variáveis no host antes de o Docker receber o comando. Por isso, -U "$POSTGRES_USER" envia uma string vazia quando essa variável só existe dentro do contentor. As aspas simples e uma shell dentro do contentor fazem a expansão no local correto:

docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

Não use docker compose run --rm db sem um comando neste caso. Isso inicia um segundo servidor Postgres sobre o mesmo volume de dados e o servidor recusa-se a arrancar:

FATAL:  lock file "postmaster.pid" already exists

O ficheiro de bloqueio está a funcionar corretamente. Dois servidores a escrever no mesmo diretório de dados poderiam corrompê-lo. Enquanto a base de dados estiver ativa, use exec para entrar no contentor em execução. Decidir se a base de dados deve fazer parte do Compose é uma questão separada. executar a base de dados no Docker ou no host apresenta as opções e os compromissos envolvidos.

Serviços que precisam de uma consola no arranque: stdin_open e tty

exec e run abrangem as shells que abre manualmente. Um serviço cujo processo principal é interativo por natureza precisa de duas chaves no ficheiro compose:

services:
  console:
    image: python:3.12-slim
    command: python
    stdin_open: true
    tty: true

stdin_open: true é docker run -i e tty: true é docker run -t. Sem estas chaves, o contentor inicia e termina imediatamente com o código 0, e docker compose ps -a mostra Exited (0). Não ocorreu nenhuma falha. python sem um terminal em stdin lê imediatamente o fim do ficheiro e termina normalmente. Este é o comportamento correto para um programa que não recebe entrada de um utilizador.

Com as duas chaves definidas, associe-se ao processo em execução:

docker attach $(docker compose ps -q console)

Desassocie-se com Ctrl-P e depois Ctrl-Q. O processo continua em execução. Esta sequência só funciona quando o contentor tem um TTY e stdin aberto. Ctrl-C envia uma interrupção para o PID 1 e para o serviço.

Deixe as duas chaves desativadas nos serviços comuns. Um servidor Web nunca lê stdin, e tty: true faz com que muitos programas mudem para a saída a cores e o armazenamento em buffer por linhas, porque assumem que alguém está a observar. Isto preenche docker compose logs com códigos de escape.

Por que o exec em scripts falha no cron e na CI: a flag -T

Um comando exec que funciona no terminal falha dentro de um cron job ou de um runner de integração contínua (CI):

the input device is not a TTY

Por padrão, o Compose solicita um pseudo-terminal. O cron não fornece um terminal ao job, portanto a solicitação falha antes de o comando ser executado. -T desativa essa solicitação:

0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump

-T é importante por outro motivo. Um TTY reescreve o fluxo de bytes ao enviá-lo, portanto um dump comprimido que passe por um TTY chega danificado. Qualquer saída redirecionada ou enviada por pipe precisa de -T.

Há mais dois detalhes sobre o cron. Passe -f com um caminho absoluto, porque o cron executa o job a partir do diretório home, onde não existe um ficheiro compose. Nesse caso, o Compose termina com no configuration file provided: not found. O exec devolve o código de saída do comando executado. Por isso, um pg_dump que falhe faz o seu script falhar com set -e, em vez de criar uma cópia de segurança vazia e indicar sucesso. Os restantes comandos usados no dia a dia estão reunidos numa folha de consulta rápida de comandos do Compose, que vale a pena manter junto desses scripts.

Por que as alterações feitas dentro de um container desaparecem

Você instala uma ferramenta com exec, edita um ficheiro de configuração, corrige o problema e, uma semana depois, a correção desaparece. Isso é o funcionamento esperado da camada gravável do container. docker compose up -d, depois de qualquer alteração na tag da imagem ou na definição do serviço, destrói o container antigo e cria um novo a partir da imagem. Todas as alterações manuais desaparecem com o container antigo.

docker compose restart é diferente. Para e inicia o mesmo container, por isso as alterações manuais permanecem. É por isso que uma correção manual pode parecer válida durante semanas e depois desaparecer durante uma atualização não relacionada. Named volumes e bind mounts sobrevivem a ambas as operações, porque os respetivos dados ficam fora do container. bind mounts e named volumes explica qual escolher para os dados que pretende manter.

Trate, portanto, uma shell exec como um local para consultar e testar. Depois de identificar a correção, escreva-a num local onde possa persistir: um pacote no Dockerfile, uma definição no ficheiro compose. Em seguida, docker compose up -d para a aplicar e confirme com outro exec que o novo container realmente a contém.

FAQ

Qual é a diferença entre docker compose exec e docker compose run?

exec executa um comando dentro de um container que já está em execução, ao lado do processo principal, e ignora o entrypoint da imagem. run cria um novo container a partir da mesma definição de serviço, com a mesma imagem, ambiente, volumes e redes, passa o comando pelo entrypoint e inicia primeiro os serviços depends_on. run também deixa as portas do serviço sem publicação, a menos que adicione --service-ports. Use exec para inspecionar o serviço em execução. Use run --rm quando o serviço estiver parado ou quando não quiser interferir nele.

Por que docker compose exec informa que o serviço não está em execução?

exec liga-se a um container existente e não pode criar um, portanto um serviço parado ou interrompido apresenta service "web" is not running. Verifique docker compose ps -a, que lista os containers encerrados com um estado como Exited (1), e consulte docker compose logs web para saber por que ele parou. Para obter uma shell mesmo assim, execute docker compose run --rm --entrypoint sh web. Isso cria um container novo a partir da mesma definição de serviço sem permitir que o comando de arranque problemático seja executado.

Como abro uma shell quando a imagem não tem bash?

docker compose exec web bash falhar com exec: "bash": executable file not found in $PATH significa que bash está ausente da imagem, o que é normal em qualquer imagem baseada em Alpine. Use docker compose exec web sh, porque BusyBox fornece /bin/sh. As imagens Distroless e scratch não contêm nenhuma shell, portanto nenhum comando exec funcionará. Mude para a tag :debug da imagem se o publicador oferecer uma, ou inicie um container de depuração nos namespaces do destino com docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot, em que $CID vem de docker compose ps -q web.

Por que o meu comando exec falha com "the input device is not a TTY" no cron?

docker compose exec solicita um pseudo-terminal por padrão, mas o cron não fornece nenhum, portanto a solicitação falha antes de o comando ser executado. Adicione -T para o desativar: docker compose exec -T db pg_dump -U postgres app. Use -T também para qualquer saída redirecionada ou canalizada, porque um TTY altera o fluxo de bytes e danifica um dump binário. No cron, passe também -f com o caminho absoluto para o ficheiro compose, ou o Compose termina com no configuration file provided: not found.

As alterações que faço dentro de um container com exec sobrevivem a um reinício?

Elas sobrevivem a docker compose restart, que reutiliza o mesmo container. Perdem-se em docker compose up -d após qualquer alteração na imagem ou na configuração, porque isso recria o container a partir da imagem e elimina a camada gravável. Os dados escritos em volumes nomeados ou bind mounts sobrevivem a ambos, porque ficam fora do container. Faça alterações de diagnóstico com exec e coloque depois a versão permanente no Dockerfile ou no ficheiro compose.