🛠️ Thực hành: dựng stack OSS tự host trên một VPS rẻ

By Ngọc Vương Trương

Bài trước bạn đã nắm vai trò của ba mảnh: MLflow giữ metadata và artifact của run, DVC version hoá dữ liệu và model, MinIO làm kho S3 rẻ. Giờ ta biến chúng thành thứ chạy thật, trên một VPS 2 GB RAM giá khoảng 5 đến 7 USD mỗi tháng.

Mục tiêu: một VPS nhỏ chạy được cả MLflow lẫn MinIO

Cuối bài này, trên máy bạn phải mở được giao diện MLflow, tạo được một experiment, log một run nhỏ, và nhìn thấy file artifact nằm trong bucket của MinIO. Không có bước nào cần thẻ tín dụng của dịch vụ cloud nào.

Đây là bước nền cho toàn bộ capstone: nếu tracking server và artifact store chưa ổn định, mọi bài train và deploy sau đó sẽ đứng trên nền cát.

Sơ đồ stack bạn sẽ dựng:

flowchart TD
  Dev["💻 Máy dev"] -->|SSH tunnel| Proxy["🔒 Cổng localhost"]
  Proxy --> MLF["🧪 MLflow server"]
  MLF --> DB["🗄️ Backend store<br/>SQLite hoặc Postgres"]
  MLF -->|S3 API| MIO["📦 MinIO"]
  DVC["🔁 DVC"] -->|S3 remote| MIO

Bản đặc tả stack

Thành phần Vai trò Chạy ở đâu
MinIO Kho artifact và data kiểu S3 Container, API cổng 9000, console cổng 9001
MLflow server Tracking + model registry Container, cổng 5000
Backend store Metadata của run, param, metric SQLite trên volume (đủ cho học), Postgres khi cần bền hơn
DVC remote Version hoá dữ liệu lớn Trỏ vào bucket MinIO qua endpoint S3

Nguyên tắc bảo vệ: không mở cổng nào ra internet ngoài 22, 80, 443. MinIO và MLflow bind vào 127.0.0.1 trên host, bạn vào bằng SSH tunnel:

ssh -L 5000:localhost:5000 -L 9001:localhost:9001 user@<ip-vps>

Sau đó mở http://localhost:5000 và http://localhost:9001 ngay trên máy mình.

Điểm nối giữa MLflow và MinIO nằm ở ba biến môi trường của container MLflow:

MLFLOW_S3_ENDPOINT_URL=http://minio:9000
AWS_ACCESS_KEY_ID=<minio-user>
AWS_SECRET_ACCESS_KEY=<minio-password>

Thiếu MLFLOW_S3_ENDPOINT_URL, boto3 sẽ cố gọi S3 thật của AWS và báo lỗi credentials. Đây là lỗi số một của người mới.

Một đoạn docker-compose.yml tối thiểu, hãy pin version cụ thể:

services:
  minio:
    image: quay.io/minio/minio:RELEASE.2024-10-13T13-34-11Z
    command: server /data --console-address ":9001"
    restart: unless-stopped
    environment:
      MINIO_ROOT_USER: mlops
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    volumes: ["minio-data:/data"]
    ports:
      - "127.0.0.1:9000:9000"
      - "127.0.0.1:9001:9001"

  mlflow:
    image: ghcr.io/mlflow/mlflow:v2.16.2
    restart: unless-stopped
    depends_on: [minio]
    environment:
      MLFLOW_S3_ENDPOINT_URL: http://minio:9000
      AWS_ACCESS_KEY_ID: mlops
      AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
    command: >
      sh -c "pip install boto3 &&
             mlflow server --host 0.0.0.0 --port 5000
             --backend-store-uri sqlite:////data/mlflow.db
             --artifacts-destination s3://mlflow
             --serve-artifacts"
    volumes: ["mlflow-data:/data"]
    ports: ["127.0.0.1:5000:5000"]

volumes:
  minio-data:
  mlflow-data:

Bài tập nhanh: vì sao MLFLOW_S3_ENDPOINT_URL dùng tên minio chứ không phải localhost?

Gợi ý đáp án

Vì biến này được đọc bên trong container MLflow. Trong mạng của Docker Compose, mỗi service được phân giải theo tên service, nên minio là đúng; localhost sẽ trỏ vào chính container MLflow và kết nối thất bại.

Thứ tự thi công: sáu mốc trong 30 phút

  1. Chuẩn bị VPS: tạo user không phải root, bật UFW (chỉ 22/80/443), thêm 2 GB swap, cài Docker và Docker Compose.
  2. Dựng MinIO trước: docker compose up -d minio, vào console 9001, tạo bucket tên mlflow, tạo access key riêng thay vì dùng root.
  3. Dựng MLflow: docker compose up -d mlflow, xem log để chắc nó kết nối được MinIO, không có stack trace.
  4. Kiểm thử end-to-end: chạy một script nhỏ log run bằng mlflow.log_metric và mlflow.log_artifact, rồi mở bucket mlflow trong console MinIO xem file có lên không.
  5. Trỏ DVC remote vào MinIO: dvc remote add -d minio s3://dvc-storage kèm endpoint, thử dvc push một file nhỏ.
  6. Chốt an toàn: đặt .env ngoài git, thử reboot VPS và kiểm tra cả hai container tự lên lại.

Mốc 4 là mốc quan trọng nhất. Đừng đi tiếp nếu artifact chưa thực sự nằm trong MinIO, vì bài capstone sau sẽ log model và bạn cần chắc chắn nó không biến mất.

Nghiệm thu: checklist trước khi sang bài sau

  • docker compose ps báo cả minio và mlflow ở trạng thái healthy hoặc running.
  • MLflow mở được qua SSH tunnel, tạo được experiment mới.
  • Một run có ít nhất 1 metric và 1 artifact file.
  • Artifact đó nhìn thấy trong bucket mlflow của MinIO console.
  • dvc push đẩy được một file nhỏ lên bucket riêng.
  • Reboot VPS xong, cả hai service tự chạy lại mà không cần thao tác tay.
  • Không có cổng 5000, 9000, 9001 nào mở ra internet (kiểm tra từ máy ngoài).

Nếu thiếu bất kỳ dòng nào, xử lý ngay tại đây. Stack nửa vời sẽ làm bạn mất hàng giờ ở module 2.

Cạm bẫy tôi đã gặp khi dựng stack kiểu này

1 GB RAM là không đủ. MLflow cộng MinIO cộng Docker dễ vượt 1 GB, OOM killer giết container giữa lúc log. Tối thiểu 2 GB, và thêm swap 2 GB cho an toàn.

Quên volume. Không mount minio-data, một lần docker compose down là mất sạch artifact. Luôn kiểm tra volume trước khi tin vào stack.

Dùng tag latest. Sáu tháng sau bạn dựng lại và mọi thứ vỡ. Pin version, ghi lại trong README.

Mở console MinIO ra internet. Console bị bot quét trong vài giờ. Bind 127.0.0.1 và dùng SSH tunnel, hoặc đặt sau Caddy có mật khẩu.

Quên --serve-artifacts. Khi đó client phải tự có quyền S3. Với cờ này, artifact đi qua MLflow server, máy dev không cần giữ access key MinIO.

Bài tập nhanh: sau khi reboot, container MLflow chạy nhưng mọi run cũ biến mất. Nghi phạm đầu tiên là gì?

Gợi ý đáp án

Backend store đang là SQLite đặt ngoài volume, hoặc volume bị xoá. Kiểm tra --backend-store-uri có trỏ vào đường dẫn nằm trong volume mount hay không. Nếu metadata quan trọng, chuyển sang Postgres cũng chạy trong Compose.

Chốt lại: bạn vừa có gì trong tay

Bạn đã có một tracking server thật, một kho artifact S3 thật, một DVC remote thật, tất cả trên một VPS rẻ và không lộ cổng nào ra ngoài. Đây chính là "phòng lab" mà mọi bài sau sẽ dùng lại: train model, đăng ký model, deploy, rồi monitoring.

Ôn nhanh bằng thẻ dưới đây trước khi sang bài checkpoint, nơi bạn sẽ tự kiểm tra xem nền tảng và stack đã đủ chắc để bước vào phần hạ tầng và serving hay chưa.

Front: MLflow lưu metadata của run ở đâu? Back: Trong backend store (SQLite khi học, Postgres khi cần bền).

Front: Artifact của MLflow nên đặt ở đâu trong stack tự host? Back: Trong một bucket S3 trên MinIO.

Front: Cổng API và cổng console của MinIO là bao nhiêu? Back: 9000 cho S3 API, 9001 cho console web.

Front: Biến môi trường nào giúp MLflow trỏ S3 client vào MinIO? Back: MLFLOW_S3_ENDPOINT_URL.

Front: Cờ nào của mlflow server cho phép client upload artifact qua server? Back: --serve-artifacts.

Front: Vì sao bind cổng vào 127.0.0.1 thay vì 0.0.0.0? Back: Chỉ vào được qua SSH tunnel, không lộ console ra internet.

Front: Lệnh SSH tunnel để mở MLflow ở máy local? Back: ssh -L 5000:localhost:5000 user@ip-vps.

Front: Tại sao VPS 1 đến 2 GB RAM cần thêm swap? Back: Tránh OOM killer giết container khi log hoặc train.

Front: restart: unless-stopped để làm gì? Back: Container tự chạy lại sau khi VPS reboot.

Front: Vì sao phải pin version image thay vì dùng latest? Back: Để tái lập được môi trường, tránh bản mới phá vỡ cấu hình.

Front: DVC remote trỏ vào đâu trong stack này? Back: Một bucket MinIO, truy cập qua endpoint S3.

Front: Quên mount volume thì hậu quả là gì? Back: Mất toàn bộ dữ liệu khi container bị xoá hoặc dựng lại.

Front: Cách nhanh nhất xác nhận artifact đã lên MinIO? Back: Mở bucket trong console MinIO hoặc dùng mc ls.

MLflow lưu metadata trong _____ và lưu artifact trong bucket của _____. Biến môi trường _____ giúp boto3 trỏ vào MinIO thay vì S3 thật của AWS. Cờ _____ cho phép client tải và upload artifact thông qua MLflow server. Cổng console mặc định của MinIO là _____.