Deployment guide

MLflow on a VPS: ML Experiment Tracking and Model Registry

Deploy on a VPS Cloud →

Tutorial

MLflow on a VPS: ML Experiment Tracking and Model Registry

Artificial Intelligence6 min read4 steps

A machine learning project quickly produces dozens of attempts: a learning rate changed here, a dataset cleaned there, and a metric jotted down in a notebook corner. Three weeks later, nobody knows which model scored highest, or with which code. MLflow is the open-source platform (Apache-2.0 licensed) that keeps all of it on a central server: parameters, metrics and files of every run, versioned models, and traces of LLM applications. This guide installs it on a VPS with Docker Compose, protects it with a password, connects your Python code, then sets up accounts, backups and updates.

Contents· Why an MLflow server1/12
  1. 01Why an MLflow server
  2. 02What MLflow brings
  3. 03MLflow versus tracking by hand
  4. 04Prerequisites
  5. 05Install MLflow with Docker Compose
  6. 06Without --app-name basic-auth, the server is open
  7. 07Send your experiments from Python
  8. 08Trace an LLM application
  9. 09The model registry
  10. 10Accounts and permissions
  11. 11Backup and updates
  12. 121-click install with ServOrbit

Why an MLflow server

Run locally, MLflow writes its results to an mlruns folder on your machine: nobody else sees them, and they disappear with the machine. A tracking server changes that. Every notebook, every training script and every team member sends their runs to the same place, and the interface compares them side by side.

The same server holds the model registry — the list of validated, versioned models, with an alias for the one in production — and, for generative AI, the traces of your calls to language models.

What MLflow brings

  • Experiment tracking — parameters, metrics and artifacts of every run, filtered and compared in the interface.
  • Model registry — versions, aliases and history, so you know exactly what runs in production.
  • LLM tracing — inputs, outputs, latency and steps of every request of your generative AI applications.
  • Artifacts served by the server — files, charts and models uploaded over HTTPS, no S3 storage to configure.
  • Automatic logging — autolog() for scikit-learn, PyTorch, Transformers, XGBoost, OpenAI or LangChain.
  • Accounts and permissions — one account per person, permissions granted experiment by experiment.

MLflow versus tracking by hand

Scroll the table

MLflow serverNotebooks and spreadsheet
Run historyEvery run recorded automaticallyWhatever someone remembered to write down
Comparing two modelsOne screen, metrics side by sideCopying numbers
Produced filesStored with the run that created themScattered across laptops
Model in productionA version and an alias in the registryA file named "final_v3"
TeamworkA shared server, accountsSending files around

Prerequisites

An Ubuntu 24.04 VPS with 2 GB of RAM is enough to start; plan 4 GB and more disk if you store many models, since artifacts take up the space. You need Docker with the Compose plugin, and a domain name pointing to the VPS, with nginx and an HTTPS certificate: your scripts and your team will connect to it.

Install MLflow with Docker Compose

  1. Prepare the directory and the secrets

    Create the application directory and a .env file holding the admin account's password, the session key and your domain.

    mkdir -p /opt/mlflow && cd /opt/mlflow
    cat > .env <<EOF
    ADMIN_PASSWORD=$(openssl rand -hex 16)
    SECRET_KEY=$(openssl rand -hex 24)
    DOMAIN=mlflow.example.ma
    EOF
    chmod 600 .env
  2. Write the docker-compose.yml

    The -full image is the official variant that ships the authentication module. At startup, the command writes the basic_auth.ini file that declares the admin account; --allowed-hosts only accepts your domain and the loopback. The port is only published on the loopback: only nginx reaches it. MLFLOW_SERVER_ENABLE_JOB_EXECUTION=false turns off the background job runner, which is memory-hungry and not needed for tracking.

    services:
      mlflow:
        image: ghcr.io/mlflow/mlflow:v3.18.0-full
        restart: unless-stopped
        ports:
          - "127.0.0.1:5000:5000"
        environment:
          ADMIN_PASSWORD: ${ADMIN_PASSWORD}
          DOMAIN: ${DOMAIN}
          MLFLOW_FLASK_SERVER_SECRET_KEY: ${SECRET_KEY}
          MLFLOW_AUTH_CONFIG_PATH: /mlflow/basic_auth.ini
          MLFLOW_SERVER_ENABLE_JOB_EXECUTION: "false"
        entrypoint: ["/bin/sh", "-c"]
        command:
          - >-
            mkdir -p /mlflow/artifacts &&
            printf '[mlflow]\ndefault_permission = NO_PERMISSIONS\ndatabase_uri = sqlite:////mlflow/basic_auth.db\nadmin_username = admin\nadmin_password = %s\nauthorization_function = mlflow.server.auth:authenticate_request_basic_auth\n' "$$ADMIN_PASSWORD" > /mlflow/basic_auth.ini &&
            exec mlflow server --app-name basic-auth --host 0.0.0.0 --port 5000 --workers 2
            --backend-store-uri sqlite:////mlflow/mlflow.db --artifacts-destination /mlflow/artifacts
            --allowed-hosts "$$DOMAIN,localhost:*,127.0.0.1:*"
        volumes:
          - mlflow-data:/mlflow
    
    volumes:
      mlflow-data: {}
  3. Start and check

    Startup takes about fifteen seconds. /health answers without authentication; the API returns 401 without credentials.

    docker compose up -d
    curl -fsS http://127.0.0.1:5000/health
    curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5000/api/2.0/mlflow/experiments/search
  4. Publish over HTTPS with nginx

    In your domain's vhost (certificate obtained with certbot), proxy to the local port and pass the Host header. Raise client_max_body_size: a model uploaded as an artifact quickly exceeds the default limit.

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 512m;
    }

Without --app-name basic-auth, the server is open

By default, mlflow server has no authentication: anyone who reaches the address can read your experiments, download your models and delete everything. Keep --app-name basic-auth, never publish port 5000 on the public IP, and keep --allowed-hosts set to your domain only: it is MLflow's protection against DNS rebinding.

Send your experiments from Python

Install the client (pip install mlflow) on the training machine, then point it to your server. Credentials are passed through environment variables:

import os
import mlflow

os.environ["MLFLOW_TRACKING_USERNAME"] = "admin"
os.environ["MLFLOW_TRACKING_PASSWORD"] = "YOUR_PASSWORD"
mlflow.set_tracking_uri("https://mlflow.example.ma")
mlflow.set_experiment("churn-model")

with mlflow.start_run():
    mlflow.log_param("learning_rate", 0.01)
    mlflow.log_metric("accuracy", 0.93)
    mlflow.log_artifact("confusion_matrix.png")

The run appears in the interface right away, with its parameters, metrics and the uploaded file. mlflow.sklearn.autolog() or mlflow.pytorch.autolog() record all of that without an extra line.

Trace an LLM application

For an assistant or an agent, tracing records every request with its model calls, its steps and their duration. Turn on your library's automatic logging, or decorate your own functions:

import mlflow

mlflow.set_experiment("support-assistant")
mlflow.openai.autolog()   # every OpenAI client call becomes a trace

@mlflow.trace
def answer(question: str) -> str:
    ...

Traces show up in the experiment's tab: you see which call was slow, which context was sent and what the model answered.

The model registry

When a run is validated, register its model: it gets a version number, and you can give it an alias such as production.

mlflow.register_model(f"runs:/{run_id}/model", "churn-model")

Your inference service then loads models:/churn-model@production: switching models means moving the alias, without touching the code.

Accounts and permissions

The admin account creates the other accounts through the API:

curl -u admin:YOUR_PASSWORD -X POST https://mlflow.example.ma/api/2.0/mlflow/users/create \
  -H 'Content-Type: application/json' \
  -d '{"username": "sara", "password": "A_LONG_PASSWORD"}'

A new account sees no experiment until one is shared with it: permissions (read, edit, manage) are granted experiment by experiment and model by model. Change the admin password through the users/update-password API (passing the old one in current_password); the new one is kept across restarts.

Backup and updates

All of MLflow's state lives in the mlflow-data volume: the experiment database, the accounts database and the artifacts folder. Back it up with the container stopped, then copy the archive off the VPS:

cd /opt/mlflow
docker compose stop
docker run --rm -v mlflow_mlflow-data:/data -v "$PWD":/backup alpine \
  tar czf /backup/mlflow-$(date +%F).tar.gz -C /data .
docker compose start

To update, read the release notes first — an update can migrate the database schema —, change the image tag in docker-compose.yml, then:

cd /opt/mlflow
docker compose pull && docker compose up -d
docker compose logs --tail 20

1-click install with ServOrbit

ServOrbit's 1-click apps install MLflow on your VPS without a command line: it deploys the official container, generates the admin account's password and the session key, enables authentication on the interface and the API, limits the accepted host names to your address and publishes the server over HTTPS on a subdomain of your domain or on the VPS's free subdomain (mlflow.{dns_slug}.servorbit-dns.com). The password is shown in your client area; all that is left is to connect your notebooks.

Your ML experiments on your own server

A ServOrbit Ubuntu 24.04 VPS with 2 GB of RAM is enough to start MLflow. 1-click install from your client area, HTTPS on your domain or on the VPS's free subdomain, password generated for you.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab