MLOps 구축기 (1) - MLflow Tracking Server 구축 (Docker Compose + PostgreSQL + MinIO)

2026. 3. 5. 18:28·AI

회사에서 AI 프로젝트를 하다 보면 항상 비슷한 문제가 생긴다.

모델을 여러 번 학습하다 보면 어떤 파라미터로 학습했는지, 정확도가 얼마였는지, 어떤 모델이 제일 좋았는지 기억이 안 난다.

처음에는 그냥 노트에 적거나 파일 이름에 버전을 붙여서 관리한다.

model_v1
model_v2
model_final
model_final_real
model_final_real2

그리고 어느 순간 깨닫는다.

이건 사람이 관리할 수 있는 문제가 아니다.

그래서 보통 MLflow 같은 실험 관리 도구를 쓴다.

MLflow는 쉽게 말하면 실험 기록 저장, 파라미터/메트릭 기록, 모델 버전 관리를 해준다.

문제는 여기서 끝이 아니다.

MLflow를 로컬에서 한 번 띄워보는 것과
실제로 서버로 운영하는 것은 완전히 다른 이야기다.

그래서 이번 시리즈에서는 개인적으로 MLOps 환경을 처음부터 구축해보는 과정을 정리해보려고 한다.

이번 글에서는 가장 기본이 되는 MLflow 실험 추적 서버를 만든다.

Docker Compose를 이용해서 MLflow 서버, PostgreSQL(메타데이터 저장), MinIO(모델 파일 저장)를 구성한다.


왜 SQLite가 아니라 PostgreSQL인가

MLflow는 기본적으로 SQLite를 사용할 수 있다.

mlflow server --backend-store-uri sqlite:///mlflow.db

하지만 SQLite는 단일 파일 DB라서 서버 환경에는 적합하지 않다. 동시 접속 처리에 한계가 있고, 운영 환경으로 확장하기 어렵다.

그래서 보통은 PostgreSQL 같은 별도 DB를 사용한다.


왜 S3 스토리지가 필요한가

MLflow는 두 가지 데이터를 저장한다. 실험 메타데이터(파라미터, 메트릭, run 정보)와 모델 파일이다.

메타데이터는 텍스트라 DB에 넣으면 되지만, 모델 파일은 수십~수백 MB가 될 수 있다. DB에 바이너리를 넣으면 느리고 비효율적이다. 그래서 메타데이터는 DB에, 모델 파일은 오브젝트 스토리지에 나눠서 저장하는 게 표준적인 패턴이다.

AWS에서는 보통 S3를 쓰지만 로컬 환경에서는 MinIO를 많이 사용한다.

MinIO는 S3 API를 그대로 지원하는 오브젝트 스토리지다. S3는 AWS에서 제공하는 클라우드 파일 저장소(Simple Storage Service)인데, 워낙 널리 쓰이다 보니 이 통신 방식이 업계 표준이 됐다. MinIO는 같은 방식으로 통신하되 로컬에서 돌리는 거라서, 나중에 AWS로 옮기고 싶으면 MinIO만 S3로 교체하면 된다.


전체 구조

이번에 만들 구조는 이렇다.

[ Python 학습 코드 (로컬) ]
        │
        │  HTTP (파라미터, 메트릭, 모델 파일)
        ▼
[ MLflow Server (Docker) ]
        │                │
        ▼                ▼
[ PostgreSQL ]     [ MinIO ]
  메타데이터         아티팩트
  (파라미터,        (모델 파일,
   메트릭,          학습 산출물)
   run 정보)

모든 서비스는 Docker Compose로 띄운다.


사전 준비

Miniforge + conda 가상환경

M1 Mac이면 Anaconda 대신 Miniforge가 arm64 네이티브라 호환성이 좋다. 기존에 Anaconda가 깔려있다면 밀고 새로 설치하는 걸 추천한다.

# 기존 Anaconda 삭제 (경로는 conda info --base 로 확인)
rm -rf /opt/anaconda3
# .zshrc에서 >>> conda initialize >>> 블록도 삭제

Miniforge 설치와 가상환경 생성.

brew install miniforge
conda init zsh
source ~/.zshrc

conda create -n mlops python=3.11 -y
conda activate mlops
pip install mlflow scikit-learn pandas numpy

Python 3.11을 선택한 이유는 MLflow, PyTorch 호환성이 현 시점에서 가장 안정적이기 때문이다.

Docker Desktop

docker.com에서 "Mac with Apple Chip" 버전을 설치한다. 설치 후 docker run hello-world로 동작 확인.

메모리 설정은 Docker Desktop → Settings → Resources에서 조정할 수 있다. 16GB Mac에서 Docker 밖에서도 모델 학습을 할 거라면 6GB 정도로 잡고 나머지를 학습용으로 남겨두는 게 안전하다. MLflow + PostgreSQL + MinIO는 6GB면 충분히 돌아간다.


Dockerfile

여기서 하나 주의할 점이 있다.

공식 MLflow Docker 이미지(ghcr.io/mlflow/mlflow)에는 PostgreSQL 드라이버와 S3 SDK가 빠져있다.

이걸 모르면 컨테이너가 올라온 직후 이런 에러를 뿜으며 재시작을 반복한다.

ModuleNotFoundError: No module named 'psycopg2'

Dockerfile로 추가 설치해야 한다.

FROM ghcr.io/mlflow/mlflow:v3.10.0
RUN pip install psycopg2-binary boto3

psycopg2-binary는 Python에서 PostgreSQL에 연결하기 위한 드라이버, boto3는 MinIO에 아티팩트를 저장할 때 필요한 AWS S3 SDK다.


docker-compose.yml

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: mlflow
      POSTGRES_PASSWORD: mlflow
      POSTGRES_DB: mlflow
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U mlflow"]
      interval: 5s
      timeout: 5s
      retries: 5

  minio:
    image: minio/minio
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    volumes:
      - minio-data:/data
    command: server /data --console-address ":9001"
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 5s
      timeout: 5s
      retries: 5

  create-bucket:
    image: minio/mc
    depends_on:
      minio:
        condition: service_healthy
    entrypoint: >
      /bin/sh -c "
      mc alias set minio http://minio:9000 minioadmin minioadmin;
      mc mb --ignore-existing minio/mlflow;
      exit 0;
      "

  mlflow:
    build: .
    ports:
      - "5001:5000"
    depends_on:
      postgres:
        condition: service_healthy
      create-bucket:
        condition: service_completed_successfully
    environment:
      AWS_ACCESS_KEY_ID: minioadmin
      AWS_SECRET_ACCESS_KEY: minioadmin
      MLFLOW_S3_ENDPOINT_URL: http://minio:9000
    command: >
      mlflow server
      --host 0.0.0.0
      --port 5000
      --backend-store-uri postgresql://mlflow:mlflow@postgres:5432/mlflow
      --serve-artifacts
      --artifacts-destination s3://mlflow/
    restart: unless-stopped

volumes:
  pgdata:
  minio-data:

몇 가지 포인트를 짚으면:

--serve-artifacts와 --artifacts-destination이 핵심이다. 이 조합이 클라이언트의 아티팩트 업로드를 MLflow 서버가 프록시해서 MinIO에 저장하게 만든다. 클라이언트는 S3 인증 정보를 몰라도 된다.

create-bucket 서비스는 MinIO에 mlflow 버킷을 미리 만들어주는 역할이다. MinIO가 healthy 상태가 된 후 실행되고, 버킷 생성 후 종료된다. 이걸 안 하면 MLflow 서버가 아티팩트를 저장할 버킷이 없어서 에러가 난다.

healthcheck와 depends_on의 condition으로 서비스 기동 순서를 제어한다. PostgreSQL과 MinIO가 준비된 후에 MLflow가 올라오게 해야 접속 실패를 피할 수 있다.

포트는 5001을 썼다. macOS의 AirPlay 수신 기능이 5000번 포트를 점유하고 있어서 충돌이 난다. 5001로 바꾸거나, 시스템 설정 → 일반 → AirDrop 및 Handoff에서 AirPlay 수신 모드를 끄면 된다.


실행

docker compose up -d --build

4개 서비스가 순서대로 올라온다. 약 30초 정도 걸린다.

docker compose ps

postgres, minio가 healthy, mlflow가 Up 상태면 정상이다.

브라우저에서 http://localhost:5001을 열면 MLflow UI가 뜬다.


학습 코드에서 MLflow 연동

인프라가 올라왔으니 실제로 모델을 학습하고 기록해본다. scikit-learn의 Iris 데이터셋으로 간단하게.

# src/train.py
import mlflow
import mlflow.sklearn
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score

mlflow.set_tracking_uri("http://localhost:5001")
mlflow.set_experiment("iris-classification")

X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

params = {
    "n_estimators": 100,
    "max_depth": 5,
    "random_state": 42
}

with mlflow.start_run():
    model = RandomForestClassifier(**params)
    model.fit(X_train, y_train)

    preds = model.predict(X_test)
    acc = accuracy_score(y_test, preds)
    f1 = f1_score(y_test, preds, average="weighted")

    mlflow.log_params(params)
    mlflow.log_metrics({"accuracy": acc, "f1_score": f1})
    mlflow.sklearn.log_model(model, "model")

    print(f"Accuracy: {acc:.4f}, F1: {f1:.4f}")

set_tracking_uri로 MLflow 서버 주소를 지정하고, start_run() 블록 안에서 학습하면서 log_params, log_metrics, log_model로 기록을 남긴다. 이 데이터가 HTTP로 MLflow 서버에 전송되고, 서버가 메타데이터는 PostgreSQL에, 모델 파일은 MinIO에 저장한다.

python src/train.py
Accuracy: 1.0000, F1: 1.0000

Iris 데이터셋이 워낙 단순해서 정확도 100%가 나왔다. 숫자 자체는 의미 없고, 파이프라인이 동작하는지가 포인트다.

MLflow UI에서 iris-classification 실험을 클릭하면 방금 실행한 run이 보인다. Parameters, Metrics 탭에 전부 기록돼있다.

MinIO 콘솔(http://localhost:9001)에 접속해보면 실제로 모델 파일이 저장된 걸 확인할 수 있다. 로그인 계정은 docker-compose.yml에서 설정한 MINIO_ROOT_USER / MINIO_ROOT_PASSWORD 값이다 (이 글에서는 minioadmin / minioadmin). mlflow 버킷 안에 model.pkl, MLmodel, conda.yaml, requirements.txt 등이 들어가있다. 학습 코드에서 log_model로 보낸 모델이 MLflow 서버를 거쳐 MinIO에 저장된 거다.


세팅할 때 알아두면 좋은 것들

MLflow 3.x + Docker에서 로컬 파일시스템 아티팩트 저장은 주의가 필요하다. SQLite + 로컬 볼륨으로 가볍게 시작하려고 했더니 log_model 호출 시 아래 에러가 났다.

OSError: [Errno 30] Read-only file system: '/mlflow'

원인은 MLflow 서버가 Docker 컨테이너 안에서 아티팩트 경로를 내부 경로로 잡는데, 로컬에서 돌아가는 Python 클라이언트가 그 경로를 Mac의 로컬 경로로 해석해서 직접 쓰려고 한 거다. Mac에는 /mlflow 경로가 없으니 당연히 실패한다. 여러 방법을 시도했지만 해결이 안 됐고, 공식 레포지토리 예제도 전부 S3 호환 스토리지를 사용하고 있었다. MinIO 같은 S3 호환 스토리지를 쓰면 아티팩트가 S3 프로토콜(s3://mlflow/)로 저장되고, MLflow 서버가 프록시 역할을 해주기 때문에 이 문제가 원천적으로 없어진다.

공식 MLflow Docker 이미지에 DB 드라이버가 없다. psycopg2-binary(PostgreSQL)와 boto3(S3)를 Dockerfile로 추가 설치해야 한다. 안 하면 컨테이너가 ModuleNotFoundError를 뿜으며 재시작을 반복한다.

macOS 5000번 포트 충돌. AirPlay 수신 기능이 5000번을 점유한다. MLflow 기본 포트가 5000이라 충돌이 나니까, 5001 같은 다른 포트를 쓰거나 AirPlay를 끄면 된다.

클라이언트-서버 버전을 맞춰야 한다. 로컬에 설치한 mlflow 패키지 버전과 Docker 서버의 MLflow 버전이 크게 다르면 API 호환 문제가 생길 수 있다. 이 글에서는 둘 다 3.10.0을 사용했다.


정리

M1 Mac 위에 Docker Compose로 MLflow 실험 추적 환경을 구축했다. PostgreSQL이 메타데이터를, MinIO가 모델 아티팩트를 저장하고, MLflow 서버가 이 둘을 연결해서 UI와 API를 제공하는 구조다.

이건 MLOps의 첫 번째 조각이다. 모델 학습할 때마다 수동으로 기록하던 걸 자동화한 것. 수십, 수백 번 실험을 돌리다 보면 이 기록이 없으면 어떤 설정이 최적이었는지 찾을 수가 없다.

다음 편에서는 하이퍼파라미터를 바꿔가며 실험을 여러 개 돌리고, MLflow UI에서 비교하는 걸 다뤄볼 예정이다.


사용한 도구

  • MLflow 3.10.0
  • Docker Desktop (Mac with Apple Chip)
  • PostgreSQL 16 Alpine
  • MinIO (S3 호환 오브젝트 스토리지)
  • Miniforge (conda)
  • Python 3.11.14
  • scikit-learn

'AI' 카테고리의 다른 글

온디바이스 AI 경량화 (1) — INT8 양자화로 CIFAR-10 모델 92% 줄이기 (TFLite PTQ)  (0) 2026.04.17
MLOps 구축기 (5) - MLflow 기반 MLOps 파이프라인 전체 정리  (1) 2026.03.13
MLOps 구축기 (4) - GitHub Actions CI/CD와 Prometheus + Grafana 모니터링  (0) 2026.03.13
MLOps 구축기 (3) - MLflow 모델을 FastAPI로 서빙하기  (0) 2026.03.08
MLOps 구축기 (2) - MLflow 하이퍼파라미터 실험 관리와 Model Registry  (0) 2026.03.08
'AI' 카테고리의 다른 글
  • MLOps 구축기 (5) - MLflow 기반 MLOps 파이프라인 전체 정리
  • MLOps 구축기 (4) - GitHub Actions CI/CD와 Prometheus + Grafana 모니터링
  • MLOps 구축기 (3) - MLflow 모델을 FastAPI로 서빙하기
  • MLOps 구축기 (2) - MLflow 하이퍼파라미터 실험 관리와 Model Registry
João Jin
João Jin
모바일 · 보안 · AI 기록
  • João Jin
    João Jin - 모바일 · 보안 · AI
    João Jin
  • 전체
    오늘
    어제
    • 분류 전체보기 (30)
      • 프로젝트 (3)
      • 개발기 (8)
      • 모바일 (8)
      • 보안 (2)
      • AI (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
    • X
  • 공지사항

  • 인기 글

  • 태그

    MCP 보안
    Native
    MCP
    안드로이드 NDK
    mcp-fence
    머신러닝
    ndk
    AI 에이전트 보안
    MLFlow
    MLOps
    Android
    온디바이스AI
    LLM 보안
    Docker
    model context protocol
    JNI
    Docker Compose
    AI
    LINE WORKS
    FastAPI
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
João Jin
MLOps 구축기 (1) - MLflow Tracking Server 구축 (Docker Compose + PostgreSQL + MinIO)
상단으로

티스토리툴바