Docker Compose: command ou entrypoint, qual usar?
ENTRYPOINT inicia o programa e command fornece argumentos. Veja as 4 combinações de sobrescrita no Compose e por que definir entrypoint remove o CMD da imagem.
Docker Compose: command e entrypoint, em uma regra
No Docker Compose, entrypoint: define o programa que é executado e command: define os argumentos passados a esse programa. O processo do container é formado pela lista de entrypoint seguida da lista de command. Todos os outros comportamentos desta página resultam dessa regra.
Essas duas chaves correspondem a duas instruções do Dockerfile. entrypoint: substitui o ENTRYPOINT da imagem. command: substitui o CMD da imagem. Elas não são independentes, e é aí que surgem os problemas: definir entrypoint: também descarta o CMD da imagem. A especificação do Compose afirma isso diretamente. Se entrypoint não for nulo, o Compose ignora qualquer command padrão da imagem.
Leia o que a imagem já declara
Antes de substituir qualquer definição, veja o que a imagem fornece.
docker image inspect --format '{{json .Config.Entrypoint}}' postgres:16
docker image inspect --format '{{json .Config.Cmd}}' postgres:16Você obtém ["docker-entrypoint.sh"] e ["postgres"], portanto o contentor executa docker-entrypoint.sh postgres. Esse script cria o diretório de dados no primeiro arranque, lê as variáveis POSTGRES_*, remove os privilégios para o utilizador postgres e, por fim, executa com exec os argumentos que recebeu. Saber qual das duas partes pretende alterar é a decisão principal. Para passar uma opção à base de dados, substitua command:. Se substituir entrypoint:, toda essa preparação deixa de ser executada.
As quatro combinações, apresentadas numa imagem pequena
Crie uma imagem cuja única função seja imprimir a lista de argumentos com que foi iniciada.
FROM alpine:3.20
ENTRYPOINT ["/bin/echo", "ep"]
CMD ["cmd"]docker build -t argdemo .services:
demo:
image: argdemoExecute docker compose up depois de cada alteração e leia a única linha registada.
- Nenhuma chave definida. O processo é
/bin/echo ep cmde o log mostraep cmd. - Apenas
command: ["cmd2"]. O processo é/bin/echo ep cmd2. O entrypoint não é alterado; apenas os argumentos mudam. - Apenas
entrypoint: ["/bin/echo", "ep2"]. O processo é/bin/echo ep2e o log mostraep2. Ocmdda imagem desaparece e nada o avisa. - Ambas as chaves definidas. O processo é
/bin/echo ep2 cmd2. Este é o único caso em que controla toda a lista de argumentos.
Por que definir entrypoint limpa o CMD da imagem
O CMD de uma imagem é gravado como a lista de argumentos padrão do ENTRYPOINT dessa imagem. Substitua o entrypoint e esses argumentos passam a pertencer a um programa que já não está em execução. Por isso, o Compose descarta-os em vez de construir uma linha de comando que o autor da imagem nunca pretendeu criar. O docker run --entrypoint comporta-se da mesma forma. Portanto, este é um comportamento do Docker, não uma particularidade do Compose.
A consequência é concreta. O nginx:1.27 declara ENTRYPOINT ["/docker-entrypoint.sh"] e CMD ["nginx", "-g", "daemon off;"]. Defina entrypoint: /custom-init.sh e o seu script será iniciado com uma lista de argumentos vazia. Um script que termina com o exec "$@" habitual não terá nada para executar. Assim, exec não faz nada, o script chega à última linha e o contentor termina com o código 0, sem qualquer mensagem de erro. Volte a definir os argumentos manualmente:
services:
web:
image: nginx:1.27
entrypoint: /custom-init.sh
command: ["nginx", "-g", "daemon off;"]A regra a reter é a seguinte: sempre que definir entrypoint:, decida no mesmo ajuste qual deve ser o command:.
Formas exec e shell e as diferenças no Compose
Um Dockerfile aceita duas sintaxes. CMD ["nginx", "-g", "daemon off;"] é a forma exec: o binário é executado diretamente, sem envolver um shell. CMD nginx -g "daemon off;" é a forma shell: o Docker a reescreve como /bin/sh -c 'nginx -g "daemon off;"', portanto um shell é executado primeiro e o seu programa torna-se um processo filho.
O Compose não segue essa regra, e isso surpreende algumas pessoas. Uma string em command: é dividida em argumentos e executada diretamente, sem um wrapper /bin/sh -c. A referência do Compose é explícita: o campo command não é executado no contexto SHELL definido na imagem. Se precisar de funcionalidades do shell, terá de invocar um shell manualmente.
É por isso que command: echo "hello $$HOSTNAME" imprime o texto literal hello $HOSTNAME. Nenhum shell recebeu a string, portanto nada a expandiu. Invoque um shell quando precisar dele:
services:
demo:
image: alpine:3.20
command: /bin/sh -c 'echo "hello $$HOSTNAME"'Sinais, PID 1 e um docker compose down limpo
docker compose stop e docker compose down enviam SIGTERM para o PID 1 dentro de cada contentor, aguardam stop_grace_period e depois enviam SIGKILL. O período de tolerância predefinido é de 10 segundos.
O PID 1 é especial no Linux. O kernel não aplica a ação predefinida de um sinal ao PID 1. Por isso, um processo que não instala um manipulador de SIGTERM simplesmente ignora SIGTERM quando é executado como PID 1. Permanece em execução durante todo o período de tolerância e depois é terminado à força. Isto interrompe qualquer ligação aberta ou transação não confirmada.
Um shell à frente do seu programa torna este problema mais provável, porque o shell é o PID 1 e a maioria dos shells não encaminha sinais para um processo filho. Alguns shells substituem-se pelo comando final numa cadeia -c. Por isso, por vezes o seu programa acaba por receber o PID 1. Isso depende do shell e da cadeia exata. Não presuma o comportamento. Leia a configuração:
docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echoSe o PID 1 aparecer como /bin/sh -c ... em vez do seu programa, existem duas soluções. Use a forma exec na imagem ou mantenha o shell e transfira o processo com exec:
services:
web:
image: myapp:1.4
command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'exec substitui o processo do shell pelo seu programa, em vez de criar um processo filho. Assim, o seu programa herda o PID 1 e recebe o sinal.
Alguns programas criam processos filhos e nunca os recolhem. Isto deixa processos zombie, porque o PID 1 também é o processo responsável por os recolher. O Compose tem uma opção para isso:
services:
web:
image: myapp:1.4
init: true
stop_grace_period: 30sinit: true executa um processo init pequeno como PID 1. Esse processo encaminha os sinais para o seu processo e recolhe os processos filhos. stop_grace_period dá mais tempo a um encerramento realmente lento. Se o seu programa esperar um sinal diferente, stop_signal: SIGQUIT altera o sinal enviado pelo Compose. Consulte o que uma imagem já solicita com docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27.
Uma stack em que docker compose down demora sempre dez segundos por serviço indica que nada está a tratar SIGTERM. Corrija isso antes de atribuir o problema às ferramentas e consulte a diferença entre docker compose down e stop para saber o que cada subcomando remove.
A mesma distinção entre exec e shell aparece noutro local. Um healthcheck escrito como test: ["CMD", "curl", "-f", "http://localhost/"] executa o binário diretamente, enquanto test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] é executado através de um shell para que || tenha significado. Como escrever healthchecks do Compose que falham corretamente explica o restante desse campo.
Acrescentar uma opção a uma imagem oficial
É isto que a maioria dos leitores procurava. Quer acrescentar uma opção a postgres, sem interferir no script de inicialização.
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:Apenas command: foi alterado, portanto docker-entrypoint.sh continua a ser executado e continua a executar o comando que forneceu. Verifique o resultado em vez de presumir que está correto:
docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'O resultado deve mostrar 200. Se ainda mostrar 100, execute docker compose config e confirme se o command esperado aparece na saída combinada. O Compose combina ficheiros de substituição substituindo command por completo, e não acrescentando conteúdo, portanto um segundo ficheiro que também defina command: substitui silenciosamente o valor anterior.
O ${POSTGRES_PASSWORD} acima é expandido pelo Compose no host a partir do seu ficheiro .env, antes de o contentor existir. Ficheiros de ambiente e segredos no Compose explica onde esse valor pode ser armazenado com segurança.
Executando uma migração pontual com docker compose run
docker compose run cria um novo contentor com base na mesma definição de serviço e substitui o comando pelo que escrever depois do nome do serviço. O entrypoint da imagem continua a ser executado, por isso o contentor é preparado exatamente como o contentor de longa duração.
docker compose run --rm app python manage.py migrate--rmelimina o contentor quando o comando termina. Sem esta opção, cada execução deixa um contentor parado, visível emdocker compose ps -a.- As portas não são publicadas. Um contentor
runignoraports:do serviço, a menos que adicione--service-ports, por isso não pode entrar em conflito com o serviço que já está em execução. - As dependências arrancam primeiro. Tudo o que estiver em
depends_oné iniciado antes do seu comando, e--no-depsignora esse comportamento. - O contentor recebe um nome gerado, como
myproject-app-run-9f2c1a, por isso nunca entra em conflito com o contentor do serviço.
Para substituir também o entrypoint, existe uma opção própria:
docker compose run --rm --entrypoint /bin/sh app -c 'python manage.py migrate'A lista de argumentos resultante é /bin/sh -c 'python manage.py migrate', porque as palavras depois do nome do serviço continuam a formar o comando. docker compose exec é a outra ferramenta e funciona de forma diferente: executa um processo dentro de um contentor que já está em execução e ignora totalmente entrypoint: e command:. Use run para uma tarefa que precise de um contentor novo e exec para inspecionar um contentor ativo. A folha de referência dos comandos do Compose apresenta os restantes subcomandos lado a lado.
Por que meu container sai imediatamente?
Comece pelo código de saída, pois ele restringe rapidamente a causa.
docker compose ps -a
docker compose logs appCódigo de saída 0 e nenhuma saída. O comando foi executado e terminou. A causa mais comum é uma substituição de entrypoint: que também removeu o CMD da imagem. Assim, o entrypoint foi executado com uma lista de argumentos vazia e não tinha nada para entregar.
Um erro que termina em permission denied. O script não tem o bit de execução dentro da imagem. Isso geralmente acontece porque o bit nunca foi definido no arquivo do repositório. Defina-o durante o build com COPY --chmod=0755 entrypoint.sh /entrypoint.sh.
Um erro que termina em no such file or directory para um arquivo que você consegue ver claramente na imagem. O script tem finais de linha do Windows. A primeira linha passa a ser lida como #!/bin/sh seguida de um byte de retorno de carro. Assim, o kernel procura um interpretador cujo nome contenha esse byte e não encontra nenhum. Execute dos2unix entrypoint.sh e depois adicione * text eol=lf a .gitattributes para evitar que o problema volte.
executable file not found in $PATH. O binário indicado em command: não está na imagem, ou você escreveu um comando interno do shell, como cd, onde só um programa real pode ser executado.
Obtendo um shell numa imagem cujo entrypoint falha
Quando o entrypoint termina antes de conseguir inspecionar alguma coisa, substitua-o:
docker compose run --rm --entrypoint /bin/sh appSe isso retornar executable file not found in $PATH, a imagem não tem shell. Imagens baseadas em Distroless e scratch normalmente não incluem um. Ainda pode ler o sistema de ficheiros a partir do exterior, sem iniciar o entrypoint:
docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probeQuando precisar de manter o contentor ativo para poder associar-se a ele várias vezes, mantenha-o num processo que nunca termina. Coloque isto num ficheiro de override que não deve ser incluído no commit:
services:
app:
entrypoint: ["tail", "-f", "/dev/null"]
command: []command: [] não é estritamente necessário, porque definir entrypoint: já removeu CMD da imagem, mas escrevê-lo regista a intenção para quem ler o ficheiro a seguir. Inicie-o e entre nele:
docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/shAgora execute manualmente o entrypoint real e monitorize onde ele para. Assim, a mensagem de erro aparece no seu terminal, em vez de surgir num contentor que terminou há meio segundo. Se ainda estiver a montar a sua primeira stack, uma primeira stack Compose numa VPS explica a organização de ficheiros pressuposta por tudo o que foi descrito acima.
FAQ
Por que o meu container termina imediatamente depois de docker compose up?
Consulte docker compose ps -a para obter o código de saída. A saída 0 sem qualquer mensagem normalmente significa que definiu entrypoint: no serviço. Isto também removeu CMD da imagem, por isso o entrypoint foi executado com uma lista de argumentos vazia e terminou. Adicione novamente os argumentos com command:. Um erro terminado em permission denied significa que o script do entrypoint não tem a permissão de execução. Um erro terminado em no such file or directory para um ficheiro que existe significa que o script tem terminações de linha do Windows. Nesse caso, a linha shebang indica um interpretador que não existe.
Definir entrypoint no Compose remove o CMD da imagem?
Sim. Se entrypoint não for nulo, o Compose ignora o comando predefinido declarado pela imagem. Esse comportamento está documentado e corresponde a docker run --entrypoint. A razão é que CMD de uma imagem é escrito como argumentos para ENTRYPOINT dessa imagem. Depois de substituir o entrypoint, os argumentos antigos deixam de ter algo a que se aplicar. Defina command: no mesmo serviço se o novo entrypoint ainda precisar de argumentos.
Uma string em command do Compose é executada através de uma shell?
Não. Ao contrário de CMD num Dockerfile, uma string em command: no Compose é dividida em argumentos e executada diretamente, sem um wrapper /bin/sh -c. Por isso, $VARIABLE nunca é expandido por uma shell dentro do container. Invoque a shell explicitamente quando precisar dela, como em command: /bin/sh -c 'echo "hello $$HOSTNAME"'. O $$ duplicado faz escape do cifrão, para que o Compose o transmita ao container em vez de o expandir no host.
Por que docker compose down demora dez segundos para um único container?
O Compose envia SIGTERM para o PID 1, espera stop_grace_period (10 segundos por predefinição) e depois envia SIGKILL. O kernel não aplica as ações predefinidas dos sinais ao PID 1. Por isso, um programa sem um handler SIGTERM ignora o sinal e espera sempre o período completo. Descubra qual é realmente o PID 1 com docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' '. Se for uma shell, altere a imagem para o formato exec ou escreva exec dentro da string da shell. Se o processo criar filhos que nunca recolhe, defina init: true no serviço.