A small but complete home MLOps project: synthetic data → model training with experiment tracking → inference over a REST API → CI/CD on GitHub Actions → Docker Compose. Everything runs on a laptop, no GPU, no external datasets to download.
The goal of this repo isn't the model itself (it's a toy RandomForest on synthetic data) — it's the end-to-end plumbing around it: data versioning, experiment tracking, reproducible pipelines, containerized training/serving, and an automated CI/CD flow. That plumbing is the same regardless of whether the model behind it is a toy classifier or a real production model.
| Concern | Tool |
|---|---|
| Data generation | scikit-learn (make_classification) |
| Data/pipeline versioning | DVC |
| Model training | scikit-learn (RandomForest) |
| Experiment tracking | MLflow |
| Inference API | FastAPI + uvicorn |
| Containerization | Docker, Docker Compose |
| CI/CD | GitHub Actions → GHCR |
| (stage 2) | k3d/minikube + Argo CD for GitOps |
make_dataset.py → prepare.py → train.py → evaluate.py → serve/app.py
(data) (split) (model) (metrics) (API)
src/data/make_dataset.pygenerates a synthetic binary classification dataset (sklearn.datasets.make_classification). No downloads, fully reproducible via--seed.src/data/prepare.pysplits the data into train/test (80/20, stratified by class) — stands in for a real preprocessing stage.src/train.pytrains aRandomForestClassifierand logs the run to MLflow: hyperparameters, metrics, and the model itself as an artifact. This is what "experiment tracking" buys you — every run is visible and comparable in the MLflow UI.src/evaluate.pyis a separate evaluation step against the test set; it writesmetrics.json, which DVC picks up as a pipeline metric (dvc metrics show), so you can diff metrics across commits/branches.src/serve/app.pyis a standalone inference process: it loadsmodel.joblibonce at startup and serves/predict. Training and serving are deliberately separate — training is heavy and infrequent, serving is light and frequent, so they get different Docker images, different resource profiles, and different lifecycles.
docker-compose.yml wraps this into three services (mlflow,
train, serve). CI runs the full pipeline on a small sample on
every PR to catch breakage before merge. CD builds and pushes the
train/serve images to GHCR after every merge to main.
- Python 3.11+ (3.12 also works)
- Docker + Docker Compose v2 (
docker compose version, no dash) - Git
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# full pipeline
make pipeline
# equivalent to:
# python src/data/make_dataset.py
# python src/data/prepare.py
# python src/train.py
# python src/evaluate.py
# start the inference service locally
make serve
# in another terminal:
curl -X POST http://localhost:8000/predict \
-H "Content-Type: application/json" \
-d '{"features": [0.1, 0.2, -0.3, 0.4, 0.0, 0.7, -0.1, 0.3]}'/features must have as many values as the dataset has feature
columns (8 by default — see --n-features in make_dataset.py).
# 1. train a model (one-off job via the "train" profile)
docker compose --profile train run --rm train
# 2. start the MLflow UI and the inference service
docker compose up -d mlflow serve
# MLflow UI: http://localhost:5000
# Inference API: http://localhost:8000/docsdvc init # once per repo
dvc repro # re-runs make_dataset → prepare → train → evaluate,
# but only the stages whose inputs actually changed
dvc metrics show # prints metrics.jsonmake test
make lintCI runs the same two commands, plus a smoke run of the whole pipeline on a small sample, plus a build of both Docker images.
- CI (
.github/workflows/ci.yml): lint, unit tests, a smoke run of the full pipeline on a small sample, and a build of both Docker images. Runs on every PR and on every push tomain. - CD (
.github/workflows/cd.yml): on every push tomain, builds and pushes theserveandtrainimages to the GitHub Container Registry, taggedlatestand<commit-sha>.
mlops-mini-lab/
├── data/
│ ├── raw/ # generated dataset (gitignored, DVC-tracked)
│ └── processed/ # train/test split (gitignored, DVC-tracked)
├── src/
│ ├── data/
│ │ ├── make_dataset.py
│ │ └── prepare.py
│ ├── train.py
│ ├── evaluate.py
│ └── serve/
│ └── app.py
├── models/ # trained model artifact (gitignored)
├── tests/
│ └── test_pipeline.py
├── docker/
│ ├── Dockerfile.train
│ └── Dockerfile.serve
├── docker-compose.yml
├── dvc.yaml # pipeline DAG for `dvc repro`
├── .github/workflows/
│ ├── ci.yml
│ └── cd.yml
├── requirements.txt
├── pyproject.toml # pytest + ruff config
├── Makefile
└── README.md
- Deploy
serveto k3d/minikube via a Helm chart - Argo CD watching a
deploy/folder — full GitOps loop - MLflow Model Registry with staging → production promotion
- Data drift monitoring (evidently / whylogs)
- docs/architecture.md -- what each component does, why it's there, and how they connect (with a diagram)
- docs/runbook.md -- step-by-step commands for every workflow, plus a troubleshooting section
- docs/decisions.md -- why things are built the way they are, alternatives considered