SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-28

Docker Compose com vários ficheiros: ordem e override

Entenda como compose.override.yaml é carregado sozinho, como a ordem dos ficheiros faz merge, por que ports mantém uma porta aberta e quando usar include.

O que o Compose faz com mais de um ficheiro

O Docker Compose pode criar um projeto a partir de vários ficheiros. Lê-os pela ordem em que os recebe e junta-os num único modelo. Assim, o ficheiro posterior prevalece quando existe um conflito de valores. Existem dois mecanismos para fazer isto na linha de comandos: um ficheiro de substituição carregado automaticamente pelo Compose e a flag -f, que é indicada manualmente. Um terceiro mecanismo está dentro do próprio ficheiro: o elemento include. Este funciona de forma diferente dos outros dois.

A junção não é uma simples substituição. Os mapeamentos são combinados chave a chave, as sequências são anexadas e um pequeno conjunto de campos é substituído na totalidade. É esta diferença que causa surpresas. A lista ports é a que apanha quase toda a gente desprevenida.

Tudo o que se segue pressupõe o Compose v2, o plugin docker compose, e não o script antigo docker-compose. Execute docker compose version para verificar. Se ainda não escreveu um ficheiro Compose, comece pelo guia básico do Docker Compose e depois volte aqui.

O ficheiro de override que o Compose carrega sem indicação

Execute docker compose up sem a flag -f. O Compose procura no diretório de trabalho e, depois, nos diretórios superiores, por compose.yaml ou docker-compose.yaml. Se existir um ficheiro de override junto ao ficheiro base, o Compose carrega-o automaticamente.

ls compose.yaml compose.override.yaml
docker compose up -d

Com os dois ficheiros presentes, o resultado é o mesmo que escrever ambos manualmente.

docker compose -f compose.yaml -f compose.override.yaml up -d

Os nomes reconhecidos pelo Compose são compose.override.yaml, compose.override.yml, e os nomes mais antigos docker-compose.override.yml e docker-compose.override.yaml. Qualquer outro nome, como compose.dev.yaml, só é carregado quando é indicado com -f.

Assim que passa um -f, o carregamento automático para. docker compose -f compose.yaml up lê exatamente esse ficheiro e ignora o override. É esta propriedade que permite o padrão de dev e prod usado mais à frente neste guia.

Isto pode funcionar nos dois sentidos num servidor. Um ficheiro de substituição deixado no diretório de deploy é carregado por todos os comandos docker compose executados nesse diretório, incluindo o comando executado pelo cron. É assim que uma stack de produção acaba por montar um diretório de código-fonte que ninguém pretendia incluir na entrega. Execute docker compose config depois de cada deploy e leia o resultado. Quando o deploy é não interativo, a verificação só é útil se alguém o informar de que algo correu mal. Essa é a função de um canal push, como um servidor ntfy autoalojado, para o qual um job do cron ou uma unidade systemd OnFailure pode enviar uma notificação.

Ordenação com -f e onde os caminhos relativos são resolvidos

O Compose constrói a configuração pela ordem em que fornece os ficheiros. Os ficheiros seguintes substituem e acrescentam conteúdo aos anteriores. Da esquerda para a direita, o último prevalece.

docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d

Todos os comandos desse projeto precisam da mesma lista de ficheiros. Execute up com dois ficheiros e logs com um, e estará a comunicar com um modelo combinado diferente. É uma forma rápida de obter um serviço que o Compose diz não existir. O risco aumenta numa stack cujas atualizações são executadas como comandos isolados, como o passo de migração da base de dados em uma plataforma de suporte Chatwoot alojada pelo próprio, onde um docker compose run emitido com uma lista de ficheiros incorreta é direcionado silenciosamente para um modelo diferente daquele que os seus serviços já utilizam. Defina a lista uma única vez com a variável de ambiente COMPOSE_FILE.

export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -d

O separador é : no Linux, e COMPOSE_PATH_SEPARATOR altera esse separador. COMPOSE_FILE também pode ficar no ficheiro .env do projeto. Assim, passa a fazer parte do checkout, em vez de ficar no histórico da shell. Qualquer valor definido explicitamente na linha de comandos prevalece sobre a variável de ambiente.

Agora, a regra que causa problemas nos bind mounts. Quando usa vários ficheiros com -f, todos os caminhos relativos desses ficheiros são resolvidos relativamente ao diretório do primeiro ficheiro. Não são resolvidos relativamente ao ficheiro que os contém. Escreva ./data:/var/lib/postgresql/data dentro de deploy/prod/compose.prod.yaml e o Compose continuará a procurar ./data junto do ficheiro base. O Docker cria então um diretório vazio nesse caminho incorreto e o contentor arranca sem conteúdo. Isto parece uma perda de dados, mas não é. Passe --project-directory para definir manualmente o caminho base ou use include, que resolve cada ficheiro relativamente ao seu próprio diretório.

O nome do projeto vem desse mesmo diretório base. Por isso, alterar o ficheiro que aparece primeiro pode renomear o projeto. Um projeto renomeado implica novos nomes de contentores e de volumes. O volume antigo continua no disco com o nome antigo. Fixe o nome com name: no nível superior do ficheiro base.

name: myapp

Quais campos são mesclados e quais são substituídos

O Compose faz a mesclagem com base no tipo do valor, não no nome do campo.

  • Os campos com valor único são substituídos. image, command, entrypoint e mem_limit assumem diretamente o valor posterior. Não é possível acrescentar um argumento a um command, porque a substituição reescreve a linha inteira.
  • Os mapeamentos são mesclados chave a chave. environment, labels, volumes e devices mantêm todas as chaves dos dois ficheiros, e o ficheiro posterior prevalece quando uma chave existe em ambos. Em environment e labels, a chave é o nome da variável ou da etiqueta. Em volumes e devices, a chave é o caminho no contentor.
  • As sequências são acrescentadas. dns, dns_search, expose, tmpfs e external_links são concatenadas. Uma configuração base que contém expose: ["3000"], mesclada com uma substituição que contém ["4000", "5000"], produz ["3000", "4000", "5000"].

Quatro sequências têm uma chave de identidade. Por isso, as entradas que coincidem nessa chave são mescladas em vez de acrescentadas. volumes, secrets e configs usam target para fazer a correspondência. ports usa a combinação de ip, target, published e protocol.

Leia essa regra de ports duas vezes, porque é onde ocorre o erro mais comum. Duas entradas de portas só são a mesma entrada quando todas essas quatro partes coincidem. Se alterar qualquer uma delas, o Compose interpreta a entrada como uma segunda porta sem relação com a primeira e mantém ambas.

Por que sua porta continua publicada após o override

Um arquivo base que publica um serviço em todas as interfaces:

services:
  web:
    image: nginx:1.27
    ports:
      - "8080:80"

Um override escrito para vinculá-lo apenas ao localhost, porque um reverse proxy ficará à 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 config

As duas entradas aparecem na saída. A parte ip é diferente, 0.0.0.0 contra 127.0.0.1, portanto são duas portas diferentes para fins do merge, e o vínculo público que você tentou remover continua no modelo. Isso é mais importante no Docker do que em outros contextos, porque uma porta publicada é gravada no iptables antes das regras do seu 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 todo o atributo e ignora as regras de merge:

services:
  web:
    ports: !override
      - "127.0.0.1:8080:80"

!override exige o Compose v2.24.4 ou mais recente. A correção portável não exige nenhuma tag: mantenha ports completamente fora do arquivo base e declare-o apenas nos arquivos específicos do ambiente. Sem nada para fazer merge, nada pode vazar. Esse é o padrão usado no exemplo completo abaixo.

Exclusão de um valor definido no ficheiro base

!reset remove um atributo, devolvendo-o ao valor predefinido ou a null. Aceita um valor e ignora-o, por isso indique um valor válido e vazio.

services:
  web:
    ports: !reset []
    environment:
      DEBUG: !reset null

!reset requer o Compose v2.24 ou posterior. Use-o quando não puder editar o ficheiro base, por exemplo, num fragmento de fornecedor que obtém externamente. Uma stack publicada a montante é precisamente esse caso: o ficheiro Compose por trás de um workspace AFFiNE autoalojado declara quatro contentores que não escreveu, e !reset permite limpar um atributo de um deles sem criar um fork do ficheiro e assumir a tarefa de o acompanhar.

include, para stacks montadas a partir de componentes

include incorpora outra aplicação Compose ao seu modelo. É um elemento de nível superior, não uma flag.

include:
  - path: ../commons/compose.yaml

Cada caminho em include é carregado como o seu próprio modelo de aplicação Compose, com o seu próprio diretório de projeto. Por isso, os caminhos relativos dentro desse ficheiro são resolvidos com base no diretório do próprio ficheiro. Essa é a diferença real em relação a -f e o motivo pelo qual include é a ferramenta correta quando o fragmento está noutra pasta ou noutro repositório. Esta é a estrutura habitual de uma stack de um fornecedor que não foi escrita por si: o ficheiro Compose com vários serviços por trás de uma instalação SSO autoalojada do Authentik pode ficar no seu próprio diretório, mantendo os próprios caminhos relativos, enquanto o seu ficheiro continua centrado nos seus próprios serviços.

A forma longa aceita subopções.

include:
  - path:
      - ../monitoring/compose.yaml
      - ../monitoring/compose.vps.yaml
    project_directory: ../monitoring
    env_file: ../monitoring/.env

path aceita uma lista, e esses ficheiros são combinados segundo as regras normais antes de o resultado ser integrado no seu modelo. project_directory define o caminho base usado para resolver os caminhos relativos no ficheiro incluído. env_file fornece ao ficheiro incluído as suas próprias variáveis para interpolação, impedindo que um fragmento partilhado leia silenciosamente o .env do seu projeto. include requer o Compose v2.20.0 ou mais recente. As mesmas opções são adequadas para um complemento de um único contentor numa stack que já executa, como Halcyon, que transforma uma biblioteca Jellyfin numa loja de aluguer dos anos 90: o respetivo ficheiro mantém a sua própria tag de imagem e o seu próprio env_file, por isso atualizá-lo nunca exige alterar o ficheiro onde a sua stack de multimédia está definida.

Os nomes de recursos duplicados entre o seu ficheiro e um ficheiro incluído são comunicados como um erro, em vez de serem combinados silenciosamente. Isto é intencional. Para alterar algo declarado por um ficheiro incluído, coloque a alteração em compose.override.yaml: a substituição é aplicada ao modelo montado, por isso pode alterar recursos incluídos sem colidir com eles. Esta prática é especialmente útil com uma stack cujo ficheiro upstream é reescrito em cada release, como os servidores de fotografias com vários contentores analisados em PhotoPrism versus Immich, onde uma ligação a localhost ou um volume adicional deve ficar na sua substituição, e não no ficheiro que a próxima atualização irá substituir.

Em resumo: include compõe aplicações separadas; -f aplica camadas de configuração a uma única aplicação.

Separação entre desenvolvimento e produção em um único VPS

Este é o padrão completo em três ficheiros. O ficheiro base declara o que é válido em todo o lado 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 a aplicação esperar por uma base de dados que responde, e não apenas por um contentor que existe, conforme explicado em healthchecks e condições de depends_on. POSTGRES_PASSWORD é interpolado a partir do ficheiro .env do projeto, que nunca deve estar no git. Consulte ficheiros env e secrets do Compose para obter variantes mais seguras.

Em seguida, compose.override.yaml, que o Compose carrega automaticamente. Este é o ficheiro 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"

Num laptop, um docker compose up sem argumentos combina esses dois ficheiros. command substitui o valor padrão da imagem porque é um valor único. LOG_LEVEL substitui info porque environment combina os valores por chave. O bind mount e as duas portas publicadas são apenas adições, e a porta da base de dados fica ligada a localhost para que um laptop numa rede partilhada não disponibilize o PostgreSQL a toda a rede.

Por fim, compose.prod.yaml. O nome não é um dos nomes que o Compose procura, portanto o ficheiro nunca é carregado por acidente.

services:
  app:
    ports:
      - "127.0.0.1:8000:3000"
    deploy:
      resources:
        limits:
          memory: 512M

No VPS, indique os dois ficheiros. É essa indicação que exclui exatamente 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 ps

ps deve listar os dois serviços como em execução, com db a apresentar (healthy). Como passou -f, compose.override.yaml não foi lido. Por isso, o comando de desenvolvimento, o bind mount do código-fonte e a porta pública 3000 não podem chegar à produção, mesmo que o ficheiro esteja no mesmo diretório. A porta 8000 está disponível apenas em localhost, pronta para um proxy. Consulte executar várias aplicações atrás do Traefik quando adicionar o segundo serviço.

Defina COMPOSE_FILE=compose.yaml:compose.prod.yaml no .env do servidor. Depois disso, os restantes comandos voltam a ser apenas docker compose logs -f app.

Uma stack com um único serviço segue a mesma estrutura, porque um rastreador de treinos openGym autoalojado tem de responder através de TLS atrás de um proxy antes de registar a primeira passkey. Um ficheiro base sem ports é o que impede uma publicação pública acidental de ultrapassar o proxy.

Leia o modelo mesclado antes de fazer o deploy

docker compose config imprime o modelo totalmente mesclado e com todas as interpolações aplicadas. Não é uma pré-visualização. É a entrada exata que o Compose vai processar. Por isso, quando a saída não corresponde ao que esperava, a saída está 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. É uma forma rápida de confirmar que um include incluiu o que esperava.

Modos de falha e o que verá

no configuration file provided: not found. O Compose não encontrou nada para ler. Está fora do diretório do projeto ou COMPOSE_FILE indica um caminho que não existe. O Compose procura o ficheiro base predefinido nos diretórios-pai, mas não procura em qualquer localização um ficheiro que tenha indicado explicitamente.

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. A interpolação é resolvida com base no ficheiro .env do projeto e no ambiente da shell. Neste caso, o diretório do projeto é o diretório do primeiro ficheiro -f. Fazer o deploy a partir de um diretório diferente daquele que contém .env gera este aviso e, em seguida, uma base de dados que recusa todas as ligações.

A sua alteração no override não aparece em docker compose config. Ou passou -f, o que desativa o carregamento automático do override, ou o Compose encontrou compose.yaml num diretório-pai e o seu ficheiro de override não está junto dele. Executar docker compose config sem outros argumentos mostra qual é o modelo que o Compose está realmente a construir.

Um bind mount está vazio e o Docker criou um diretório que não pediu. O caminho relativo foi resolvido com base no diretório do primeiro ficheiro. Corrija o caminho, passe --project-directory ou mova o fragmento para depois de include.

Os contentores voltam com nomes novos e um volume parece vazio. O nome do projeto mudou, porque segue o diretório do primeiro ficheiro. Adicione um name: de nível superior ao ficheiro base para impedir que os nomes continuem a mudar. O volume antigo continua disponível com o prefixo antigo, e docker volume ls irá mostrá-lo.

Uma porta que removeu no override continua aberta. A ports merge acrescentou a entrada em vez de a substituir. Confirme com docker compose config e, em seguida, use !override ou mova ports para fora do ficheiro base.

FAQ

O Compose carrega automaticamente o compose.override.yaml?

Sim, quando executa docker compose sem a flag -f. O Compose procura compose.yaml ou docker-compose.yaml no diretório de trabalho e nos diretórios superiores. Se existir um ficheiro de substituição no mesmo diretório, esse ficheiro é carregado em segundo lugar. Os nomes reconhecidos são compose.override.yaml, compose.override.yml, docker-compose.override.yml e docker-compose.override.yaml. A passagem de qualquer -f desativa este comportamento. Nesse caso, docker compose -f compose.yaml up lê apenas um ficheiro.

Em que ordem são combinados vários ficheiros -f?

Da esquerda para a direita. O Compose cria a configuração pela ordem em que fornece os ficheiros. Cada ficheiro substitui valores e adiciona definições às dos ficheiros anteriores. Por isso, o último ficheiro na linha prevalece 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 motivo a minha porta continua publicada depois de a substituir?

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 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 posterior. Em alternativa, não inclua ports no ficheiro base, para não existir nada a combinar.

Qual é a diferença entre include e -f?

-f combina vários ficheiros numa única aplicação. Os caminhos relativos de todos os ficheiros são resolvidos relativamente ao diretório do primeiro ficheiro. include inclui uma aplicação Compose separada. Cada ficheiro incluído mantém o seu próprio diretório do projeto, pelo que os respetivos caminhos relativos são resolvidos relativamente a esse diretório. Use -f para camadas de ambiente da sua própria stack. Use include para um fragmento mantido noutro local. include requer o Compose v2.20.0 ou posterior.

Como removo um valor definido pelo ficheiro base?

Use a tag !reset no Compose v2.24 ou posterior. Escreva ports: !reset [] ou MY_VAR: !reset null no ficheiro de substituição. O atributo volta ao valor predefinido ou a null. O valor fornecido à tag é obrigatório, mas é ignorado. Se quiser substituir um atributo em vez de o limpar, !override faz isso. Este recurso requer a versão v2.24.4 ou posterior.