How to Install MLflow on Ubuntu 24.04 — ML Experiment Tracking Server
Machine learning projects generate a sprawl of notebooks, parameter combinations, and half-remembered hyperparameters. Without a system of record, reproducing a model from two months ago becomes archaeology. MLflow solves this by giving every training run a canonical home: parameters, metrics, code versions, and artifacts are all logged automatically and browsable through a polished web UI. This guide walks you through installing a production-grade MLflow tracking server on an Ubuntu 24.04 VPS, backed by PostgreSQL for metadata and MinIO for artifact storage, fronted by nginx with SSL and HTTP basic authentication.
Prefer a one-click deployment? Spin up a pre-configured MLOps VPS with MLflow, MinIO, and PostgreSQL already wired together. Launch an MLOps server now and start logging experiments in minutes.
Table of Contents
What is MLflow?
MLflow is an open-source platform for managing the end-to-end machine learning lifecycle, originally created at Databricks and now a Linux Foundation project. It is organized around four loosely coupled components that you can adopt independently or together.
MLflow Tracking is the most widely used component. It records parameters, metrics, tags, source code references, and output artifacts for every training run into a centralized server. A run is any execution of your ML code — a notebook cell, a script, a distributed training job — and the tracking API makes logging as simple as mlflow.log_metric("accuracy", 0.92). The web UI then lets you compare dozens of runs side by side, plot metrics over time, and drill into individual artifacts.
MLflow Projects package ML code in a reusable, reproducible format. A project is just a directory with an MLproject YAML file that declares the conda or Docker environment, entry points, and parameters. You can run a project locally, on a remote machine, or on Kubernetes using a single command, and MLflow handles environment creation and parameter passing.
MLflow Models is a standardized packaging format for models that decouples training framework from serving infrastructure. A saved model directory contains the model weights, a MLmodel manifest listing available "flavors" (sklearn, pytorch, onnx, pyfunc, and many others), and the inference dependencies. Any downstream tool that understands MLflow Models can load and serve it without knowing how it was trained.
MLflow Model Registry is a centralized catalogue of registered models with versioning, stage transitions (None, Staging, Production, Archived), annotations, and access control. It is where data scientists hand off a trained model to the MLOps team and where CI/CD pipelines pick up the "Production" version for deployment.
Together these components form an opinionated but flexible MLOps stack that works with any Python ML library and scales from single-laptop experimentation to multi-team production.
Why Self-Host MLflow?
Managed experiment tracking services like Weights & Biases, Neptune.ai, and Comet charge per-seat or per-tracked-run. Typical pricing lands around $50-$200 per user per month for team plans, and costs scale with artifact storage, retention, and concurrent runs. For a five-person data science team, that is $3,000-$12,000 per year in SaaS subscriptions before you log a single gigabyte of artifacts.
Self-hosted MLflow on a modest VPS runs the entire platform for the cost of one server, typically $20-$50 per month. Because MLflow is open source under the Apache 2.0 license, there are no per-user, per-run, or per-artifact fees. You also keep every byte of training data, model weight, and metric inside your own VPC, which matters enormously for regulated industries (healthcare, finance, defence) and for companies whose training data contains customer PII.
Self-hosting also unlocks deep integration with your existing infrastructure. You can point MLflow at the same PostgreSQL cluster that runs your app, store artifacts in an existing S3 bucket or MinIO deployment, sit behind your corporate SSO, and emit audit logs into the same SIEM as the rest of your stack. Managed services force you to live inside their walls; a self-hosted deployment is a first-class citizen in yours.
The trade-off is operational work: you own the upgrades, backups, and uptime. The setup below minimizes that burden by using boring, battle-tested components (PostgreSQL, nginx, systemd) that your ops team already knows how to run.
Prerequisites
Before you begin, make sure you have the following in place:
- An Ubuntu 24.04 LTS VPS with at least 2 vCPU, 4 GB RAM, and 40 GB disk. The CloudCore Professional plan is a good fit; bump up disk size if you expect to store many large model artifacts.
- Root or sudo access via SSH.
- A domain name (e.g.
mlflow.example.com) with an A record pointing at the VPS IP. This is required for Let's Encrypt SSL. - Outbound internet access on ports 80 and 443 for certificate issuance and package installs.
- Basic familiarity with the Linux command line and Python virtual environments.
Step 1: Update System and Install Python
Start with a clean, patched system. SSH in as a sudo user and bring the package index up to date:
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3 python3-pip python3-venv python3-dev \
build-essential libpq-dev curl wget git ufwVerify the Python version. Ubuntu 24.04 ships Python 3.12 by default, which MLflow 2.x fully supports:
python3 --version
Python 3.12.3
Open the firewall for SSH, HTTP, and HTTPS. We will proxy MLflow behind nginx, so only these three ports need to be public:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enableStep 2: Create a Python Virtual Environment
Install MLflow into a dedicated virtualenv owned by a service user rather than the system Python. This keeps upgrades isolated and avoids conflicts with other tooling.
sudo useradd -m -s /bin/bash mlflow
sudo -u mlflow -H bash -c '
python3 -m venv /home/mlflow/venv
source /home/mlflow/venv/bin/activate
pip install --upgrade pip wheel
pip install "mlflow[extras]==2.16.2" psycopg2-binary boto3
'The mlflow[extras] extra pulls in database drivers and cloud storage clients. psycopg2-binary is the PostgreSQL driver, and boto3 is what MLflow uses to talk to S3-compatible artifact stores like MinIO.
Confirm the install:
sudo -u mlflow /home/mlflow/venv/bin/mlflow --version
mlflow, version 2.16.2
Step 3: Install PostgreSQL as the Backend Store
MLflow's default SQLite backend works fine for a laptop but falls over under concurrent writes from a team. PostgreSQL is the recommended production backend and handles hundreds of concurrent runs without breaking a sweat.
Install PostgreSQL 16 from the Ubuntu repositories:
sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable --now postgresqlCreate a dedicated database and role for MLflow. Generate a strong password and note it — you will reference it in the tracking server command.
sudo -u postgres psql <<EOF
CREATE ROLE mlflow WITH LOGIN PASSWORD 'change-me-to-a-strong-secret';
CREATE DATABASE mlflow OWNER mlflow;
GRANT ALL PRIVILEGES ON DATABASE mlflow TO mlflow;
EOFPostgreSQL on Ubuntu listens on localhost only by default, which is exactly what we want — the tracking server and database share a host. Verify connectivity:
PGPASSWORD='change-me-to-a-strong-secret' psql -h 127.0.0.1 -U mlflow -d mlflow -c '\conninfo'You should see You are connected to database "mlflow" as user "mlflow".
Step 4: Install MinIO as the Artifact Store
MLflow stores two classes of data: structured metadata (runs, parameters, metrics) in PostgreSQL and blob artifacts (model weights, plots, datasets) in an object store. For self-hosted deployments, MinIO is the canonical S3-compatible object store — a single Go binary that speaks the S3 API.
Download and install MinIO:
wget https://dl.min.io/server/minio/release/linux-amd64/minio -O /tmp/minio
sudo install -m 755 /tmp/minio /usr/local/bin/minio
sudo useradd -r -s /sbin/nologin minio-user || true
sudo mkdir -p /var/lib/minio /etc/minio
sudo chown -R minio-user:minio-user /var/lib/minio /etc/minioCreate environment config at /etc/default/minio:
sudo tee /etc/default/minio >/dev/null <<'EOF'
MINIO_ROOT_USER=mlflow-admin
MINIO_ROOT_PASSWORD=change-me-to-another-strong-secret
MINIO_VOLUMES="/var/lib/minio"
MINIO_OPTS="--address :9000 --console-address :9001"
EOF
sudo chmod 640 /etc/default/minioCreate a systemd unit for MinIO:
sudo tee /etc/systemd/system/minio.service >/dev/null <<'EOF' [Unit] Description=MinIO Object Storage After=network-online.target Wants=network-online.target[Service] User=minio-user Group=minio-user EnvironmentFile=/etc/default/minio ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES Restart=always LimitNOFILE=65536
[Install] WantedBy=multi-user.target EOF
sudo systemctl daemon-reload sudo systemctl enable --now minio
Install the MinIO client mc and create a bucket for MLflow artifacts:
wget https://dl.min.io/client/mc/release/linux-amd64/mc -O /tmp/mc sudo install -m 755 /tmp/mc /usr/local/bin/mc
mc alias set local http://127.0.0.1:9000 mlflow-admin change-me-to-another-strong-secret mc mb local/mlflow-artifacts mc version enable local/mlflow-artifacts
Enabling bucket versioning protects against accidental overwrites and provides an undo path if a bad model logs over a good one.
If you prefer managed S3 (AWS S3, Backblaze B2, Wasabi, Cloudflare R2), skip the MinIO steps and substitute the S3 endpoint, access key, and secret in the next section. MLflow treats any S3-compatible bucket identically.
Step 5: Launch the MLflow Tracking Server
With PostgreSQL and MinIO both running, you can start the tracking server. MLflow needs three pieces of configuration: a backend store URI for metadata, a default artifact root for blob storage, and AWS-style credentials for S3 access.
Create an environment file for MLflow at /etc/default/mlflow:
sudo tee /etc/default/mlflow >/dev/null <<'EOF'
MLFLOW_BACKEND_STORE_URI=postgresql+psycopg2://mlflow:[email protected]:5432/mlflow
MLFLOW_DEFAULT_ARTIFACT_ROOT=s3://mlflow-artifacts/
MLFLOW_S3_ENDPOINT_URL=http://127.0.0.1:9000
AWS_ACCESS_KEY_ID=mlflow-admin
AWS_SECRET_ACCESS_KEY=change-me-to-another-strong-secret
MLFLOW_HOST=127.0.0.1
MLFLOW_PORT=5000
EOF
sudo chown root:mlflow /etc/default/mlflow
sudo chmod 640 /etc/default/mlflowTest the server manually before wrapping it in systemd:
sudo -u mlflow -H bash -c '
set -a; source /etc/default/mlflow; set +a
/home/mlflow/venv/bin/mlflow server \
--backend-store-uri "$MLFLOW_BACKEND_STORE_URI" \
--default-artifact-root "$MLFLOW_DEFAULT_ARTIFACT_ROOT" \
--host "$MLFLOW_HOST" \
--port "$MLFLOW_PORT"
'The server should log Listening at: http://127.0.0.1:5000. In another terminal, confirm it responds:
curl http://127.0.0.1:5000/health
OK
Ctrl-C to stop the manual run; the systemd unit in Step 7 will take over.
Step 6: Reverse Proxy with nginx, SSL, and Basic Auth
Exposing the MLflow server directly to the internet is a bad idea — it has no built-in authentication, CSRF protection, or TLS. Put nginx in front of it.
Install nginx and certbot:
sudo apt install -y nginx certbot python3-certbot-nginx apache2-utilsCreate a password file for HTTP basic auth. This is the simplest layer of access control and pairs well with SSL:
sudo htpasswd -c /etc/nginx/.mlflow-htpasswd yossef
enter a password when prompted
Write the nginx site config at /etc/nginx/sites-available/mlflow:
sudo tee /etc/nginx/sites-available/mlflow >/dev/null <<'EOF' server { listen 80; server_name mlflow.example.com;location / { auth_basic "MLflow"; auth_basic_user_file /etc/nginx/.mlflow-htpasswd;
proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s; proxy_send_timeout 300s; client_max_body_size 500M; } } EOF
sudo ln -sf /etc/nginx/sites-available/mlflow /etc/nginx/sites-enabled/mlflow sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t && sudo systemctl reload nginx
The large client_max_body_size matters: model artifacts can easily be hundreds of megabytes, and the default 1 MB limit will silently truncate uploads.
Issue a Let's Encrypt certificate. Certbot will automatically rewrite the nginx config to enable HTTPS and redirect HTTP to HTTPS:
sudo certbot --nginx -d mlflow.example.com --agree-tos --no-eff-email -m [email protected]Certbot installs a systemd timer that renews certificates 30 days before expiry. Browse to https://mlflow.example.com, accept the basic auth prompt, and you should see the MLflow UI.
Step 7: Create a systemd Service
Wrap the MLflow tracking server in a systemd unit so it restarts on failure and boots at startup:
sudo tee /etc/systemd/system/mlflow.service >/dev/null <<'EOF' [Unit] Description=MLflow Tracking Server After=network-online.target postgresql.service minio.service Wants=network-online.target Requires=postgresql.service minio.service[Service] Type=simple User=mlflow Group=mlflow EnvironmentFile=/etc/default/mlflow ExecStart=/home/mlflow/venv/bin/mlflow server \ --backend-store-uri ${MLFLOW_BACKEND_STORE_URI} \ --default-artifact-root ${MLFLOW_DEFAULT_ARTIFACT_ROOT} \ --host ${MLFLOW_HOST} \ --port ${MLFLOW_PORT} \ --workers 4 Restart=always RestartSec=5 LimitNOFILE=65536
[Install] WantedBy=multi-user.target EOF
sudo systemctl daemon-reload sudo systemctl enable --now mlflow sudo systemctl status mlflow
The --workers 4 flag runs MLflow behind Gunicorn with four worker processes, which is appropriate for a 2-4 vCPU box. Check the unit is healthy with journalctl -u mlflow -f and reload it after any env file change with sudo systemctl restart mlflow.
Step 8: Log Your First Experiment
Switch to a workstation (laptop, CI runner, GPU box) and install the client:
pip install mlflow==2.16.2 scikit-learnSet the tracking URI and basic auth credentials via environment variables:
export MLFLOW_TRACKING_URI=https://mlflow.example.com
export MLFLOW_TRACKING_USERNAME=yossef
export MLFLOW_TRACKING_PASSWORD=your-basic-auth-passwordWrite a minimal script that trains a model and logs everything MLflow cares about:
# train.py import mlflow import mlflow.sklearn from sklearn.datasets import load_iris from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, f1_scoremlflow.set_experiment("iris-classification")
X, y = load_iris(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
with mlflow.start_run(run_name="rf-baseline"): n_estimators = 100 max_depth = 5
mlflow.log_param("n_estimators", n_estimators) mlflow.log_param("max_depth", max_depth)
model = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth, random_state=42) model.fit(X_train, y_train)
preds = model.predict(X_test) mlflow.log_metric("accuracy", accuracy_score(y_test, preds)) mlflow.log_metric("f1_macro", f1_score(y_test, preds, average="macro"))
mlflow.sklearn.log_model(model, artifact_path="model", registered_model_name="iris-rf")
# Log a plot as an artifact import matplotlib.pyplot as plt from sklearn.metrics import ConfusionMatrixDisplay ConfusionMatrixDisplay.from_estimator(model, X_test, y_test) plt.savefig("confusion_matrix.png") mlflow.log_artifact("confusion_matrix.png")
Run it:
python train.pyRefresh the MLflow UI and you should see an iris-classification experiment with one run, tagged parameters, two metrics, the confusion matrix artifact, and a Models entry with version 1 of iris-rf.
Step 9: Framework Integrations
MLflow ships first-class autologging for the major ML frameworks. Calling mlflow.<framework>.autolog() at the top of a script captures parameters, metrics, and the trained model without explicit log calls.
scikit-learn:
import mlflow mlflow.sklearn.autolog()
... your usual sklearn code; parameters, metrics, and model are logged automatically
PyTorch:
import mlflow import mlflow.pytorchmlflow.pytorch.autolog()
with mlflow.start_run(): # your training loop for epoch in range(epochs): loss = train_one_epoch(model, loader, optimizer) mlflow.log_metric("train_loss", loss, step=epoch) mlflow.pytorch.log_model(model, "model")
For PyTorch Lightning, autologging captures every metric logged via self.log() and checkpoints the model at the end.
TensorFlow / Keras:
import mlflow import mlflow.tensorflowmlflow.tensorflow.autolog()
model.fit(...) and model.evaluate(...) are logged automatically
XGBoost and LightGBM have analogous mlflow.xgboost.autolog() and mlflow.lightgbm.autolog() entry points. See the MLflow tracking docs for the full matrix.
Step 10: Model Registry and Deployment
The Model Registry turns a logged model into a versioned, stageable artifact that downstream systems can consume.
Register a model from an existing run:
import mlflow
client = mlflow.MlflowClient()
result = mlflow.register_model(
model_uri=f"runs:/{run_id}/model",
name="iris-rf",
)
print(f"Registered {result.name} v{result.version}")Transition a version to Production:
client.transition_model_version_stage(
name="iris-rf",
version=3,
stage="Production",
archive_existing_versions=True,
)archive_existing_versions=True automatically moves the previously-promoted version to Archived, ensuring only one production version exists at a time.
Load the production model for inference:
import mlflow.pyfunc
model = mlflow.pyfunc.load_model("models:/iris-rf/Production")
predictions = model.predict(new_data)The models:/... URI scheme always resolves to whatever version currently holds the requested stage, so promoting a new version in the UI is an atomic swap for all consumers.
Serve a model as a REST endpoint. MLflow ships a built-in model server:
mlflow models serve \
--model-uri models:/iris-rf/Production \
--host 0.0.0.0 \
--port 5001 \
--env-manager localThen send inference requests:
curl -X POST http://localhost:5001/invocations \
-H "Content-Type: application/json" \
-d '{"inputs": [[5.1, 3.5, 1.4, 0.2]]}'For production serving, wrap the mlflow models serve command in its own systemd unit behind nginx, or export the model as a Docker image with mlflow models build-docker --model-uri models:/iris-rf/Production --name iris-rf:prod and deploy it to Kubernetes.
Docker Compose Alternative
If you prefer containers to bare-metal installs, the entire stack (PostgreSQL + MinIO + MLflow) runs cleanly in Docker Compose. Save the following as docker-compose.yml:
version: "3.9"services: postgres: image: postgres:16 restart: unless-stopped environment: POSTGRES_USER: mlflow POSTGRES_PASSWORD: change-me-to-a-strong-secret POSTGRES_DB: mlflow volumes: - pgdata:/var/lib/postgresql/data
minio: image: minio/minio:latest restart: unless-stopped command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: mlflow-admin MINIO_ROOT_PASSWORD: change-me-to-another-strong-secret volumes: - miniodata:/data ports: - "9001:9001"
minio-init: image: minio/mc:latest depends_on: [minio] entrypoint: > /bin/sh -c " sleep 5; mc alias set local http://minio:9000 mlflow-admin change-me-to-another-strong-secret; mc mb -p local/mlflow-artifacts; exit 0; "
mlflow: image: ghcr.io/mlflow/mlflow:v2.16.2 restart: unless-stopped depends_on: [postgres, minio, minio-init] environment: MLFLOW_S3_ENDPOINT_URL: http://minio:9000 AWS_ACCESS_KEY_ID: mlflow-admin AWS_SECRET_ACCESS_KEY: change-me-to-another-strong-secret command: > bash -c " pip install psycopg2-binary boto3 && mlflow server --backend-store-uri postgresql+psycopg2://mlflow:change-me-to-a-strong-secret@postgres:5432/mlflow --default-artifact-root s3://mlflow-artifacts/ --host 0.0.0.0 --port 5000 --workers 4 " ports: - "127.0.0.1:5000:5000"
volumes: pgdata: miniodata:
Bring it up with docker compose up -d, then point the same nginx config from Step 6 at 127.0.0.1:5000. The Compose approach is convenient for dev environments and for operators comfortable with Docker, but the bare-metal install above integrates more cleanly with OS-level monitoring and backups.
Backups
Two systems need regular backups: the PostgreSQL metadata and the MinIO artifact bucket.
PostgreSQL dump — schedule a nightly pg_dump to a safe location:
sudo tee /usr/local/bin/backup-mlflow-db.sh >/dev/null <<'EOF' #!/bin/bash set -euo pipefail BACKUP_DIR=/var/backups/mlflow DATE=$(date +%Y%m%d-%H%M%S) mkdir -p "$BACKUP_DIR" sudo -u postgres pg_dump -Fc mlflow > "$BACKUP_DIR/mlflow-$DATE.dump" find "$BACKUP_DIR" -name 'mlflow-*.dump' -mtime +30 -delete EOF sudo chmod +x /usr/local/bin/backup-mlflow-db.sh
echo "0 3 * root /usr/local/bin/backup-mlflow-db.sh" | sudo tee /etc/cron.d/mlflow-db-backup
MinIO mirror — use mc mirror to replicate the artifact bucket to a second location (another MinIO instance, AWS S3, or Backblaze B2):
mc alias set offsite https://s3.us-west-002.backblazeb2.com B2_KEY_ID B2_APP_KEY
mc mirror --overwrite --remove local/mlflow-artifacts offsite/my-mlflow-backupWrap that in a cron job on the same nightly schedule. For point-in-time recovery, enable bucket versioning (already done in Step 4) and lifecycle rules to expire old versions after your retention window.
Security Hardening
HTTP basic auth is a reasonable baseline but has limits — no per-user audit trail, no SSO, no role-based access. For production multi-tenant deployments, consider:
- Authentik or Keycloak in front of nginx via
auth_requestfor OIDC SSO with groups and audit logging. - MLflow's built-in authentication plugin (
mlflow server --app-name basic-auth) which adds a user/permission model backed by SQLite. See Authentication docs. - Network isolation — bind the tracking server, PostgreSQL, and MinIO to a private network and expose only nginx to the public internet.
- Least-privilege S3 credentials — give MLflow a scoped IAM key that can only read and write the
mlflow-artifactsbucket, not the entire account. - Fail2ban on the nginx auth log to block credential-stuffing attacks.
- Disable the deletion of runs in the UI by running with
--artifacts-onlyon a read-replica for analysts, while writes go through a separate protected endpoint.
Troubleshooting
UI loads but artifacts fail to upload with An error occurred while calling o72.load. This almost always means the MLflow server cannot reach MinIO. Check MLFLOW_S3_ENDPOINT_URL in /etc/default/mlflow, confirm MinIO is listening on port 9000, and verify the access key and secret match. Run mc alias set check http://127.0.0.1:9000 <key> <secret> && mc ls check/ from the server to test credentials independently.
sqlalchemy.exc.OperationalError: could not connect to server. PostgreSQL is not running or the connection string is wrong. Check sudo systemctl status postgresql, then test the URI manually with psql "postgresql://mlflow:[email protected]:5432/mlflow".
401 Unauthorized from the Python client. Basic auth credentials are missing. Export MLFLOW_TRACKING_USERNAME and MLFLOW_TRACKING_PASSWORD, or embed them in the URI: https://user:[email protected].
Artifact downloads time out in nginx. Large models exceed the default proxy timeout. Increase proxy_read_timeout and proxy_send_timeout to 600s or more, and raise client_max_body_size to accommodate your biggest artifact.
Migration errors after upgrade. MLflow applies Alembic migrations to the backend store on startup. If a migration fails, check journalctl -u mlflow -n 200 for the exact error. Most issues are resolved by ensuring psycopg2-binary and sqlalchemy versions match the MLflow release notes for your target version.
Gunicorn worker timeout on large runs. Add --gunicorn-opts "--timeout 300" to the mlflow server command for jobs that stream many metrics in one request.
FAQ
Can I use SQLite instead of PostgreSQL? Yes, for a single-user dev setup. Pass --backend-store-uri sqlite:///mlflow.db. For any shared deployment, use PostgreSQL — SQLite serializes all writes and will lock up under concurrency.
Do I need MinIO if I have AWS S3? No. Replace MLFLOW_DEFAULT_ARTIFACT_ROOT with s3://your-bucket/ and remove MLFLOW_S3_ENDPOINT_URL. MLflow will use the default AWS endpoint.
How do I migrate from Weights & Biases? There is no one-click import, but W&B exports runs as JSON and artifacts as files. A short Python script using the W&B API and mlflow.log_* can bulk-migrate experiments. The cultural migration is harder than the data one — allocate time to retrain the team on the UI.
Does MLflow work with Jupyter notebooks? Yes. mlflow.set_tracking_uri() and the logging APIs work identically inside a notebook. For automatic capture, install the nbformat extra and MLflow will log the notebook itself as an artifact.
Can multiple tenants share one MLflow instance? Partially. The built-in basic-auth plugin supports experiment-level permissions, but there is no hard multi-tenant isolation. For strong isolation (regulated industries, multi-customer SaaS), run one MLflow instance per tenant behind a shared nginx with hostname-based routing.
How do I scale horizontally? Run multiple MLflow server instances behind a load balancer, all pointing at the same PostgreSQL and S3 backend. MLflow is stateless aside from its stores. For very large teams, shard experiments across multiple backends.
Next Steps
You now have a production-grade MLflow tracking server: PostgreSQL-backed metadata, MinIO-backed artifacts, nginx SSL termination, systemd supervision, and nightly backups. From here, logical next steps are:
- Wire MLflow into your CI/CD pipeline so every training run in GitHub Actions or GitLab CI automatically logs to the tracker.
- Add a model-promotion workflow using GitHub Actions and the MLflow Model Registry API — a pull request approval transitions a model from Staging to Production.
- Layer Evidently AI or WhyLabs on top for production monitoring and drift detection, feeding alerts back into MLflow tags.
- Deploy a Feast feature store on the same VPS to close the loop on training/serving skew.
- Explore MLflow Recipes for an opinionated project template that encodes best practices.