Como usar o comando docker compose run para executar tarefas únicas

DockerBeginner
Pratique Agora

Introdução

Neste laboratório, você aprenderá como usar efetivamente o comando docker compose run para executar tarefas únicas dentro de seus serviços Docker Compose. Esta é uma técnica poderosa para executar comandos administrativos, depurar ou realizar operações específicas sem iniciar a pilha completa de serviços.

Exploraremos vários cenários, incluindo a substituição do comando de serviço padrão, a habilitação de portas de serviço para interação, o mapeamento manual de portas, a execução de comandos sem iniciar serviços vinculados e a remoção automática do contêiner após a execução. Ao final deste laboratório, você estará proficiente no uso de docker compose run para diversas tarefas únicas.

Executar um comando único substituindo o comando do serviço

Nesta etapa, aprenderemos como executar um comando único em um contêiner Docker, substituindo o comando padrão especificado na imagem Docker ou no Dockerfile. Isso é útil para executar tarefas administrativas, depurar ou executar um script específico dentro do ambiente do contêiner sem iniciar o serviço principal.

Primeiro, vamos baixar uma imagem Docker simples que podemos usar para esta demonstração. Usaremos a imagem ubuntu.

docker pull ubuntu:latest

Você deve ver a saída indicando que a imagem está sendo baixada.

Using default tag: latest
latest: Pulling from library/ubuntu
...
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest

Agora, vamos executar um comando único em um contêiner baseado na imagem ubuntu. Usaremos o comando docker run com o nome da imagem e o comando que queremos executar. Por exemplo, vamos executar o comando ls -l / para listar o conteúdo do diretório raiz no contêiner.

docker run ubuntu ls -l /

Este comando criará um novo contêiner a partir da imagem ubuntu, executará o comando ls -l / dentro dele e, em seguida, sairá. Você deve ver uma saída semelhante a esta, mostrando o conteúdo do diretório raiz:

total 68
drwxr-xr-x   2 root root  4096 Oct 26 00:00 bin
drwxr-xr-x   2 root root  4096 Oct 26 00:00 boot
drwxr-xr-x   5 root root   360 Nov  1 00:00 dev
drwxr-xr-x  19 root root  4096 Nov  1 00:00 etc
drwxr-xr-x   2 root root  4096 Oct 26 00:00 home
drwxr-xr-x   7 root root  4096 Oct 26 00:00 lib
drwxr-xr-x   2 root root  4096 Oct 26 00:00 lib64
drwxr-xr-x   2 root root  4096 Oct 26 00:00 media
drwxr-xr-x   2 root root  4096 Oct 26 00:00 mnt
drwxr-xr-x   2 root root  4096 Oct 26 00:00 opt
drwxr-xr-x   2 root root  4096 Oct 04 14:00 proc
drwx------   2 root root  4096 Oct 26 00:00 root
drwxr-xr-x   2 root root  4096 Oct 26 00:00 run
drwxr-xr-x   2 root root  4096 Oct 26 00:00 sbin
drwxr-xr-x   2 root root  4096 Oct 26 00:00 srv
drwxr-xr-x   2 root root  4096 Oct 26 00:00 sys
drwxrwxrwt   2 root root  4096 Oct 26 00:00 tmp
drwxr-xr-x  11 root root  4096 Oct 26 00:00 usr
drwxr-xr-x  12 root root  4096 Oct 26 00:00 var

Neste caso, o comando padrão da imagem ubuntu é tipicamente um shell como /bin/bash. Ao fornecer ls -l / após o nome da imagem, estamos dizendo ao Docker para executar este comando específico em vez do padrão.

Vamos tentar outro comando, por exemplo, pwd para imprimir o diretório de trabalho atual dentro do contêiner.

docker run ubuntu pwd

A saída deve ser /, indicando que o diretório raiz é o diretório de trabalho padrão.

/

Isso demonstra como você pode executar facilmente comandos arbitrários dentro de um contêiner sem precisar interagir interativamente com o contêiner ou modificar o comando padrão da imagem.

Executar um comando com as portas do serviço habilitadas

Nesta etapa, exploraremos como executar um comando em um contêiner Docker, enquanto ainda habilitamos as portas que o serviço dentro do contêiner está configurado para expor. Isso é útil quando você precisa executar um comando temporário para depuração ou administração, mas ainda deseja que o serviço principal seja acessível de fora do contêiner.

Para esta demonstração, usaremos uma imagem de servidor web simples. Vamos baixar a imagem nginx, que é um servidor web popular.

docker pull nginx:latest

Você deve ver a saída indicando que a imagem está sendo baixada.

Using default tag: latest
latest: Pulling from library/nginx
...
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest

A imagem nginx padrão está configurada para ouvir na porta 80 dentro do contêiner. Para tornar esta porta acessível a partir da nossa máquina host, precisamos mapear uma porta do host para a porta do contêiner usando a flag -p com o comando docker run. Vamos mapear a porta do host 8080 para a porta do contêiner 80.

Agora, em vez de executar o serviço Nginx padrão, vamos executar um comando simples como echo "Hello from the container" enquanto mantemos o mapeamento de porta habilitado.

docker run -p 8080:80 nginx echo "Hello from the container"

Você pode esperar que isso inicie o servidor Nginx e, em seguida, imprima "Hello from the container". No entanto, quando você fornece um comando após o nome da imagem em docker run, esse comando substitui o comando padrão. Portanto, neste caso, o contêiner executará o comando echo e, em seguida, sairá. O servidor Nginx não será iniciado, mesmo que tenhamos especificado o mapeamento de porta.

A saída será simplesmente:

Hello from the container

E se você tentar acessar http://localhost:8080 em seu navegador web ou usando curl, você descobrirá que a conexão foi recusada porque o servidor Nginx não está em execução.

curl http://localhost:8080

A saída provavelmente será:

curl: (7) Failed to connect to localhost port 8080 after ... connection refused

Isso ilustra um ponto importante: quando você substitui o comando padrão com docker run <image> <command>, o contêiner executará apenas o comando fornecido e não iniciará o serviço que a imagem foi projetada para executar. Portanto, o mapeamento de porta, embora configurado, não estará ativo porque o serviço que está ouvindo nessa porta não está em execução.

Para executar um comando enquanto o serviço está em execução e suas portas estão habilitadas, você normalmente iniciaria o serviço em segundo plano e, em seguida, executaria seu comando. No entanto, o comando docker run foi projetado para executar um único comando e, em seguida, sair. Para obter o efeito de executar um comando junto com um serviço em execução, você normalmente usaria docker exec em um contêiner que já está executando o serviço. Exploraremos docker exec em uma etapa posterior.

Para o propósito desta etapa, a principal conclusão é entender que fornecer um comando para docker run substitui o ponto de entrada/comando padrão e, portanto, o serviço configurado para ser executado por padrão não será iniciado, mesmo que os mapeamentos de porta sejam especificados.

Executar um comando com mapeamento manual de portas

Nesta etapa, continuaremos explorando o mapeamento de porta com docker run, focando em como especificar explicitamente as portas do host e do contêiner. Embora a etapa anterior tenha mostrado que a substituição do comando padrão impede que o serviço seja executado, entender o mapeamento manual de porta é crucial para quando você quiser que o serviço seja acessível.

Continuaremos usando a imagem nginx. Como lembrete, a imagem nginx expõe a porta 80 dentro do contêiner. Para tornar isso acessível a partir da nossa máquina host, usamos a flag -p seguida por host_port:container_port.

Vamos executar o contêiner nginx e mapear a porta do host 8081 para a porta do contêiner 80. Desta vez, não forneceremos um comando de substituição, então o serviço Nginx padrão será iniciado. Também o executaremos no modo detached (-d) para que ele seja executado em segundo plano.

docker run -d -p 8081:80 nginx

Você deve ver uma longa sequência de caracteres, que é o ID do contêiner, indicando que o contêiner foi iniciado no modo detached.

<container_id>

Agora que o contêiner está em execução e a porta está mapeada, você pode acessar a página de boas-vindas do Nginx da sua máquina host usando curl ou um navegador web.

curl http://localhost:8081

Você deve ver o conteúdo HTML da página de boas-vindas padrão do Nginx. Isso confirma que o servidor Nginx está em execução dentro do contêiner e é acessível a partir da sua máquina host via porta 8081.

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</body>
</html>

Isso demonstra como o mapeamento manual de porta permite que você controle qual porta do host é usada para acessar um serviço em execução em uma porta específica dentro de um contêiner. Isso é essencial para evitar conflitos de porta em sua máquina host e para tornar os serviços em contêiner acessíveis.

Para parar o contêiner em execução, você pode usar o comando docker stop seguido pelo ID ou nome do contêiner. Você pode encontrar o ID do contêiner executando docker ps.

docker ps

Isso mostrará uma lista de contêineres em execução. Encontre o ID do contêiner para o contêiner nginx que você acabou de iniciar.

CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                  NAMES
<container_id>   nginx     "nginx -g 'daemon off"   About a minute ago   Up About a minute   0.0.0.0:8081->80/tcp   <container_name>

Agora, pare o contêiner usando seu ID. Substitua <container_id> pelo ID real da sua saída.

docker stop <container_id>

O ID do contêiner será impresso novamente, confirmando que o contêiner foi parado.

<container_id>

Se você tentar acessar http://localhost:8081 novamente após parar o contêiner, a conexão será recusada.

Executar um comando sem iniciar serviços vinculados

Nesta etapa, aprenderemos como executar um comando em um contêiner Docker que faz parte de uma aplicação multi-contêiner, sem iniciar os outros serviços vinculados. Isso é particularmente útil para executar migrações de banco de dados, scripts de configuração ou comandos de depuração em um serviço sem a necessidade de levantar toda a pilha da aplicação.

Embora o Docker Compose seja a ferramenta padrão para gerenciar aplicações multi-contêiner e tenha recursos para executar comandos únicos em serviços específicos, demonstraremos os conceitos subjacentes do Docker aqui. Como o Docker Compose não está pré-instalado neste ambiente, focaremos em usar o comando docker run com rede.

Vamos simular um cenário simples com dois contêineres: uma aplicação web e um banco de dados. Usaremos uma imagem ubuntu genérica para representar nossa aplicação web e uma imagem postgres para o banco de dados.

Primeiro, baixe a imagem postgres:

docker pull postgres:latest

Você deve ver a saída indicando que a imagem está sendo baixada.

Using default tag: latest
latest: Pulling from library/postgres
...
Status: Downloaded newer image for postgres:latest
docker.io/library/postgres:latest

Agora, vamos criar uma rede Docker para que nossos contêineres possam se comunicar entre si por nome.

docker network create my-app-network

Você deve ver o ID da rede impresso.

<network_id>

Em seguida, vamos executar o contêiner postgres e conectá-lo à nossa rede. Também definiremos uma senha para o usuário PostgreSQL.

docker run -d --network my-app-network --name my-database -e POSTGRES_PASSWORD=mypassword postgres

Você deve ver o ID do contêiner impresso, indicando que o contêiner do banco de dados está em execução em segundo plano.

<container_id>

Agora, imagine que nosso contêiner de "aplicação web" precisa executar um comando que interaja com o banco de dados, como um script de migração de banco de dados. Normalmente, se estivéssemos usando o Docker Compose, poderíamos executar um comando no serviço web e o Docker Compose cuidaria da configuração da rede e da vinculação.

Usando apenas docker run, se executássemos o contêiner da aplicação web e ele tentasse se conectar a my-database, ele normalmente precisaria estar na mesma rede.

Vamos executar um comando em um contêiner ubuntu conectado à mesma rede, simulando um comando que pode interagir com o banco de dados. Apenas tentaremos pingar o contêiner do banco de dados por seu nome (my-database).

docker run --network my-app-network ubuntu ping -c 4 my-database

Este comando irá:

  1. Criar um novo contêiner a partir da imagem ubuntu.
  2. Conectá-lo ao my-app-network.
  3. Executar o comando ping -c 4 my-database dentro do contêiner.

Como o contêiner ubuntu está na mesma rede que o contêiner my-database, ele pode resolver o nome my-database para o endereço IP do contêiner do banco de dados e pingá-lo.

Você deve ver a saída mostrando as solicitações e respostas de ping:

PING my-database (172.18.0.2) 56(84) bytes of data.
64 bytes from my-database.my-app-network (172.18.0.2): icmp_seq=1 ttl=64 time=0.050 ms
64 bytes from my-database.my-app-network (172.18.0.2): icmp_seq=2 ttl=64 time=0.054 ms
64 bytes from my-database.my-app-network (172.18.0.2): icmp_seq=3 ttl=64 time=0.054 ms
64 bytes from my-database.my-app-network (172.18.0.2): icmp_seq=4 ttl=64 time=0.054 ms

--- my-database ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3060ms
rtt min/avg/max/mdev = 0.050/0.053/0.054/0.001 ms

Isso demonstra que você pode executar um comando único em um contêiner e fazê-lo interagir com outros contêineres na mesma rede, sem a necessidade de iniciar o serviço padrão do contêiner que executa o comando (neste caso, o contêiner ubuntu não tem um "serviço" típico). A chave é conectar o contêiner que executa o comando à mesma rede dos serviços com os quais ele precisa interagir.

Finalmente, vamos limpar o contêiner do banco de dados em execução e a rede.

docker stop my-database
my-database
docker rm my-database
my-database
docker network rm my-app-network
my-app-network

Executar um comando e remover o contêiner automaticamente

Nesta etapa, aprenderemos como executar um comando em um contêiner Docker e remover automaticamente o contêiner assim que o comando for concluído. Isso é muito útil para tarefas únicas, scripts ou jobs onde você não precisa que o contêiner persista após a conclusão da sua execução. Remover automaticamente os contêineres ajuda a manter seu sistema limpo e evita o acúmulo de contêineres parados.

Usaremos a imagem ubuntu novamente para esta demonstração. Executaremos um comando simples, como imprimir uma mensagem e, em seguida, sair. Para remover automaticamente o contêiner após a conclusão do comando, usamos a flag --rm com o comando docker run.

Vamos executar um contêiner com a flag --rm e executar o comando echo "This container will be removed automatically".

docker run --rm ubuntu echo "This container will be removed automatically"

Este comando irá:

  1. Criar um novo contêiner a partir da imagem ubuntu.
  2. Executar o comando echo "This container will be removed automatically" dentro do contêiner.
  3. Assim que o comando echo for concluído, o contêiner será removido automaticamente.

Você deve ver a saída do comando echo:

This container will be removed automatically

Após a conclusão do comando, o contêiner é parado e removido. Para verificar se o contêiner foi removido, você pode usar o comando docker ps -a, que lista todos os contêineres, incluindo os parados.

docker ps -a

Você não deve ver o contêiner que acabou de ser executado na lista. Se você executou o comando sem a flag --rm, o contêiner ainda apareceria na saída com o status "Exited".

Vamos tentar outro exemplo. Executaremos um comando que pausa por alguns segundos usando sleep e, em seguida, sai, novamente com a flag --rm.

docker run --rm ubuntu sh -c "echo 'Starting sleep...'; sleep 5; echo 'Sleep finished.'"

Este comando usa sh -c para executar um script simples que imprime mensagens antes e depois de dormir por 5 segundos.

Você verá a primeira mensagem imediatamente:

Starting sleep...

Em seguida, após cerca de 5 segundos, você verá a segunda mensagem:

Sleep finished.

Assim que o script for concluído, o contêiner será removido automaticamente. Você pode verificar isso novamente com docker ps -a.

docker ps -a

O contêiner que executou o comando sleep não deve estar na lista de contêineres.

Usar a flag --rm é uma boa prática para contêineres que são projetados para executar uma tarefa específica e, em seguida, sair, pois ajuda a gerenciar o espaço em disco e mantém sua lista de contêineres limpa.

Resumo

Neste laboratório, aprendemos como usar o comando docker compose run para executar tarefas únicas dentro de um serviço Docker Compose. Começamos entendendo como substituir o comando padrão definido na configuração de um serviço, permitindo-nos executar comandos arbitrários para fins administrativos ou de depuração.

Em seguida, exploramos como gerenciar a conectividade de rede para essas tarefas únicas, especificamente, habilitando as portas do serviço e mapeando portas manualmente. Por fim, aprendemos como controlar as dependências executando um comando sem iniciar os serviços vinculados e como remover automaticamente o contêiner após a conclusão do comando, garantindo um ambiente limpo.