{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "4f6eae6c",
   "metadata": {},
   "source": [
    "# Part 9: 프로덕션 레벨 API와 Docker\n",
    "\n",
    "**⬅️ 이전 시간: [Part 8: FastAPI를 이용한 모델 서빙](../08_model_serving_with_fastapi/part_8_model_serving_with_fastapi.md)**\n",
    "**➡️ 다음 시간: [Part 10: 전문가로 가는 길](../10_expert_path/part_10_expert_path.md)**\n",
    "\n",
    "---\n",
    "\n",
    "## 1. 학습 목표 (Learning Objectives)\n",
    "\n",
    "이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.\n",
    "\n",
    "- 유지보수와 협업에 용이하도록 FastAPI 프로젝트를 기능별로 구조화할 수 있습니다.\n",
    "- `APIRouter`를 사용하여 API 엔드포인트를 모듈화하고 관리할 수 있습니다.\n",
    "- \"제 컴퓨터에선 되는데요...\" 문제를 해결하는 Docker의 원리를 이해하고, 이미지/컨테이너 개념을 설명할 수 있습니다.\n",
    "- `Dockerfile`을 작성하여 파이썬 애플리케이션을 컨테이너 이미지로 만들 수 있습니다.\n",
    "- `docker-compose.yml`을 사용하여 여러 서비스(API 서버, DB 등)를 한 번에 실행하고 관리할 수 있습니다.\n",
    "\n",
    "## 2. 핵심 키워드 (Keywords)\n",
    "\n",
    "`프로젝트 구조화`, `관심사 분리(SoC)`, `APIRouter`, `의존성 주입(DI)`, `Docker`, `컨테이너(Container)`, `이미지(Image)`, `Dockerfile`, `Docker Compose`\n",
    "\n",
    "## 3. 도입: '1인 식당'에서 '프랜차이즈 레스토랑'으로\n",
    "\n",
    "지난 시간에는 FastAPI로 '1인 식당'처럼 빠르게 AI 모델 API를 만들었습니다. 프로토타입으로는 훌륭하지만, 메뉴가 다양해지고(기능 추가) 여러 셰프가 협업해야 하는(팀 개발) 실제 '프랜차이즈 레스토랑' 환경에서는 한계가 명확합니다.\n",
    "\n",
    "이번 주차에는 `main.py` 하나에 모든 것을 담던 방식에서 벗어나, **유지보수와 확장이 용이한 프로젝트 구조**로 우리 레스토랑을 리팩터링합니다. 또한, 어떤 백화점에 입점하든 동일한 맛과 서비스를 보장하는 **'밀키트(Docker 컨테이너)'** 기술을 배워, 우리 AI 서비스를 세상 어디에든 쉽고 안정적으로 배포하는 능력을 갖추게 될 것입니다.\n",
    "\n",
    "> [!TIP]\n",
    "> 본 파트의 모든 예제 코드는 `../../source_code/part_9_fastapi_production` 폴더에서 직접 실행하고 수정해볼 수 있습니다.\n",
    "\n",
    "---\n",
    "\n",
    "## 4. 1단계: 프로덕션 레벨 프로젝트 구조 설계\n",
    "\n",
    "> **🎯 1-2일차 목표:** '관심사 분리' 원칙에 따라 FastAPI 프로젝트를 기능별 파일과 폴더로 재구성합니다.\n",
    "\n",
    "`main.py` 하나에 모든 코드가 있는 것은, 주방장이 혼자 주문받고, 요리하고, 서빙까지 하는 것과 같습니다. 우리는 이제 역할을 분담하여 '프랜차이즈 본사'처럼 체계적인 시스템을 만들 것입니다.\n",
    "\n",
    "- **`settings.py`**: 본사의 운영 방침 (환경 설정)\n",
    "- **`schemas.py`**: 모든 지점에서 통일된 '주문서 양식' (데이터 형식 정의)\n",
    "- **`routers/`**: '한식 코너', '중식 코너' 등 메뉴별로 분리된 주방 (기능별 API)\n",
    "- **`main.py`**: 모든 코너를 총괄하고 손님을 맞는 '총지배인'\n",
    "\n",
    "### 4-1. 프로젝트 폴더 및 라우터 분리\n",
    "먼저 아래와 같이 프로젝트 구조를 만듭니다. `main.py`에 있던 예측 관련 로직을 `routers/predictions.py`로 옮깁니다.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "59709506",
   "metadata": {},
   "outputs": [],
   "source": [
    "/fastapi_project\n",
    "├── app/\n",
    "│   ├── __init__.py\n",
    "│   ├── main.py\n",
    "│   └── routers/\n",
    "│       ├── __init__.py\n",
    "│       └── predictions.py\n",
    "├── requirements.txt\n",
    "└── iris_model.pkl"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "111735a8",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "**`app/routers/predictions.py` (예측 코너 주방)**:\n",
    "`APIRouter`를 사용하여 예측 기능만을 위한 독립적인 API 그룹을 만듭니다.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2024693f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from fastapi import APIRouter, HTTPException\n",
    "from pydantic import BaseModel\n",
    "from typing import List\n",
    "import joblib\n",
    "import numpy as np\n",
    "\n",
    "# 1. 이 라우터만의 '주문서 양식' 정의\n",
    "class PredictionRequest(BaseModel):\n",
    "    features: List[float]\n",
    "\n",
    "# 2. APIRouter 인스턴스 생성\n",
    "router = APIRouter(prefix=\"/predict\", tags=[\"predictions\"])\n",
    "\n",
    "# 3. 모델 로드 (임시)\n",
    "model = joblib.load(\"iris_model.pkl\")\n",
    "class_names = ['setosa', 'versicolor', 'virginica']\n",
    "\n",
    "# 4. 예측 엔드포인트 정의\n",
    "@router.post(\"/\", summary=\"붓꽃 품종 예측\")\n",
    "def predict_iris(request: PredictionRequest):\n",
    "    model_input = np.array(request.features).reshape(1, -1)\n",
    "    prediction_idx = model.predict(model_input)[0]\n",
    "    confidence = model.predict_proba(model_input)[0][prediction_idx]\n",
    "    \n",
    "    return {\n",
    "        \"predicted_class\": class_names[prediction_idx],\n",
    "        \"confidence\": float(confidence)\n",
    "    }"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e033fd40",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### 4-2. 최종 통합 (`main.py`)\n",
    "'총지배인' `main.py`에서 분리된 '예측 코너'를 전체 레스토랑에 포함시킵니다.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "367ccdff",
   "metadata": {},
   "outputs": [],
   "source": [
    "from fastapi import FastAPI\n",
    "from .routers import predictions\n",
    "\n",
    "app = FastAPI(title=\"AI 레스토랑 (프랜차이즈 ver)\")\n",
    "\n",
    "# \"예측 코너\" 라우터 포함\n",
    "app.include_router(predictions.router)\n",
    "\n",
    "@app.get(\"/\")\n",
    "def read_root():\n",
    "    return {\"message\": \"AI 레스토랑에 오신 것을 환영합니다.\"}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8ab6bc79",
   "metadata": {},
   "source": [
    "\n",
    "이제 `uvicorn app.main:app --reload`로 서버를 실행하고 `http://127.0.0.1:8000/docs`에 접속하면, `predictions`라는 태그로 그룹화된 API를 확인할 수 있습니다.\n",
    "\n",
    "---\n",
    "\n",
    "## 5. 2단계: Docker로 우리 앱 포장하기\n",
    "\n",
    "> **🎯 3-4일차 목표:** Dockerfile을 작성하여 우리 앱을 이미지로 만들고, docker-compose로 실행합니다.\n",
    "\n",
    "### 5-1. 왜 Docker를 써야 할까?\n",
    "> **💡 비유: '어디서든 똑같은 맛을 내는 밀키트'**\n",
    ">\n",
    "> 우리 레스토랑의 레시피와 재료를 완벽하게 준비해도, 지점마다 주방 환경(OS, 라이브러리 버전 등)이 다르면 음식 맛이 달라질 수 있습니다. \"제 주방(컴퓨터)에서는 되는데요?\" 라는 문제는 개발자들의 흔한 악몽입니다.\n",
    ">\n",
    "> **Docker**는 우리 레스토랑의 모든 것(코드, 라이브러리, 설정)을 하나의 **'밀키트(이미지)'**로 완벽하게 포장하는 기술입니다. 이 밀키트는 어떤 주방(서버)에서 데우든 항상 똑같은 환경에서, 똑같은 맛의 요리를 만들어냅니다.\n",
    "\n",
    "### 5-2. Dockerfile: 밀키트 레시피 작성\n",
    "`Dockerfile`은 이 밀키트를 만드는 '레시피'입니다. 프로젝트 최상위 폴더에 `Dockerfile`이라는 이름으로 파일을 만듭니다.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "414b289c",
   "metadata": {},
   "outputs": [],
   "source": [
    "dockerfile\n",
    "# 1. 베이스 이미지 선택 (기본 주방 키트 가져오기)\n",
    "FROM python:3.9-slim\n",
    "\n",
    "# 2. 작업 디렉토리 설정 (조리대 위치 지정)\n",
    "WORKDIR /code\n",
    "\n",
    "# 3. 의존성 파일 먼저 복사 (소스가 바뀌어도 의존성이 같으면 캐시 재사용)\n",
    "COPY ./requirements.txt /code/requirements.txt\n",
    "\n",
    "# 4. 의존성 설치 (필요한 조미료 채우기)\n",
    "RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt\n",
    "\n",
    "# 5. 소스 코드와 모델 파일 복사 (레시피와 식재료 넣기)\n",
    "COPY ./app /code/app\n",
    "COPY ./iris_model.pkl /code/iris_model.pkl\n",
    "\n",
    "# 6. 서버 실행 (레스토랑 개점!)\n",
    "# 0.0.0.0 주소로 실행해야 Docker 외부에서 접속 가능\n",
    "CMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7990dabb",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### 5-3. Docker Compose: 프랜차이즈 매장 동시 오픈\n",
    "`docker-compose.yml`은 여러 '밀키트'(서비스)들을 정의하고 한 번에 관리하는 '프랜차이즈 오픈 계획서'입니다. 지금은 API 서버 하나지만, 나중에 DB 등이 추가되면 그 위력을 실감하게 됩니다.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9b289699",
   "metadata": {},
   "outputs": [],
   "source": [
    "yaml\n",
    "# docker-compose.yml\n",
    "version: '3.8'\n",
    "\n",
    "services:\n",
    "  # 서비스 이름 정의 (우리 레스토랑 지점)\n",
    "  api:\n",
    "    # 어떤 Dockerfile을 사용할지 지정\n",
    "    build: .\n",
    "    # 포트 매핑 (외부 8000번 포트 -> 컨테이너 내부 8000번 포트)\n",
    "    ports:\n",
    "      - \"8000:8000\"\n",
    "    # 컨테이너에 이름 부여\n",
    "    container_name: fastapi_iris_app"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a6d3ae3",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "---\n",
    "\n",
    "## 6. 캡스톤 프로젝트 연계 미니 프로젝트: 나만의 AI API '밀키트' 만들기\n",
    "\n",
    "지난 8장 미니 프로젝트에서 여러분은 '1인 식당'처럼 빠르게 동작하는 API를 만들었습니다. 이제 이 식당을 '프랜차이즈 본사'처럼 체계적으로 리팩터링하고, 어디서든 동일한 품질을 보장하는 '밀키트(Docker 이미지)'로 포장하는 작업을 진행합니다. 이 과정은 캡스톤 프로젝트의 코드를 체계적으로 관리하고, 안정적으로 배포하는 핵심 역량을 길러줍니다.\n",
    "\n",
    "### 프로젝트 목표\n",
    "- 8장 미니 프로젝트에서 만든 '나만의 AI 모델 API'를 '관심사 분리' 원칙에 따라 체계적인 폴더 구조로 리팩터링할 수 있습니다.\n",
    "- `APIRouter`를 사용하여 API 엔드포인트 로직을 `main.py`에서 완전히 분리할 수 있습니다.\n",
    "- `Dockerfile`을 작성하여, 내 모델과 API 코드가 포함된 '나만의 Docker 이미지'를 성공적으로 빌드할 수 있습니다.\n",
    "- `docker-compose up` 명령어로, 직접 만든 AI API 컨테이너를 실행하고 API가 정상적으로 동작하는 것을 확인할 수 있습니다.\n",
    "\n",
    "### 개발 과정\n",
    "1.  **프로젝트 구조 리팩터링**:\n",
    "    - 8장에서 만든 `my_api` 프로젝트를 아래와 같은 구조로 변경합니다.\n",
    "      "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "359863eb",
   "metadata": {},
   "outputs": [],
   "source": [
    "/my_prod_api\n",
    "      ├── app/\n",
    "      │   ├── __init__.py\n",
    "      │   ├── main.py\n",
    "      │   ├── schemas.py       # Pydantic 모델 분리\n",
    "      │   └── routers/\n",
    "      │       ├── __init__.py\n",
    "      │       └── predict.py   # 예측 로직 분리\n",
    "      ├── models/              # 모델/토크나이저 등은 별도 폴더로\n",
    "      │   └── sentiment_model.pth\n",
    "      ├── requirements.txt\n",
    "      ├── Dockerfile\n",
    "      └── docker-compose.yml"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "be7b8021",
   "metadata": {},
   "source": [
    "\n",
    "    - `schemas.py`: `ReviewRequest`, `SentimentResponse` 등 Pydantic 모델들을 이 파일로 옮깁니다.\n",
    "    - `routers/predict.py`: `APIRouter`를 생성하고, 모델 로딩과 예측 로직을 이 파일로 옮깁니다. (모델 파일 경로는 `../models/sentiment_model.pth`와 같이 수정되어야 합니다.)\n",
    "\n",
    "2.  **`main.py` 재작성**:\n",
    "    - `main.py`는 이제 '총지배인' 역할만 합니다. FastAPI 앱을 생성하고, `predict.py`에 정의된 라우터를 `app.include_router()`로 포함시키는 코드만 남깁니다.\n",
    "\n",
    "3.  **`requirements.txt` 생성**:\n",
    "    - 8장 프로젝트에 필요했던 모든 라이브러리(`fastapi`, `uvicorn`, `torch`, `pydantic` 등)를 `requirements.txt`에 명시합니다.\n",
    "\n",
    "4.  **`Dockerfile` 작성**:\n",
    "    - 본문 예제를 참고하여 `Dockerfile`을 작성합니다.\n",
    "    - `COPY` 명령어를 사용하여 `app/` 폴더 전체와 `models/` 폴더 전체를 컨테이너의 작업 디렉토리로 복사해야 합니다.\n",
    "      "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e3c88438",
   "metadata": {},
   "outputs": [],
   "source": [
    "dockerfile\n",
    "      # ...\n",
    "      COPY ./app /code/app\n",
    "      COPY ./models /code/models\n",
    "      # ..."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "231659c4",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "5.  **`docker-compose.yml` 작성 및 실행**:\n",
    "    - 본문 예제를 참고하여 `docker-compose.yml` 파일을 작성합니다.\n",
    "    - 터미널에서 `docker-compose up --build` 명령을 실행하여 이미지를 빌드하고 컨테이너를 실행합니다.\n",
    "    - `http://localhost:8000/docs`에 접속하여, 리팩터링되고 Docker 컨테이너 안에서 실행 중인 여러분의 API가 이전과 똑같이 잘 동작하는지 확인합니다.\n",
    "\n",
    "### 캡스톤 프로젝트 연계 방안\n",
    "이번 미니 프로젝트에서 완성한 **프로젝트 구조**와 **Dockerfile**, **docker-compose.yml**은 여러분의 캡스톤 프로젝트의 '뼈대'가 됩니다. 새로운 기능(예: 사용자 인증, 결과 저장)이 필요할 때마다, 이 구조에 따라 `routers`에 새로운 파일을 추가하고, DB 컨테이너를 `docker-compose.yml`에 추가하는 방식으로 체계적인 확장이 가능합니다. 이는 최종 프로젝트의 기술적 완성도와 협업 효율성을 크게 높여줄 것입니다.\n",
    "\n",
    "---\n",
    "\n",
    "## ⚠️ What Could Go Wrong? (토론 주제)\n",
    "\n",
    "이제 우리는 Docker를 사용하여 어디서든 동일하게 실행되는 애플리케이션을 만들 수 있게 되었습니다. 하지만 실제 프로덕션 환경은 '실행되는 것' 이상의 것을 요구합니다. 다음 시나리오들에 대해 토론해보세요.\n",
    "\n",
    "1.  **점점 비대해지는 Docker 이미지**\n",
    "    -   **상황**: 처음에는 200MB였던 우리 팀의 Docker 이미지가, 여러 기능이 추가되면서 이제는 2GB를 넘어섰습니다. 이미지를 빌드하고 레지스트리에 푸시하는 시간이 너무 오래 걸리고, 서버에 배포하는 시간도 길어졌습니다.\n",
    "    -   **토론**:\n",
    "        -   Docker 이미지 크기가 커지는 일반적인 원인들은 무엇일까요? (예: `python:3.9` 같은 큰 베이스 이미지 사용, `.dockerignore` 파일의 부재, 빌드 캐시나 불필요한 도구가 이미지에 남는 경우 등)\n",
    "        -   **다단계 빌드(Multi-stage build)**란 무엇이며, 이 기법이 어떻게 더 작고 안전한 프로덕션 이미지를 만드는 데 도움이 될 수 있을까요?\n",
    "\n",
    "2.  **환경 설정의 대혼란**\n",
    "    -   **상황**: 한 개발자가 로컬 DB 접속 주소(`\"localhost:5432\"`)를 소스 코드에 하드코딩했습니다. 그의 컴퓨터에서는 앱이 완벽하게 동작했지만, 다른 DB를 사용하는 테스트 환경에 배포하자마자 앱이 실패했습니다.\n",
    "    -   **토론**:\n",
    "        -   소스 코드에 설정값(DB 주소, API 키 등)을 하드코딩하는 것이 왜 나쁜 습관일까요?\n",
    "        -   개발, 테스트, 프로덕션 환경별로 달라지는 설정들을 관리하는 더 나은 방법들은 무엇이 있을까요? (예: 환경 변수, `.env` 파일, `docker-compose.yml`의 `env_file` 옵션 활용)\n",
    "\n",
    "3.  **\"죽었는데 살아있다\"고 말하는 헬스 체크(Health Check)**\n",
    "    -   **상황**: 우리 서비스의 헬스 체크는 단순히 컨테이너가 '실행 중'인지만 확인합니다. 어느 날 코드의 버그로 인해 `/predict` 엔드포인트가 계속 `500 Internal Server Error`를 반환하기 시작했지만, 컨테이너 자체는 정상적으로 실행 중이었습니다. 그 결과, 오케스트레이션 시스템(예: Kubernetes, Docker Swarm)은 서비스가 건강하다고 판단하여 계속해서 고장난 컨테이너로 트래픽을 보냈고, 사용자들은 장애를 겪었습니다.\n",
    "    -   **토론**:\n",
    "        -   단순히 '프로세스가 실행 중인지'만 확인하는 헬스 체크의 문제점은 무엇일까요?\n",
    "        -   의미 있는 헬스 체크란 무엇을 확인해야 할까요? 단순히 `/health` 엔드포인트에서 `200 OK`를 반환하는 것으로 충분할까요, 아니면 DB 연결이나 모델 가용성 같은 핵심 의존성을 함께 확인해야 할까요?\n",
    "\n",
    "4.  **무심코 `root`로 실행한 컨테이너**\n",
    "    -   **상황**: 빠른 개발을 위해, `Dockerfile`에서 별다른 사용자 설정 없이 컨테이너를 `root` 사용자로 실행해왔습니다. 최근 실시한 보안 점검에서 이 부분이 주요 취약점으로 지적되었습니다.\n",
    "    -   **토론**:\n",
    "        -   컨테이너를 `root` 권한으로 실행하는 것이 왜 보안상 위험할까요? 만약 공격자가 우리 애플리케이션의 취약점을 통해 쉘(shell)을 획득한다면 어떤 일이 벌어질 수 있을까요?\n",
    "        -   `Dockerfile`에서 `root`가 아닌 일반 사용자(non-root user)를 생성하고, 그 사용자로 애플리케이션을 실행하도록 만드는 구체적인 단계를 설명해보세요.\n",
    "\n",
    "## 7. 되짚어보기 (Summary)\n",
    "\n",
    "이번 주차에는 '1인 식당'을 '프랜차이즈 레스토랑'으로 확장하는 여정을 통해, 프로덕션 레벨의 AI 서비스를 구축하는 핵심 기술을 배웠습니다.\n",
    "\n",
    "- **프로젝트 구조화**: `APIRouter`를 사용해 기능별로 코드를 분리하여, 유지보수와 협업이 용이한 프로젝트 구조를 설계했습니다.\n",
    "- **Docker 컨테이너화**: '밀키트' 비유를 통해 Docker의 필요성을 이해하고, `Dockerfile`을 작성하여 우리 앱을 재현 가능한 이미지로 만들었습니다.\n",
    "- **Docker Compose**: `docker-compose.yml`을 통해 여러 서비스를 명령어 하나로 관리하는 방법을 익혀, 복잡한 애플리케이션 배포의 기초를 다졌습니다.\n",
    "\n",
    "---\n",
    "\n",
    "## 8. 트러블슈팅 (Troubleshooting)\n",
    "\n",
    "- **`docker-compose up` 실행 시 `Cannot connect to the Docker daemon...` 오류가 발생하나요?**\n",
    "  - Docker 데몬(백그라운드 서비스)이 실행 중이 아니라는 의미입니다.\n",
    "    - **Mac/Windows**: Docker Desktop 앱이 실행 중인지 확인하고, 아니라면 실행해주세요.\n",
    "    - **Linux**: `sudo systemctl start docker` 명령으로 Docker 데몬을 시작하거나, `docker-compose` 앞에 `sudo`를 붙여 실행해보세요 (`sudo docker-compose up`).\n",
    "- **`docker-compose up` 실행 시 `build path ... does not exist` 오류가 발생하나요?**\n",
    "  - `docker-compose.yml`이 있는 위치에서 `Dockerfile`을 찾을 수 없다는 의미입니다. `docker-compose.yml`과 `Dockerfile`이 프로젝트 최상위 폴더에 함께 있는지 확인하세요.\n",
    "- **`CMD [\"uvicorn\", \"app.main:app\", ...]` 실행 중 `ModuleNotFoundError: No module named 'app'` 오류가 발생하나요?**\n",
    "  - Docker 컨테이너 안에서 `app`이라는 폴더/모듈을 찾지 못하는 경우입니다. `Dockerfile`의 `COPY ./app /code/app` 부분이 올바르게 작성되었는지, 그리고 실제 프로젝트 구조와 일치하는지 확인하세요. 작업 디렉토리(`WORKDIR`)가 `/code`로 설정되어 있으므로, 파이썬은 `/code/app`을 찾게 됩니다.\n",
    "- **`/docs` 접속은 되는데, `Try it out` 실행 시 모델 파일을 찾지 못한다는 오류(`FileNotFoundError`)가 발생하나요?**\n",
    "  - `Dockerfile`에 `COPY ./iris_model.pkl /code/iris_model.pkl` 라인을 추가하여 모델 파일을 이미지 안으로 복사했는지 확인하세요. 컨테이너는 호스트 머신(여러분의 컴퓨터)의 파일 시스템을 직접 볼 수 없으므로, 필요한 모든 파일을 이미지에 포함시켜야 합니다.\n",
    "- **`docker-compose up` 후 `http://localhost:8000` 접속이 안 되나요?**\n",
    "  - `docker-compose.yml`의 `ports` 설정이 ` \"8000:8000\" `으로 올바르게 되어 있는지 확인하세요. 이는 여러분 컴퓨터의 `8000`번 포트를 컨테이너의 `8000`번 포트로 연결하는 설정입니다.\n",
    "  - `Dockerfile`의 `CMD` 명령어에 `\"--host\", \"0.0.0.0\"` 옵션이 포함되어 있는지 확인하세요. 컨테이너 내부에서 실행되는 서버는 `0.0.0.0` 주소로 바인딩되어야만 외부(호스트 머신)에서 접속할 수 있습니다. `127.0.0.1`은 컨테이너 자기 자신만을 의미합니다.\n",
    "\n",
    "더 자세한 문제 해결 가이드나 다른 동료들이 겪은 문제에 대한 해결책이 궁금하다면, 아래 문서를 참고해주세요.\n",
    "\n",
    "- **➡️ [Geumdo-Docs: TROUBLESHOOTING.md](../../../TROUBLESHOOTING.md)**\n",
    "\n",
    "---\n",
    "\n",
    "## 9. 더 깊이 알아보기 (Further Reading)\n",
    "- [TestDriven.io: FastAPI and Docker](https://testdriven.io/blog/dockerizing-fastapi/): Docker를 사용한 FastAPI 배포에 대한 상세한 튜토리얼\n",
    "- [Docker 공식 문서: Get Started](https://docs.docker.com/get-started/): Docker의 기본 개념부터 차근차근 배울 수 있는 최고의 자료\n",
    "- [『코어 자바스크립트』 저자의 Docker/k8s 강의](https://www.inflearn.com/course/%EB%8F%84%EC%BB%A4-k8s-%ED%95%B5%EC%8B%AC%EA%B0%9C%EB%념-%EC%9D%B5%ED%9E%88%EA%B8%B0): Docker와 쿠버네티스의 핵심 원리를 비유를 통해 쉽게 설명하는 강의\n",
    "\n",
    "---\n",
    "\n",
    "**➡️ 다음 시간: [Part 10: 전문가로 가는 길](../10_expert_path/part_10_expert_path.md)** "
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 5
}
