Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Mlflow Ubuntu
GUIDEInstall Guides

How to Install MLflow on Ubuntu 24.04 — ML Experiment Tracking

19 min read

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?
  • Why Self-Host MLflow?
  • Prerequisites
  • Step 1: Update System and Install Python
  • Step 2: Create a Python Virtual Environment
  • Step 3: Install PostgreSQL as the Backend Store
  • Step 4: Install MinIO as the Artifact Store
  • Step 5: Launch the MLflow Tracking Server
  • Step 6: Reverse Proxy with nginx, SSL, and Basic Auth
  • Step 7: Create a systemd Service
  • Step 8: Log Your First Experiment
  • Step 9: Framework Integrations — sklearn, PyTorch, TensorFlow
  • Step 10: Model Registry and Deployment
  • Docker Compose Alternative
  • Backups
  • Security Hardening
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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:

    bash
    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 ufw

    Verify the Python version. Ubuntu 24.04 ships Python 3.12 by default, which MLflow 2.x fully supports:

    bash
    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:

    bash
    sudo ufw allow OpenSSH
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw --force enable

    Step 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.

    bash
    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:

    bash
    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:

    bash
    sudo apt install -y postgresql postgresql-contrib
    sudo systemctl enable --now postgresql

    Create a dedicated database and role for MLflow. Generate a strong password and note it — you will reference it in the tracking server command.

    bash
    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;
    EOF

    PostgreSQL on Ubuntu listens on localhost only by default, which is exactly what we want — the tracking server and database share a host. Verify connectivity:

    bash
    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:

    bash
    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/minio

    Create environment config at /etc/default/minio:

    bash
    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/minio

    Create a systemd unit for MinIO:

    bash
    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:

    bash
    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:

    bash
    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/mlflow

    Test the server manually before wrapping it in systemd:

    bash
    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:

    bash
    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:

    bash
    sudo apt install -y nginx certbot python3-certbot-nginx apache2-utils

    Create a password file for HTTP basic auth. This is the simplest layer of access control and pairs well with SSL:

    bash
    sudo htpasswd -c /etc/nginx/.mlflow-htpasswd yossef
    

    enter a password when prompted

    Write the nginx site config at /etc/nginx/sites-available/mlflow:

    bash
    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:

    bash
    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:

    bash
    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:

    bash
    pip install mlflow==2.16.2 scikit-learn

    Set the tracking URI and basic auth credentials via environment variables:

    bash
    export MLFLOW_TRACKING_URI=https://mlflow.example.com
    export MLFLOW_TRACKING_USERNAME=yossef
    export MLFLOW_TRACKING_PASSWORD=your-basic-auth-password

    Write a minimal script that trains a model and logs everything MLflow cares about:

    python
    # 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_score

    mlflow.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:

    bash
    python train.py

    Refresh 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:

    python
    import mlflow
    mlflow.sklearn.autolog()
    

    ... your usual sklearn code; parameters, metrics, and model are logged automatically

    PyTorch:

    python
    import mlflow
    import mlflow.pytorch

    mlflow.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:

    python
    import mlflow
    import mlflow.tensorflow

    mlflow.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:

    python
    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:

    python
    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:

    python
    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:

    bash
    mlflow models serve \
        --model-uri models:/iris-rf/Production \
        --host 0.0.0.0 \
        --port 5001 \
        --env-manager local

    Then send inference requests:

    bash
    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:

    yaml
    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:

    bash
    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):

    bash
    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-backup

    Wrap 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_request for 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-artifacts bucket, 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-only on a read-replica for analysts, while writes go through a separate protected endpoint.
    Review the MLflow security page for the current threat model and recommendations.

    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.
    If you want this stack deployed and maintained for you, the VPS-Server.host team offers managed MLOps VPS plans with MLflow, MinIO, and PostgreSQL pre-configured, monitored, and backed up. Talk to our MLOps team to scope your deployment.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket