Docker Compose
Docker Compose é o orquestrador de containers do Docker. Um orquestrador é uma ferramenta ou plataforma que gerencia e coordena a implantação, operação e escalabilidade de aplicativos distribuídos e contêineres em um ambiente de computação em nuvem ou em um ambiente de infraestrutura distribuída.
O Docker compose usa o arquivo docker-compose.yml para definir e gerenciar aplicativos Docker multi-contêineres. Este arquivo é escrito no formato YAML (YAML Ain't Markup Language), que é uma linguagem de serialização de dados legível por humanos e fácil de entender.
Dentro do arquivo docker-compose.yml, você pode definir vários aspectos do seu aplicativo, incluindo os serviços, redes, volumes e outras configurações necessárias.
É possível criar um serviço com o Compose apontando um arquivo Dockerfile.
Instalando o Docker Compose
A documentação oficial do Docker fornece cenários detalhados para a instalação do Docker Compose.
docker-compose-plugin
Esse pacote é recomendado se você já tem o Docker Engine e o Docker CLI instalados. O Docker Compose V2 é então instalado como um plugin integrado ao CLI do Docker. Após a instalação, os comandos são executados no formatodocker compose.Docker Compose standalone
É o método de instala do Docker Compose como um binário separado (independente do Docker CLI). Este é o método tradicional para usar o Docker Compose.
O Ubuntu possui três pacotes do Docker compose disponíveis:
docker-compose
É o Docker Compose V1 clássico, distribuído como um script independente em Python. Não é um plugin do Docker CLI, ou seja, precisa ser instalado e executado como um binário separado (o plugin se chamadocker-composeem vez dedocker compose- esse último é executado como um plugin). A versão 1 está obsoleta e foi substituída pelo Docker Compose V2.docker-compose-plugin - (Prefiro sempre usar esse método)
Já explicado acima, é o plugin oficial do Docker CLI para o Docker Compose V2. Por ser um plugin do Docker CLI, é integrado diretamente ao CLI do Docker. Em vez de rodardocker-compose, usamos o comandodocker compose. Pode coexistir com odocker-composeV1, mas é recomendável usar apenas um dos dois para evitar confusão.
Como já disse, esse pacote é recomendado se você já tem o Docker Engine e o Docker CLI instalados.docker-compose-v2
Esse é um binário separado do Docker Compose. Funciona como uma versão paralela, instalada separadamente e utilizada comodocker-compose(similar à V1, mas é V2). É útil em sistemas onde o plugin do Docker CLI (docker-compose-plugin) não está disponível ou não pode ser utilizado.
O meu Docker Compose está na versão 2.
docker compose <comando>
docker-compose <comando>
Para ver a versão do docker compose:
dpkg -l | grep compose
ii docker-compose-plugin 2.25.0-1~ubuntu.22.04~jammy amd64 Docker Compose (V2) plugin for the Docker CLI.
Docker Compose v1 e v2
A primeira versão do binário de CLI do Docker Compose era escrito em Python. O arquivo do Docker Compose V1 começava com o elemento version como sendo a primeira linha no arquivo docker-compose.yml, este campo especificava a versão do formato do arquivo Compose, que determinava como os serviços e configurações eram definidos. Os valores podiam variar entre 2.0 e 3.8, cada um associado a diferentes recursos e compatibilidades.
O Docker Compose V2, anunciado em 2020 e reescrito em Go, trouxe diversas melhorias em relação à versão anterior, incluindo a remoção da obrigatoriedade do campo version no arquivo docker-compose.yml. No Compose V2, você ainda pode especificar o campo version no arquivo YAML se desejar, mas ele não é mais obrigatório, pois o Compose agora infere automaticamente a versão do formato com base na estrutura e no conteúdo do arquivo. Isso simplifica a configuração, especialmente para novos projetos.
A seguir, exemplos básicos comparando a sintaxe entre o Compose V1 e V2. Docker Compose v2:
version: '3.8'
services:
apiweb:
build:
context: apiweb
dockerfile: dockerfile
restart: on-failure
ports:
- "80:80"
networks:
mynetwork:
mysql:
build:
context: mysql
dockerfile: dockerfile
restart: on-failure
environment:
MYSQL_ROOT_PASSWORD: examplepassword
MYSQL_DATABASE: nodedb
MYSQL_PASSWORD: mypassword
volumes:
- mysql_data:/var/lib/mysql
networks:
mynetwork:
networks:
mynetwork:
driver: bridge
volumes:
proj:
mysql_data:
Docker Compose v1:
version: '3.8'
services:
apiweb:
build:
context: ./apiweb
dockerfile: dockerfile
restart: on-failure
ports:
- "80:80"
networks:
- mynetwork
mysql:
build:
context: ./mysql
dockerfile: dockerfile
restart: on-failure
environment:
MYSQL_ROOT_PASSWORD: examplepassword
MYSQL_DATABASE: nodedb
MYSQL_PASSWORD: mypassword
volumes:
- mysql_data:/var/lib/mysql
networks:
- mynetwork
networks:
mynetwork:
driver: bridge
volumes:
proj:
mysql_data:
Parâmetros do Docker Compose
O Docker Compose tem poucos opções na CLI, já que a maior parte da configuração é feita diretamente no arquivo .yaml. No entanto, algumas opções disponíveis podem ser bastante úteis e merecem destaque. Abaixo estão alguns exemplos:
| Opção | Descrição |
|---|---|
| --dry-run | Executa o comando no modo de teste (sem execução real). |
| --env-file | Especifica um arquivo de ambiente alternativo. |
| -f, --file | Arquivos de configuração do Compose. |
| --progress | Define o tipo de saída de progresso (auto, tty, plain, quiet). |
| --project-directory | Especifica um diretório de trabalho alternativo. |
| -p, --project-name | Nome do projeto. |
Tambem existem comandos que podemos executar com o docker compose, vou deixar os mais comuns:
| Comando | Descrição |
|---|---|
| docker compose build | Construir ou reconstruir serviços. |
| docker compose config | Analisar, resolver e renderizar o arquivo compose no formato canônico. |
| docker compose cp | Copiar arquivos/pastas entre um contêiner de serviço e o sistema de arquivos local. |
| docker compose create | Cria contêineres para um serviço. |
| docker compose up | Criar e iniciar contêineres. |
| docker compose down | Parar e remover contêineres, redes. |
| docker compose events | Receber eventos em tempo real de contêineres. |
| docker compose exec | Executar um comando em um contêiner em execução. |
| docker compose images | Listar imagens usadas pelos contêineres criados. |
| docker compose kill | Parar forçadamente os contêineres de serviço. |
| docker compose logs | Visualizar saída dos contêineres. |
| docker compose ls | Listar projetos compose em execução. |
| docker compose port | Imprimir a porta pública para um bind de porta. |
| docker compose ps | Listar contêineres. |
| docker compose pull | Puxar imagens de serviço. |
| docker compose push | Enviar imagens de serviço. |
| docker compose restart | Reiniciar contêineres de serviço. |
| docker compose rm | Remover contêineres de serviço parados. |
| docker compose run | Executar um comando único em um serviço. |
| docker compose start | Iniciar serviços. |
| docker compose stop | Parar serviços. |
| docker compose top | Exibir os processos em execução. |
| docker compose pause | Pausar serviços. |
| docker compose unpause | Despausar serviços. |
| docker compose version | Mostrar informações da versão do Docker Compose. |
| docker compose wait | Bloquear até que o primeiro contêiner de serviço pare. |
| docker compose scale | Permite aumentar o número de réplicas de um serviço específico. |
| docker compose -f path/file.yaml ACTION | Permite especificar um arquivo de configuração personalizado para o Docker Compose. Este parâmetro é seguido pelo caminho para o arquivo de configuração. As ações foram descritas nas opções acima. |
Fazendo build de imagens no Compose
No arquivo docker-compose.yml, nós podemos especificar que um serviço deve ser construído a partir de um Dockerfile, para isso, temos que usar a opção build, que deve indicar o caminho do Dockerfile e o contexto de build (o diretório base para o processo de construção). Abaixo podemos ver um exemplo:
services:
app:
build:
context: .
dockerfile: Dockerfile
O Docker Compose só construirá uma nova imagem nas seguintes situações:
- Alterações no Dockerfile: Se o arquivo Dockerfile especificado (ex.:
dockerfile: Dockerfile) for modificado. - Alterações no contexto de build: Se qualquer arquivo ou diretório dentro do caminho especificado no
contextsofrer alteração. - Se você forçar a reconstrução: Usando o comando
docker-compose build(para (re)criar a imagem) oudocker-compose up --build(para (re)criar e já efetuar o deploy).
O Docker Compose, ao realizar o build de imagens, se beneficia do mesmo mecanismo de camadas do Docker para gerenciar o cache. O Docker Compose rastreia alterações nos arquivos dentro do contexto especificado. Se algum arquivo no contexto mudar, o Compose considera que o cache pode não ser válido e repassa essas alterações ao mecanismo do Docker.
Se o Dockerfile ou os comandos contidos nele forem alterados, o Compose também considera que uma nova imagem precisa ser construída. O Docker Compose calcula hashes ou checksums baseados nos arquivos do contexto e no Dockerfile para determinar se algo mudou. Isso é mais um nível de rastreamento gerenciado pelo Compose antes de passar para o Docker.
Seções do docker-compose.yml
Abaixo podemos ver algumas das principais seções e funcionalidades que são definidas dentro do arquivo docker-compose.yml, essas são as seções mais comuns encontradas em uso com o Docker Compose.
| Campo | Descrição |
|---|---|
| version | Esta é a primeira linha do arquivo docker-compose.yml e define a versão do arquivo docker-compose.yml que o Docker Compose está usando (o padrão é 3.8). Você pode ver as opções que cada versão fornece aqui. |
| services | É onde definimos os diferentes serviços que compõem o aplicativo Docker. Cada serviço é um contêiner Docker que executa uma parte específica do seu aplicativo. O bloco services é um dos blocos mais importantes e frequentemente utilizados no Docker Compose. |
| volumes | Esta seção é usada para definir volumes que podem ser usados por contêineres para persistir dados ou compartilhar dados entre contêineres. |
| networks | Esta seção é usada para definir redes personalizadas para os contêineres do seu aplicativo. |
includeA diretiva include no Docker Compose permite que você reutilize outros arquivos de composição do Docker ou fatorize partes do modelo do seu aplicativo em arquivos separados. Isso é útil em situações em que seu aplicativo Docker Compose depende de configurações ou serviços definidos em outros arquivos de composição ou se você deseja dividir seu arquivo de composição em partes menores e mais gerenciáveis.
Exemplo:
version: '3.8'
services:
frontend:
# Definição do serviço frontend
networks:
# Definição de redes
include:
- services.yml
- networks.yml
Nome de Projeto
No Compose, o nome padrão do projeto é derivado do nome base do diretório do projeto, ou seja, se o diretório se chama pye1, o nome do projeto será pye1. No entanto, você tem flexibilidade para definir um nome de projeto personalizado. Para ver como definir, veja aqui.
Uma das formas mais simples é definir parâmetro COMPOSE_PROJECT_NAME no docker-compose.yml. Esse nome é importante porque é utilizado como um prefixo para os nomes de contêineres, redes, volumes e outros recursos gerados pelo Docker Compose. Você pode definir o nome do projeto no arquivo .env, que deve ficar no mesmo diretório do arquivo docker-compose.yml.
Você também pode especificar o nome do projeto diretamente ao executar um comando Docker Compose, usando a flag --project-name ou apenas -p:
docker compose --project-name meu_projeto up
Também podemos definir o nome do projeto diretamente no arquivo docker-compose.yml usando o campo name no topo do arquivo.
name: meu_projeto_personalizado
services:
web:
image: nginx
ports:
- "80:80"
Services
Dentro do bloco services, você lista os serviços que deseja definir, cada um com suas próprias configurações. Cada serviço é identificado por um nome único e pode ter várias configurações associadas a ele. Algumas das configurações comuns que você pode definir para cada serviço incluem:
image
A imagem Docker a ser usada para criar o contêiner do serviço.ports
As portas TCP ou UDP que o contêiner do serviço expõe para permitir a comunicação com outros contêineres ou serviços. Nesse campo, temosportaHost:portaContainer.environment
Variáveis de ambiente que serão passadas para o contêiner do serviço durante a execução. Muitos serviços podem deixar variáveis vazias para que sejam definidos os valores externos. É possível utilizarenv_fileao invés de environment. Oenv_filepode apontar pra um arquivo .env qualquer dentro do host.volumes
Diretórios ou pontos de montagem que são montados dentro do contêiner do serviço. Esses volumes são usados para persistir dados ou compartilhar dados entre contêineres. Eles são declarados assim:- /path/on/host:/path/in/container:ro.networks
Atribuição do serviço a uma ou mais redes definidas no arquivodocker-compose.yml.config
As configurações (configs) são montadas como arquivos no sistema de arquivos do contêiner de um serviço. O local do ponto de montagem dentro do contêiner é padronizado como/<config-name>em contêineres Linux eC:\<config-name>em contêineres Windows.
secret
Osecretsé uma maneira de fornecer dados sensíveis, como senhas, chaves SSH ou certificados TLS, de forma segura aos serviços em contêineres sem expô-los diretamente no arquivo de configuração do Compose ou no Dockerfile. Isso é especialmente útil quando você precisa compartilhar informações confidenciais entre vários serviços ou contêineres.CMD, entrypoint
No Docker, os comandosCMDeENTRYPOINTsão usados para definir qual comando será executado quando um contêiner for iniciado.Qualquer imagem Docker deve ter uma declaração
ENTRYPOINTouCMDpara que um contêiner seja iniciado. Embora as instruçõesENTRYPOINTeCMDpossam parecer semelhantes à primeira vista, existem diferenças fundamentais na forma como elas constroem imagens de contêiner.O
CMDdefine o comando padrão a ser executado quando o contêiner é iniciado. É possível substituir esses parâmetros na CLI do Docker durante a execução do contêiner.O
ENTRYPOINTdefine o comando principal a ser executado quando o contêiner é iniciado.command
A opçãocommanddentro de um serviço permite especificar qual comando deve ser executado quando o contêiner associado a esse serviço for iniciado. Ela tem o mesmo efeito de sobrescrever o CMD definido na imagem, sem alterar o ENTRYPOINT.user
A opçãouserdefine qual usuário (UID e GID) o processo principal do contêiner deve usar ao ser iniciado. Ela substitui o usuário padrão definido na imagem (viaUSERno Dockerfile). O usuário não precisa existir na imagem se você informar o formatoUID:GID. No entanto, se você utilizar um nome de usuário, esse nome deve existir dentro da imagem.depends_on
Definição de dependências, significa que uma aplicação só poderia ser iniciada depois que outra aplicação estiver UP.restart
É usado para especificar o comportamento de reinicialização do contêiner associado a esse serviço em caso de falha ou reinicialização do Docker. Existem várias opções que você pode usar com a diretivarestartpara definir o comportamento de reinicialização do contêiner:no:
Esta é a opção padrão e significa que o contêiner não será reiniciado automaticamente em caso de falha.always
Esta opção garante que o contêiner seja sempre reiniciado automaticamente, independentemente do motivo da falha.on-failure
Esta opção especifica que o contêiner será reiniciado automaticamente apenas se ele sair com um código de erro não zero.unless-stopped
Esta opção garante que o contêiner seja reiniciado automaticamente sempre que ele sair, a menos que seja explicitamente parado pelo usuário.
Opção Config
Para que os serviços consigam acessar as configurações, é necessário colocar explicitamente um atributo configs dentro do elemento de nível superior de services. Podemos trabalhar com configurações locais, onde a configação fica no compose.yaml ou configurações externas, onde as configurações ficam num arquivo externo.
Quando é definido uma configuração externa usando o atributo external: true, isso indica ao sistema que a configuração já existe em algum lugar fora do docker-compose.yml. O Docker Compose não é responsável por criar ou gerenciar essas configurações externas, apenas as utiliza.
Se a configuração não for declarada como external: true, isso significa que a configuração está dentro do arquivo docker-compose.yml.
A configuração pertence ao usuário que executa o comando do contêiner mas pode ser substituída pela configuração do serviço. Possui permissões legíveis por todos (modo 0444), a menos que o serviço esteja configurado para substituir isso.
A declaração de configs de nível superior define ou faz referência a dados de configuração concedidos aos serviços em sua aplicação Compose. A origem da configuração é file ou external.
file: A configuração é criada com o conteúdo do arquivo no caminho especificado.
environment: O conteúdo da configuração é criado com o valor de uma variável de ambiente.
content: O conteúdo é criado com o valor inline.
external: Se definido como
true, especifica que esta configuração já foi criada. O Compose não tenta criá-la e, se não existir, ocorrerá um erro. Quandoexternalé definido comotrue, todos os outros atributos além donameserão irrelevantes.name: O nome do objeto de configuração no mecanismo de contêineres para procurar. Este campo pode ser usado para referenciar configurações que contenham caracteres especiais. O nome é utilizado como está e não será escopo com o nome do projeto.
O <project_name>_http_config é criado quando o aplicativo é implementado, registrando o conteúdo do httpd.conf como os dados de configuração. Isso de certa forma faz referencia a uma configuração externa.
configs:
http_config:
file: ./httpd.conf
Alternativamente, http_config pode ser declarado como externo. O Compose procura http_config para expor os dados de configuração aos serviços relevantes.
configs:
http_config:
external: true
O <project_name>_app_config é criado quando o aplicativo é implementado, registrando o conteúdo embutido como dados de configuração. Isso significa que o Compose infere variáveis ao criar a configuração, o que permite ajustar o conteúdo de acordo com a configuração do serviço:
configs:
app_config:
content: |
debug=${DEBUG}
spring.application.admin.enabled=${DEBUG}
spring.application.name=${COMPOSE_PROJECT_NAME}
Exemplo completo:
services:
frontend:
image: example/webapp
ports:
- "443:8043"
configs:
- my-config
configs:
my-config:
file: ./path/to/config/file.conf
Opção Secret
O secrets é uma variante do configmas o foco são os dados confidenciais, com restrições específicas para esse uso. Os serviços só podem acessar segredos quando explicitamente concedidos por um atributo secrets dentro do elemento de nível superior services. Em outras palabras, secret é qualquer dado, como uma senha, certificado ou chave de API, que não deve ser transmitido por uma rede ou armazenado sem criptografia em um Dockerfile ou no código-fonte do seu aplicativo.
A declaração secret de nível superior define ou faz referência a dados sensíveis concedidos aos serviços em sua aplicação Compose. A origem do segredo é um arquivo ou ambiente.
- file: O
secretsé criado com o conteúdo do arquivo no caminho especificado. - external: É um secret já existente (criado previamente fora do Compose), tipicamente no contexto do Docker Swarm ou via comando
docker secret create. Geralmente só funciona em contexto de Swarm/docker stack deploy. Se você estiver rodando o Compose normal (sem Swarm), pode aparecer um aviso como "External secrets are not available to containers created by docker-compose".
O secret chamado server-certificate abaixo é criado como <project_name>_server-certificate quando o aplicativo é implementado, registrando o conteúdo do server.cert como um segredo de plataforma.
secrets:
server-certificate:
file: ./server.cert
O secret chamado token abaixo é criado como <project_name>_token quando o aplicativo é implementado, registrando o conteúdo da variável de ambiente OAUTH_TOKEN como um segredo de plataforma.
secrets:
token:
external:
name: nome_existente
Exemplo completo:
services:
myapp:
image: myapp:latest
secrets:
- my_secret
secrets:
my_secret:
file: ./my_secret.txt
Para mais detalhes de Secrets, vejam os links abaixo:
https://docs.docker.com/compose/use-secrets/
https://docs.docker.com/compose/compose-file/05-services/#secrets
Opção Deploy
A opção deploy no Docker Compose é usada para configurar políticas de implantação e orquestração de serviços quando você está utilizando ferramentas de orquestração, como o Docker Swarm. Tradicionalmente, essa seção era ignorada quando usávamos docker compose up sem Swarm, sendo válida apenas em clusters com Swarm via docker stack deploy.
No entanto, nas versões mais recentes do Compose (v2, spec unificada), algumas das configurações de deploy já são funcionais mesmo sem Swarm, como o número de réplicas. Por isso, parte dessas opções já "funcionam localmente".
Essa seção permite configurar comportamentos avançados, como réplicas, limites de recursos, estratégias de atualização e regras de colocação de serviços no cluster. Exemplo Básico:
services:
web:
image: nginx
deploy:
replicas: 3
resources:
limits:
cpus: "0.5"
memory: "512M"
reservations:
cpus: "0.25"
memory: "256M"
restart_policy:
condition: on-failure
update_config:
parallelism: 2
delay: 10s
placement:
constraints:
- node.role == manager
replicas
Define quantas instâncias do serviço devem existir no cluster. Agora pode funcionar localmente também (dependendo da versão do Compose). Internamente, as aplicações e os contêineres se veem por nomes DNS e IPs dentro da rede. O Docker (tipo engine) pode fazer DNS Round-Robin para nomes de serviço, o que pode distribuir solicitações internas.resources
Define limites e reservas de recursos para o serviço.- limits: Especifica o máximo de CPU e memória que o serviço pode usar.
- reservations: Garante a alocação mínima de CPU e memória para o serviço.
restart_policy
Configura a política de reinício para os contêineres. Condições incluem:none,on-failureouany.update_config
Define como os serviços devem ser atualizados.- parallelism: Número de réplicas que podem ser atualizadas simultaneamente.
- delay: Tempo de espera entre as atualizações de réplicas.
placement
Especifica restrições de onde o serviço pode ser executado. Um exemplo énode.role == manager, isso garante que o serviço seja executado apenas nos nós do tipo manager.
Network
Existem dois elementos para network, um de nível superior que permite configurar redes nomeadas que podem ser reutilizadas em vários serviços. Já o segundo elemento, deve ser usado em conjunto como atributo e deve ser colocado no elemento de nível superior services.
Por padrão, o Compose configura uma única rede para seu aplicativo. Cada contêiner de um serviço ingressa na rede padrão e pode ser acessado por outros contêineres nessa rede e descoberto pelo nome do serviço.
Em vez de especificar suas próprias redes, você também pode alterar as configurações da rede padrão de todo o aplicativo definindo uma entrada em redes chamada padrão. O exemplo abaixo mostra como você pode alterar as configurações da rede padrão, no caso, mudando o driver a ser usado.
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
db:
image: postgres
networks:
default:
# Use a custom driver
driver: custom-driver-1
Veja o exemplo abaixo:
version: '3.8'
services:
frontend:
image: example/webapp
networks:
- front-tier
- back-tier
networks:
front-tier:
back-tier:
Inicialmente o frontend está conectao em duas redes: front-tier e back-tier. Caso essas redes existam, as configurações de cada rede será aplicada. Mas se você não criou explicitamente as redes back-tier e front-tier antes de execupar o compose, cada uma das redes usarão a rede padrão do Docker Compose para se comunicar. Essa rede padrão é criada automaticamente pelo Docker Compose para cada projeto e é chamada de default.
Dentro do elementos network no nível superior, podemos configurar alguns atributos, a tabela abaixo mostra quais são:
| Atributo | Explicação |
|---|---|
| driver | Especifica qual driver deve ser usado para essa rede. O Docker Compose retorna um erro se o driver não estiver disponível na plataforma. |
| driver_opts | Especifica uma lista de opções como pares chave-valor para passar para o driver. Essas opções dependem do driver. Consulte a documentação do driver para obter mais informações. |
| attachable | Se definido como verdadeiro, contêineres independentes devem poder se conectar a essa rede, além dos serviços. Se um contêiner independente se conectar à rede, ele poderá se comunicar com serviços e outros contêineres independentes que também estão conectados à rede. |
| enable_ipv6 | Habilita a rede IPv6. |
| external | Se definido como verdadeiro: especifica que o ciclo de vida desta rede é mantido fora do aplicativo. Qualquer outro valor é irrelevante. |
| ipam | Especifica uma configuração IPAM personalizada. Para ver como configurar uma rede IPAM e quais opções estão disponíveis, acesse aqui. |
| internal | Por padrão, o Compose fornece conectividade externa às redes. internal: true permite criar uma rede isolada externamente. |
| labels | Adiciona metadados aos contêineres usando rótulos. É recomendável usar a notação de DNS reverso para evitar conflitos com rótulos usados por outros softwares. |
| name | Define um nome personalizado para a rede. |
IPAM (IP Address Management) refere-se ao processo de gerenciamento de endereços IP dentro de uma rede. Isso inclui a atribuição de endereços IP para contêineres, a configuração de sub-redes e faixas de endereços IP disponíveis, bem como a configuração de gateways e outros parâmetros de rede.
Em
networkdentro deservicespodemos sspecificar um endereço IP estático para um contêiner de serviço usar quando ele ingressar na rede. Para essa configuração , veja aqui.
Exemplo de docker compose
Aqui está um exemplo simples de um arquivo docker-compose.yml que usa todas as opções descritas anteriormente:
version: '3.8'
services:
meu-servico:
image: minha-imagem
ports:
- "8080:80"
environment:
- MYSQL_ROOT_PASSWORD=minhaSenha
volumes:
- meu-volume:/app/data
networks:
- minha-rede
restart: always
networks:
minha-rede:
volumes:
meu-volume:
- O serviço
meu-servicoé definido para usar a imagemminha-imagem. - Ele expõe a porta
80do contêiner para a porta8080do host. - Define a variável de ambiente
MYSQL_ROOT_PASSWORDparaminhaSenha. - Monta um volume chamado
meu-volumeno caminho/app/datadentro do contêiner. - É atribuído à rede personalizada
minha-rede. - A opção
restarté configurada comoalways, o que garante que o contêiner seja sempre reiniciado automaticamente em caso de falha.Isso é apenas um exemplo básico e pode ser expandido conforme necessário para atender às necessidades específicas do seu aplicativo Docker.
Docker Compose na prática
Vamos criar uma aplicação usando o Docker Compose. A aplicação deve ter um banco de dados, uma API (Back-end) e uma aplicação web (Front-end). Ambas devem conversar entre si e utilizar a mesma rede criada pelo arquivo docker-compose.yml.
A configuração pode estar separada em Dockerfiles ou descritas totalmente no docker-compose.yml.
version: '3.8'
services:
apiweb:
image: node:21-alpine
restart: on-failure
ports:
- "80:80"
volumes:
- ./proj:/app/data
networks:
mynetwork:
command: node /app/data/index.js
mysql:
image: mysql:8.1.0
restart: on-failure
environment:
MYSQL_ROOT_PASSWORD: examplepassword
MYSQL_DATABASE: nodedb # Cria o banco de dados
MYSQL_PASSWORD: mypassword
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
mynetwork:
networks:
mynetwork:
driver: bridge
volumes:
proj:
mysql_data:
Usando o container do mysql como exemplo, o docker hub do Mysql possui todos os dados para subir o container do mysql, incluindo as variáveis de ambientes que podemos usar. É fundamental que seja lido a página da documentação do container que está usando.
No começo da página ainda é exibido um link para o dockerfile, dessa forma, podemos ver como a imagem foi criada.
Abaixo segue uma descrição de cada opção usada no arquivo docker-compose.yml:
services:
- Esta é uma seção onde você define os diferentes serviços que compõem sua aplicação Docker.
apiweb:
- Este é o nome do serviço.
image: Define a imagem a ser usada para criar este contêiner. Neste caso, énode:21-alpine.restart: Define a política de reinicialização do contêiner em caso de falha.ports: Mapeia as portas do contêiner para o host. Neste caso, mapeia a porta 80 do contêiner para a porta 80 do host.volumes: Define os volumes a serem montados no contêiner. Aqui, o diretório./projdo host é montado em/app/datado contêiner.networks: Especifica a rede à qual este serviço pertence.command: Define o comando a ser executado quando o contêiner é iniciado. Neste caso, énode /app/data/index.js. Sem isso o container vai morrer.
mysql:
- Outro serviço chamado
mysql. image: Define a imagem a ser usada para criar este contêiner. Aqui, émysql:8.1.0.restart: Define a política de reinicialização do contêiner em caso de falha.environment: Define as variáveis de ambiente necessárias para o contêiner MySQL.volumes: Define os volumes a serem montados no contêiner. Aqui, um volume chamadomysql_dataé montado em/var/lib/mysqle um arquivoinit.sqldo host é montado em/docker-entrypoint-initdb.d/init.sql.networks: Especifica a rede à qual este serviço pertence.
- Outro serviço chamado
networks:
- Esta seção define as redes personalizadas para os contêineres.
mynetwork: Nome da rede definida.driver: Define o driver de rede a ser usado para esta rede. Aqui, ébridge, que é um driver padrão do Docker.
volumes:
- Esta seção define os volumes que podem ser usados pelos contêineres.
proj: Define um volume nomeado.mysql_data: Define outro volume nomeado.
No diretório proj, tenho os códigos em NodeJS que subirá a aplicação, não cabe nesse momento fornecer os códigos. Um outro detalhe é que é usado um script SQL para criar a tabela e fazer um input na tabela. Poderia deixar tudo dentro do código Node mas como um exemplo simples, achei melhor deixar fora.
Abaixo segue o script sql usado.
CREATE TABLE IF NOT EXISTS nodedb (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
INSERT INTO nodedb (name) VALUES ('FULANO');
Melhorando o Docker Compose
Agora vamos melhorar isso, já que não é criado uma imagem Docker, e sim apenas o container usando uma imagem base. Vamos melhorar o docker compose, usar o dockerfile para criar a imagem e o docker compose vai usar essas imagens para criar o container. Depois de criadas, vamos rodar a ferramenta Trivy para analisar e detalhar o que cada imagem gerou de vulnerabilidade.
Crie o dockerfile para o serviço chamado apiweb.
FROM node:21-alpine
COPY ./proj /app/data
WORKDIR /app/data
ENTRYPOINT ["node", "/app/data/index.js"]
Crie o dockerfile para o serviço chamado mysql.
FROM mysql:8.1.0
COPY ./init.sql /docker-entrypoint-initdb.d/init.sql
Agora modifique o docker-compose para fazer o build do dockerfile e criar o container.
version: '3.8'
services:
apiweb:
build:
context: apiweb
dockerfile: dockerfile
restart: on-failure
ports:
- "80:80"
networks:
mynetwork:
mysql:
build:
context: mysql
dockerfile: dockerfile
restart: on-failure
environment:
MYSQL_ROOT_PASSWORD: examplepassword
MYSQL_DATABASE: nodedb
MYSQL_PASSWORD: mypassword
volumes:
- mysql_data:/var/lib/mysql
networks:
mynetwork:
networks:
mynetwork:
driver: bridge
A estrutura com os diretórios ficaram assim:
cmd: tree -L 2
.
├── apiweb
│ ├── dockerfile
│ ├── package-lock.json
│ └── proj
├── docker-compose.yaml
├── mysql
│ ├── dockerfile
│ └── init.sql
├── mysql_data
Agora vamos executar:
docker compose up -d
[+] Running 2/3
⠹ Network teste_mynetwork Created 0.3s
✔ Container teste-apiweb-1 Started 0.2s
✔ Container teste-mysql-1 Started
Acesse o browser via curl para verificar se está funcionando:
curl -sk http://192.168.121.122/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sequelize</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<h1>Usuários cadastrados</h1>
<div class="container">
<div class="item">
<p>FULANO</p>
</div>
</div>
</body>
</html>
Agora vamos verificar se existem vulnerabilidades:
# Verificando vulnerabilidades na imagem 'apiweb':
trivy image --severity CRITICAL,HIGH teste-apiweb
2024-03-31T17:16:53.703Z INFO Vulnerability scanning is enabled
2024-03-31T17:16:53.704Z INFO Secret scanning is enabled
2024-03-31T17:16:53.704Z INFO If your scanning is slow, please try '--scanners vuln' to disable secret scanning
2024-03-31T17:16:53.704Z INFO Please see also https://aquasecurity.github.io/trivy/v0.50/docs/scanner/secret/#recommendation for faster secret detection
2024-03-31T17:16:53.712Z INFO Detected OS: alpine
2024-03-31T17:16:53.712Z INFO Detecting Alpine vulnerabilities...
2024-03-31T17:16:53.713Z INFO Number of language-specific files: 1
2024-03-31T17:16:53.713Z INFO Detecting node-pkg vulnerabilities...
teste-apiweb (alpine 3.19.1)
Total: 0 (HIGH: 0, CRITICAL: 0)
# Verificando vulnerabilidades na imagem 'mysql':
trivy image --severity CRITICAL,HIGH teste-mysql
2024-03-31T17:17:33.473Z INFO Vulnerability scanning is enabled
2024-03-31T17:17:33.473Z INFO Secret scanning is enabled
2024-03-31T17:17:33.473Z INFO If your scanning is slow, please try '--scanners vuln' to disable secret scanning
2024-03-31T17:17:33.473Z INFO Please see also https://aquasecurity.github.io/trivy/v0.50/docs/scanner/secret/#recommendation for faster secret detection
2024-03-31T17:17:33.521Z INFO Detected OS: oracle
2024-03-31T17:17:33.521Z INFO Detecting Oracle Linux vulnerabilities...
2024-03-31T17:17:33.523Z INFO Number of language-specific files: 2
2024-03-31T17:17:33.523Z INFO Detecting gobinary vulnerabilities...
2024-03-31T17:17:33.523Z INFO Detecting python-pkg vulnerabilities...
teste-mysql (oracle 8.8)
Total: 3 (HIGH: 3, CRITICAL: 0)
# Obtendo apenas os CVEs do mysql:
trivy image --severity CRITICAL,HIGH teste-mysql | grep -Eio 'cve-[0-9]{4}-[0-9]{5}' | sort | uniq
CVE-2019-19921
cve-2021-40528
CVE-2021-40528
cve-2023-27561
CVE-2023-27561
cve-2023-37920
CVE-2023-37920
cve-2023-40217
CVE-2023-40217
cve-2023-50782
CVE-2023-50782
cve-2024-21626
CVE-2024-21626
cve-2024-26130
CVE-2024-26130
Exemplo de Docker compose para MkDocs
services:
mkdocs:
build:
context: . # Diretório onde o Dockerfile está localizado
dockerfile: mkdocs-dockerfile # Nome do Dockerfile que você está usando
restart: on-failure
container_name: mkdocs
volumes:
# - ./mkdocs:/app/data # Usado para criar o projeto!!
- ./mkdocs/courses:/app/data # usado quando o projeto já existe!!
working_dir: /app/data
user: "1000:1000"
expose:
- "8000"
ports:
- 8000:8000
command: >
sh -c 'pip install Pygments --break-system-packages && mkdocs serve -a 0.0.0.0:8000' # usado quando o projeto já existe!!
#sh -c 'mkdocs new courses;' # Usado para criar o projeto!!
FROM alpine:latest
# Instalar shadow (necessário para adduser)
RUN apk add --no-cache shadow python3 py3-pip
# Instalar mkdocs-material
RUN pip install mkdocs-material --break-system-packages
# Criar um usuário não-root com UID 1000
RUN adduser -D -u 1000 -g 1000 -s /bin/sh -h /app/data mkdocsuser
# Definir permissões para o novo usuário no diretório
RUN chown -R mkdocsuser:mkdocsuser /app/data
RUN chmod 770 -R /app/data
# Definir o diretório de trabalho
WORKDIR /app/data
# Mudar o usuário padrão para o não-root criado
USER mkdocsuser
# Expor a porta 8000 para acesso ao MkDocs
EXPOSE 8000
# Rodar pela primeira vez para criar o projeto:
#CMD ["mkdocs", "new", "courses"]
wait-for-it.sh
O script wait-for-it.sh é uma ferramenta útil para garantir que um serviço dependente (como um banco de dados) esteja pronto antes de iniciar outro serviço, especialmente em ambientes Docker com várias dependências. O wait-for-it.sh é um script bash que verifica se um serviço está acessível em uma porta TCP específica antes de prosseguir. Ele é recomendado na documentação oficial do Docker para lidar com dependências de inicialização.
Se você não usar o wait-for-it.sh ou uma abordagem semelhante para garantir que um serviço dependente, como um banco de dados, esteja pronto antes de o serviço principal ser iniciado, poderá enfrentar erros de conexão. Isso ocorre porque, embora o contêiner do banco de dados esteja em execução (UP), a aplicação dentro dele pode ainda não estar totalmente inicializada e pronta para receber conexões.
O que o script wait-for-it.sh faz é simples, mas crucial: ele aguarda até que o serviço dentro do contêiner (como o banco de dados) esteja funcional, garantindo que o serviço principal só seja iniciado quando a aplicação dependente estiver pronta para se comunicar. Isso evita erros e falhas relacionadas à tentativa de conexão prematura. Para baixá-lo:
wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh
chmod +x wait-for-it.sh
Usando ele com Alpine para testar o banco de dados:
FROM alpine:latest
COPY ./wait-for-it.sh /usr/local/bin/wait-for-it.sh
RUN chmod +x /usr/local/bin/wait-for-it.sh
Agora no compose faça:
version: '3.8'
services:
alpine:
build:
context: .
dockerfile: Dockerfile-alpine
ports:
- 8080:2368
environment:
database__client: mysql
database__connection__host: db
database__connection__user: root
database__connection__password: senha
database__connection__database: dbtest
entrypoint: ["wait-for-it.sh", "db:3306", "--timeout=300", "--", "docker-entrypoint.sh"]
command: ["node", "current/index.js"]
depends_on:
- db
db:
image: mysql:8.0
restart: always
environment:
MYSQL_ROOT_PASSWORD: senha
volumes:
- "db:/var/lib/mysql"
networks:
- private
O pulo do gato está em usar o entrypoint como entrypoint: ["wait-for-it.sh", "db:3306", "--timeout=300", "--", "docker-entrypoint.sh"], o entrypoint deve ser ajustado para incluir o script wait-for-it.sh. Esse script verifica se o banco de dados está disponível na porta 3306 antes de continuar. O parâmetro --timeout=300 especifica que o script irá aguardar até 300 segundos antes de desistir.
Esse é apenas um exemplo bem simples de como usar esse script!
Depends on
No Docker Compose, podemos usar o parâmetro depends_on com condition: service_healthy para garantir que o serviço dependente esteja em um estado saudável antes que outro serviço seja iniciado. A condição service_healthy depende de um check de saúde (healthcheck) que você define no serviço.
O compose abaixo foi retirado do projeto do Mailman via Docker.
services:
mailman-core:
image: maxking/mailman-core:0.4 # Use a specific version tag (tag latest is not published)
container_name: mailman-core
hostname: mailman-core
restart: unless-stopped
volumes:
- /opt/mailman/core:/opt/mailman/
stop_grace_period: 30s
links:
- database:database
depends_on:
database:
condition: service_healthy
environment:
- DATABASE_URL=postgresql://mailman:mailmanpass@database/mailmandb
- DATABASE_TYPE=postgres
- DATABASE_CLASS=mailman.database.postgresql.PostgreSQLDatabase
- HYPERKITTY_API_KEY=someapikey
ports:
- "127.0.0.1:8001:8001" # API
- "127.0.0.1:8024:8024" # LMTP - incoming emails
networks:
mailman:
database:
environment:
- POSTGRES_DB=mailmandb
- POSTGRES_USER=mailman
- POSTGRES_PASSWORD=mailmanpass
image: postgres:12-alpine
volumes:
- /opt/mailman/database:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready --dbname mailmandb --username mailman"]
interval: 10s
timeout: 5s
retries: 5
O wait-for-it.sh é um script externo que você pode usar em seu container para aguardar até que um serviço (como o banco de dados) esteja disponível e pronto para aceitar conexões. Ele basicamente faz uma verificação contínua de um endereço e porta até que o serviço esteja acessível, por exemplo, verificando se o banco de dados responde na porta configurada.
Ambos wait-for-it.sh e depends_on têm o mesmo propósito de garantir que um serviço esteja pronto antes que outro serviço dependa dele.
A diferença é que o depends_on usado com condition: service_healthy é uma solução nativa do Compose, usando a verificação de saúde configurada diretamente no serviço. É mais simples e gerenciado pelo próprio Docker Compose.
Já o wait-for-it.sh é um script externo que precisa ser executado manualmente e é mais flexível, permitindo que você aguarde até que um serviço esteja acessível em uma porta específica. Não depende de healthcheck integrado ao Docker, e você tem que configurar a lógica de espera manualmente.
Quando você usa o parâmetro depends_on com a opção condition, o valor pode ser um dos seguintes:
service_healthy
O serviço dependente só será iniciado quando o serviço referenciado (o serviço no qual você está dependendo) passar no healthcheck. Ou seja, o serviço depende da condição de saúde do serviço referenciado (como o banco de dados estar pronto para aceitar conexões).service_started
O serviço dependente será iniciado assim que o serviço referenciado começar a ser iniciado, independentemente de seu status de saúde. Isso significa que o Docker não espera o serviço estar "saudável", apenas que ele tenha começado a iniciar.
O healthcheck no Docker Compose é uma funcionalidade que permite verificar o estado de saúde de um serviço em execução. Ele define como o Docker deve testar se o serviço está funcionando corretamente, o que ajuda a garantir que o serviço esteja realmente pronto para ser usado antes de outro serviço tentar se conectar a ele.
A principal função do healthcheck é verificar a disponibilidade de um serviço de maneira automatizada. Se o serviço não passar no healthcheck, o Docker marcará esse serviço como não saudável. Isso pode ser útil para implementar estratégias de espera, como no caso de dependências entre serviços no Docker Compose (por exemplo, garantir que um banco de dados esteja pronto antes de iniciar um servidor de aplicação).
O healthcheck tem os seguintes componentes:
test
O comando a ser executado para verificar a saúde do serviço. Pode ser um comando simples ou um script que retorna um código de saída indicando se o serviço está saudável ou não.
Exemplo de comando:["CMD", "curl", "-f", "http://localhost/"]verifica se o serviço está acessível na porta HTTP local.
Também pode usarCMD-SHELL, que executa um shell para o comando (útil para comandos mais complexos).interval
O intervalo entre as verificações. O valor padrão é 30s. Esse é o tempo entre cada execução do test.retries
O número de tentativas que podem falhar, se todas falharem o docker considera que o container não está saudável. O valor padrão é 3.timeout
O tempo máximo para o comando de verificação ser executado antes de ser considerado falho. Se o comando não for concluído dentro desse tempo, ele será tratado como uma falha. O valor padrão é 30 segundos.start_period
O tempo após o início do container até o início da verificação de saúde. Durante esse período, o Docker não irá considerar falhas no healthcheck. O valor padrão é 0s.
Âncoras e Referências
No docker-compose.yml, podemos usar algo chamado âncoras e referências, eles evitam a repetição de configurações semelhantes em diferentes partes do arquivo. Isso faz com que o arquivo fique mais limpo, reutilizável e fácil de manter.
Sem o uso de Âncoras
Em um arquivo docker-compose.yml simples, onde não usamos âncoras, teríamos que repetir as configurações em cada serviço. Abaixo podemos ver um exemplo de como isso seria feito sem âncoras:
version: '3'
services:
webapp:
image: webapp-image
volumes:
- /path/to/data
- /path/to/config
environment:
- ENV_VAR1=value1
- ENV_VAR2=value2
backend:
image: backend-image
volumes:
- /path/to/data
- /path/to/config
environment:
- ENV_VAR1=value1
- ENV_VAR2=value2
Neste caso, estamos repetindo as mesmas configurações de volumes e variáveis de ambiente para os serviços webapp e backend. Isso pode ser problemático em arquivos grandes, pois qualquer alteração nas configurações de volumes ou variáveis de ambiente precisaria ser feita em cada lugar onde essas configurações aparecem.
Com o uso de Âncoras e Referências
Agora, vamos refatorar o arquivo utilizando âncoras e referências para evitar a duplicação de configurações. As âncoras são definidas com & e podem ser reutilizadas com *.
version: '3'
x-volumes:
&default-volumes
- /path/to/data
- /path/to/config
x-environment:
&default-environment
- ENV_VAR1=value1
- ENV_VAR2=value2
services:
webapp:
image: webapp-image
volumes:
*default-volumes
environment:
*default-environment
backend:
image: backend-image
volumes:
*default-volumes
environment:
*default-environment
As âncoras &default-volumes e &default-environment são definidas sob a chave x-volumes e x-environment respectivamente. Elas armazenam os valores de volumes e variáveis de ambiente que serão reutilizados.
Para usar essas configurações em serviços, usamos *default-volumes e *default-environment dentro de cada serviço. Agora, se for necessário modificar os volumes ou variáveis de ambiente, basta alterar a definição da âncora no topo do arquivo, e todas as referências serão automaticamente atualizadas.
O prefixo x- é uma convenção para extensões (ou "aliases") no formato YAML. Esse prefixo indica que a chave não é uma parte padrão do Docker Compose, mas sim uma chave personalizada, usada para armazenar configurações reutilizáveis.
O Compose não interpreta diretamente as chaves com x-, o que as torna perfeitas para serem usadas para armazenar valores que desejamos referenciar. Podemos criar as âncoras e referências sem o prefixo x- caso desejado.
dotenv
Quando trabalhamos com docker-compose, é comum nos depararmos com a necessidade de reutilizar arquivos de configuração em diferentes ambientes, como desenvolvimento local, homologação e produção. Cada um desses ambientes pode exigir portas diferentes, usuários e senhas específicos, caminhos distintos e até nomes de containers personalizados.
Se todas essas informações forem escritas diretamente no arquivo docker-compose.yaml, além de dificultar a manutenção, isso aumenta o risco de expor dados sensíveis ou de gerar conflitos ao subir os serviços em contextos distintos. Também se torna complicado versionar esse tipo de configuração, pois cada alteração local pode impactar outras pessoas do time ou afetar o comportamento dos containers fora do ambiente original.
Para resolver esse problema, podemos utilizar arquivos .env, que funcionam como fontes externas de variáveis de ambiente. O docker-compose lê esse arquivo automaticamente, e todas as variáveis declaradas ali podem ser referenciadas no docker-compose.yaml usando a sintaxe ${VARIAVEL}. Isso permite manter o arquivo docker.compose.yaml limpo, reutilizável e genérico, enquanto os detalhes específicos de cada ambiente ficam isolados no .env.
Dessa forma, conseguimos trocar informações como senhas, nomes de banco de dados ou números de porta sem alterar o arquivo principal de configuração, bastando apenas atualizar o .env.
Além disso, é recomendado criar um arquivo .env.example contendo apenas os nomes das variáveis esperadas, sem os valores reais. Isso facilita o compartilhamento da estrutura com outros desenvolvedores sem comprometer a segurança.
Arquivos .env são muito práticos para definir variáveis de ambiente nos containers, mas também podem expor informações sensíveis se não forem tratados com cuidado.
Evite colocar senhas, tokens, chaves de API ou credenciais diretamente nesses arquivos, especialmente em repositórios versionados ou ambientes compartilhados.
Mesmo que o container pareça isolado, o conteúdo do .env pode ser acessado por outros processos com permissões suficientes, ou ser incluído em logs e backups sem proteção adequada.
Para entender melhor o risco de ser usar senhas, tokens, chaves de API ou credenciais em arquivos .env, acesse Cuidado ao usar senhas em .env.
Usemos o compose abaixo como exemplo:
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mysql-dev
#restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: dbtest
MYSQL_USER: usuario
MYSQL_PASSWORD: senha123
TZ: America/Sao_Paulo
command:
- --authentication-policy=caching_sha2_password
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --innodb_force_recovery=0
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
Nesse arquivo as senhas e usuários estão "hard coded" no arquivo docker-compose.yaml, isso quer dizer que para mudar a senha ou o usuário eu teria que alterar diretamente no arquivo. Outro problema é que compartilhar isso com outros desenvolvedores seria um problema.
Para isso vamos usar o .env, tornando nosso arquivo docker-compose.yaml mais reutilizavél, podemos ter apenas ele e subir diferentes ambientes como dev, prod, test e etc. Primeiro crie o arquivo .env no mesmo lugar que está o docker-compose.yaml:
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=dbtest
MYSQL_USER=usuario
MYSQL_PASSWORD=senha123
TZ=America/Sao_Paulo
Agora vamos reescrever nosso docker-compose.yaml para usar o .env:
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mysql-dev
#restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: ${TZ}
command:
- --authentication-policy=caching_sha2_password
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --innodb_force_recovery=0
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
Com isso, o docker-compose vai automaticamente substituir cada ${VARIAVEL} pelos valores definidos no .env na hora de subir os serviços. Assim, o arquivo de composição fica mais limpo e seguro, e você pode manter diferentes .env para cada ambiente, sem precisar duplicar ou editar o docker-compose.yaml. Também é possível ignorar o .env no Git usando um .gitignore, mantendo apenas um .env.example com as chaves definidas e sem valores, facilitando a colaboração em equipe.
Sempre use dotenv
Utilizar arquivos .env em projetos com docker-compose não é apenas uma boa ideia, é algo essencial e praticamente obrigatório se você quiser seguir boas práticas de segurança e manter seu projeto minimamente profissional. Quando senhas, usuários, portas, nomes de bancos e outros dados sensíveis ficam explícitos no docker-compose.yaml, você corre o risco de expor essas informações acidentalmente em repositórios públicos, ambientes de produção ou até em commits internos que deveriam ser seguros.
Novamente, cuidado ao incluir senhas, tokens, chaves de API ou outras credenciais em arquivos .env. Mesmo sendo uma prática mais organizada do que defini-las diretamente no docker-compose.yaml, isso não torna o método seguro, as informações continuarão expostas como variáveis de ambiente dentro do container, podendo ser lidas por outros processos ou usuários com acesso nele.
Sempre trate o .env como um arquivo sensível e só utilize para definir paramêtros de configuração da aplicação, mas nunca inclua senhas, tokens, chaves de API ou outras credenciais dentro dele.
Mesmo que o projeto esteja em um repositório privado, isso ainda representa um descuido grave do ponto de vista da segurança e mostra falta de experiência profissional. Separar esses valores em um arquivo .env permite que você mantenha essas informações fora do versionamento, deixando o arquivo principal limpo, reutilizável e sem risco de vazamento.
Além disso, essa abordagem torna muito mais fácil alternar entre diferentes ambientes, como desenvolvimento, testes e produção, apenas trocando o conteúdo do .env, sem precisar editar o docker-compose.yaml o tempo todo.
Ignorar o uso de .env significa colocar em risco a integridade do seu ambiente, a confidencialidade das informações e ainda comprometer a escalabilidade do projeto em equipe. Por isso, usar dotenv com docker-compose não é apenas recomendado, é um passo obrigatório para quem quer manter segurança, organização e boas práticas no desenvolvimento de sistemas.
Compose Secrets
O Compose mais recente (especialmente a versão 3.1+ do Docker Compose e no Compose V2) trouxe suporte ao uso de secrets: mesmo em modo standalone (fora do Swarm). Isso significa que você pode declarar segredos diretamente no docker-compose.yaml e montá-los como arquivos dentro do container, embora a funcionalidade seja basicamente um bind-mount de um arquivo secreto no host, não envolvendo criptografia dedicada pelo Docker.
Por exemplo:
version: "3.9"
services:
db:
image: mysql:latest
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
MYSQL_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_root_password
- db_password
secrets:
db_root_password:
file: ./db_root_password.txt
db_password:
file: ./db_password.txt
Com isso, o Compose irá montar os arquivos ./db_root_password.txt e ./db_password.txt dentro dos containers, em /run/secrets/.... Já imagens oficiais como MySQL e Postgres reconhecem as variáveis _FILE para ler credenciais via arquivo em vez de variável de ambiente.
No entanto, é importante entender que essa aproximação não traz os benefícios de segurança que o Swarm oferece, como criptografia em trânsito e em repouso, controle fino de acesso, armazenamento em memória e etc. pois o sistema apenas cria um bind-mount para o arquivo secreto no host.
Como definir Limites de CPU e Memória
Já comentei anteriormente sobre como definir limites de recursos para containers quando falei sobre a Opção Deploy, mas vale reservar um tópico dedicado exclusivamente a esse assunto, para explicar com mais detalhes como o controle de CPU e memória funciona em containers.
Por padrão, se nenhum limite de recurso for especificado, um contêiner pode consumir toda a CPU e memória disponíveis no host, isso pode acabar prejudicando outros serviços no mesmo ambiente e até o host. Por esse motivo é importante definir limites adequados de CPU e memória, dessa forma, estamos prevenindo sobrecarga do host e mantendo a estabilidade do sistema como um todo.
Entretanto, não é uma tarefa muito fácil determinar esses limites sem dados prévios de carga ou conhecimento específico da aplicação, nesse contexto, teremos que usar uma abordagem genérica baseada em observação, teste e ajuste progressivo.
Antes de sair definindo os valores, é importante entender como o Docker lida com limites de CPU e memória.
Limite de Memória
Define o máximo de memória RAM física que o contêiner pode usar. Se o contêiner atingir esse limite, processos dentro dele podem ser encerrados (OOM kill).
Por exemplo, limitar um contêiner a 125 MB de memória previne que ele consuma mais do que o necessário, evitando falhas por falta de memória no host. Sem um limite, um contêiner poderia consumir memória excessiva e afetar todo o sistema. Opcionalmente, podemos definir swap com --memory-swap (total de RAM + swap) para permitir alguma sobrevida para o processo.
Para definir isso com Compose podemos fazer da seguinte forma:
services:
teste_memoria:
image: nginx
deploy:
resources:
limits:
memory: "512M"
Para definir com a CLI do Docker diretamente, podemos fazer:
# Iniciando um novo container com limite de memória:
$ docker container run -ti --name teste_memoria -m 512M debian
# Alterando a memória de um container em execução:
$ docker container update -m 256m teste_memoria
Reserva de Memória
É um limite "suave" (soft), ele é e deve ser inferior ao limite rígido de memória (que é o limite descrito acima). Ele funciona como uma garantia mínima, não como um teto máximo de mamória que pode ser usada. Basicamente, ele define quanto de memória o contêiner deve ter reservado para si, o valor que o sistema tenta garantir se houver disputa por memória entre processos ou containers.
Diferente do limite acima, que é o limite máximo absoluto que o contêiner pode usar. Por exemplo, podemos reservar 256 MB enquanto que o limite é 384 MB, permitindo picos de consumo passando da reserva e indo até limite o máximo definido.
Esse parâmetro deve ser menor que o limite de memória e não garante que o contêiner nunca passe da reserva, ele apenas sinaliza um patamar de uso típico esperado.
Para definir isso com Compose podemos fazer da seguinte forma:
services:
teste_memoria:
image: nginx
deploy:
resources:
reservations:
memory: "256M"
Para definir com a CLI do Docker diretamente, podemos fazer:
# Iniciando um novo container com limite de memória:
$ docker container run -ti --name teste_memoria --memory-reservation 512M debian
# Alterando a memória de um container em execução:
$ docker container update --memory-reservation 256M teste_memoria
CPU Shares
Por padrão todo contêiner tem um peso de CPU igual a 1024. Esse valor é ajustável via --cpu-shares ou no Compose (campo cpu_shares), ele representa o peso relativo de CPU do contêiner em relação a outros containers.
É um limite soft, ou seja, quando a CPU do host estiver altamente disputado, contêineres com peso maior recebem proporcionalmente mais ciclos de CPU que os de peso menor.
Por exemplo, definir cpu_shares: 512 para um serviço dará a ele aproximadamente metade da prioridade de CPU comparado a outro com 1024, em cenários de alta utilização. Importante notar que CPU shares não reservam CPU exclusivamente, apenas determinam prioridade de uso da CPU.
Limite de CPU (Quota/Cores)
O parâmetro --cpus define quanto tempo total de CPU o contêiner pode usar em relação a todos os núcleos disponíveis, e não está limitado a um único núcleo físico.
A opção --cpus="1.5" informa que o contêiner pode usar 150% do tempo de um único núcleo, ou seja, equivalente a 1 núcleo e meio, somando todos os núcleos. Já a opção --cpus="2.5" informa que ele pode usar até 2 núcleos e meio de CPU no total (250% de um núcleo), distribuídos entre os núcleos disponíveis do host.
Internamente, o Docker converte --cpus em:
--cpu-period=100000 --cpu-quota=<valor>
Por exemplo:
--cpus=1.5 → --cpu-quota=150000
--cpus=2.5 → --cpu-quota=250000
O kernel aplica isso por cgroup, controlando o tempo total de CPU que o contêiner pode consumir por período (100 ms por padrão). Para 1.5, isso corresponde a configurar uma fração do tempo de CPU disponível (no exemplo, 1.5 núcleos = quota de 150000 µs em um período de 100000 µs).
Assim, um contêiner com cpus: 0.5 (50%) será atrasado (throttled) pelo Docker quando tentar exceder esse uso, garantindo que não ultrapasse metade de um núcleo de CPU contínuo. Diferentemente de shares, o quota é um limite estrito, ele impõe um teto absoluto de uso de CPU por segundo.
Para definir isso com Compose podemos fazer da seguinte forma:
services:
teste_cpu:
image: nginx
deploy:
resources:
limits:
cpus: "0.5"
Para definir com a CLI do Docker diretamente, podemos fazer:
# Iniciando um novo container com limite de memória:
$ docker container run -ti --name teste_cpu --cpus=0.5 debian
# Alterando a memória de um container em execução:
$ docker container update --cpus=0.5 teste_cpu
Quando você usa --cpus, o valor representa quanto tempo total de CPU o container pode usar simultaneamente, somando todos os núcleos.
--cpus=1 → 100% de 1 núcleo
--cpus=0.5 → 50% de 1 núcleo
--cpus=2 → 200% de 1 núcleo (ou 2 núcleos cheios)
Em servidores com vários núcleos (ex.: 16 CPUs lógicas), isso não limita a afinidade, apenas o tempo de processamento.
--cpus=0.25 a 0.5
Suficiente pra lidar com picos leves sem monopolizar CPU, útil para nginx estático, Redis, API simples, sidecars, bots, proxies etc.--cpus=1 a 2
Garante fluidez, mas ainda compartilha bem o host, útil para aplicações web (Node, Python, PHP-FPM), serviços de fila, processadores de tarefas curtas.--cpus=3 a 8
Mais "fôlego", mas vale usar também--cpuset-cpuspara fixar núcleos específicos e evitar competição. Útil para build runners, processamentos de mídia, análises, CI/CD.
Evite definir --cpus igual ao número total de CPUs do host. Deixe pelo menos 10 até 20% livres para o sistema e outros containers.
Imagine que sua aplicação consome até 5.00% de uso de CPU em picos (ou seja, 0,05 de um núcleo), então você pode limitar de forma confortável, deixando uma margem de segurança para variações.
Se você quiser definir exatamente o valor observado, pode configurar --cpus=0.05, que seria o equivalente teórico. Mas eu recomendo sempre deixar uma margem de segurança, você pode sempre dobrar o valor observado, nesse caso, poderiamos usar --cpus=0.1. Para uma margem mais confortável, podemos multiplicar por 3, ficando --cpus=0.15.
Período de CPU e um pouco mais
Quando você inicia um container com Docker (ou runtime compatível), pode controlar quais núcleos ele pode usar e quanto tempo pode usar nesses núcleos, através de parâmetros de cgroups e do escalonador do kernel.
O argumento --cpuset-cpus define um conjunto de núcleos específicos (CPUs lógicas) nos quais o container pode executar seus processos. Por exemplo, --cpuset-cpus="0,1" permite que o contêiner use apenas os núcleos 0 e 1. Isso limita onde o processo roda, mas não o quantum de tempo que pode usar.
Já o argumento --cpu-period e --cpu-quota trabalham juntos para impor um limite de tempo de CPU pelo mecanismo CFS (Completely Fair Scheduler). O --cpu-period define um intervalo de tempo (em microssegundos) para medição, e o --cpu-quota define quantos microssegundos daquele intervalo o container pode usar.
Por exemplo, --cpu-period=100000 e --cpu-quota=50000 limitam o container a 50% de CPU. Se você usar --cpus, o Docker traduz esse valor em quota / period automaticamente.
Os parâmetros de CPU "real-time" são mais especializados e só fazem sentido se o kernel e o daemon estiverem configurados para suportar real-time scheduling, as opções são:
--cpu-rt-period
Define um período real-time em microssegundos, é o intervalo de tempo no qual tarefas em tempo real são consideradas.--cpu-rt-runtime
Define quantos microssegundos desse período real-time o container pode realmente executar tarefas de prioridade real-time. Por exemplo, se o--cpu-rt-periodfor 1.000.000 µs e--cpu-rt-runtimefor 950.000 µs, então esses contêineres podem usar até 95 % do tempo nesse modo, deixando 5 % para outras tarefas.
Esses parâmetros só têm efeito se:
- O kernel for compilado com suporte à escalonador real-time e
CONFIG_RT_GROUP_SCHEDativado. - O daemon Docker (ou Podman) for iniciado com permissões ou configurações que permitam uso de real-time (por exemplo, via
--cpu-rt-runtimeno dockerd). - O container tenha a capacidade
CAP_SYS_NICEse precisar elevar prioridades reais.
Se essas condições não forem atendidas, Docker pode rejeitar os parâmetros ou eles não terem efeito.
Monitorando o Uso Real de Recursos dos Contêineres
Para definir os limites, primeiro precisamos observar quanta CPU e memória os contêineres realmente consomem. Felizmente, o Docker oferece ferramentas de monitoramento nativas e há também soluções de terceiros.
Podemos usar o docker stats, ele é mais simples de usar. Ele mostra em tempo real o uso de CPU (%), memória (em MB e % do limite), I/O de rede e disco de todos os contêineres em execução.
O docker stats é útil para monitoramento pontual ou de curta duração. Você pode deixá-lo rodando durante testes para ver picos de uso. Entretanto, ele não armazena histórico, para análise de longo prazo, é preciso exportar os dados ou usar outras ferramentas.
Para acompanhamento mais aprofundado ao longo do tempo, podemos usar ferramentas como cAdvisor, Prometheus/Grafana, Datadog, Sematext, Zabbix, etc., que coletam métricas de contêiner periodicamente.
O cAdvisor, por exemplo, expõe métricas detalhadas de cgroups (CPU, memória, I/O) de cada contêiner que podem ser agregadas pelo Prometheus.
Com essas ferramentas, você pode configurar alertas, por exemplo, se um contêiner passar consistentemente de 85% CPU ou 90% memória, emitir um aviso. Conforme recomenda a Sematext, use alertas baseados em limites definidos para ajustar proativamente os recursos antes de ocorrerem falhas.
Além de métricas numéricas, monitore logs do contêiner (docker logs) que possam indicar problemas de memória (por exemplo, exceptions de OOM em aplicações Java/Python) ou lentidão por CPU.
O comando docker inspect <container> mostra configurações e status, incluindo os limites impostos e estatísticas básicas de CPU/memória já utilizadas. Ferramentas de observabilidade dentro da aplicação (APM) também ajudam a entender onde os recursos estão sendo consumidos.
Em casos de troubleshooting, você pode entrar no contêiner e usar comandos como top, htop ou ps para ver processos que mais consomem CPU/MEM dentro dele. Isso é útil para confirmar que o uso de recursos está alinhado com expectativas (por exemplo, um único processo monopolizando CPU, ou uso de memória crescendo com o tempo indicando possível vazamento).
Benchmarking e Análise de Comportamento
Quando não se possui dados prévios, é recomendável gerar você mesmo algumas cargas de teste para entender o comportamento do contêiner. Algumas práticas e ferramentas úteis são:
Testes de Stress Sintéticos
Uma técnica eficaz é usar a ferramentastress(disponível em várias distros Linux) para simular carga máxima de CPU e memória dentro do contêiner. Por exemplo, executestress --cpu 1 --timeout 60sdentro do contêiner para forçar 100% de uso de 1 núcleo por 60 segundos.
Observe viadocker statscomo o contêiner se comporta, ele deve atingir próximo de 100% CPU, se um limite--cpusestiver imposto (ex: 0.5), verifique se o uso fica travado em ~50% do CPU (indicando throttling correto).
Pode executar tambémstress --vm 1 --vm-bytes 50M --timeout 60spara alocar ~50 MB de memória adicional. Isso ajuda a verificar se o limite de memória está funcionando, por exemplo, se o contêiner tem limite de 50 MB, esse comando deve levá-lo próximo do teto, veja nodocker statsse o uso atinge ~50 MB e se o contêiner não ultrapassa (pode ocorrer OOM kill se tentar exceder).
Esses testes extremos garantem que as restrições configuradas realmente entram em vigor e ajudam a entender o ponto de quebra do contêiner sob pressão. Após ostress, monitore se o contêiner se recupera (libera memória, reduz CPU) ou se há efeitos colaterais.Benchmark da Aplicação Real
Além destressque é genérico, é importante simular cargas reais da aplicação. Use ferramentas de carga adequadas ao tipo de serviço, por exemplo,hey,JMeter,Locustouautocannonpara APIs web, geração de transações simuladas para uma aplicação de processamento. Scripts de teste para consumidores de fila, etc. Gradualmente aumente o volume de trabalho (mais requisições por segundo, mais usuários simulados, mais jobs em paralelo) e veja como o uso de CPU/memória do contêiner responde.
Identifique picos máximos durante esses testes e também se há degradação de performance (latência crescendo muito quando CPU atinge 100%, por exemplo).Análise de Comportamento ao Longo do Tempo
Alguns contêineres podem ter uso de recursos que cresce lentamente (por exemplo, consumo de memória acumulando devido a cache ou vazamento de memória). Portanto, além de testes de curta duração, execute o contêiner por longos períodos com carga leve para observar. Métricas a observar, uso de memória tende a estabilizar ou só aumenta? O CPU fica ocioso na maior parte do tempo ou há picos periódicos? Comportamentos anômalos aqui podem indicar que você deve deixar mais folga no limite (no caso de uso crescente) ou que pode otimizar a aplicação.Perfis de Uso Diferentes
Se a aplicação possui diferentes modos de operação (por exemplo, um servidor web que em horário comercial atende requisições intensamente, mas à noite roda batch de relatórios), monitore separadamente esses cenários. Talvez você precise configurar limites que acomodem o pior caso razoável de cada recurso.