raw
DevOps

Docker-Compose로 여러 프로젝트 배포해보자

2025.04.13·11분

Docker compose

Docker Compse 는 다중 컨테이너 애플리케이션이다. 공유 할 수 있도록 개발된 도구로 단일 명령을 사용하여 모두 실행 또는 종료할 수 있도록 개발된 도구다.

언제 사용하나요?

개발 환경을 표준화

팀 구성원이 수동으로 개발 환경을 정비하면 시간이 너무 오래걸린다. 또한 실수 비용도 커지기 때문에, Docker Compose를 사용하여 설정 파일을 전달하면, 개발 환경의 세부 사항을 걱정하지 않고 하나의 명령으로 환경을 정비 할 수 있다. 작업도 간소화 가능

테스트 환경의 자동화

CI/CD 할 경우, end to end 테스트를 자동화하려면 이상적으로 테스트마다 독립적인 환경을 제공하는 것이 좋은데, Docker Compose는 테스트 환경을 명령어 1개로 시작 가능하다

🔥단일 호스트 배포🔥

운영 환경이 단일 서버일 경우 Docker Compose를 사용하여 배포 가능하다. 공부가 목적이나 서버 비용을 절약하는 방법 중 하나이다. pretier를 사용 중이거나 여러 서버를 증설할 필요가 없는 분은 단일 호스트 배포를 통해 여러 프로젝트를 할 수 있는 것이다.

그림은 위처럼 여러개의 컨테이너가 네트워크에 소속되어 있는 모습이고, 임의로 컨테이너를 네트워크에 연결하기 위해서는 docker network connect network container 명령을 사용 가능하다 그러면 해당 네트워크가 가진 private IP 대역의 IP 중 하나를 container가 할당 받는다.

단일 호스트 배포의 흐름

나는 우선 두개의 프로젝트 각각 도메인을 설정했다. 하나의 EC2route53 으로 연결 해줬다.

사용자가 도메인A나 도메인B에 접속할 때 흐름

  1. DNS 확인: 사용자가 브라우저에 URL(예: 도메인A)을 입력합니다. DNS 서버가 해당 도메인을 EC2 인스턴스의 공인 IP 주소로 변환합니다.

  2. HTTP 요청 수신: 사용자의 요청이 EC2 인스턴스의 80포트(HTTP)로 전송됩니다. EC2의 포트 80은 nginx-proxy 컨테이너에 바인딩되어 있습니다.

  3. Nginx 프록시의 요청 처리: nginx-proxy 컨테이너는 요청 헤더의 Host 필드를 확인합니다. 해당 도메인(csmorning.co.kr 또는 kidsbugs.co.kr)에 맞는 설정을 찾습니다. HTTP 요청을 HTTPS로 리다이렉트합니다(301 상태 코드).

  4. HTTPS 리다이렉트 및 암호화 연결: 브라우저는 HTTPS 요청을 EC2 인스턴스의 443포트로 다시 전송합니다. nginx-proxy는 도메인에 맞는 SSL 인증서를 사용하여 암호화된 연결을 설정합니다.

  5. 내부 서비스 라우팅: nginx-proxy는 요청을 적절한 내부 서비스로 전달합니다: 기본 경로('/') 요청은 해당 도메인의 landing-page 컨테이너로 전달 '/api' 경로 요청은 해당 도메인의 api-server 컨테이너로 전달

6.프론트엔드 처리(landing-page 컨테이너): React 기반의 프론트엔드 애플리케이션이 요청을 처리합니다. 정적 자원(HTML, CSS, JavaScript)이 사용자 브라우저로 전송됩니다. 브라우저에서 React 앱이 렌더링됩니다.

7.API 요청 처리(필요한 경우): 프론트엔드 앱이 데이터가 필요한 경우 /api 엔드포인트로 요청합니다. 이 요청은 다시 nginx-proxy를 통해 api-server 컨테이너로 전달됩니다. api-server는 요청을 처리하고(필요시 데이터베이스 조회), 응답을 반환합니다. 프론트엔드는 이 데이터로 화면을 업데이트합니다.

text
1사용자 → 브라우저 → DNS → EC2 IP 주소
2
3HTTP 요청 (80포트)
4
5nginx-proxy 컨테이너 → HTTPS 리다이렉트
6
7HTTPS 요청 (443포트)
8
9nginx-proxy (SSL 암호화, 도메인 확인)
10
11 ├─→ landing-page 컨테이너 (/) → React 앱 렌더링
12 │ ↓
13 │ 사용자에게 UI 표시
14
15 └─→ api-server 컨테이너 (/api) → 데이터 처리
16
17 데이터베이스 조회/업데이트 (필요시)
18
19 JSON 응답 반환 → 프론트엔드 업데이트

EC2 인스턴스와 Docker 구조

csmornig.co.krkidsbugs.co.kr 두개의 도메인으로 요청 처리 흐름 시퀀스 다이어그램으로 표현해봤다.

루트에서 nginx-proxy를 통해 컨테이너가 호스트 네트워크 모드로 실행되면서 모든 외부 요청을 받아서 적절한 내부 컨테이너로 라우팅을 하는 것이다.

공통 리소스(SSL 처리, 라우팅)는 중앙 nginx-proxy를 통해 관리한다.

장단점

장점

  • 도메인 기반 멀티 호스팅 단일 서버에서 여러 도메인과 서비스를 호스팅할 수 있습니다. 각 도메인별로 완전히 독립적인 애플리케이션을 실행할 수 있습니다.

  • 리소스 효율성 여러 VM이나 서버를 사용하는 대신 하나의 EC2 인스턴스에서 여러 서비스를 실행함으로써 비용을 절감합니다. 하드웨어 자원(CPU, 메모리)을 효율적으로 공유합니다.

  • 네트워크 격리 각 프로젝트가 독립적인 Docker 네트워크에서 실행되어 보안이 향상됩니다. 한 서비스의 문제가 다른 서비스에 미치는 영향을 최소화합니다.

  • SSL 관리 중앙화 단일 nginx-proxy에서 모든 도메인의 SSL 인증서를 관리하므로 구성이 간소화됩니다. Let's Encrypt 인증서 갱신 등의 작업을 한 곳에서 처리할 수 있습니다.

  • 유지보수 용이성 라우팅 규칙을 nginx-proxy의 설정 파일 하나에서 관리할 수 있습니다. 새 서비스 추가 시 기존 서비스를 중단하지 않고도 구성할 수 있습니다.

단점

  • 단일 장애점 (Single Point of Failure) nginx-proxy에 문제가 발생하면 모든 서비스가 영향을 받습니다. 하나의 EC2 인스턴스에 의존하므로 인스턴스 장애 시 모든 서비스가 중단됩니다.

  • 리소스 경합 여러 서비스가 동일한 물리적 자원을 공유하므로 한 서비스의 과도한 자원 사용이 다른 서비스의 성능에 영향을 줄 수 있습니다. 특히 트래픽이 많은 시간대에 문제가 될 수 있습니다.

  • 복잡성 증가 단일 서버에서 여러 서비스를 관리하는 것은 개별 서버보다 구성이 복잡합니다. 네트워크 설정, 포트 관리, 라우팅 규칙을 이해하고 관리해야 합니다.

  • 확장성 제한 수직적 확장(더 큰 EC2 인스턴스로 업그레이드)은 가능하지만, 수평적 확장(여러 서버에 분산)이 어렵습니다. 트래픽이 크게 증가하면 결국 단일 인스턴스의 한계에 도달할 수 있습니다.

  • 보안 우려 호스트 네트워크 모드는 컨테이너에 더 많은 네트워크 액세스 권한을 부여하므로 보안 격리가 약화될 수 있습니다. 모든 서비스가 동일한 인스턴스에 있으므로 한 서비스의 보안 취약점이 다른 서비스에 영향을 줄 수 있습니다.

문제

github action으로 ci/cd 스크립트 작성 시 컨테이너 간 네트워크 연결에서 타이밍 이슈를 조심해야한다

ubuto
1name: Deploy Kidsbugs
2
3on:
4 push:
5 branches: [ main ]
6
7jobs:
8 deploy:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v3
12
13 - name: Set up SSH
14 uses: webfactory/ssh-agent@v0.7.0
15 with:
16 ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
17
18 - name: Add host key to known_hosts
19 run: |
20 mkdir -p ~/.ssh
21 ssh-keyscan ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
22
23 - name: Deploy to server
24 run: |
25 ssh ${{ secrets.SSH_USER }}@${{ secrets.SERVER_IP }} << 'EOF'
26 cd ~/kidsbugs
27 git pull origin main
28 docker-compose down
29 docker-compose build
30 docker-compose up -d
31 # 잠시 기다린 후 nginx-proxy 재시작
32 sleep 10
33 cd ~/nginx-proxy
34 docker-compose restart
35 EOF

이 처럼 sleep 을 추가해서 잠시 기다리면 간단하면서도 효과적으로 해결할 수 있다.

api 수요가 없는 랜딩페이지 작업이 제일 효율이 좋은 것 같다.