개발 서버에서 실행 중인 docker-compose.yml 전문. 서버 소스 루트에 있는 파일로, Compose 설치도 이 파일을 사용. 아래 블록은 서버 저장소의 파일에서 자동 생성(scripts/gen-compose.ts). 원본의 주석은 생략하고 각 항목의 의미는 아래 표에 정리.

name: geo-mlops-mlflow

x-logging: &default-logging
  driver: json-file
  options:
    max-size: "50m"
    max-file: "5"

services:
  app:
    build:
      context: .
      args:
        GIT_COMMIT: ${GIT_COMMIT:-}
        BUILT_AT: ${BUILT_AT:-}
    container_name: geo-mlops-app
    logging: *default-logging
    restart: unless-stopped
    mem_limit: ${GEO_APP_MEM_LIMIT:-16g}
    network_mode: host
    env_file:
      - path: /etc/geo-mlops/app.env
        required: false
    environment:
      AWS_REQUEST_CHECKSUM_CALCULATION: when_required
      AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
    volumes:
      - /etc/rancher/k3s/k3s.yaml:/etc/rancher/k3s/k3s.yaml:ro
    healthcheck:
      test:
        - CMD
        - python3
        - -c
        - "import urllib.request as u; u.urlopen('http://localhost:10000/api/v1/health', timeout=3)"
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    depends_on:
      mlflow-postgres:
        condition: service_healthy
      createbucket:
        condition: service_completed_successfully

  mlflow-postgres:
    image: postgres:18-alpine
    container_name: geo-mlops-mlflow-postgres
    logging: *default-logging
    restart: unless-stopped
    environment:
      POSTGRES_USER: mlflow
      POSTGRES_PASSWORD: mlflow
      POSTGRES_DB: mlflow
      PGDATA: /var/lib/postgresql/data/pgdata
    ports:
      - "5433:5432"
    volumes:
      - mlflow-postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U mlflow -d mlflow"]
      interval: 5s
      timeout: 5s
      retries: 5

  rustfs:
    image: rustfs/rustfs:1.0.0-beta.7
    container_name: geo-mlops-mlflow-rustfs
    logging: *default-logging
    restart: unless-stopped
    environment:
      RUSTFS_ACCESS_KEY: rustfsadmin
      RUSTFS_SECRET_KEY: rustfsadmin
      RUSTFS_VOLUMES: /data
      RUSTFS_ADDRESS: 0.0.0.0:9000
      RUSTFS_CONSOLE_ENABLE: "true"
      RUSTFS_CONSOLE_ADDRESS: 0.0.0.0:9001
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - rustfs-data:/data

  createbucket:
    image: amazon/aws-cli:2.34.63
    container_name: geo-mlops-mlflow-createbucket
    logging: *default-logging
    depends_on:
      - rustfs
    restart: "no"
    environment:
      AWS_ACCESS_KEY_ID: rustfsadmin
      AWS_SECRET_ACCESS_KEY: rustfsadmin
      AWS_DEFAULT_REGION: us-east-1
    entrypoint: >
      /bin/sh -c "
      until aws --endpoint-url http://rustfs:9000 s3 ls; do
        echo 'waiting for rustfs...'; sleep 2;
      done;
      aws --endpoint-url http://rustfs:9000 s3 mb s3://mlflow || true;
      aws --endpoint-url http://rustfs:9000 s3 mb s3://datasets || true;
      aws --endpoint-url http://rustfs:9000 s3 mb s3://registry || true;
      echo 'artifact bucket ready: s3://mlflow/';
      "

  prometheus:
    image: prom/prometheus:v2.55.1
    container_name: geo-mlops-prometheus
    logging: *default-logging
    restart: unless-stopped
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --web.enable-lifecycle
    ports:
      - "9090:9090"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./monitoring/alert-rules.yml:/etc/prometheus/alert-rules.yml:ro
      - prometheus-data:/prometheus
    depends_on:
      - alertmanager

  alertmanager:
    image: prom/alertmanager:v0.27.0
    container_name: geo-mlops-alertmanager
    logging: *default-logging
    restart: unless-stopped
    command:
      - --config.file=/etc/alertmanager/alertmanager.yml
      - --storage.path=/alertmanager
    ports:
      - "9093:9093"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - ./monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
      - alertmanager-data:/alertmanager

  proxpi:
    image: epicwink/proxpi:1.3.0@sha256:0748b92ddf75405d9d83fbd1705517f951e67e0540af86e0a9bf4de388ba86c0
    container_name: geo-mlops-proxpi
    logging: *default-logging
    restart: unless-stopped
    ports:
      - "5000:5000"
    volumes:
      - proxpi-cache:/var/cache/proxpi
    environment:
      PROXPI_CACHE_DIR: /var/cache/proxpi

  apt-cacher-ng:
    image: sameersbn/apt-cacher-ng@sha256:82f55f9c8f627cee8ef5f710c1745388d79d3a3f2f3150353e6500021bec11b4
    container_name: geo-mlops-apt-cacher-ng
    logging: *default-logging
    restart: unless-stopped
    ports:
      - "3142:3142"
    volumes:
      - apt-cacher-ng-data:/var/cache/apt-cacher-ng

volumes:
  mlflow-postgres-data:
    name: geo-mlops-mlflow-postgres-data
  proxpi-cache:
    name: geo-mlops-proxpi-cache
  apt-cacher-ng-data:
    name: geo-mlops-apt-cacher-ng-data
  rustfs-data:
    name: geo-mlops-mlflow-rustfs-data
  prometheus-data:
    name: geo-mlops-prometheus-data
  alertmanager-data:
    name: geo-mlops-alertmanager-data

서비스

서비스컨테이너이미지역할포트
appgeo-mlops-app소스의 Dockerfile 로 빌드API 서버. 내장 MLflow 와 이미지 레지스트리(/v2) 포함10000 (호스트 네트워크)
mlflow-postgresgeo-mlops-mlflow-postgrespostgres:18-alpine앱과 MLflow 가 함께 쓰는 DB5433 → 5432
rustfsgeo-mlops-mlflow-rustfsrustfs/rustfs:1.0.0-beta.7S3 호환 오브젝트 저장소 (MLflow 아티팩트, 데이터셋, 이미지 레지스트리)9000 (S3 API), 9001 (웹 콘솔)
createbucketgeo-mlops-mlflow-createbucketamazon/aws-cli:2.34.631회 실행. 버킷 mlflow·datasets·registry 생성 후 종료없음
prometheusgeo-mlops-prometheusprom/prometheus:v2.55.1앱 지표(/api/v1/metrics) 수집과 경보 규칙 평가9090
alertmanagergeo-mlops-alertmanagerprom/alertmanager:v0.27.0발생한 경보를 앱 웹훅으로 전달9093
proxpigeo-mlops-proxpiepicwink/proxpi:1.3.0 (다이제스트 고정)이미지 빌드 파드용 pip 패키지 캐시5000
apt-cacher-nggeo-mlops-apt-cacher-ngsameersbn/apt-cacher-ng (다이제스트 고정)이미지 빌드 파드용 apt 패키지 캐시3142
  • 모든 서비스(createbucket 제외)는 restart: unless-stopped. 서버를 재부팅해도 다시 기동.
  • app 은 mlflow-postgres 가 healthy 가 되고 createbucket 이 정상 종료된 뒤 기동. 헬스체크는 10초마다 /api/v1/health 호출(첫 30초는 실패해도 대기).
  • 모든 컨테이너 로그는 json-file 드라이버로 50 MB × 5개까지만 보관. 오래된 로그는 자동 삭제.
  • 학습·파이프라인·서빙은 이 파일에 없음. 앱이 k3s 클러스터에 Kubernetes Job·Deployment 로 생성(k3s·GPU 준비).

앱 컨테이너의 네트워크

app 은 network_mode: host 로 호스트의 네트워크를 그대로 사용. 그래서 서버 설정의 기본값만으로 다른 서비스에 접속.

대상앱이 사용하는 기본 주소서버 설정
DBpostgresql+psycopg2://mlflow:mlflow@localhost:5433/mlflowGEO_MLOPS_DATABASE_URL, GEO_MLOPS_MLFLOW_BACKEND_URI
오브젝트 저장소http://localhost:9000GEO_MLOPS_MLFLOW_S3_ENDPOINT_URL
Prometheushttp://localhost:9090GEO_MLOPS_PROMETHEUS_URL
Alertmanagerhttp://localhost:9093GEO_MLOPS_ALERTMANAGER_URL
k3s APIk3s.yaml 에 적힌 https://127.0.0.1:6443GEO_MLOPS_KUBERNETES_KUBECONFIG

반대 방향(Prometheus·Alertmanager 에서 앱으로)은 extra_hosts 의 host.docker.internal:host-gateway 로 호스트에 접속. Prometheus 는 host.docker.internal:10000 을 수집하고, Alertmanager 는 같은 주소의 웹훅으로 경보 전송(Compose 설치의 Alertmanager 웹훅 설정 참고).

호스트에서 읽는 파일

호스트 경로사용하는 서비스내용
/etc/geo-mlops/app.envapp (env_file)앱의 비밀 설정(GEO_MLOPS_*). 없어도 기동(required: false). 이 경우 기본 서명 키 등으로 실행되고 시스템 진단 화면에 치명 항목으로 표시
/etc/rancher/k3s/k3s.yamlapp (읽기 전용)k3s 접속 정보. 없으면 Docker 가 빈 디렉터리를 만들어 앱이 클러스터를 찾지 못하므로 k3s 를 먼저 설치
./monitoring/prometheus.ymlprometheus수집 대상과 경보 규칙 파일 위치
./monitoring/alert-rules.ymlprometheus경보 규칙
./monitoring/alertmanager.ymlalertmanager경보 전달 경로(앱 웹훅 주소와 토큰)

./monitoring/… 은 docker-compose.yml 이 있는 디렉터리 기준. 실행 중인 컨테이너는 생성될 때의 마운트를 유지하므로, 설정 파일을 고친 뒤에는 컨테이너 재생성 필요(아래 배포 절차의 두 번째 명령).

실행할 때 지정하는 셸 변수

app.env 가 아니라 docker compose 를 실행하는 셸에서 읽는 값.

변수기본값용도
GIT_COMMIT빈 값빌드한 커밋. 시스템 진단 화면의 배포 정보에 표시
BUILT_AT빈 값빌드 시각(UTC, ISO 8601). 위와 같음
GEO_APP_MEM_LIMIT16g앱 컨테이너 메모리 상한. 초과하면 앱만 종료되고 자동 재기동(Compose 설치의 메모리 상한 참고)

앱의 환경 변수

environment 의 두 값은 내장 MLflow 가 RustFS 로 아티팩트를 올릴 때 필요. 최신 S3 클라이언트는 기본으로 Content-Length 없이 체크섬을 붙여 업로드하는데, RustFS 가 이를 MissingContentLength 로 거부하기 때문.

변수값
AWS_REQUEST_CHECKSUM_CALCULATIONwhen_required
AWS_RESPONSE_CHECKSUM_VALIDATIONwhen_required

볼륨

볼륨서비스내용
geo-mlops-mlflow-postgres-datamlflow-postgres앱·MLflow DB. 백업 대상
geo-mlops-mlflow-rustfs-datarustfs아티팩트, 데이터셋 파일, 이미지 레지스트리. 백업 대상
geo-mlops-prometheus-dataprometheus지표 시계열
geo-mlops-alertmanager-dataalertmanager사일런스, 알림 전송 상태
geo-mlops-proxpi-cacheproxpipip 패키지 캐시. 삭제해도 다음 빌드 때 다시 채워짐
geo-mlops-apt-cacher-ng-dataapt-cacher-ngapt 패키지 캐시. 위와 같음

볼륨 이름을 고정(name:)해 두었으므로 소스 디렉터리를 옮겨도 같은 데이터 사용. Compose 는 백업을 수행하지 않음. DB 덤프와 오브젝트 복제는 백업 참고. docker compose down -v 는 모든 볼륨을 삭제.

패키지 캐시 (proxpi, apt-cacher-ng)

이미지 빌드 파드가 pip·apt 패키지를 내려받을 때 거치는 캐시. 빌드 캐시에 없는 레이어(새 버전, 새 모델)를 빌드할 때만 사용.

  • 빌드 파드는 파드 네트워크의 게이트웨이 10.42.0.1 로 접속(파드 안의 localhost 는 파드 자신). 서버 설정을 비워 두면 앱이 http://10.42.0.1:5000/index/(pip), http://10.42.0.1:3142(apt)를 자동 사용.
  • 캐시는 미러가 아니므로, 이 두 서비스가 꺼져 있으면 빌드가 원본으로 우회하지 않고 실패. 캐시 없이 운영하는 환경은 GEO_MLOPS_BUILD_PACKAGE_PROXY_ENABLED=false 지정.
  • proxpi 에는 PyPI 만 연결. PyTorch 패키지 인덱스를 추가하면 합쳐진 목록에서 다른 가속기용 torch 빌드가 선택될 수 있음.
  • 두 이미지는 태그 대신 다중 아키텍처 인덱스의 다이제스트로 고정. x86_64 와 ARM64 서버에서 같은 파일 사용.

자세한 내용은 빌드 캐시 참고.

개발 서버의 배포 절차

개발 서버는 서버 저장소의 기본 브랜치가 갱신될 때마다 소스 루트에서 다음을 실행.

export GIT_COMMIT=$(git rev-parse HEAD)
export BUILT_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
docker compose up -d --build --remove-orphans
docker compose up -d --force-recreate --no-deps prometheus alertmanager
docker image prune -f
  1. 앱 이미지를 다시 빌드하고 파일 전체를 반영. 서비스 이름을 지정하지 않아야 모든 서비스가 파일과 일치. --remove-orphans 는 파일에서 삭제된 서비스의 컨테이너 정리.
  2. Prometheus·Alertmanager 컨테이너를 재생성해 monitoring/ 의 설정 파일 변경을 반영. 지표와 알림 상태는 볼륨에 있으므로 유지(재생성 동안 몇 초간 수집 공백).
  3. 이전 빌드가 남긴 이름 없는 이미지 삭제.

이어서 앱 컨테이너가 healthy 인지, Prometheus 가 앱을 수집 중인지(up{job="geo-mlops"} == 1) 확인.

docker inspect --format='{{.State.Health.Status}}' geo-mlops-app
curl -s --get http://localhost:9090/api/v1/query --data-urlencode 'query=up{job="geo-mlops"} == 1'

app.env 가 없어도 기동하므로 배포 성공이 비밀 설정의 존재를 보장하지 않음. 배포 후 시스템 진단 화면에서 치명 항목과 배포 커밋 확인.

2026-10-02 기준 플랫폼으로 작성

© MLOps Central Platform