Docker Compose com vários arquivos: regras de merge
Entenda como compose.override.yaml é carregado sozinho, a ordem dos arquivos, por que ports mantém uma porta aberta e quando usar include em dev e prod.
O que o Compose faz com mais de um arquivo
O Docker Compose pode criar um projeto a partir de vários arquivos. Ele os lê na ordem em que são recebidos e os mescla em um único modelo. Assim, um arquivo posterior prevalece sobre qualquer valor conflitante. Há dois mecanismos para fazer isso pela linha de comando: um arquivo de substituição que o Compose carrega automaticamente e a flag -f, que você informa manualmente. Um terceiro mecanismo fica dentro do próprio arquivo: o elemento include. Ele funciona de forma diferente dos dois anteriores.
A mesclagem não é uma simples substituição. Os mapeamentos são mesclados chave por chave, as sequências são anexadas e um pequeno conjunto de campos é substituído por inteiro. Essa diferença causa as surpresas. A lista ports é a que confunde quase todo mundo.
Tudo abaixo pressupõe o Compose v2 e o plugin docker compose, não o script antigo docker-compose. Execute docker compose version para verificar. Se você ainda não criou um arquivo do Compose, comece pelo guia básico do Docker Compose e depois volte.
O arquivo de substituição que o Compose carrega sem ser informado
Execute docker compose up sem a opção -f. O Compose procura no diretório de trabalho e depois nos diretórios pai por compose.yaml ou docker-compose.yaml. Se houver um arquivo de substituição ao lado do arquivo base, o Compose também o carrega automaticamente.
ls compose.yaml compose.override.yaml
docker compose up -dCom os dois arquivos presentes, o resultado é o mesmo que especificá-los manualmente.
docker compose -f compose.yaml -f compose.override.yaml up -dOs nomes reconhecidos pelo Compose são compose.override.yaml, compose.override.yml e os nomes antigos docker-compose.override.yml e docker-compose.override.yaml. Qualquer outro nome, como compose.dev.yaml, só é carregado quando você o informa com -f.
Assim que você informa um -f, o carregamento automático é interrompido. docker compose -f compose.yaml up lê exatamente esse arquivo e ignora o arquivo de substituição. É essa propriedade que sustenta o padrão de dev e prod usado mais adiante neste guia.
Isso pode causar problemas em um servidor. Um arquivo de substituição deixado no diretório de deploy é carregado por todo comando docker compose executado nesse diretório, inclusive pelo comando executado pelo cron. Assim, uma stack de produção pode acabar montando com bind um diretório de código-fonte que ninguém pretendia enviar. Execute docker compose config após qualquer deploy e leia o resultado.
Ordenação com -f e onde os caminhos relativos são resolvidos
O Compose cria a configuração na ordem em que você fornece os arquivos. Os arquivos seguintes substituem e adicionam conteúdo aos anteriores. Da esquerda para a direita, o último valor prevalece.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -dTodos os comandos desse projeto precisam usar a mesma lista de arquivos. Execute up com dois arquivos e logs com um arquivo, e você estará trabalhando com um modelo mesclado diferente. Essa é uma maneira rápida de obter um serviço que o Compose informa não existir. Defina a lista uma única vez usando a variável de ambiente COMPOSE_FILE.
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -dO separador é : no Linux, e COMPOSE_PATH_SEPARATOR o altera. COMPOSE_FILE também pode ficar no arquivo .env do projeto. Assim, ele fará parte do checkout, e não do histórico do shell. Qualquer valor definido explicitamente na linha de comando tem precedência sobre a variável de ambiente.
Agora, a regra que causa problemas nos bind mounts. Quando você usa vários arquivos com -f, todos os caminhos relativos em todos esses arquivos são resolvidos com base no diretório do primeiro arquivo, e não no diretório do arquivo que contém cada caminho. Escreva ./data:/var/lib/postgresql/data dentro de deploy/prod/compose.prod.yaml, e o Compose ainda procurará ./data ao lado do arquivo base. O Docker então criará um diretório vazio nesse caminho incorreto, e o contêiner será iniciado sem conteúdo nele. Isso parece perda de dados, mas não é. Passe --project-directory para definir o caminho base manualmente ou use include, que resolve cada arquivo com base no próprio diretório.
O nome do projeto vem desse mesmo diretório base. Portanto, alterar qual arquivo aparece primeiro pode renomear o projeto. Um projeto renomeado terá novos nomes de contêineres e volumes, enquanto o volume antigo continuará no disco com o nome anterior. Em vez disso, fixe o nome usando name: no nível superior do arquivo base.
name: myappQuais campos são mesclados e quais são substituídos
O Compose faz a mesclagem pelo tipo do valor, não pelo nome do campo.
- Campos de valor único são substituídos.
image,command,entrypointemem_limitassumem diretamente o valor posterior. Não é possível anexar um argumento a umcommand, porque a substituição reescreve a linha inteira. - Mapeamentos são mesclados chave por chave.
environment,labels,volumesedevicespreservam todas as chaves dos dois arquivos, e o arquivo posterior vence para qualquer chave presente em ambos. Paraenvironmentelabels, a chave é o nome da variável ou do rótulo. Paravolumesedevices, a chave é o caminho do contêiner. - Sequências são anexadas.
dns,dns_search,expose,tmpfseexternal_linkssão concatenados. Uma configuração base contendoexpose: ["3000"]mesclada com uma substituição contendo["4000", "5000"]produz["3000", "4000", "5000"].
Quatro sequências têm uma chave de identidade, portanto as entradas que coincidem nessa chave são mescladas em vez de anexadas. volumes, secrets e configs coincidem em target. ports coincide com base na combinação de ip, target, published e protocol.
Leia a regra de ports duas vezes, porque esse é o ponto problemático. Duas entradas de porta são a mesma entrada somente quando todas as quatro partes coincidem. Altere qualquer uma delas, e o Compose verá uma segunda porta não relacionada. Por isso, ele preservará ambas.
Por que sua porta continua publicada após a substituição
Um arquivo base que publica um serviço em todas as interfaces:
services:
web:
image: nginx:1.27
ports:
- "8080:80"Uma substituição escrita para vinculá-lo somente ao localhost, porque um proxy reverso ficará na frente dele:
services:
web:
ports:
- "127.0.0.1:8080:80"Verifique o resultado antes de presumir que funcionou.
docker compose -f compose.yaml -f compose.prod.yaml configAs duas entradas estão na saída. A parte ip é diferente, 0.0.0.0 em comparação com 127.0.0.1, portanto são duas portas diferentes para a mesclagem, e o vínculo público que você tentou remover ainda está no modelo. Isso é mais importante no Docker do que em outros contextos, porque uma porta publicada é gravada no iptables antes das regras do firewall. O mecanismo é explicado em por que as portas publicadas do Docker passam pelo ufw.
Há duas correções. A explícita é a tag !override, que substitui o atributo inteiro e ignora as regras de mesclagem:
services:
web:
ports: !override
- "127.0.0.1:8080:80"!override requer o Compose v2.24.4 ou mais recente. A correção portátil não requer nenhuma tag: mantenha ports completamente fora do arquivo base e declare-o somente nos arquivos específicos de cada ambiente. Sem nada para mesclar, nada é exposto. Esse é o padrão usado no exemplo detalhado abaixo.
Excluindo um valor definido pelo arquivo base
!reset remove um atributo, restaurando-o ao valor padrão ou a null. Ele exige um valor e o ignora, portanto informe um valor válido e vazio.
services:
web:
ports: !reset []
environment:
DEBUG: !reset null!reset exige o Compose v2.24 ou mais recente. Use-o quando você não puder editar o arquivo base, como no caso de um fragmento de fornecedor que você inclui.
include, para stacks montados a partir de componentes
include inclui outra aplicação Compose no seu modelo. É um elemento de nível superior, não uma flag.
include:
- path: ../commons/compose.yamlCada caminho em include é carregado como seu próprio modelo de aplicação Compose, com seu próprio diretório de projeto. Assim, os caminhos relativos dentro desse arquivo são resolvidos em relação ao diretório do próprio arquivo. Essa é a diferença real em relação a -f e o motivo pelo qual include é a ferramenta adequada quando o fragmento está em outra pasta ou em outro repositório.
A forma longa aceita subopções.
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath aceita uma lista, e esses arquivos são mesclados pelas regras normais antes que o resultado seja incorporado ao seu modelo. project_directory define o caminho base usado para resolver caminhos relativos no arquivo incluído. env_file fornece ao arquivo incluído suas próprias variáveis para interpolação, impedindo que um fragmento compartilhado leia silenciosamente o .env do seu projeto. include requer o Compose v2.20.0 ou mais recente.
Nomes de recursos duplicados entre o seu arquivo e um arquivo incluído são reportados como erro, em vez de serem mesclados silenciosamente. Isso é intencional. Para alterar algo declarado por um arquivo incluído, coloque a alteração em compose.override.yaml: a substituição é aplicada ao modelo montado, portanto pode modificar recursos incluídos sem colidir com eles.
Em resumo: include compõe aplicações separadas; -f aplica camadas de configuração sobre uma única aplicação.
Uma separação entre desenvolvimento e produção em um único VPS
Veja o padrão completo em três arquivos. O arquivo base declara o que é válido em todos os ambientes e não publica nenhuma porta.
name: myapp
services:
app:
image: ghcr.io/example/app:1.4.2
environment:
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
LOG_LEVEL: info
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
db_data:A condição depends_on faz o aplicativo aguardar um banco de dados que responda, em vez de apenas aguardar a existência de um container. Isso é explicado em healthchecks e condições de depends_on. POSTGRES_PASSWORD é interpolado do arquivo .env do projeto, que nunca deve ser incluído no git. Consulte arquivos de ambiente e secrets do Compose para conhecer as variantes mais seguras.
Em seguida, compose.override.yaml, que o Compose carrega automaticamente. Esse é o arquivo do desenvolvedor.
services:
app:
build: .
command: npm run dev
environment:
LOG_LEVEL: debug
ports:
- "3000:3000"
volumes:
- ./src:/app/src
db:
ports:
- "127.0.0.1:5432:5432"Em um laptop, um docker compose up sem argumentos mescla esses dois arquivos. command substitui o padrão da imagem porque aceita um único valor. LOG_LEVEL substitui info porque environment faz a mesclagem por chave. A montagem bind e as duas portas publicadas são apenas adições, e a porta do banco de dados é vinculada a localhost para que um laptop em uma rede compartilhada não ofereça o PostgreSQL aos demais dispositivos.
Por fim, compose.prod.yaml. O nome desse arquivo não é um dos nomes que o Compose procura, portanto ele nunca é carregado por acidente.
services:
app:
ports:
- "127.0.0.1:8000:3000"
deploy:
resources:
limits:
memory: 512MNo VPS, você especifica os dois arquivos. Essa especificação é exatamente o que exclui o override.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml psps deve listar os dois serviços como em execução, com db exibindo (healthy). Como você passou -f, compose.override.yaml não foi lido. Assim, o comando de desenvolvimento, a montagem bind do código-fonte e a porta pública 3000 não podem chegar à produção, mesmo com o arquivo no mesmo diretório. A porta 8000 está disponível apenas em localhost, pronta para um proxy. Consulte executar vários aplicativos atrás do Traefik quando adicionar o segundo serviço.
Defina COMPOSE_FILE=compose.yaml:compose.prod.yaml no .env do servidor. Depois disso, seus demais comandos voltam a ser apenas docker compose logs -f app.
Leia o modelo mesclado antes de fazer a implantação
docker compose config imprime o modelo totalmente mesclado e interpolado. Não é uma prévia. É a entrada exata que o Compose usará. Portanto, quando a saída divergir do que você esperava, a saída estará correta.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml config --no-interpolate
docker compose -f compose.yaml -f compose.prod.yaml config --services--no-interpolate mantém ${VAR} sem expansão. Use-o antes de colar a saída em qualquer lugar, porque config simples imprime todos os segredos resolvidos em texto claro. --services lista apenas os nomes dos serviços. Essa é uma forma rápida de confirmar que um include carregou o que você esperava.
Modos de falha e o que você verá
no configuration file provided: not found. O Compose não encontrou nada para ler. Você está fora do diretório do projeto ou COMPOSE_FILE especifica um caminho que não existe. O Compose pesquisa os diretórios pai em busca do arquivo base padrão, mas não pesquisa em nenhum local um arquivo que você nomeou manualmente.
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. A interpolação é resolvida com base no arquivo .env do projeto e no ambiente do shell. O diretório do projeto é o diretório do primeiro arquivo -f. Fazer o deploy a partir de um diretório diferente daquele que contém .env gera este aviso e, em seguida, um banco de dados que recusa todas as conexões.
Sua alteração no override não aparece em docker compose config. Você passou -f, que desativa o carregamento automático do override, ou o Compose encontrou compose.yaml em um diretório pai, e o arquivo de override não está no mesmo diretório. Executar docker compose config sem outros argumentos mostra qual modelo o Compose está realmente construindo.
Um bind mount está vazio e o Docker criou um diretório que você não solicitou. O caminho relativo foi resolvido com base no diretório do primeiro arquivo. Corrija o caminho, passe --project-directory ou mova o fragmento para depois de include.
Os containers voltam com novos nomes e um volume parece vazio. O nome do projeto mudou, porque ele é definido com base no diretório do primeiro arquivo. Adicione um name: no nível superior do arquivo base para impedir que os nomes mudem. O volume antigo ainda está disponível com o prefixo antigo, e docker volume ls o exibirá.
Uma porta removida no override ainda está aberta. A mesclagem de ports adicionou a porta em vez de substituí-la. Confirme com docker compose config e use !override ou remova ports do arquivo base.
FAQ
O Compose carrega compose.override.yaml automaticamente?
Sim, quando você executa docker compose sem a flag -f. O Compose pesquisa o diretório de trabalho e os diretórios pai por compose.yaml ou docker-compose.yaml. Se houver um arquivo de substituição ao lado dele, esse arquivo será carregado em seguida. Os nomes reconhecidos são compose.override.yaml, compose.override.yml, docker-compose.override.yml e docker-compose.override.yaml. O uso de qualquer -f desativa esse comportamento. Assim, docker compose -f compose.yaml up lê apenas um arquivo.
Em que ordem vários arquivos -f são mesclados?
Da esquerda para a direita. O Compose cria a configuração na ordem em que os arquivos são informados. Cada arquivo substitui e adiciona conteúdo aos arquivos anteriores. Portanto, o último arquivo na linha de comando vence em qualquer conflito. A mesma lista deve ser usada em todos os comandos desse projeto. É para isso que serve COMPOSE_FILE=compose.yaml:compose.prod.yaml.
Por que minha porta continua publicada depois que eu a substituí?
Porque as entradas ports são identificadas pelo conjunto completo de ip, target, published e protocol. Uma substituição de 127.0.0.1:8080:80 sobre uma base de 8080:80 difere na parte ip. Por isso, o Compose trata a definição como uma segunda porta e mantém ambas. Execute docker compose config para ver as duas entradas. Use ports: !override no Compose v2.24.4 ou mais recente, ou remova ports do arquivo base para que não haja nada a mesclar.
Qual é a diferença entre include e -f?
-f aplica camadas de vários arquivos a uma única aplicação. Todos os caminhos relativos de todos os arquivos são resolvidos em relação ao diretório do primeiro arquivo. include incorpora uma aplicação Compose separada. Cada arquivo incluído mantém seu próprio diretório do projeto, portanto seus caminhos relativos são resolvidos em relação a ele. Use -f para camadas de ambiente da sua própria stack e include para um fragmento mantido em outro local. include requer o Compose v2.20.0 ou mais recente.
Como removo um valor definido pelo arquivo base?
Use a tag !reset no Compose v2.24 ou mais recente. Escreva ports: !reset [] ou MY_VAR: !reset null no arquivo de substituição. O atributo voltará ao valor padrão ou a null. O valor fornecido à tag é obrigatório, mas será ignorado. Se quiser substituir um atributo em vez de limpá-lo, !override faz isso e requer a versão v2.24.4 ou mais recente.