Docker Compose 완전 정복: 지저분한 환경변수, GitHub Actions에서 탈출하기

Jayson·2025년 9월 19일
0
post-thumbnail

서론: 환경 변수 관리의 어려움

개발 과정에서 "제 로컬 환경에서는 문제가 없는데요."라는 상황을 마주하는 경우가 많아요. 이러한 문제의 원인 중 하나로 환경 변수(Environment Variables) 관리를 꼽을 수 있어요. 특히 CI/CD 파이프라인, 로컬 개발 환경, 실제 운영 서버 간의 설정 불일치는 예측하기 어려운 버그와 배포 실패로 이어지곤 해요.

과거에는 GitHub Actions의 Secrets 기능을 사용해서 애플리케이션의 환경 변수를 관리했어요. 처음에는 편리했지만, 프로젝트 규모가 커지면서 몇 가지 문제점이 발생했습니다. 예를 들어, 데이터베이스 URL을 변경하기 위해 수많은 GitHub Actions Secrets를 개별적으로 수정하는 작업은 생산성을 저해하는 요인이었어요. 더 큰 문제는 로컬 환경의 .env 파일, CI/CD 환경의 GitHub Actions Secrets, 그리고 EC2 인스턴스에 직접 설정된 값들이 서로 달라 발생하는 '설정 지옥(configuration hell)'이었습니다. 이러한 불일치는 로컬 테스트는 통과하지만 CI/CD 파이프라인에서는 실패하는 상황을 반복적으로 만들었죠.

이러한 문제를 해결하고 모든 환경에서 설정의 일관성과 유지보수성을 확보하고자 Docker Compose를 도입하게 됐습니다. Docker Compose는 여러 컨테이너를 실행하는 도구를 넘어, 애플리케이션의 구조와 설정을 코드로 관리하는 강력한 수단을 제공해요. 이 글에서는 Docker Compose의 단순한 사용법을 나열하는 대신, GitHub Actions와 연동하며 겪었던 실제 문제를 해결하고, 로컬부터 운영까지 일관된 환경 변수 관리 전략을 구축한 경험을 공유하고자 합니다.


1. Docker Compose의 세 가지 환경 변수 관리 방법

효과적인 CI/CD 파이프라인 개선을 위해서는 Docker Compose가 제공하는 세 가지 핵심적인 환경 변수 주입 방법을 이해해야 해요. 각 방법은 고유한 사용 사례와 장단점을 가지며, 이를 명확히 파악하는 것이 중요합니다.

1. environment 섹션을 이용한 직접 명시

가장 간단한 방법은 docker-compose.yml 파일 내 environment 키를 사용해 환경 변수를 직접 정의하는 것이에요.

# docker-compose.yml
services:
  backend:
    image: my-app:latest
    environment:
      - NODE_ENV=development
      - LOG_LEVEL=debug

이 방식은 NODE_ENV나 기본 포트처럼 환경에 따라 변하지 않는 비민감성 설정에 적합해요. 하지만 API 키나 데이터베이스 암호 같은 민감 정보를 버전 관리 파일에 직접 기록하는 것은 심각한 보안 취약점을 야기할 수 있으므로, 민감 정보 관리에는 사용해서는 안 됩니다.

2. .env 파일을 활용한 로컬 환경 구성

Docker Compose는 프로젝트의 루트 디렉터리에 위치한 .env 파일을 자동으로 인식하고 그 안의 변수들을 불러와요. 이 변수들은 docker-compose.yml 파일 내에서 ${VARIABLE} 구문을 통해 사용되거나 컨테이너 내부로 직접 전달될 수 있습니다.

# .env
TAG=latest
WEB_PORT=8080
DB_HOST=localhost
# docker-compose.yml
services:
  frontend:
    image: my-frontend:${TAG}
    ports:
      - "${WEB_PORT}:80"
  backend:
    image: my-backend:latest
    environment:
      - DB_HOST=${DB_HOST}

이 방식은 로컬 개발 환경의 편의성을 크게 높여줘요. 각 개발자는 자신의 환경에 맞는 .env 파일을 유지할 수 있으며, 이 파일은 .gitignore에 추가하여 Git 저장소에 포함되지 않도록 관리합니다. 이를 통해 민감 정보 유출을 방지하고 개발 환경의 유연성을 확보할 수 있죠.

3. env_file 옵션을 통한 환경별 설정 분리

env_file 옵션은 서비스별로 특정 환경 변수 파일을 지정하여, 컨테이너 실행 시점에 해당 파일의 내용을 주입하는 방식이에요. 이를 통해 개발(dev), 스테이징(staging), 운영(prod) 등 환경별로 설정 파일을 체계적으로 분리하여 관리할 수 있습니다.

/config
  - dev.env
  - prod.env
docker-compose.yml
# docker-compose.yml
services:
  api:
    image: my-api:latest
    env_file:
      - ./config/prod.env

여기서 루트 .env 파일과 env_file 옵션의 동작 방식 차이를 이해하는 것이 중요해요. 루트 .env 파일은 Docker Compose가 docker-compose.yml 파일을 해석하기 전에 읽어 들여 파일 내 변수 치환에 사용돼요. 반면 env_file 옵션은 컨테이너가 실행되는 시점에 컨테이너 내부에 환경 변수를 설정하는 역할을 합니다. 따라서 env_file에 정의된 변수로는 docker-compose.yml 파일 내의 image: my-app:${TAG}와 같은 값을 치환할 수 없어요.


2. GitHub Actions 워크플로우 마이그레이션

이론을 바탕으로 실제 GitHub Actions 워크플로우를 개선하는 과정을 살펴볼게요. 기존 방식과 Docker Compose를 도입하여 개선된 방식을 비교하며 설명합니다.

Before: 기존의 복잡한 방식

기존 워크플로우는 docker run 명령어에 수많은 -e 플래그를 통해 GitHub Actions Secrets를 직접 주입하는 방식을 사용했어요.

# .github/workflows/deploy.yml - BEFORE
# ... (설정 생략)
        script: |
          docker pull my-app:latest
          docker stop my-app-container || true
          docker rm my-app-container || true
          docker run -d --name my-app-container \
            -p 80:8080 \
            -e DATABASE_URL=${{ secrets.DATABASE_URL }} \
            -e REDIS_HOST=${{ secrets.REDIS_HOST }} \
            -e API_KEY=${{ secrets.API_KEY }} \
            -e JWT_SECRET=${{ secrets.JWT_SECRET }} \
            -e SENTRY_DSN=${{ secrets.SENTRY_DSN }} \
            #... 그리고 15개 이상의 -e 플래그들...
            my-app:latest

이 방식은 docker run 명령어가 길어져 가독성이 떨어지고, 환경 간 설정 불일치 문제가 발생하기 쉬우며, 사소한 실수로 인한 배포 실패율이 높다는 명확한 단점이 있었어요.

After: 선언적 구성을 통한 개선

새로운 접근 방식은 애플리케이션의 구조를 docker-compose.yml로 중앙화하고, CI/CD 파이프라인에서는 배포 서버에 .env 파일을 동적으로 생성하도록 변경하는 것이에요.

1단계: docker-compose.yml 파일 작성

먼저, 애플리케이션의 구성을 정의하는 docker-compose.yml 파일을 프로젝트에 생성하고 버전 관리에 포함시킵니다.

# docker-compose.yml
version: '3.8'
services:
  app:
    image: my-app:latest
    container_name: my-app-container
    ports:
      - "80:${APP_PORT}"
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_HOST=${REDIS_HOST}
      - API_KEY=${API_KEY}
      - JWT_SECRET=${JWT_SECRET}
      - SENTRY_DSN=${SENTRY_DSN}
      #... 다른 모든 변수들도 여기에 선언

2단계: GitHub Actions 워크플로우 수정

다음으로, 배포 워크플로우가 서버에 접속한 뒤 GitHub Actions Secrets를 이용해 .env 파일을 생성하고, docker-compose up 명령어로 서비스를 실행하도록 수정해요.

# .github/workflows/deploy.yml - AFTER
# ... (설정 생략)
        script: |
          # GitHub Secrets로부터 동적으로 .env 파일 생성
          echo "Creating .env file on server..."
          echo "APP_PORT=8080" > /path/to/app/.env
          echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> /path/to/app/.env
          echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> /path/to/app/.env
          echo "API_KEY=${{ secrets.API_KEY }}" >> /path/to/app/.env
          echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> /path/to/app/.env
          echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> /path/to/app/.env

          # 최신 이미지를 pull하고 Docker Compose로 재시작
          cd /path/to/app
          docker pull my-app:latest
          docker-compose up -d --force-recreate

이러한 변화를 통해 배포 로직이 docker-compose up -d라는 단일 명령어로 간결해지고, docker-compose.yml 파일이 버전 관리되어 코드로서의 인프라(Infrastructure as Code)를 실현할 수 있어요. 또한, 개발자와 CI/CD 환경 모두 동일한 명령어를 사용하게 되어 환경 불일치 문제를 근본적으로 해결하고, 민감 정보는 GitHub Actions Secrets에서 안전하게 중앙 관리할 수 있습니다.


3. 운영 환경을 위한 고급 전략 및 주의사항

워크플로우를 성공적으로 개선했다면, 운영 환경에서 발생할 수 있는 잠재적 문제를 예방하고 시스템을 더 견고하게 만들 전략들을 알아볼게요.

.gitignore의 중요성

가장 기본적이면서도 중요한 규칙은 .env 파일이나 민감 정보가 포함된 파일을 Git 저장소에 커밋하지 않는 것이에요. 이를 위해 .gitignore 파일을 적극적으로 활용해야 합니다.

# .gitignore

# Environment variables
.env
*.env
.env.*

# .env.example 파일은 예외로 두어, 필요한 환경 변수 목록을 공유
!/.env.example

.env.example 파일을 만들어 필요한 환경 변수 목록과 형식을 명시하고 이 파일은 저장소에 커밋하는 것이 좋은 관행이에요. 이는 새로운 팀원에게 필요한 설정을 알려주는 문서 역할을 합니다.

환경 변수 우선순위 이해

Docker Compose에서는 여러 소스를 통해 동일한 환경 변수가 정의될 수 있어요. 이때 어떤 값이 최종적으로 적용되는지 결정하는 우선순위 규칙을 이해하는 것은 중요해요.

우선순위는 높은 순서부터 CLI 인수, docker-compose.ymlenvironment, env_file 옵션, 루트 .env 파일, Dockerfile의 ENV 명령어 순으로 적용됩니다.

우선순위소스예시비고
1 (가장 높음)CLI run -edocker compose run -e DEBUG=true web...다른 모든 설정을 덮어씁니다.
2environment 키environment:Compose 파일에 명시적으로 설정된 값입니다.
3env_file 옵션env_file: [ "prod.env" ]지정된 파일에서 값을 로드합니다.
4.env 파일프로젝트 루트의 .env 파일 내 DEBUG=trueCompose가 자동으로 로드하는 기본값입니다.
5 (가장 낮음)Dockerfile ENVENV DEBUG=false이미지에 내장된 기본값입니다.

Docker Secrets와의 비교

환경 변수 방식은 효율적이지만, docker inspect 명령어로 쉽게 조회되거나 로그에 포함되어 유출될 위험이 있어요. 더 높은 수준의 보안이 요구되는 환경에서는 Docker Secrets 사용이 권장돼요.

Docker Secrets는 환경 변수 대신, 컨테이너 내부의 특정 경로(기본적으로 /run/secrets/<secret_name>)에 읽기 전용 파일로 마운트됩니다.

# docker-compose.yml with secrets
version: '3.8'
services:
  db:
    image: postgres:14
    environment:
      # _FILE 접미사는 환경 변수 대신 파일에서 비밀번호를 읽도록 지시합니다.
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./db_password.txt

이 방식은 애플리케이션 프로세스가 공격당하더라도 파일 시스템의 특정 경로를 직접 읽지 않는 한 핵심 정보가 노출되지 않아, 최소 권한 원칙을 준수하는 더 안전한 방법입니다.


결론: 일관된 환경 변수 관리의 중요성

우리는 분산된 설정, 환경 간 불일치, 수동 작업의 비효율성이라는 문제에서 출발하여 Docker Compose를 통해 환경 변수 관리를 중앙화하고 자동화하는 과정을 거쳤어요.

docker-compose.yml을 설정의 '단일 진실 공급원(Single Source of Truth)'으로 삼고, CI/CD 파이프라인에서 .env 파일을 동적으로 생성하는 전략은 다음과 같은 이점을 가져왔습니다.

  • 개발 생산성 향상: "내 컴퓨터에서는 되는데"라는 고질적인 문제를 해결하여 디버깅 시간을 줄였습니다.
  • 휴먼 에러 감소: 복잡한 docker run 명령어를 간결한 docker-compose up으로 대체하여 배포 과정의 실수를 최소화했습니다.
  • 환경 간 일관성 확보: 모든 환경에서 동일한 방식으로 설정을 관리하여 예측 가능하고 안정적인 배포 파이프라인을 구축했습니다.

이 글에서 제시한 방법은 단순히 기술적인 기법이 아니라, 성숙한 DevOps 문화를 구축하는 과정의 일부라고 할 수 있어요. Docker Compose를 통해 선언적이고 자동화된 방식으로 설정을 관리함으로써, 우리는 더 중요한 가치인 안정적인 서비스 개발과 운영에 집중할 수 있습니다.

profile
Small Big Cycle

0개의 댓글