← 블로그(으)로 돌아가기

블로그

격리되고 재현 가능한 웹 호스팅을 위한 Docker Compose 마스터하기

Docker Compose를 활용하여 격리되고 재현 가능하며 쉽게 관리할 수 있는 웹 호스팅 환경을 구축하고 일반적인 배포 문제를 해결하는 방법을 알아보세요.

요약

"내 컴퓨터에서는 작동하는데"라는 문제는 웹 개발자와 시스템 관리자에게 지속적인 골칫거리입니다. Docker는 컨테이너화 기술을 통해 애플리케이션과 그 종속성을 격리된 환경으로 패키징하여 강력한 솔루션을 제공합니다. 하지만 웹 서버, 데이터베이스, 캐싱 계층과 같이 서로 연결된 여러 서비스를 관리하는 것은 복잡해질 수 있습니다. 이 글에서는 다중 컨테이너 Docker 애플리케이션의 정의 및 관리를 단순화하는 강력한 도구인 Docker Compose에 대해 자세히 알아봅니다. 단일 구성 파일에서 전체 웹 호스팅 스택을 정의하는 방법을 살펴보고, 개발, 스테이징 및 프로덕션 전반에 걸쳐 일관성을 보장하며, 궁극적으로 더 안정적이고 재현 가능한 배포로 이어지도록 합니다.

'내 컴퓨터에서는 작동하는데'를 넘어서: Docker Compose로 웹 호스팅 스택 관리하기

악명 높은 "내 컴퓨터에서는 작동하는데" 증후군은 소프트웨어 개발에서 보편적인 고통점입니다. 이는 개발자의 로컬 환경과 프로덕션 서버 간의 단절을 의미하며, 좌절스러운 디버깅 세션과 신뢰할 수 없는 배포로 이어집니다. Docker는 컨테이너화 기술을 통해 일관된 실행 환경을 약속하는 강력한 해독제로 부상했습니다. 하지만 웹 애플리케이션이 단일 프로세스가 아니라 웹 서버, 데이터베이스, 캐싱 계층, 메시지 큐 등 복잡한 서비스 생태계라면 어떻게 될까요?

다른 환경에서 이러한 상호 연결된 구성 요소를 수동으로 관리하는 것은 빠르게 혼돈으로 이어질 수 있습니다. 바로 여기서 Docker Compose가 빛을 발합니다. 간단한 YAML 파일을 사용하여 다중 컨테이너 Docker 애플리케이션을 정의하고 실행할 수 있는 도구입니다. 개별 컨테이너 명령과 씨름하는 대신, 전체 애플리케이션의 서비스, 네트워크 및 볼륨을 설명하면 Docker Compose가 이를 오케스트레이션하는 작업을 처리합니다.

이 글에서는 격리되고 재현 가능하며 관리 가능한 웹 호스팅 환경을 구축하기 위한 Docker Compose의 실제 적용 사례를 안내합니다. 기본 Docker 사용을 넘어 배포 마찰을 최소화하고 안정성을 극대화하는 강력한 호스팅 설정을 구축하는 방법을 시연합니다.

문제: 현대 웹 스택의 복잡성

현대 웹 애플리케이션은 거의 진공 상태에서 존재하지 않습니다. 일반적인 설정에는 다음이 포함될 수 있습니다.

  • 웹 서버: 애플리케이션의 프런트엔드를 제공합니다 (예: Nginx, Apache).
  • 애플리케이션 서버/런타임: 백엔드 코드를 실행합니다 (예: Node.js, Python/Gunicorn, PHP-FPM).
  • 데이터베이스: 영구 데이터를 저장합니다 (예: PostgreSQL, MySQL, MongoDB).
  • 캐시: 자주 액세스하는 데이터를 저장하여 성능을 향상시킵니다 (예: Redis, Memcached).
  • 기타 서비스: 메시지 큐, 검색 엔진 또는 백그라운드 작업 프로세서 등.

이러한 각 구성 요소는 자체 종속성, 구성 요구 사항 및 네트워킹 요구 사항을 가지고 있습니다. 새 서버 또는 개발자의 노트북에서 각 구성 요소를 수동으로 설정하고 구성하는 것은 시간이 많이 걸리고 오류가 발생하기 쉬우며 일관되게 복제하기 어렵습니다. 이는 다음과 같은 결과를 초래합니다.

  • 일관성 없는 환경: 개발, 스테이징 및 프로덕션 환경 간의 차이점.
  • 종속성 지옥: 라이브러리 또는 시스템 패키지의 다른 버전 간의 충돌.
  • 수동 구성 오류: 설정 중 오타 또는 누락된 단계.
  • 어려운 온보딩: 새 팀원이 개발 환경을 실행하는 데 어려움을 겪습니다.
  • 느린 배포 주기: 개발에서 프로덕션으로 코드를 가져오는 프로세스가 번거롭습니다.

솔루션: 선언적 인프라를 위한 Docker Compose

Docker Compose는 단일 docker-compose.yml 파일에서 전체 애플리케이션 스택을 정의할 수 있도록 하여 이러한 문제를 해결합니다. 이 파일은 각 서비스, 해당 이미지, 포트, 볼륨, 환경 변수 및 서비스 간의 통신 방법을 지정하는 청사진 역할을 합니다.

docker-compose.yml의 주요 개념:

  • version: Compose 파일 형식 버전을 지정합니다. 최신 버전을 사용하는 것이 좋습니다.
  • services: 애플리케이션의 각 컨테이너화된 구성 요소를 정의하는 핵심 섹션입니다.
    • image: 서비스에 사용할 Docker 이미지 (예: nginx:latest, postgres:14). 사용자 지정 이미지의 경우 Dockerfile을 지정하기 위해 build를 사용할 수도 있습니다.
    • ports: 호스트 머신에서 컨테이너로 포트를 매핑합니다 (예: 80:80은 호스트 포트 80을 컨테이너 포트 80으로 매핑합니다).
    • volumes: 영구 데이터 또는 구성을 위해 호스트 디렉터리 또는 명명된 볼륨을 컨테이너에 마운트합니다 (예: ./html:/usr/share/nginx/html).
    • environment: 컨테이너 내에서 환경 변수를 설정합니다 (예: POSTGRES_USER=myuser).
    • depends_on: 서비스 간의 종속성을 지정하여 특정 순서로 시작되도록 합니다 (준비 상태를 보장하지는 않음).
    • networks: 서비스가 통신할 사용자 지정 네트워크를 정의합니다.
  • networks: 서비스가 참여하여 격리된 통신을 할 수 있는 사용자 지정 네트워크를 정의합니다.
  • volumes: 영구 데이터 저장을 위한 명명된 볼륨을 정의합니다.

실제 단계: 샘플 웹 호스팅 스택 구축

정적 웹사이트를 Nginx로 제공하고 동적 콘텐츠를 위한 PostgreSQL 데이터베이스를 사용하는 일반적인 웹 호스팅 시나리오를 구성해 보겠습니다. 성능을 위해 Redis 캐시도 추가하겠습니다.

1. 프로젝트 구조:

프로젝트 디렉터리를 생성합니다 (예: my-web-app). 내부에 다음이 포함됩니다.

my-web-app/
├── docker-compose.yml
├── nginx/
│   └── default.conf
└── html/
    └── index.html

2. nginx/default.conf (기본 Nginx 구성):

이 파일은 Nginx에 정적 파일을 제공하고 잠재적으로 애플리케이션 서버로 요청을 프록시하는 방법을 알려줍니다 (간단하게 하기 위해 여기서는 정적 파일에 중점을 둡니다).

server {
    listen 80;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }
}

3. html/index.html (웹사이트 콘텐츠):

테스트를 위한 간단한 HTML 파일입니다.

<!DOCTYPE html>
<html>
<head>
    <title>Welcome to My Dockerized Site!</title>
</head>
<body>
    <h1>Hello from Docker Compose!</h1>
    <p>This site is served by Nginx in a container.</p>
</body>
</html>

4. docker-compose.yml (설정의 핵심):

이 파일은 Nginx, PostgreSQL, Redis의 세 가지 서비스를 정의합니다.

version: '3.8'

services:
  webserver:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - db
      - cache
    networks:
      - app-network

  db:
    image: postgres:14
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mysecretpassword
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - app-network

  cache:
    image: redis:latest
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  db_data:

docker-compose.yml 설명:

  • webserver 서비스: 공식 Nginx 이미지를 사용합니다. 호스트 포트 80을 컨테이너 포트 80으로 매핑합니다. 로컬 html 디렉터리를 웹사이트 콘텐츠로, 사용자 지정 nginx/default.conf를 Nginx 구성으로 마운트합니다. 중요한 것은 dbcachedepends_on하여 이러한 서비스가 웹 서버보다 먼저 시작되어야 함을 나타냅니다. 사용자 지정 app-network에 연결됩니다.
  • db 서비스: 공식 PostgreSQL 이미지를 사용합니다. 데이터베이스 생성, 사용자 및 암호에 대한 필수 환경 변수를 설정합니다. 명명된 볼륨 db_data는 컨테이너가 제거되고 다시 생성되어도 데이터베이스 데이터가 유지되도록 하는 데 사용됩니다. 또한 app-network에 연결됩니다.
  • cache 서비스: 공식 Redis 이미지를 사용합니다. 이 예제에서는 영구 데이터가 필요 없는 간단한 서비스이며 app-network에 연결됩니다.
  • networks: app-network라는 단일 브리지 네트워크를 정의합니다. 이는 격리 및 통신에 중요합니다. 기본적으로 Docker Compose는 네트워크를 생성하지만 명시적으로 정의하면 더 많은 제어와 명확성을 얻을 수 있습니다. 동일한 사용자 지정 네트워크의 서비스는 서비스 이름을 호스트 이름으로 사용하여 서로 통신할 수 있습니다 (예: 웹 서버는 구성 및 컨텍스트에 따라 localhost:5432 또는 db:5432에서 db에 연결할 수 있습니다).
  • volumes: db_data 명명된 볼륨을 정의합니다. Docker는 이러한 볼륨의 수명 주기를 관리합니다.

5. 스택 실행:

터미널에서 프로젝트 디렉터리(my-web-app/)로 이동하여 다음을 실행합니다.

docker compose up -d
  • docker compose: Docker Compose 명령을 호출합니다.
  • up: docker-compose.yml에 정의된 컨테이너를 생성하고 시작합니다.
  • -d: 컨테이너를 분리 모드(백그라운드)로 실행합니다.

6. 확인:

웹 브라우저를 열고 http://localhost로 이동합니다. index.html 파일의 콘텐츠가 표시되어야 합니다.

데이터베이스와 캐시가 실행 중인지 확인하려면 컨테이너를 검사할 수 있습니다.

docker compose ps

그러면 webserver, db, cache 컨테이너의 상태가 표시됩니다.

7. 스택 중지:

완료되면 컨테이너, 네트워크 및 볼륨을 중지하고 제거합니다 (선택 사항).

docker compose down

명명된 볼륨을 제거하려면 (데이터베이스 데이터가 삭제됨) 다음을 사용합니다.

docker compose down -v

격리 및 재현성 작동 방식

격리:

Docker Compose는 여러 가지 방법으로 격리를 보장합니다.

  • 프로세스 격리: 각 서비스는 자체 파일 시스템, 프로세스 공간 및 네트워크 인터페이스를 가진 자체 컨테이너에서 실행되며 호스트 및 다른 컨테이너와 격리됩니다.
  • 네트워크 격리: 사용자 지정 네트워크(app-network)를 정의하여 서비스 간의 통신 방식을 제어합니다. 기본적으로 다른 네트워크의 컨테이너는 통신할 수 없습니다. 동일한 네트워크의 서비스는 명시적으로 허용되거나 포트를 노출하는 경우에만 통신할 수 있습니다. 우리 예제에서는 webserver가 서비스 이름을 사용하여 dbcache 서비스에 액세스할 수 있지만, 데이터베이스 및 캐시 포트에 대한 외부 액세스는 기본적으로 노출되지 않아 보안이 강화됩니다.
  • 종속성 관리: depends_on은 시작 순서를 관리하여 서비스가 아직 시작되지 않은 종속성에 연결하려고 시도하는 문제를 방지하는 데 도움이 됩니다.

재현성:

docker-compose.yml 파일은 애플리케이션 환경의 단일 진실 공급원입니다. Docker 및 Docker Compose가 설치된 사람은 누구나 프로젝트를 복제하고 docker compose up -d를 실행하면 동일하고 작동하는 환경을 얻을 수 있습니다. 이는 환경 자체가 버전 관리되고 일관되게 배포되도록 함으로써 "내 컴퓨터에서는 작동하는데" 문제를 해결합니다.

고급 고려 사항 및 주의 사항

  • depends_on 대 서비스 준비 상태: depends_on은 컨테이너가 시작되었음을 보장할 뿐입니다. 컨테이너 내부의 애플리케이션이 연결을 수락할 준비가 되었음을 보장하지는 않습니다. 데이터베이스의 경우 이는 일반적인 문제입니다. 애플리케이션 코드에서 상태 확인 또는 재시도 메커니즘을 구현하거나 진입점 내에서 wait-for-it.sh 스크립트와 같은 도구를 사용해야 할 수 있습니다.
  • 프로덕션 배포: Docker Compose는 개발 및 스테이징에 훌륭하지만 프로덕션의 경우 더 강력한 오케스트레이션이 필요한 경우가 많습니다. Kubernetes 또는 Docker Swarm과 같은 도구는 로드 밸런싱, 자체 복구 및 롤링 업데이트를 처리하는 대규모 컨테이너화된 애플리케이션 관리를 위해 설계되었습니다. 그러나 Docker Compose 파일은 종종 이러한 고급 오케스트레이터에 맞게 조정되거나 기반으로 사용될 수 있습니다.
  • 이미지 관리: 프로덕션의 경우 예측 가능한 배포를 보장하기 위해 latest 대신 특정 이미지 태그(예: postgres:14.5)를 사용하는 것이 좋습니다. 애플리케이션 코드의 경우 Dockerfile을 사용하여 사용자 지정 이미지를 빌드할 수도 있습니다.
  • 보안: 데이터베이스 암호와 같은 민감한 정보에 항상 주의하십시오. 환경 변수를 사용하고 프로덕션 환경의 경우 docker-compose.yml에 직접 하드코딩하는 대신 Docker secrets 또는 외부 비밀 관리 도구를 고려하십시오.
  • 리소스 제한: 프로덕션의 경우 한 서비스가 호스트의 모든 사용 가능한 리소스를 소비하는 것을 방지하기 위해 컨테이너에 대한 리소스 제한(CPU, 메모리)을 정의하는 것이 좋습니다.
  • 네트워킹 복잡성: 애플리케이션이 성장함에 따라 복잡한 네트워킹 구성을 관리하는 것이 어려워질 수 있습니다. Docker의 네트워킹 기능은 강력하지만 신중한 계획이 필요합니다.

결론

Docker Compose는 웹 애플리케이션 배포 및 관리 방식을 변화시킵니다. docker-compose.yml 파일에서 전체 스택을 선언적으로 정의할 수 있도록 함으로써 개발 및 배포 워크플로에 탁월한 일관성, 격리 및 재현성을 제공합니다. 애플리케이션뿐만 아니라 전체 운영 환경을 패키징함으로써 "내 컴퓨터에서는 작동하는데" 문제를 직접적으로 해결합니다. 개인 프로젝트를 설정하는 개인 개발자이든 대규모 팀의 일원이든, Docker Compose를 마스터하는 것은 더 안정적이고 유지 관리 가능하며 효율적인 웹 호스팅 솔루션을 구축하는 데 중요한 단계입니다. 이는 더 고급 컨테이너 오케스트레이션 기술에 대한 견고한 기반을 마련하고 궁극적으로 더 원활한 개발 주기와 더 강력한 프로덕션 시스템으로 이어집니다.

Sources (5)