Skip to main content
Summary

Compose 파일이 커질 때 생기는 문제

앞 장에서는 web, app, db로 구성된 Compose 환경을 만들었다. 처음에는 하나의 compose.yaml만으로 충분하다. 하지만 실제 프로젝트에서는 곧 이런 요구가 생긴다.

  • 개발 환경에서는 소스 코드를 바인드 마운트하고 개발 서버를 실행하고 싶다.

  • 테스트 환경에서는 별도의 데이터베이스 이름과 임시 설정을 쓰고 싶다.

  • 운영 환경에서는 불필요한 관리자 도구를 띄우고 싶지 않다.

  • Redis, Grafana 같은 보조 서비스는 필요할 때만 실행하고 싶다.

  • 같은 서버에서 같은 Compose 프로젝트를 여러 벌 실행해야 한다.

이때 모든 설정을 하나의 파일에 계속 밀어 넣으면 파일은 빠르게 복잡해진다. 실무적인 Compose 사용의 핵심은 "기본 설정은 공통으로 두고, 달라지는 설정만 분리하는 것"이다. 이번 장에서는 .env, environment, env_file, 여러 Compose 파일, profiles, 프로젝트 이름을 사용해 이 문제를 해결한다.

Compose 파일을 실무적으로 확장하기AI 생성 이미지

.env, environment, env_file의 차이

Compose에서 환경 변수를 다룰 때 가장 많이 헷갈리는 지점은 .env와 env_file이다. 이름은 비슷하지만 역할이 다르다.

.env 파일은 Compose 파일을 읽을 때 ${변수명} 값을 채우기 위해 사용된다. 즉, Compose 설정 파일의 빈칸을 채우는 용도다. 반면 environment와 env_file은 컨테이너 안에 환경 변수를 전달하는 용도다.

예를 들어 프로젝트 루트에 다음 .env 파일을 만든다.

COMPOSE_PROJECT_NAME=myshop
POSTGRES_DB=shop
POSTGRES_USER=shop
POSTGRES_PASSWORD=dev_password
APP_PORT=8080

그리고 compose.yaml에서 이 값을 사용한다.

services:
  app:
    build: ./app
    ports:
      - "${APP_PORT:-3000}:3000"
    environment:
      NODE_ENV: ${NODE_ENV:-development}
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

여기서 .env의 APP_PORT, POSTGRES_USER 같은 값은 Compose가 compose.yaml을 해석할 때 사용된다. 하지만 .env에 적었다고 해서 모든 값이 자동으로 컨테이너 안에 들어가는 것은 아니다. 컨테이너 안에서 필요한 값은 위 예제처럼 environment에 명시해야 한다.

env_file은 여러 환경 변수를 파일로 묶어 컨테이너에 전달할 때 쓴다.

# app.env
LOG_LEVEL=debug
FEATURE_SIGNUP=true
services:
  app:
    build: ./app
    env_file:
      - ./app.env
    environment:
      NODE_ENV: development

이 경우 LOG_LEVEL, FEATURE_SIGNUP은 app 컨테이너 안의 환경 변수로 들어간다. environment와 env_file에 같은 이름의 변수가 함께 있다면, 보통 Compose 파일에 직접 적은 environment 값이 더 명시적인 설정으로 쓰인다. 그래서 공통 값은 env_file에 두고, 꼭 덮어써야 하는 값만 environment에 적으면 관리하기 쉽다.

정리하면 다음과 같다.

구분주된 역할컨테이너 안으로 자동 전달 여부
.envCompose 파일의 ${VAR} 치환아니오
environment특정 서비스 컨테이너에 환경 변수 직접 전달예
env_file파일에 적힌 값을 특정 서비스 컨테이너에 전달예

민감 정보도 주의해야 한다. 비밀번호, 토큰, 개인 키를 .env에 넣을 수는 있지만, 이 파일을 Git에 올리면 안 된다. 보통 실제 .env는 .gitignore에 추가하고, 필요한 변수 이름만 담은 .env.example을 저장소에 남긴다.

# .env.example
POSTGRES_DB=shop
POSTGRES_USER=shop
POSTGRES_PASSWORD=change_me
APP_PORT=8080

운영 환경의 진짜 비밀번호는 저장소가 아니라 CI/CD 비밀 값, 클라우드 비밀 관리 도구, Docker secrets 같은 별도 수단으로 관리하는 편이 안전하다. 환경 변수는 편리하지만 docker inspect나 프로세스 정보로 노출될 수 있으므로 "비밀 값을 영구적으로 안전하게 보관하는 장소"로 생각하면 안 된다.

개발용 설정은 compose.override.yaml로 분리하기

Compose는 기본적으로 compose.yaml을 읽는다. 같은 디렉터리에 compose.override.yaml이 있으면 함께 읽어 기본 설정 위에 덧붙인다. 이 파일은 로컬 개발자마다 필요한 설정을 얹을 때 유용하다.

기본 compose.yaml은 최대한 환경 공통 설정만 담는다.

services:
  app:
    build: ./app
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://shop:${POSTGRES_PASSWORD}@db:5432/shop
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: shop
      POSTGRES_USER: shop
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

개발용 compose.override.yaml에는 개발할 때만 필요한 설정을 둔다.

services:
  app:
    environment:
      NODE_ENV: development
    volumes:
      - ./app:/usr/src/app
    command: npm run dev
    ports:
      - "3000:3000"

  db:
    ports:
      - "5432:5432"

이렇게 하면 docker compose up -d만 실행해도 두 파일이 합쳐진다.

docker compose up -d
docker compose config

docker compose config는 여러 파일과 환경 변수 치환이 끝난 최종 설정을 보여 준다. Compose 파일을 나누기 시작했다면 이 명령을 자주 확인하는 습관을 들이는 것이 좋다.

여러 파일이 합쳐질 때는 "뒤 파일이 앞 파일을 전부 덮어쓴다"라고 단순하게 생각하면 안 된다. command, image처럼 하나의 값만 갖는 항목은 뒤 파일의 값으로 바뀌지만, ports 같은 일부 목록 항목은 앞 파일의 값에 뒤 파일의 값이 추가될 수 있다. environment처럼 이름이 있는 값은 같은 변수명은 덮어쓰고, 새로운 변수명은 추가된다.1

그래서 파일을 분리한 뒤에는 반드시 실제 병합 결과를 확인해야 한다.

docker compose config

특히 바인드 마운트나 build.context, env_file처럼 경로를 쓰는 항목도 주의해야 한다. 여러 Compose 파일을 -f로 합칠 때 상대 경로는 보통 첫 번째 Compose 파일, 즉 기준 파일을 중심으로 해석된다. override 파일이 다른 디렉터리에 있더라도 그 파일의 위치를 기준으로 경로가 잡히는 것이 아니므로, 경로가 예상과 다르게 해석되지 않았는지 docker compose config로 확인하는 편이 안전하다.

단, compose.override.yaml을 항상 Git에 올릴 필요는 없다. 팀 전체가 공유하는 개발 설정이면 저장소에 포함하고, 개인별 포트나 로컬 경로처럼 사람마다 다른 설정이면 compose.local.yaml 같은 이름으로 따로 두고 Git에서 제외하는 방식이 낫다.

테스트용 설정은 별도 파일로 명확히 실행하기

테스트 환경은 개발 환경과 목적이 다르다. 개발 환경은 사람이 브라우저로 확인하기 편해야 하지만, 테스트 환경은 반복 실행과 격리가 중요하다. 이럴 때는 -f 옵션으로 Compose 파일을 여러 개 지정한다. Compose는 여러 파일을 순서대로 합쳐 환경별 설정을 만들 수 있다.1

예를 들어 compose.test.yaml을 만든다.

services:
  app:
    environment:
      NODE_ENV: test
      DATABASE_URL: postgres://test:test_password@db:5432/shop_test
    command: npm test

  db:
    environment:
      POSTGRES_DB: shop_test
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test_password

테스트 실행은 다음처럼 한다.

docker compose \
  -p myshop-test \
  -f compose.yaml \
  -f compose.test.yaml \
  up --build --abort-on-container-exit --exit-code-from app

여기서 중요한 옵션은 -p myshop-test다. Compose 프로젝트 이름을 개발 환경과 다르게 지정하면 네트워크, 볼륨, 컨테이너 이름이 분리된다. 덕분에 개발용 myshop 환경을 켜 둔 상태에서도 테스트용 myshop-test 환경을 따로 실행할 수 있다.

또 하나 기억할 점이 있다. 위처럼 -f compose.yaml -f compose.test.yaml을 직접 지정하면, 기본 개발용 파일인 compose.override.yaml은 자동으로 끼어들지 않는다. 테스트 실행에 개발용 포트 공개나 바인드 마운트가 섞이지 않게 하려면, 이처럼 사용할 파일 목록을 명확히 적는 편이 좋다.

테스트 후에는 정리한다.

docker compose \
  -p myshop-test \
  -f compose.yaml \
  -f compose.test.yaml \
  down -v

down -v는 Compose가 만든 볼륨까지 삭제한다. 테스트 데이터처럼 보존할 필요가 없는 데이터에는 적합하지만, 개발 데이터베이스에 실수로 실행하면 데이터가 사라질 수 있다. 프로젝트 이름과 파일 목록을 확인한 뒤 실행해야 한다.

profiles로 선택 서비스 실행하기

모든 서비스를 항상 실행할 필요는 없다. 예를 들어 PostgreSQL은 애플리케이션 실행에 필수일 수 있지만, Adminer 같은 데이터베이스 관리 도구나 Grafana 같은 모니터링 도구는 매번 필요하지 않다. Redis도 기능 개발 중에는 필요하지만 단순 화면 작업에는 필요 없을 수 있다.

이럴 때 profiles를 사용한다. 프로필이 없는 서비스는 기본으로 실행되고, 프로필이 있는 서비스는 해당 프로필을 켰을 때만 함께 실행된다.2

services:
  app:
    build: ./app
    environment:
      DATABASE_URL: postgres://shop:${POSTGRES_PASSWORD}@db:5432/shop
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: shop
      POSTGRES_USER: shop
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    profiles: ["cache"]

  adminer:
    image: adminer
    profiles: ["devtools"]
    ports:
      - "8081:8080"

  grafana:
    image: grafana/grafana-oss:latest
    profiles: ["monitoring"]
    ports:
      - "3001:3000"

volumes:
  db-data:

기본 서비스만 실행한다.

docker compose up -d

Redis까지 필요하면 cache 프로필을 켠다.

docker compose --profile cache up -d

관리자 도구와 모니터링 도구를 함께 실행할 수도 있다.

docker compose --profile devtools --profile monitoring up -d

매번 옵션을 입력하기 번거롭다면 .env에 활성화할 프로필을 적을 수 있다.

COMPOSE_PROFILES=devtools,cache

이제 docker compose up -d만 실행해도 devtools, cache 프로필이 함께 켜진다. 단, 팀 프로젝트에서는 이 설정을 조심해야 한다. 어떤 개발자는 가벼운 기본 환경을 원하고, 어떤 개발자는 모든 도구를 켜고 싶어 한다. 팀 공통 .env.example에는 가능한 값을 안내하고, 실제 .env는 개인이 선택하게 두는 편이 좋다.

참고로 프로필이 붙은 서비스라도 명령에서 서비스 이름을 직접 지정하면 그 서비스만 실행 대상으로 삼을 수 있다. 예를 들어 docker compose up redis처럼 실행하면 redis 서비스는 실행된다. 하지만 일상적인 개발 명령에서는 --profile cache처럼 프로필을 켜는 방식이 더 의도가 분명하다.

프로젝트 이름, 서비스 이름, 컨테이너 이름

Compose를 실무에서 쓰다 보면 이름 때문에 생기는 문제가 많다. 특히 같은 서버에서 여러 프로젝트를 실행하거나, 같은 프로젝트를 개발용·테스트용으로 나눠 실행할 때 그렇다.

Compose에는 세 가지 이름이 있다.

첫째, 프로젝트 이름이다. 프로젝트 이름은 Compose가 만드는 리소스 이름의 앞부분에 붙는다. 예를 들어 프로젝트 이름이 myshop이고 서비스가 app, db라면 컨테이너 이름은 대체로 myshop-app-1, myshop-db-1처럼 만들어진다. 네트워크와 볼륨 이름에도 프로젝트 이름이 붙어 서로 다른 Compose 프로젝트가 충돌하지 않도록 돕는다.3

프로젝트 이름은 보통 다음 방법 중 하나로 정한다.

# 명령 실행 시 직접 지정
docker compose -p myshop-dev up -d

# 환경 변수로 지정
COMPOSE_PROJECT_NAME=myshop-dev docker compose up -d

또는 Compose 파일에 name을 둘 수도 있다.

name: myshop

services:
  app:
    build: ./app

둘째, 서비스 이름이다. services 아래의 app, db, redis 같은 이름이 서비스 이름이다. 같은 Compose 네트워크 안에서는 이 서비스 이름이 호스트명처럼 동작한다. 그래서 애플리케이션은 데이터베이스에 접속할 때 localhost가 아니라 db를 사용해야 한다.

셋째, 컨테이너 이름이다. Compose에는 container_name이라는 항목도 있지만, 특별한 이유가 없다면 쓰지 않는 편이 좋다.

services:
  app:
    image: myapp
    container_name: myapp

이렇게 고정하면 이름은 보기 쉬워지지만 문제가 생긴다. 같은 Compose 프로젝트를 두 벌 띄우기 어렵고, 서비스를 여러 개로 확장하는 것도 불편해진다. 예를 들어 app 서비스를 두 개 이상 띄우고 싶어도 모든 컨테이너가 같은 container_name을 가질 수는 없다. Compose가 프로젝트 이름과 서비스 이름을 조합해 자동으로 이름을 만들게 두는 편이 충돌이 적다.

실무에서 추천하는 구성

처음부터 파일을 많이 나누면 오히려 어렵다. 다음 정도의 구성이 가장 다루기 쉽다.

myshop/
├── compose.yaml              # 공통 설정
├── compose.override.yaml     # 팀 공통 개발 설정
├── compose.test.yaml         # 테스트 설정
├── .env.example              # 필요한 변수 목록
├── .env                      # 개인 로컬 값, Git 제외
└── app/

실행 명령은 목적별로 정리해 둔다.

# 개발 환경
docker compose up -d --build

# 최종 설정 확인
docker compose config

# 테스트 환경
docker compose -p myshop-test -f compose.yaml -f compose.test.yaml up --build --abort-on-container-exit --exit-code-from app

# Redis와 관리자 도구까지 포함한 개발 환경
docker compose --profile cache --profile devtools up -d

# 정리
docker compose down

팀에서 명령이 길어지기 시작하면 README나 Makefile에 명령을 적어 두는 것도 좋다. Compose 파일을 나누는 목적은 멋진 구조를 만드는 것이 아니라, 누구나 같은 방식으로 실행하고 같은 결과를 얻도록 만드는 것이다.

핵심은 단순하다. 공통 설정은 compose.yaml에 둔다. 개발, 테스트, 운영처럼 달라지는 설정은 별도 파일로 나눈다. 항상 필요하지 않은 서비스는 profiles로 숨긴다. 그리고 프로젝트 이름을 의도적으로 정해 리소스 충돌을 막는다.

Compose 파일이 커진다는 것은 애플리케이션 구성이 현실에 가까워지고 있다는 뜻이다. 이때 설정을 잘 나누면 복잡도는 늘어나도 혼란은 줄어든다.

참고

1Use multiple Compose files | Docker Docs

2Using profiles with Compose | Docker Docs

3Configure pre-defined environment variables in Docker Compose

책 목록

Docker와 Docker Compose 입문: 컨테이너 개발 환경 구축부터 운영 준비까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기