14장. Streamlit 화면과 Docker 컨테이너
docker compose 로 여러 서비스 띄우기
식당을 열려면 홀, 주방, 창고가 함께 문을 열어야 합니다. 하나씩 따로 여는 대신 "개점 순서표" 한 장으로 함께 엽니다. docker compose 는 화면, API, 모델 서버(Ollama) 컨테이너를 compose.yaml 한 파일로 함께 띄우고, 서로 서비스 이름으로 찾게 해 줍니다.
세 카드는 이 장의 프로젝트가 쓰는 서비스입니다. 화면(ui)은 8501 번 포트로 사용자를 받고 http://api:8014 로 API 를 부릅니다. API(api)는 http://ollama:11434 로 모델 서버를 부릅니다. 모델 서버(ollama)는 모델 파일을 볼륨에 둡니다. compose 가 만든 네트워크 안에서는 서비스 이름이 곧 주소입니다. 여기서 개점 순서표가 compose.yaml, 홀과 주방과 창고가 세 서비스입니다.
compose.yaml 의 칸
| 칸 | 뜻 | 예 |
|---|---|---|
services | 띄울 컨테이너들 | ui, api, ollama |
build | Dockerfile 이 있는 폴더 | ./api |
image | 만들지 않고 받아 쓸 이미지 | ollama/ollama |
ports | PC 포트:컨테이너 포트 | "8501:8501" |
environment | 환경 변수 | API_URL: http://api:8014 |
depends_on | 먼저 떠야 할 서비스 | [api] |
volumes | 남겨야 할 데이터 | ollama:/root/.ollama |
compose.yaml 읽어 보기
import yaml
compose = yaml.safe_load("""
services:
api: {build: ./api, ports: ["8013:8013"]}
ui: {build: ./ui, ports: ["8501:8501"], environment: {API_URL: "http://api:8013"}, depends_on: [api]}
""")
for name, svc in compose["services"].items():
print(name, svc.get("ports"), svc.get("environment", {}))코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
import yaml | YAML 파일을 읽는 PyYAML 라이브러리다. YAML 은 "YAML Ain't Markup Language" 의 머리글자로, 사람이 읽기 쉬운 설정 파일 꼴이다 |
yaml.safe_load(글) | YAML 글을 파이썬의 사전과 목록으로 바꾼다. safe(안전한)는 글 안에 숨긴 파이썬 명령을 실행하지 않는다는 뜻이다 |
{build: ./api, ports: [...]} | YAML 의 한 줄 꼴이다. 중괄호는 사전, 대괄호는 목록이 된다 |
svc.get("ports") | 서비스 설정에서 ports 칸을 읽는다. 없으면 None 이다 |
svc.get("environment", {}) | environment 칸이 없으면 빈 사전을 쓴다 |
이 코드가 뜻하는 것
식당의 개점 순서표를 읽고 "홀은 몇 번 문으로, 주방은 몇 번 문으로 손님을 받는지" 적어 보는 일과 같습니다. 여기서 개점 순서표가 compose.yaml, 문 번호가 ports, 홀 직원이 받은 주방 전화번호가 API_URL 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
compose = yaml.safe_load("""...""") | compose 설정 글을 사전으로 읽는다 | 순서표를 펼친다 |
compose["services"].items() | 서비스 이름과 설정을 하나씩 꺼낸다 | 홀, 주방을 하나씩 본다 |
print(name, svc.get("ports"), ...) | 포트와 환경 변수를 찍는다 | 문 번호와 받은 전화번호를 적는다 |
결과에서 api 는 ['8013:8013'] 포트만 있고 환경 변수가 없습니다. ui 는 {'API_URL': 'http://api:8013'} 을 받아, compose 안에서 서비스 이름 api 로 API 를 찾습니다.
작게 줄여서 눈으로 보기
depends_on 을 따라가면 어떤 차례로 띄워야 하는지 정해집니다. 파이썬의 graphlib 으로 그 차례를 구해 봅니다. compose 가 이 코드를 쓰는 것은 아니고, 생각을 흉내 낸 것입니다.
from graphlib import TopologicalSorter
# 서비스 이름 → depends_on 에 적은 서비스들
depends_on = {"ui": ["api"], "api": ["ollama"], "ollama": []}
print(list(TopologicalSorter(depends_on).static_order()))['ollama', 'api', 'ui']| 결과 | 읽는 법 |
|---|---|
'ollama' 가 맨 앞 | 아무에게도 기대지 않는 모델 서버를 먼저 띄운다 |
'api' 가 가운데 | ollama 에 기대므로 그다음이다 |
'ui' 가 맨 끝 | api 에 기대므로 마지막이다 |
graphlib 은 graph(점과 화살표로 이은 그림)를 다루는 표준 모듈이고, TopologicalSorter 는 "먼저 할 것부터 줄 세우는 도구"입니다. static_order() 가 그 줄을 돌려줍니다. 위의 주의할 점처럼, 이 차례는 "먼저 띄운다"일 뿐 "다 뜰 때까지 기다린다"가 아닙니다.
주의할 점
depends_on은 "먼저 띄운다"일 뿐 "다 뜰 때까지 기다린다"가 아닙니다. 화면이 API 보다 먼저 준비되어 처음 몇 초 동안 연결 실패가 날 수 있으니, 화면이 연결 실패를 알리고 다시 시도하게 만듭니다.- 컨테이너 안의
localhost는 그 컨테이너 자신입니다. 다른 서비스는 서비스 이름(api,ollama)으로 부릅니다.