Docker Compose 다중 컨테이너 네트워크 구성과 장애 분석
Docker Compose 다중 컨테이너 네트워크 구성과 장애 분석
1. Docker Compose 네트워크를 이해해야 하는 이유
실제 서비스는 하나의 컨테이너만으로 구성되는 경우가 많지 않습니다.
일반적인 웹 서비스는 다음과 같이 여러 컨테이너가 역할을 나누어 동작합니다.
- Nginx: 외부 사용자 요청 수신 및 Reverse Proxy
- FastAPI: 비즈니스 로직 처리
- PostgreSQL: 데이터 저장
- Redis: 캐시 및 세션 관리
각 컨테이너는 독립적인 네트워크 환경을 가지고 있지만, 서비스가 정상적으로 동작하려면 서로 통신할 수 있어야 합니다.
예를 들어 사용자가 웹 브라우저에서 서비스를 호출하면 다음과 같은 흐름으로 요청이 전달됩니다.
사용자 브라우저
|
| HTTP :8080
v
+-------------------+
| Nginx Container |
| Port 80 |
+-------------------+
|
| HTTP :8000
v
+-------------------+
| FastAPI Container |
| Port 8000 |
+-------------------+
|
| PostgreSQL :5432
v
+-------------------+
| PostgreSQL |
| Port 5432 |
+-------------------+
이 구조에서 중요한 것은 컨테이너마다 IP 주소가 존재하지만, 운영자가 IP 주소를 직접 관리할 필요는 없다는 점입니다.
Docker Compose는 서비스 이름을 이용한 내부 DNS를 제공하기 때문에 다음과 같이 통신할 수 있습니다.
Nginx → api:8000 FastAPI → db:5432
이러한 구조를 이해하면 컨테이너가 재생성되어 IP 주소가 변경되더라도 서비스 이름을 기준으로 안정적인 통신을 유지할 수 있습니다.
2. Docker Compose 기본 네트워크 동작
Docker Compose는 별도의 네트워크를 지정하지 않으면 프로젝트 단위로 기본 Bridge 네트워크를 생성합니다.
예를 들어 다음과 같이 구성했다고 가정하겠습니다.
services:
web:
image: nginx:alpine
api:
image: python:3.12-slim
db:
image: postgres:17
Compose를 실행하면 대략 다음과 같은 네트워크가 만들어집니다.
Docker Host
|
+--------------------------------+
| Docker Compose Network |
| |
| +---------+ |
| | web | |
| +---------+ |
| | |
| +---------+ |
| | api | |
| +---------+ |
| | |
| +---------+ |
| | db | |
| +---------+ |
| |
+--------------------------------+
같은 Compose 네트워크에 연결된 컨테이너는 서비스 이름으로 서로를 찾을 수 있습니다.
예를 들어 api 컨테이너에서 PostgreSQL에 접속할 때 다음 주소를 사용합니다.
db:5432
여기서 db는 컨테이너의 IP 주소가 아니라 Docker Compose에 정의된 서비스 이름입니다.
컨테이너 내부에서 localhost를 사용하면 안 되는 이유
다음 설정은 잘못된 예입니다.
DB_HOST = "localhost" DB_PORT = 5432
FastAPI 컨테이너 내부에서 localhost는 FastAPI 컨테이너 자신을 의미합니다.
PostgreSQL은 별도의 컨테이너에서 실행되고 있으므로 연결할 수 없습니다.
올바른 설정은 다음과 같습니다.
DB_HOST = "db" DB_PORT = 5432
이 차이는 Docker 네트워크 장애를 분석할 때 가장 먼저 확인해야 하는 부분입니다.
3. 실습 환경 구성
이번 실습에서는 다음과 같은 디렉터리 구조를 사용하겠습니다.
docker-compose-lab/
|
+-- compose.yaml
+-- .env
|
+-- nginx/
| +-- default.conf
|
+-- api/
+-- Dockerfile
+-- requirements.txt
+-- main.py
각 컨테이너의 역할은 다음과 같습니다.
서비스역할내부 포트webNginx Reverse Proxy80apiFastAPI 애플리케이션8000dbPostgreSQL 데이터베이스5432
외부 사용자는 Nginx에만 접근하도록 구성하고, API와 DB는 Docker 내부 네트워크를 통해서만 통신하도록 설계하겠습니다.
4. Docker Compose 네트워크 설계
이번 실습에서는 네트워크를 두 개로 분리합니다.
- frontend: Nginx와 FastAPI 통신
- backend: FastAPI와 PostgreSQL 통신
외부 사용자
|
| 8080
v
+--------------+
| Nginx (web) |
+--------------+
|
frontend Network
|
+--------------+
| FastAPI (api)|
+--------------+
|
backend Network
|
+--------------+
| PostgreSQL |
| (db) |
+--------------+
이 구성에서는 다음과 같은 통신이 가능합니다.
출발지목적지통신 가능 여부외부 사용자Nginx가능NginxFastAPI가능FastAPIPostgreSQL가능NginxPostgreSQL불가능외부 사용자PostgreSQL직접 접근 불가
이처럼 서비스별 네트워크를 분리하면 불필요한 통신 경로를 줄일 수 있습니다.
5. 환경변수 파일 생성
먼저 데이터베이스 접속 정보를 .env 파일에 작성합니다.
vi .env
내용:
POSTGRES_DB=appdb POSTGRES_USER=appuser POSTGRES_PASSWORD=replace_with_long_random_secret
실제 운영 환경에서는 충분히 긴 무작위 비밀번호를 사용해야 합니다.
.env 파일에는 민감한 정보가 포함되므로 Git 저장소에 업로드하지 않도록 주의합니다.
vi .gitignore .env
6. FastAPI 컨테이너 구성
6-1. Dockerfile 작성
vi api/Dockerfile FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
여기서 반드시 확인해야 하는 부분은 다음 설정입니다.
--host 0.0.0.0
컨테이너 내부에서 애플리케이션이 127.0.0.1에만 바인딩되면 다른 컨테이너에서 접근할 수 없습니다.
따라서 컨테이너 외부에서도 접근할 수 있도록 0.0.0.0으로 실행해야 합니다.
6-2. Python 패키지 작성
vi api/requirements.txt fastapi uvicorn psycopg[binary]
6-3. FastAPI 애플리케이션 작성
vi api/main.py
import os
import psycopg
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/")
def root():
return {
"message": "Docker Compose Network Lab"
}
@app.get("/health")
def health():
return {
"status": "ok"
}
@app.get("/db-health")
def db_health():
try:
with psycopg.connect(
host=os.getenv("DB_HOST", "db"),
port=int(os.getenv("DB_PORT", "5432")),
dbname=os.getenv("DB_NAME"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
connect_timeout=3
) as conn:
with conn.cursor() as cur:
cur.execute("SELECT 1")
cur.fetchone()
return {
"database": "ok"
}
except Exception as exc:
raise HTTPException(
status_code=503,
detail="database unavailable"
) from exc
여기서 DB 접속 대상이 localhost가 아니라 db로 설정되어 있다는 점을 확인합니다.
또한 /health는 API 프로세스의 기본 상태를 확인하고, /db-health는 실제 PostgreSQL 연결까지 확인합니다.
7. Nginx Reverse Proxy 구성
Nginx는 외부에서 들어오는 HTTP 요청을 FastAPI 컨테이너로 전달합니다.
vi nginx/default.conf
server {
listen 80;
server_name _;
location / {
proxy_pass http://api:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
여기서 핵심 설정은 다음과 같습니다.
proxy_pass http://api:8000;
Nginx 컨테이너는 frontend 네트워크를 통해 FastAPI 컨테이너에 접근합니다.
따라서 api라는 서비스 이름을 사용하면 Docker 내부 DNS가 해당 컨테이너의 IP 주소를 찾아줍니다.
8. compose.yaml 작성
이제 전체 컨테이너와 네트워크를 정의합니다.
vi compose.yaml
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
api:
condition: service_healthy
networks:
- frontend
api:
build: ./api
environment:
DB_HOST: db
DB_PORT: "5432"
DB_NAME: ${POSTGRES_DB}
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
expose:
- "8000"
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"
interval: 10s
timeout: 3s
retries: 5
depends_on:
db:
condition: service_healthy
networks:
- frontend
- backend
db:
image: postgres:17
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test:
- CMD-SHELL
- 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"'
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
networks:
frontend:
backend:
internal: true
volumes:
postgres_data:
주요 설정 설명
1. ports
ports: - "8080:80"
호스트 서버의 8080 포트를 컨테이너의 80 포트로 연결합니다.
외부 사용자는 다음 주소로 접속합니다.
http://서버IP:8080
2. expose
expose: - "8000"
컨테이너가 내부적으로 사용하는 포트를 문서화하는 설정입니다.
expose 자체가 방화벽처럼 접근을 차단하는 기능은 아닙니다. 실제 통신 가능 여부는 네트워크 연결과 서비스의 리스닝 상태 등에 따라 결정됩니다.
3. networks
networks: - frontend - backend
FastAPI는 두 네트워크에 모두 연결됩니다.
따라서 Nginx와 PostgreSQL 양쪽에 접근할 수 있습니다.
반면 Nginx는 frontend에만, PostgreSQL은 backend에만 연결되어 있습니다.
4. internal
backend: internal: true
backend 네트워크를 외부와 격리된 내부 네트워크로 구성합니다.
단, 이는 완전한 보안 경계를 의미하지 않습니다. 호스트 관리자 권한이나 다른 네트워크에 연결된 컨테이너를 통한 접근까지 차단하는 것은 아니므로 별도의 접근 통제와 보안 설정이 필요합니다.
5. depends_on과 healthcheck
depends_on:
db:
condition: service_healthy
단순히 컨테이너가 실행되었다는 이유만으로 애플리케이션이 DB에 접속할 수 있는 것은 아닙니다.
PostgreSQL 프로세스가 실행 중이더라도 초기 데이터베이스 생성 작업이 진행 중일 수 있습니다.
healthcheck는 실제 서비스가 준비되었는지 확인하고, service_healthy는 해당 상태를 기준으로 시작 순서를 제어합니다.
9. Docker Compose 실행
먼저 Compose 설정에 오류가 없는지 확인합니다.
docker compose config
문제가 없다면 다음 명령으로 컨테이너를 빌드하고 실행합니다.
docker compose up --build -d
실행 상태 확인:
docker compose ps
정상적으로 구성되었다면 web, api, db 컨테이너가 실행되고, healthcheck가 정상 완료된 서비스는 healthy 상태로 표시됩니다.
로그 확인:
docker compose logs -f
특정 서비스만 확인하려면 다음과 같이 실행합니다.
docker compose logs -f web docker compose logs -f api docker compose logs -f db
10. 컨테이너 간 네트워크 통신 확인
이제 실제로 서비스 이름을 이용한 통신이 가능한지 확인하겠습니다.
10-1. 외부에서 Nginx 접속
curl -i http://127.0.0.1:8080/health
정상 응답 예시:
{
"status": "ok"
}
10-2. Nginx에서 FastAPI DNS 확인
docker compose exec web \ getent hosts api
정상적으로 DNS가 동작하면 api 서비스의 컨테이너 IP가 출력됩니다.
10-3. FastAPI에서 PostgreSQL DNS 확인
docker compose exec api \
python -c "import socket; print(socket.getaddrinfo('db', 5432))"
서비스 이름 db가 IP 주소로 변환되는지 확인합니다.
10-4. FastAPI에서 PostgreSQL 포트 연결 확인
docker compose exec api \
python -c "import socket; s=socket.create_connection(('db',5432),3); print('TCP CONNECT OK'); s.close()"
정상 연결 시 다음과 같이 출력됩니다.
TCP CONNECT OK
이 명령은 TCP 연결 여부만 확인합니다. 실제 데이터베이스 인증과 SQL 실행까지 확인하려면 다음 명령을 사용합니다.
curl http://127.0.0.1:8080/db-health
정상 응답:
{
"database": "ok"
}
11. Docker 네트워크 상세 확인
Docker 네트워크 상태를 확인하는 것은 장애 분석에서 매우 중요합니다.
11-1. 네트워크 목록
docker network ls
Compose 프로젝트에서 생성한 네트워크를 확인할 수 있습니다.
11-2. 네트워크 상세 정보
docker network inspect 프로젝트명_frontend docker network inspect 프로젝트명_backend
네트워크에 연결된 컨테이너, IP 주소, 네트워크 설정 등을 확인할 수 있습니다.
실제 네트워크 이름은 프로젝트 디렉터리명이나 -p 옵션 등에 따라 달라질 수 있으므로 docker network ls에서 먼저 확인해야 합니다.
11-3. 컨테이너 IP 확인
docker inspect 컨테이너명
IP 주소만 확인하려면 다음과 같이 실행할 수 있습니다.
docker inspect \
-f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
컨테이너명
컨테이너 IP는 재생성 과정에서 변경될 수 있습니다.
따라서 운영 설정에는 IP 주소를 직접 입력하기보다 서비스 이름을 사용하는 것이 좋습니다.
12. 실전 장애 분석
이제 실제 운영 환경에서 발생할 수 있는 문제를 살펴보겠습니다.
장애 1. Nginx에서 FastAPI로 연결되지 않는 경우
증상:
502 Bad Gateway
Nginx 로그:
connect() failed (111: Connection refused) while connecting to upstream
가능한 원인:
- FastAPI 프로세스가 중지됨
- FastAPI가 127.0.0.1에만 바인딩됨
- Nginx와 FastAPI가 같은 네트워크에 연결되지 않음
- FastAPI의 포트 설정이 잘못됨
점검:
docker compose ps docker compose logs api docker compose exec api ss -lntp
FastAPI가 다음과 같이 리스닝하고 있는지 확인합니다.
0.0.0.0:8000
장애 2. FastAPI에서 PostgreSQL에 접속하지 못하는 경우
증상:
connection refused
또는:
could not translate host name "db"
점검 순서:
docker compose ps
docker compose logs db
docker compose exec api \
python -c "import socket; print(socket.getaddrinfo('db',5432))"
DNS 조회가 실패하면 네트워크 연결과 서비스 이름을 확인해야 합니다.
DNS는 정상인데 TCP 연결이 실패한다면 PostgreSQL의 실행 상태, 리스닝 포트, 네트워크 연결을 확인합니다.
장애 3. localhost로 DB에 접속하는 경우
잘못된 설정:
DB_HOST = "localhost"
컨테이너 내부에서 localhost는 자기 자신을 의미합니다.
수정:
DB_HOST = "db"
Docker Compose에서는 서비스 이름을 사용하는 것이 기본적인 연결 방식입니다.
장애 4. 컨테이너는 실행 중인데 서비스가 응답하지 않는 경우
컨테이너 상태가 Up이라고 해서 애플리케이션이 정상이라는 뜻은 아닙니다.
다음 항목을 확인합니다.
docker compose ps docker compose logs --tail=100 api docker inspect 컨테이너명
특히 다음 정보를 확인해야 합니다.
- 컨테이너의 실행 상태
- Healthcheck 결과
- 재시작 횟수
- 애플리케이션 오류 로그
- 네트워크 연결 상태
장애 5. 포트 충돌이 발생하는 경우
예를 들어 호스트 서버의 8080 포트를 다른 프로세스가 이미 사용하고 있다면 컨테이너 실행 과정에서 포트 바인딩 오류가 발생할 수 있습니다.
확인:
ss -lntp | grep 8080
기존 프로세스가 사용 중이라면 Compose의 호스트 포트를 변경할 수 있습니다.
ports: - "8081:80"
이후 다음 주소로 접속합니다.
http://서버IP:8081
13. 운영 환경에서 반드시 확인해야 할 사항
Docker Compose를 운영 환경에 적용할 때는 다음 사항을 고려해야 합니다.
1. DB 포트를 외부에 공개하지 않는다.
PostgreSQL은 내부 네트워크에서만 접근하도록 구성하는 것이 일반적입니다.
외부 관리 접속이 필요하다면 접근 가능한 IP를 제한하고 별도의 보안 정책을 적용해야 합니다.
2. 데이터 볼륨을 유지한다.
다음 설정은 PostgreSQL 데이터를 컨테이너 외부의 Docker 볼륨에 저장합니다.
volumes: - postgres_data:/var/lib/postgresql/data
컨테이너를 삭제하거나 재생성해도 볼륨을 유지하면 데이터가 보존됩니다.
다만 볼륨이 백업을 대신하지는 않습니다.
3. 운영 환경에서는 무분별한 볼륨 삭제를 금지한다.
docker compose down
위 명령은 컨테이너와 네트워크를 내리지만 일반적으로 named volume은 유지합니다.
반면 다음 명령은 Compose에서 사용하는 볼륨까지 삭제할 수 있습니다.
docker compose down -v
운영 데이터가 저장된 볼륨이 삭제될 수 있으므로 사용 전에 반드시 확인해야 합니다.
4. 비밀번호와 인증 정보를 관리한다.
.env 파일은 편리하지만 안전한 비밀정보 저장소를 대신하지 않습니다.
운영 환경에서는 파일 권한을 제한하고, 가능한 경우 Docker Secrets 또는 별도의 비밀정보 관리 시스템을 사용하는 것이 좋습니다.
5. 서비스 간 네트워크를 분리한다.
외부 요청을 받는 Nginx, 애플리케이션, 데이터베이스를 역할에 따라 분리하면 불필요한 통신 경로를 줄일 수 있습니다.
14. 시스템 엔지니어가 기억해야 할 핵심 명령어
명령어설명docker compose configCompose 설정 검증docker compose up -d컨테이너 실행docker compose ps서비스 상태 확인docker compose logs컨테이너 로그 확인docker compose exec실행 중인 컨테이너 명령 실행docker network ls네트워크 목록docker network inspect네트워크 상세 정보docker inspect컨테이너 상세 정보docker compose down컨테이너와 네트워크 종료docker compose down -v볼륨까지 삭제할 수 있으므로 주의
15. 마무리
Docker Compose 네트워크를 이해하면 단일 컨테이너 운영에서 벗어나 실제 서비스에 가까운 다중 컨테이너 환경을 구성할 수 있습니다.
이번 편에서 반드시 기억해야 할 내용은 다음과 같습니다.
- Docker Compose는 프로젝트 단위의 기본 네트워크를 생성한다.
- 같은 네트워크에 연결된 컨테이너는 서비스 이름으로 서로 통신할 수 있다.
- 컨테이너 내부에서
localhost는 자기 자신을 의미한다. - 컨테이너 간 통신에는 서비스 이름과 컨테이너 포트를 사용한다.
ports는 호스트와 컨테이너의 포트를 연결한다.depends_on만으로 애플리케이션의 실제 준비 상태가 보장되지는 않는다.healthcheck와 네트워크 분리를 이용하면 운영 안정성을 높일 수 있다.
시스템 엔지니어는 컨테이너가 실행되는지만 확인해서는 안 됩니다.
요청이 어느 네트워크를 통과하는지, DNS가 정상적으로 동작하는지, 목적지 포트가 열려 있는지, 애플리케이션이 실제 요청을 처리할 준비가 되었는지까지 확인할 수 있어야 합니다.
다음 편에서는 Docker를 넘어 Kubernetes 환경으로 확장하겠습니다.
다음 편: 시스템 엔지니어 실전 교육 24편 — Kubernetes 네트워크 구조와 CNI, Pod 간 통신 원리
Compose 설정의 핵심 오류를 바로잡아줘실습 명령어를 Rocky Linux 기준으로 보완해줘
댓글
아직 댓글이 없습니다.
로그인 후 댓글을 남길 수 있습니다.