Build an MLOps Pipeline with MLflow

Track experiments, register models, and serve predictions with MLflow.

Introduction

30 Second Summary

Machine learning teams constantly retrain and tweak their models. Without a system to track what changed, compare results, and deploy the best version, things get chaotic fast.

In this project, you will build a complete MLOps pipeline that trains multiple wine quality prediction models, tracks every experiment, registers the best model, and serves it as a live prediction API.

What You'll Build

A wine quality prediction system powered by scikit-learn, with full experiment tracking, model versioning, and model serving through MLflow.

By the end of this project, you'll have:

  • A local MLflow Tracking Server storing all experiment data with parameters, metrics, and model artifacts you can visually compare.
  • A trained ML model registered in the MLflow Model Registry with version control and a "champion" alias for production identification.
  • A live prediction endpoint serving your best model as a REST API for real-time inference.
  • Secret Mission: An automated model promotion pipeline that programmatically selects the best model and promotes it to champion.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, and loops). No prior machine learning or MLOps experience is required.

Before We Start

Before you can train models and track experiments, you need a working Python environment with the right tools installed.

In this step, you will install Python, create an isolated virtual environment, and set up MLflow and scikit-learn. You will also start the MLflow Tracking Server so it is ready to receive experiment data in later steps.

In this step, get ready to:
  • Install Python and create a virtual environment.
  • Install MLflow and scikit-learn.
  • Start the MLflow Tracking Server.
Install Python and create a virtual environment

This project requires Python 3.11 or higher because scikit-learn 1.8.0 depends on it. Let's verify your Python version first.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check your Python version by running the command for your operating system:

macOS

python3 --version

Windows

python --version

You should see output in the format Python 3.x.x. Check that the version number is 3.11 or higher.

✔️ I see version 3.11 or higher

Your Python version is ready. Continue to the next instruction below.

ⓧ I see an older version

Your system has Python installed, but it is below 3.11. macOS often ships with an older Python by default even after you install a newer version.

  • Download and install Python 3.11 or higher from the official site: https://www.python.org/downloads/.
  • On macOS, if python3 --version still shows the old version after installing, try python3.11 --version or python3.12 --version to invoke the newly installed version directly.
  • On Windows, the installer usually updates python to the new version automatically. Reopen PowerShell after installing and recheck.

Once you confirm a version of 3.11 or higher, continue below.

ⓧ Command not found

Python is not installed on your machine.

  • Download and install Python 3.11 or higher from the official site: https://www.python.org/downloads/.
  • On Windows, make sure to check the Add python.exe to PATH box during installation.
  • After installation, close and reopen your terminal. Run the version check command again to confirm.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir mlops-pipeline
cd mlops-pipeline
  • Create a virtual environment by running the command for your operating system:

macOS

python3 -m venv venv

Windows

python -m venv venv
  • Activate the virtual environment:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

What is a virtual environment?

A virtual environment is an isolated Python workspace. Packages you install inside it do not affect the rest of your system.

This keeps your project's dependencies self-contained. If you ever need to start fresh, you can delete the venv folder and recreate it.

Don't see (venv) in your prompt?

Make sure you ran the activate command for your operating system. On macOS, it is source venv/bin/activate. On Windows, it is venv\Scripts\activate.

If you see a permissions error on Windows, try running PowerShell as Administrator and enter Set-ExecutionPolicy RemoteSigned first. Then retry the activate command.

Help me activate my Python virtual environment.

Install MLflow and scikit-learn

With the virtual environment active, you can now install the two core libraries for this project.

  • Install MLflow and scikit-learn by running this command:
pip install mlflow==3.12.0 scikit-learn==1.8.0

What does this command do?

This installs two packages pinned to specific versions. mlflow==3.12.0 is the MLOps platform you will use for experiment tracking, model registration, and serving.

scikit-learn==1.8.0 is the machine learning library that provides the model and dataset you will train with. Pinning versions ensures everyone gets identical behaviour.

The installation may take a minute or two. Once it finishes, you should see a success message.

  • Verify MLflow installed correctly by running:
mlflow --version

You should see mlflow, version 3.12.0 in the output.

  • Verify scikit-learn installed correctly by running:
python -c "import sklearn; print(sklearn.__version__)"

You should see 1.8.0 printed in the output.

Seeing an error during installation?

Make sure your virtual environment is active. You should see (venv) in your terminal prompt. If not, re-run the activation command from the previous substep.

If pip cannot find a compatible version of scikit-learn, your Python version may be below 3.11. Run python --version inside the activated environment to double-check.

Help me fix my MLflow or scikit-learn installation error.

Start the MLflow Tracking Server

The MLflow Tracking Server is a lightweight web application that stores your experiment data and provides a UI for exploring results.

  • Start the MLflow Tracking Server by running this command:
mlflow server --host 127.0.0.1 --port 8080

What does this command do?

This starts a local web server on port 8080. All experiment parameters, metrics, and model artifacts you log later will be stored by this server.

The --host 127.0.0.1 flag restricts access to your local machine only. The --port 8080 flag sets the port number.

Your terminal is now occupied by the running server. It will stay running as long as this terminal window is open.

  • Open a new browser tab and navigate to http://127.0.0.1:8080.

You should see the MLflow UI with an empty experiments list. This confirms your Tracking Server is running.

Keep the server running

Do not close the terminal window running the MLflow server. For the rest of this project, open a second terminal tab or window for running scripts. Make sure to activate the virtual environment in the new terminal too.

Page won't load in the browser?

Check your terminal for error messages. If you see a message about the port being in use, another application is occupying port 8080. Stop the other application or change the port by using --port 8081 instead.

Make sure you typed the URL exactly as shown, including the http:// prefix. Some browsers may redirect to HTTPS by default.

Help me get the MLflow Tracking Server running.

Your environment is ready. Next up, you will write a training script and log your first experiment to the MLflow server you just started.

Train Your First Model and Log an Experiment

Your MLflow Tracking Server is up and running. Now it is time to put it to work by training a machine learning model and recording everything about that training process.

In this step, you will create a Python script that trains multiple wine quality prediction models with different hyperparameters and logs every detail to MLflow. Each training run captures the parameters you chose, the performance metrics, and the trained model itself.

In this step, get ready to:
  • Create a training script that connects to your MLflow server.
  • Train multiple models with different hyperparameters and log the results.
  • View your logged experiments in the MLflow UI.
Create the training script with experiment tracking

Every MLflow workflow starts by connecting to a Tracking Server and creating an experiment. An experiment is a named container that groups related training runs together so you can compare them later.

  • Create a new file called train.py in your project folder.
  • Add the imports and MLflow connection setup by copying this code into train.py:
import mlflow
import mlflow.sklearn
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score

# Connect to MLflow Tracking Server
mlflow.set_tracking_uri("http://127.0.0.1:8080")

# Create or get experiment
mlflow.set_experiment("wine-quality-experiment")

# Load wine dataset
wine = load_wine()
X = wine.data
y = wine.target

# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

What does this code do?

  • mlflow.set_tracking_uri("http://127.0.0.1:8080") tells your script to send all tracking data to the server you started in Step 1.
  • mlflow.set_experiment("wine-quality-experiment") creates a named experiment (or retrieves it if it already exists). All runs you log will appear under this experiment.
  • load_wine() loads a built-in scikit-learn dataset with 178 wine samples and 13 chemical features. No external downloads needed.
  • train_test_split() reserves 20% of the data for testing so you can measure how well the model predicts unseen data.

You won't see a change yet. The next chunk adds the training loop that actually produces results.

Log parameters, metrics, and the model

The core of experiment tracking is recording what you tried and what happened. For each training run, you will log the hyperparameters (your choices), the metrics (the results), and the trained model itself.

  • Add the training loop below the code you just wrote in train.py:
# Define hyperparameter combinations to try
param_sets = [
    {"n_estimators": 50, "max_depth": 5, "random_state": 42},
    {"n_estimators": 100, "max_depth": 10, "random_state": 42},
    {"n_estimators": 150, "max_depth": 15, "random_state": 42},
    {"n_estimators": 200, "max_depth": None, "random_state": 42},
]

for params in param_sets:
    with mlflow.start_run():
        # Log parameters
        mlflow.log_param("n_estimators", params["n_estimators"])
        mlflow.log_param("max_depth", params["max_depth"])
        mlflow.log_param("random_state", params["random_state"])

        # Train model
        model = RandomForestRegressor(**params)
        model.fit(X_train, y_train)

        # Make predictions and calculate metrics
        predictions = model.predict(X_test)
        mse = mean_squared_error(y_test, predictions)
        r2 = r2_score(y_test, predictions)

        # Log metrics
        mlflow.log_metric("mse", mse)
        mlflow.log_metric("r2_score", r2)

        # Log model
        mlflow.sklearn.log_model(
            model,
            artifact_path="model",
            registered_model_name=None,
        )

        print(f"Run logged: n_estimators={params['n_estimators']}, max_depth={params['max_depth']}, MSE={mse:.4f}, R2={r2:.4f}")

print("\nAll runs logged. Open http://127.0.0.1:8080 to compare experiments.")

What does this code do?

  • param_sets defines four different hyperparameter combinations. Each set varies n_estimators (number of trees) and max_depth (how deep each tree grows).
  • mlflow.start_run() begins a new tracked run. Everything logged inside this context manager belongs to that single run.
  • mlflow.log_param() records the hyperparameter choices you made for this run.
  • mlflow.log_metric() records the results. MSE (mean squared error) measures prediction error. R2 score measures how well the model explains the data.
  • mlflow.sklearn.log_model() saves the trained model as an artifact so you can reload and serve it later.
  • Save train.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before running it in the next substep.

ⓧ I'd like to double check the full code

import mlflow
import mlflow.sklearn
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score

# Connect to MLflow Tracking Server
mlflow.set_tracking_uri("http://127.0.0.1:8080")

# Create or get experiment
mlflow.set_experiment("wine-quality-experiment")

# Load wine dataset
wine = load_wine()
X = wine.data
y = wine.target

# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Define hyperparameter combinations to try
param_sets = [
    {"n_estimators": 50, "max_depth": 5, "random_state": 42},
    {"n_estimators": 100, "max_depth": 10, "random_state": 42},
    {"n_estimators": 150, "max_depth": 15, "random_state": 42},
    {"n_estimators": 200, "max_depth": None, "random_state": 42},
]

for params in param_sets:
    with mlflow.start_run():
        # Log parameters
        mlflow.log_param("n_estimators", params["n_estimators"])
        mlflow.log_param("max_depth", params["max_depth"])
        mlflow.log_param("random_state", params["random_state"])

        # Train model
        model = RandomForestRegressor(**params)
        model.fit(X_train, y_train)

        # Make predictions and calculate metrics
        predictions = model.predict(X_test)
        mse = mean_squared_error(y_test, predictions)
        r2 = r2_score(y_test, predictions)

        # Log metrics
        mlflow.log_metric("mse", mse)
        mlflow.log_metric("r2_score", r2)

        # Log model
        mlflow.sklearn.log_model(
            model,
            artifact_path="model",
            registered_model_name=None,
        )

        print(f"Run logged: n_estimators={params['n_estimators']}, max_depth={params['max_depth']}, MSE={mse:.4f}, R2={r2:.4f}")

print("\nAll runs logged. Open http://127.0.0.1:8080 to compare experiments.")
Run the script and explore the MLflow UI

With your training script saved, it is time to run it and see the results flow into your MLflow Tracking Server.

  • Open a new terminal window (keep the MLflow server running in the other one).
  • Activate your virtual environment if it is not already active.
  • Run the training script by executing this command:
python train.py
  • You should see four lines of output, one for each training run, showing the hyperparameters and metrics for each model.

What should I see?

Your terminal prints one line per run showing the hyperparameters used and the resulting MSE and R2 values. The exact metric numbers depend on random splits, but you should see all four runs complete without errors.

  • Open your browser and navigate to http://127.0.0.1:8080.
  • Click on the wine-quality-experiment in the left sidebar.
  • You should see four runs listed in the experiment view. Each run shows the parameters and metrics you logged.
  • Click on any run to explore its details. You will see the parameters, metrics, and a model artifact under the Artifacts tab.

What is an artifact?

An artifact is any output file from a training run. In this case, MLflow saved your trained model so you can reload it later without retraining. Artifacts can also include images, data files, or configuration.

Not seeing runs in the UI?

  • Make sure your MLflow Tracking Server is still running in the other terminal window. If it stopped, restart it with mlflow server --host 127.0.0.1 --port 8080.
  • Check that you see the wine-quality-experiment listed in the left sidebar. If you only see Default, your script may have failed before creating the experiment. Check your terminal for error messages.
  • If you see a connection error in the terminal, confirm the tracking URI in your script matches http://127.0.0.1:8080 and the server is running on the same port.

Why aren't my MLflow runs showing up in the UI?

You now have four tracked experiments sitting in your MLflow server, each with full visibility into what was tried and what happened. Next up, you will compare these runs side by side to find the best-performing model.

Compare Multiple Experiments with Different Hyperparameters

In the previous step, you built your training script and logged a single experiment run to MLflow. Now it is time to run multiple experiments with different hyperparameters and compare them side by side.

The real power of experiment tracking shows up when you have several runs to compare. By training the same model with different settings, you can identify which combination of hyperparameters produces the best results.

In this step, get ready to:
  • Run multiple training experiments with different hyperparameter combinations.
  • Compare experiment runs visually in the MLflow UI.
  • Identify the best-performing model based on MSE and R2 score.
Run multiple experiments with varying hyperparameters

Your train.py script already contains a loop that trains a RandomForestRegressor with four different hyperparameter combinations. Each combination varies n_estimators (the number of trees) and max_depth (how deep each tree can grow).

What are the four combinations?

Your script tests these hyperparameter sets:

  • 50 trees with max depth 5 (small and shallow).
  • 100 trees with max depth 10 (medium).
  • 150 trees with max depth 15 (larger and deeper).
  • 200 trees with no depth limit (full-sized).

Each run logs its parameters, trains the model, calculates MSE and R2 score, and stores the model artifact in MLflow.

  • Run the training script to log all four experiments by running this command:
python train.py

You should see four "Run logged" lines in the terminal, each showing different hyperparameter values and metrics.

Not seeing four runs logged?

  • Make sure your MLflow Tracking Server is still running in the other terminal window. If it stopped, restart it with mlflow server --host 127.0.0.1 --port 8080.
  • Confirm your virtual environment is activated in the terminal where you run the script.
  • Check that you saved train.py after any changes from the previous step.

Help me troubleshoot why my training script is not logging all four runs.

Compare runs in the MLflow UI

With multiple runs logged, the MLflow UI becomes your comparison dashboard. You can sort, filter, and visualize runs to quickly spot which hyperparameter set performed best.

  • Open your browser and navigate to http://127.0.0.1:8080.
  • Click on the wine-quality-experiment experiment in the left sidebar.
  • You should see multiple runs listed in the table view. Click the Columns button.
  • Add the mse and r2_score metric columns, plus the n_estimators and max_depth parameter columns.
  • Click the mse column header to sort runs by MSE in ascending order.
  • The run at the top of the list (with the lowest MSE) is your best-performing model.

How do I read MSE and R2 score?

MSE (Mean Squared Error) measures how far predictions are from actual values. Lower is better.

R2 score measures how well the model explains the data's variation. Higher is better, with 1.0 being a perfect fit. When you sort by MSE ascending, the top run should also have one of the highest R2 scores.

  • Select all runs by clicking the checkbox at the top of the run list.
  • Click Compare to open the comparison view.
  • Explore the parallel coordinates chart to see how different parameter values relate to your metrics.

What does the comparison view show?

The comparison view places all selected runs side by side. You can see parameters and metrics in a table, and the chart view draws lines connecting each run's hyperparameters to its resulting metrics.

This makes it easy to spot patterns. For example, you might notice that increasing n_estimators tends to improve performance, while increasing max_depth has diminishing returns.

  • Note the run ID of the best-performing model (lowest MSE). You will use this in the next step to register the model.

Keep the MLflow UI open

You will come back to the MLflow UI in the next step to verify your model registration. Keep the tab open in your browser.

You now have a clear picture of which hyperparameters produce the best wine quality predictions. Next up, you will register that best model in the MLflow Model Registry so you can version it and serve it.

Register Your Best Model in the Model Registry

In the previous step, you compared multiple experiment runs and identified the best-performing model by its MSE score. Now it is time to formally register that model so it has a stable identity you can reference for deployment.

The Model Registry is MLflow's system for versioning and managing models independently from the experiments that produced them. Without registration, your best model is just another run buried in a list of experiments.

In this step, get ready to:
  • Register the best-performing model programmatically using MlflowClient.
  • Set a "champion" alias on the registered model version for easy reference.
  • Verify the model appears in the MLflow UI Model Registry.
Register the best model programmatically

Your experiment has four logged runs, each with different hyperparameters and metrics. You need a script that queries the tracking server for the best run, then registers its model in the Model Registry with a versioned entry.

  • Create a new file called register_model.py in your project directory.
  • Add the following code to search for the best run and register the model:
import mlflow
from mlflow import MlflowClient

# Connect to MLflow Tracking Server
mlflow.set_tracking_uri("http://127.0.0.1:8080")

client = MlflowClient()

# Search for the best run by lowest MSE
experiment = client.get_experiment_by_name("wine-quality-experiment")
runs = client.search_runs(
    experiment_ids=[experiment.experiment_id],
    order_by=["metrics.mse ASC"],
    max_results=1,
)

if not runs:
    print("No runs found. Run train.py first.")
    exit()

best_run = runs[0]
best_run_id = best_run.info.run_id
best_mse = best_run.data.metrics["mse"]
best_r2 = best_run.data.metrics["r2_score"]

print(f"Best run: {best_run_id}")
print(f"  MSE: {best_mse:.4f}")
print(f"  R2 Score: {best_r2:.4f}")

# Register the best model
model_uri = f"runs:/{best_run_id}/model"
model_name = "wine-quality-model"

result = mlflow.register_model(model_uri, model_name)
print(f"\nModel registered: {model_name}, version {result.version}")

What does this code do?

client.get_experiment_by_name() fetches the experiment object so you can access its ID. client.search_runs() queries all runs within that experiment, sorted by MSE in ascending order. The max_results=1 parameter ensures only the single best run is returned.

mlflow.register_model() takes the run's model URI (runs:/<run_id>/model) and creates a versioned entry in the Model Registry under the name wine-quality-model. Each time you register, MLflow increments the version number automatically.

Set a "champion" alias on the best model version

A registered model can have multiple versions over time. Aliases give you a stable reference name that always points to the version you want to use in production.

  • Add the following code to the bottom of register_model.py, below the print statement that confirms registration:
# Set the champion alias
client.set_registered_model_alias(
    name=model_name,
    alias="champion",
    version=result.version,
)
print(f"Alias 'champion' set to version {result.version}")
print(f"\nModel URI for serving: models:/{model_name}@champion")

What does this code do?

client.set_registered_model_alias() assigns the mutable alias "champion" to your registered model version. From now on, any code that references models:/wine-quality-model@champion will load this specific version.

If you register a better model later, you can reassign the alias without changing any downstream code. This decouples deployment from training.

  • Save register_model.py.
  • Run the script by executing:
python register_model.py
  • You should see output showing the best run ID, its MSE and R2 scores, a confirmation that the model was registered, and the alias assignment.
  • Open http://127.0.0.1:8080 in your browser.
  • Click the **Models** link in the top navigation bar to open the Registered Models page.
  • You should see wine-quality-model listed with version 1 and the "champion" alias.

Not seeing the model in the Registry?

  • Make sure your MLflow Tracking Server is still running in its terminal window. If it stopped, restart it with mlflow server --host 127.0.0.1 --port 8080.
  • Check that you ran train.py first. The register script needs existing runs to query.
  • If you see "No runs found", verify the experiment name in both scripts matches exactly: wine-quality-experiment.

Help me debug why my model isn't appearing in the MLflow Model Registry.

✔️ Awesome, I've got everything!

Great. Double check you have saved register_model.py and confirmed the model appears in the MLflow UI.

ⓧ I'd like to double check the full code

Here is the complete register_model.py file for comparison:

import mlflow
from mlflow import MlflowClient

# Connect to MLflow Tracking Server
mlflow.set_tracking_uri("http://127.0.0.1:8080")

client = MlflowClient()

# Search for the best run by lowest MSE
experiment = client.get_experiment_by_name("wine-quality-experiment")
runs = client.search_runs(
    experiment_ids=[experiment.experiment_id],
    order_by=["metrics.mse ASC"],
    max_results=1,
)

if not runs:
    print("No runs found. Run train.py first.")
    exit()

best_run = runs[0]
best_run_id = best_run.info.run_id
best_mse = best_run.data.metrics["mse"]
best_r2 = best_run.data.metrics["r2_score"]

print(f"Best run: {best_run_id}")
print(f"  MSE: {best_mse:.4f}")
print(f"  R2 Score: {best_r2:.4f}")

# Register the best model
model_uri = f"runs:/{best_run_id}/model"
model_name = "wine-quality-model"

result = mlflow.register_model(model_uri, model_name)
print(f"\nModel registered: {model_name}, version {result.version}")

# Set the champion alias
client.set_registered_model_alias(
    name=model_name,
    alias="champion",
    version=result.version,
)
print(f"Alias 'champion' set to version {result.version}")
print(f"\nModel URI for serving: models:/{model_name}@champion")

Your best model is now registered and tagged as the champion. Next up, you will serve this champion model as a live REST API that returns real-time predictions.

Serve Your Champion Model as a REST API

You have a trained model registered in the Model Registry with a "champion" alias pointing to your best version. That alias acts like a stable address that always resolves to your current best model.

The final pillar of the MLOps lifecycle is model serving. In this step, you will spin up a REST API that loads the champion model and returns real-time predictions to any client that sends input data.

In this step, get ready to:
  • Serve the champion model as a local REST API using MLflow's built-in serving.
  • Test the prediction endpoint with a sample request.
  • Create a batch prediction script that loads and runs inference on test data.
Serve the model using MLflow's built-in serving

MLflow can deploy any registered model as a local HTTP server with a single command. The server exposes a /invocations endpoint that accepts POST requests with input data and returns predictions.

  • Open a new terminal window (keep your MLflow Tracking Server running in the original one).
  • Activate your virtual environment in this new terminal.
  • Start the model serving process by running this command:
mlflow models serve --model-uri "models:/wine-quality-model@champion" --port 5001 --no-conda

What does this command do?

  • models:/wine-quality-model@champion tells MLflow to load the model version that currently holds the "champion" alias.
  • --port 5001 runs the server on port 5001 (separate from the Tracking Server on port 8080).
  • --no-conda tells MLflow to use your current Python environment instead of creating a new conda environment.
  • Wait for the server to start. You should see output indicating the server is listening on http://127.0.0.1:5001.

The server runs in the foreground

This command keeps running and occupies the terminal. You will need a third terminal window to send requests in the next substep.

Server not starting?

  • Make sure the MLflow Tracking Server is still running on port 8080. The serve command needs it to resolve the model URI.
  • Confirm you ran register_model.py in the previous step. The model must be registered with a "champion" alias before it can be served.
  • If port 5001 is already in use, stop whatever is running on it or choose a different port number.

Help me debug my MLflow model serving error.

Test the prediction endpoint

With the server running, you can send HTTP requests to get predictions. The /invocations endpoint accepts JSON input with your feature data.

  • Open a third terminal window and activate your virtual environment.
  • Send a test prediction request by running this command:
curl http://127.0.0.1:5001/invocations -H "Content-Type:application/json" --data '{"inputs": [[14.23, 1.71, 2.43, 15.6, 127.0, 2.8, 3.06, 0.28, 2.29, 5.64, 1.04, 3.92, 1065.0]]}'

What does this request do?

  • The inputs key wraps a 2D array where each inner array is one sample with 13 feature values (matching the wine dataset's 13 features).
  • The Content-Type:application/json header tells the server to expect JSON input.
  • MLflow's server calls the model's predict() method and returns the result as JSON.
  • You should see a JSON response containing a prediction value, similar to:
{"predictions": [1.4567]}

Your exact number will differ, but seeing a predictions key with a numeric value confirms your model is serving successfully.

Getting a connection refused error?

  • Make sure the mlflow models serve command finished starting. It may take 10-20 seconds to load the model before accepting requests.
  • Double-check you are targeting port 5001 in your curl command, not 8080.
  • On Windows, curl is available by default on Windows 10+. If it does not work, you can use the Python predict.py script in the next substep instead.

Help me troubleshoot my MLflow prediction endpoint.

Create a test script for batch predictions

For production workflows, you often want to load a model directly in Python and run batch inference on a dataset. MLflow's mlflow.pyfunc.load_model() lets you load any registered model by its alias URI and call predict() locally.

  • Create a new file called predict.py in your project folder and paste the following code:
import mlflow
import numpy as np
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split

# Connect to MLflow Tracking Server
mlflow.set_tracking_uri("http://127.0.0.1:8080")

# Load the champion model
model_name = "wine-quality-model"
model_uri = f"models:/{model_name}@champion"

print(f"Loading model from: {model_uri}")
model = mlflow.pyfunc.load_model(model_uri)

# Load test data
wine = load_wine()
X = wine.data
y = wine.target
_, X_test, _, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Make predictions
predictions = model.predict(X_test)

# Display results
print(f"\nPredictions for first 5 samples:")
print(f"{'Predicted':<12} {'Actual':<12}")
print("-" * 24)
for pred, actual in zip(predictions[:5], y_test[:5]):
    print(f"{pred:<12.4f} {actual:<12.4f}")

print(f"\nTotal test samples: {len(X_test)}")
print(f"Mean prediction: {np.mean(predictions):.4f}")

What does this script do?

  • mlflow.pyfunc.load_model(model_uri) loads the champion model version directly into Python as a generic prediction function.
  • It uses the same wine dataset and train/test split (random_state=42) as train.py, so the test set matches what was used during training.
  • model.predict(X_test) runs batch inference on all test samples at once.
  • The output table compares predicted values against actual target values for the first 5 samples.
  • Save predict.py.
  • Run the batch prediction script by running this command:
python predict.py
  • You should see a table of predicted vs actual values, the total sample count, and the mean prediction value.

Seeing a model not found error?

  • Make sure the MLflow Tracking Server is still running on port 8080. The script connects to it to resolve the model URI.
  • Verify you ran register_model.py previously and that the "champion" alias was set successfully.

Help me fix my model loading error.

✔️ Awesome, I've got everything!

Great. Double check you have saved predict.py and that running it produced a predictions table.

ⓧ I'd like to double check the full code

import mlflow
import numpy as np
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split

# Connect to MLflow Tracking Server
mlflow.set_tracking_uri("http://127.0.0.1:8080")

# Load the champion model
model_name = "wine-quality-model"
model_uri = f"models:/{model_name}@champion"

print(f"Loading model from: {model_uri}")
model = mlflow.pyfunc.load_model(model_uri)

# Load test data
wine = load_wine()
X = wine.data
y = wine.target
_, X_test, _, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Make predictions
predictions = model.predict(X_test)

# Display results
print(f"\nPredictions for first 5 samples:")
print(f"{'Predicted':<12} {'Actual':<12}")
print("-" * 24)
for pred, actual in zip(predictions[:5], y_test[:5]):
    print(f"{pred:<12.4f} {actual:<12.4f}")

print(f"\nTotal test samples: {len(X_test)}")
print(f"Mean prediction: {np.mean(predictions):.4f}")

Your MLOps pipeline is fully operational. You have experiment tracking, model versioning, and real-time serving all working together. Next up, take on the Secret Mission to automate model promotion.

Secret mission

Automated Model Selection and Promotion

In production MLOps, you don't manually pick the best model. Build an automated pipeline that programmatically compares all experiment runs, identifies the winner, and promotes it to champion without human intervention.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • MLflow Tracking Server (running on port 8080).
  • MLflow Model Serving process (running on port 5001).
  • Virtual environment with MLflow and scikit-learn installed.
  • Project folder containing train.py, register_model.py, predict.py, auto_promote.py, and MLflow experiment data.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with different hyperparameters.

  • Your MLflow Tracking Server stays running at http://127.0.0.1:8080.
  • Your model serving endpoint stays available at http://127.0.0.1:5001.
  • All experiment data, registered models, and Python scripts remain in your project folder.

Pause - I'll come back to this later

Shut down the running processes to free up your terminal sessions. Your files and experiment data stay on disk so you can restart later.

  • Go to the terminal running the MLflow model serving process and press Ctrl+C to stop it.
  • Go to the terminal running the MLflow Tracking Server and press Ctrl+C to stop it.
  • Deactivate your virtual environment by running:
deactivate

When you come back, navigate to your project folder, reactivate the virtual environment, and restart the MLflow server.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your experiment data, registered models, and all Python scripts permanently.

  • Go to the terminal running the MLflow model serving process and press Ctrl+C to stop it.
  • Go to the terminal running the MLflow Tracking Server and press Ctrl+C to stop it.
  • Deactivate your virtual environment by running:
deactivate
  • Navigate to the parent of your project folder (e.g. your Desktop) and delete the project folder.

On macOS/Linux:

cd ~/Desktop
rm -rf mlops-mlflow

On Windows:

cd ~\Desktop
rmdir /s /q mlops-mlflow

Replace mlops-mlflow with whatever you named your project folder.

  • Confirm the folder is gone by running:
ls

Your project folder should no longer appear in the output. All experiment data, model artifacts, and scripts are now removed.

Nice Work!

Nice Work!

Nice work! You have built a complete MLOps pipeline from scratch using MLflow and scikit-learn. You now understand the core lifecycle that ML Engineers use daily in production: experiment tracking, model management, and model serving.

Here's what you accomplished:

  • Stood up an MLflow Tracking Server and logged multiple experiment runs with parameters, metrics, and model artifacts for visual comparison in the MLflow UI.
  • Registered your best model in the MLflow Model Registry with version control and assigned a champion alias for production-ready identification.
  • Served your champion model as a live REST API that loads the registered model and returns real-time predictions.
  • Built an automated model promotion pipeline that programmatically compares all runs and promotes the best model to champion.

Ready to quiz yourself?