SSD Nodes Learn 8GB RAM — 연 $66
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-01

Docker Compose 여러 파일 병합과 override 사용법

Docker Compose v2에서 compose.override.yaml이 자동 로드되는 조건과 파일 병합 순서, ports가 기존 포트를 계속 여는 이유, dev와 prod를 나누는 include 사용법을 설명합니다.

여러 파일에 대해 Compose가 수행하는 작업

Docker Compose는 여러 파일에서 하나의 프로젝트를 빌드할 수 있습니다. Compose는 파일을 받은 순서대로 읽고 하나의 모델로 병합하므로, 나중에 읽은 파일이 충돌하는 값에 우선합니다. 명령줄에서는 2가지 메커니즘으로 이 작업을 수행합니다. 하나는 Compose가 자체적으로 로드하는 재정의 파일이고, 다른 하나는 직접 전달하는 -f 플래그입니다. 파일 내부에는 3번째 메커니즘인 include 요소가 있으며, 앞의 2가지와 다르게 작동합니다.

병합은 단순한 덮어쓰기가 아닙니다. 매핑은 키별로 병합되고, 시퀀스는 뒤에 추가되며, 일부 필드는 전체가 교체됩니다. 이 차이 때문에 예상하지 못한 결과가 발생합니다. ports 목록은 거의 모든 사용자가 문제를 겪는 항목입니다.

이후의 모든 내용은 이전 docker-compose 스크립트가 아니라 docker compose 플러그인을 사용하는 Compose v2를 전제로 합니다. 확인하려면 docker compose version를 실행합니다. 아직 Compose 파일을 작성하지 않았다면 Docker Compose 기본 가이드부터 시작한 다음 이 문서로 돌아옵니다.

Compose가 지정하지 않아도 로드하는 override 파일

-f 플래그 없이 docker compose up을 실행하면 Compose는 작업 디렉터리와 상위 디렉터리에서 compose.yaml 또는 docker-compose.yaml을 검색합니다. 기본 파일 옆에 override 파일이 있으면 Compose는 해당 파일도 자동으로 로드합니다.

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

두 파일이 모두 있으면 두 파일을 직접 입력한 것과 같습니다.

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

Compose가 인식하는 이름은 compose.override.yaml, compose.override.yml, 그리고 이전 형식인 docker-compose.override.ymldocker-compose.override.yaml입니다. 예를 들어 compose.dev.yaml처럼 다른 이름을 사용하는 파일은 -f로 지정한 경우에만 로드됩니다.

-f을 하나라도 지정하는 순간 자동 로드가 중지됩니다. docker compose -f compose.yaml up는 지정한 파일 하나만 정확히 읽고 override 파일은 무시합니다. 이 동작을 기반으로 이 가이드에서는 이후에 dev 및 prod 패턴을 구성합니다.

서버에서는 이 동작이 양면성을 가집니다. 배포 디렉터리에 override 파일을 남겨 두면 해당 디렉터리에서 실행하는 모든 기본 docker compose 명령이 이 파일을 로드합니다. cron 작업에서 실행하는 명령도 포함됩니다. 이로 인해 운영 스택이 배포할 의도가 없었던 소스 디렉터리를 bind mount할 수 있습니다. 배포 후에는 docker compose config을 실행하고 출력 결과를 확인합니다.

-f를 사용한 순서 지정 및 상대 경로가 확인되는 위치

Compose는 파일을 지정한 순서대로 구성을 작성합니다. 이후 파일은 앞선 파일의 설정을 재정의하고 추가합니다. 왼쪽에서 오른쪽으로 적용되며 마지막 설정이 우선합니다.

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

해당 프로젝트의 모든 명령에는 동일한 파일 목록이 필요합니다. 두 파일을 사용해 up을 실행하고 한 파일을 사용해 logs을 실행하면 서로 다른 병합 모델을 사용하게 됩니다. 이는 Compose가 존재하지 않는 서비스라고 판단하는 상황을 빠르게 유발할 수 있습니다. 대신 COMPOSE_FILE 환경 변수로 목록을 한 번만 설정합니다.

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

Linux에서는 구분자가 :이며, COMPOSE_PATH_SEPARATOR이 이를 변경합니다. COMPOSE_FILE는 프로젝트의 .env 파일에 저장할 수도 있습니다. 그러면 셸 기록이 아니라 checkout의 일부가 됩니다. 명령줄에서 직접 설정한 값은 환경 변수보다 우선합니다.

이제 bind mount를 중단시키는 규칙을 설명합니다. -f와 함께 여러 파일을 사용하면 모든 파일의 상대 경로가 해당 경로를 포함한 파일이 아니라 첫 번째 파일이 있는 디렉터리를 기준으로 확인됩니다. deploy/prod/compose.prod.yaml 안에 ./data:/var/lib/postgresql/data를 작성해도 Compose는 기본 파일과 같은 위치에서 ./data을 찾습니다. 그러면 Docker는 잘못된 경로에 빈 디렉터리를 생성하고, 컨테이너는 그 안에 아무것도 없는 상태로 시작합니다. 이는 데이터 손실처럼 보이지만 실제 데이터 손실은 아닙니다. --project-directory을 전달해 기본 경로를 직접 설정하거나, 각 파일을 해당 파일이 있는 디렉터리를 기준으로 확인하는 include를 사용합니다.

프로젝트 이름도 동일한 기본 디렉터리에서 가져옵니다. 따라서 첫 번째 파일을 변경하면 프로젝트 이름이 바뀔 수 있습니다. 프로젝트 이름이 바뀌면 새 컨테이너 이름과 새 볼륨 이름이 생성됩니다. 기존 볼륨은 이전 이름으로 디스크에 그대로 남아 있습니다. 기본 파일의 최상위 수준에 name:을 설정해 프로젝트 이름을 고정합니다.

name: myapp

병합되는 필드와 대체되는 필드

Compose는 필드 이름이 아니라 값의 형식에 따라 병합합니다.

  • 단일 값 필드는 대체됩니다. image, command, entrypointmem_limit는 나중 값으로 즉시 대체됩니다. command에 인수를 하나 추가할 수 없습니다. 재정의하면 전체 줄이 다시 작성되기 때문입니다.
  • 매핑은 키별로 병합됩니다. environment, labels, volumesdevices는 두 파일의 모든 키를 유지하며, 양쪽 파일에 있는 키는 나중 파일의 값이 우선합니다. environmentlabels에서는 키가 변수 이름 또는 레이블 이름입니다. volumesdevices에서는 키가 컨테이너 경로입니다.
  • 시퀀스는 뒤에 추가됩니다. dns, dns_search, expose, tmpfsexternal_links은 연결됩니다. expose: ["3000"]를 포함하는 기본 설정을 ["4000", "5000"]을 포함하는 재정의 설정과 병합하면 ["3000", "4000", "5000"]이 생성됩니다.

4개의 시퀀스에는 식별 키가 있으므로, 해당 키가 일치하는 항목은 뒤에 추가되지 않고 병합됩니다. volumes, secretsconfigstarget를 기준으로 일치합니다. portsip, target, publishedprotocol의 조합을 기준으로 일치합니다.

ports 규칙은 함정이므로 2번 읽어야 합니다. 2개의 포트 항목은 이 4가지 부분이 모두 일치할 때만 같은 항목으로 간주됩니다. 이 중 하나라도 변경하면 Compose는 두 번째 항목을 서로 관련 없는 별도의 포트로 인식하므로 두 항목을 모두 유지합니다.

override 후에도 포트가 계속 공개되는 이유

모든 인터페이스에서 서비스를 게시하는 기본 파일입니다.

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

reverse proxy가 앞단에 배치되므로 localhost에만 바인딩하도록 작성한 override입니다.

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

정상적으로 적용되었다고 가정하기 전에 결과를 확인합니다.

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

출력에 두 항목이 모두 표시됩니다. ip 부분이 0.0.0.0127.0.0.1와 다르므로, 병합 과정에서는 서로 다른 포트로 처리됩니다. 따라서 제거하려고 한 공개 바인딩이 여전히 모델에 남아 있습니다. 이 문제는 다른 환경보다 Docker에서 더 중요합니다. 게시된 포트가 방화벽 규칙보다 먼저 iptables에 기록되기 때문입니다. 이 메커니즘은 Docker에서 게시된 포트가 ufw를 우회하는 이유에서 설명합니다.

해결 방법은 2가지입니다. 명시적인 방법은 !override 태그를 사용하는 것입니다. 이 태그는 전체 속성을 대체하고 병합 규칙을 건너뜁니다.

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

!override에는 Compose v2.24.4 이상이 필요합니다. 이식성이 높은 방법에는 태그가 필요하지 않습니다. 기본 파일에서 ports을 완전히 제외하고 환경별 파일에서만 선언합니다. 병합할 항목이 없으면 유출될 항목도 없습니다. 아래의 완성된 예제에서 이 패턴을 사용합니다.

기본 파일에서 설정한 값 삭제

!reset은 특성을 제거하여 기본값 또는 null로 되돌립니다. 값이 필요하지만 해당 값은 무시하므로, 유효하면서 비어 있는 값을 지정합니다.

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

!reset에는 Compose v2.24 이상이 필요합니다. 예를 들어 기본 파일을 직접 편집할 수 없고 가져오는 공급업체 fragment인 경우 사용합니다.

여러 부분으로 구성된 스택에 사용하는 include

include는 다른 Compose 애플리케이션을 모델에 추가합니다. 최상위 요소이며 플래그가 아닙니다.

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

include의 각 경로는 자체 Compose 애플리케이션 모델로 로드됩니다. 각 모델에는 자체 프로젝트 디렉터리가 있으므로 해당 파일의 상대 경로는 파일이 있는 디렉터리를 기준으로 확인됩니다. 이것이 -f와의 실제 차이이며, 조각이 다른 폴더나 다른 저장소에 있을 때 include가 적합한 이유입니다.

긴 형식에서는 하위 옵션을 사용할 수 있습니다.

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

path는 목록을 허용하며, 해당 파일들은 일반 규칙에 따라 먼저 병합된 후 그 결과가 모델에 추가됩니다. project_directory는 포함된 파일의 상대 경로를 확인할 때 사용할 기본 경로를 설정합니다. env_file은 포함된 파일에 자체 보간 변수를 제공하므로 공유 조각이 프로젝트의 .env를 모르게 읽는 것을 방지합니다. include는 Compose v2.20.0 이상이 필요합니다.

사용자의 파일과 포함된 파일 사이에 리소스 이름이 중복되면 조용히 병합되지 않고 오류로 보고됩니다. 이는 의도된 동작입니다. 포함된 파일에서 선언한 내용을 변경하려면 변경 사항을 compose.override.yaml에 작성합니다. 재정의는 조합된 모델에 적용되므로 포함된 리소스와 충돌하지 않고 해당 리소스를 수정할 수 있습니다.

간단히 말하면, include는 별도의 애플리케이션을 조합하고, -f는 하나의 애플리케이션에 설정을 계층적으로 적용합니다.

A dev and prod split on one VPS

Here is the whole pattern in three files. The base file declares what is true everywhere, and it publishes no ports at all.

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:

The depends_on condition is what makes the app wait for a database that answers rather than a container that merely exists, explained in healthchecks and depends_on conditions. POSTGRES_PASSWORD is interpolated from the project .env file, which never belongs in git. See env files and Compose secrets for the safer variants.

Next, compose.override.yaml, which Compose loads on its own. This is the developer's file.

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"

On a laptop, a bare docker compose up merges those two files. command replaces the image default because it is single valued. LOG_LEVEL replaces info because environment merges by key. The bind mount and the two published ports are pure additions, and the database port is bound to localhost so a laptop on a shared network is not offering PostgreSQL to the room.

Last, compose.prod.yaml. Its name is not one Compose looks for, so it is never loaded by accident.

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

On the VPS you name both files, and that act of naming is exactly what excludes the 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 should list both services as running, with db showing (healthy). Because you passed -f, compose.override.yaml was not read, so the dev command, the source bind mount and the public port 3000 cannot reach production even though the file is sitting in the same directory. Port 8000 is on localhost only, ready for a proxy: see running several apps behind Traefik when you add the second service.

Set COMPOSE_FILE=compose.yaml:compose.prod.yaml in the server's .env and the rest of your commands go back to being plain docker compose logs -f app.

배포 전에 병합된 모델 읽기

docker compose config은 완전히 병합되고 보간된 모델을 출력합니다. 이는 미리 보기가 아닙니다. Compose가 실제로 처리할 정확한 입력이므로 출력이 예상과 다르면 출력이 올바른 것입니다.

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${VAR}을 확장하지 않은 상태로 둡니다. 출력 내용을 다른 곳에 붙여넣기 전에 사용해야 합니다. 일반적인 config은 확인된 모든 보안 값을 평문으로 출력하기 때문입니다. --services은 서비스 이름만 나열하므로 include이 예상한 항목을 가져왔는지 빠르게 확인할 수 있습니다.

오류 유형과 표시되는 내용

no configuration file provided: not found. Compose가 읽을 내용을 찾지 못했습니다. 프로젝트 디렉터리 외부에 있거나 COMPOSE_FILE에 존재하지 않는 경로가 지정되어 있습니다. Compose는 기본 base 파일을 찾기 위해 상위 디렉터리를 검색하지만, 사용자가 직접 지정한 파일은 어느 위치에서도 검색하지 않습니다.

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. 보간은 프로젝트의 .env 파일과 셸 환경을 기준으로 확인됩니다. 여기서 프로젝트 디렉터리는 첫 번째 -f 파일이 있는 디렉터리입니다. .env이 있는 디렉터리와 다른 디렉터리에서 배포하면 이 경고가 표시되고, 모든 연결을 거부하는 데이터베이스가 생성됩니다.

docker compose config에 override 편집 내용이 표시되지 않습니다. -f을 전달하여 자동 override 로드를 비활성화했거나, Compose가 상위 디렉터리에서 compose.yaml을 찾았고 override 파일이 해당 파일과 같은 위치에 없기 때문입니다. 다른 인수 없이 docker compose config을 실행하면 Compose가 실제로 빌드하는 모델을 확인할 수 있습니다.

bind mount가 비어 있고 Docker가 요청하지 않은 디렉터리를 생성합니다. 상대 경로가 첫 번째 파일의 디렉터리를 기준으로 확인되었습니다. 경로를 수정하거나 --project-directory을 전달하거나, 해당 조각을 include 뒤로 이동합니다.

컨테이너가 새 이름으로 다시 생성되고 volume이 비어 있는 것처럼 보입니다. 프로젝트 이름이 변경되었기 때문입니다. 프로젝트 이름은 첫 번째 파일이 있는 디렉터리를 따릅니다. base 파일에 최상위 name:을 추가하면 이름이 더 이상 변경되지 않습니다. 이전 volume은 이전 접두사 아래에 그대로 있으며, docker volume ls로 확인할 수 있습니다.

override에서 제거한 포트가 여전히 열려 있습니다. ports 병합이 교체하지 않고 추가했기 때문입니다. docker compose config로 확인한 다음 !override을 사용하거나 ports을 base 파일에서 이동합니다.

FAQ

Compose는 compose.override.yaml을 자동으로 로드합니까?

예. -f 플래그 없이 docker compose를 실행하면 자동으로 로드합니다. Compose는 작업 디렉터리와 상위 디렉터리에서 compose.yaml 또는 docker-compose.yaml을 검색합니다. override 파일이 해당 파일과 같은 디렉터리에 있으면 두 번째로 로드합니다. 인식되는 이름은 compose.override.yaml, compose.override.yml, docker-compose.override.ymldocker-compose.override.yaml입니다. -f를 하나라도 지정하면 이 동작이 비활성화되므로 docker compose -f compose.yaml up은 파일 하나만 읽습니다.

여러 -f 파일은 어떤 순서로 병합됩니까?

왼쪽에서 오른쪽 순서로 병합됩니다. Compose는 지정한 순서대로 파일의 구성을 작성합니다. 각 파일은 앞선 파일의 설정을 재정의하고 추가합니다. 따라서 명령줄의 마지막 파일이 충돌하는 설정을 우선합니다. 해당 프로젝트의 모든 명령에는 동일한 파일 목록을 사용해야 하며, COMPOSE_FILE=compose.yaml:compose.prod.yaml이 이 목적에 사용됩니다.

재정의했는데 포트가 계속 게시되는 이유는 무엇입니까?

ports 항목은 ip, target, publishedprotocol의 전체 집합으로 식별되기 때문입니다. 8080:80을 기반으로 127.0.0.1:8080:80을 재정의하면 ip 부분이 달라집니다. 따라서 Compose는 이를 두 번째 포트로 처리하고 두 항목을 모두 유지합니다. docker compose config을 실행하면 두 항목이 표시됩니다. Compose v2.24.4 이상에서는 ports: !override을 사용하십시오. 또는 병합할 대상이 없도록 기본 파일에서 ports을 제외하십시오.

include와 -f의 차이점은 무엇입니까?

-f은 여러 파일을 하나의 애플리케이션에 계층화합니다. 모든 파일의 상대 경로는 첫 번째 파일의 디렉터리를 기준으로 확인됩니다. include은 별도의 Compose 애플리케이션을 가져옵니다. 포함된 애플리케이션의 각 경로는 자체 프로젝트 디렉터리를 유지하므로 상대 경로가 해당 애플리케이션 자체를 기준으로 확인됩니다. 자체 스택의 환경 계층에는 -f을 사용하고, 외부에서 관리하는 fragment에는 include을 사용하십시오. include에는 Compose v2.20.0 이상이 필요합니다.

기본 파일이 설정한 값을 제거하려면 어떻게 합니까?

Compose v2.24 이상에서는 !reset 태그를 사용하십시오. 재정의 파일에 ports: !reset [] 또는 MY_VAR: !reset null을 작성하면 해당 속성이 기본값 또는 null로 돌아갑니다. 태그에 지정하는 값은 필수이지만 무시됩니다. 속성을 지우는 대신 교체하려면 !override을 사용하십시오. 이 기능에는 v2.24.4 이상이 필요합니다.