MLflow replaces Weights & Biases with a self-hosted platform for ML experiment tracking, model registry governance, and agent observability.
Databricks launched MLflow in 2018 to establish an open specification for tracking and packaging machine learning experiments across fragmented frameworks. That origin established a vendor-neutral architecture combining run tracking, artifact logging, model registry governance, and LLM observability. Today it remains the standard open-source baseline for teams keeping experiment history on their own infrastructure.
MLflow prioritizes infrastructure control, writing directly to existing databases and object storage instead of routing telemetry through vendor servers. DevOps teams back metadata with PostgreSQL or MySQL and store artifacts in Amazon S3, Google Cloud Storage, or MinIO. Unlike Weights & Biases, which requires proprietary agents and hosted telemetry, MLflow instruments over sixty frameworks—from Scikit-learn and PyTorch to LangChain—via standard REST APIs without exposing datasets or tokens outside private networks.
Weights & Biases excels at collaborative multimedia dashboards, side-by-side computer vision panels, and hosted hyperparameter sweeps. MLflow offers a more utilitarian UI focused on metric curves and trace trees, leaving custom cross-run comparisons to SQL or external scripts. In production, however, MLflow avoids vendor lock-in through standardized MLmodel packaging flavors that bundle dependencies into portable artifacts for Kubernetes, Docker, and internal inference runtimes.
Is MLflow free for commercial use? Yes. It is open-source software under the Apache-2.0 license, allowing internal deployment and commercial use without license fees.
MLflow vs Weights & Biases — which should you choose? Choose MLflow for a self-hosted platform connected to your own storage without seat licenses; choose Weights & Biases for managed hosting and advanced multimedia visualization panels.
Can MLflow track experiments without a server? Yes. You can log runs directly to a local directory or SQLite database on your machine and review results with the local tracking UI.
MLflow is the practical standard for teams that want a dependable, self-hosted experiment tracker and model registry without recurring SaaS seat bills; it is less suited for groups requiring turnkey multimedia dashboards without managing servers. Continuous maintenance and an Apache-2.0 license that permits internal adaptation make it a reliable foundation for self-hosted machine learning platforms.
We started this exact setup from scratch in a clean Linux machine on Nov 1, 2026 and checked it through MLflow's own API: every check passed. Recipes are re-checked regularly and removed if they stop working.
mkdir -p mlflow && cd mlflow
curl -fsSL https://open-source.store/tools/mlflow/compose.yml -o compose.ymlcat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 12)
AWS_ACCESS_KEY_ID=$(openssl rand -hex 16)
AWS_SECRET_ACCESS_KEY=$(openssl rand -hex 32)
MLFLOW_TRACKING_URI=http://localhost:5000
MLFLOW_SERVER_ALLOWED_HOSTS=localhost,127.0.0.1,localhost:5000,127.0.0.1:5000
EOFPOSTGRES_PASSWORD — Password of the PostgreSQL database. Generated above; keep it secret.AWS_ACCESS_KEY_ID — Access key for the internal RustFS artifact storage. Generated above; keep it secret.AWS_SECRET_ACCESS_KEY — Secret key for the internal RustFS artifact storage. Generated above; keep it secret.MLFLOW_TRACKING_URI — Public URL where the MLflow tracking server is reached; set to your public domain or reverse proxy URL.MLFLOW_SERVER_ALLOWED_HOSTS — Allowed hostnames for the MLflow server (comma-separated); set your public domain or reverse proxy hostname when accessing remotely.docker compose up -dservices:
postgres:
image: postgres:15
environment:
POSTGRES_USER: mlflow
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: mlflow
volumes:
- pgdata:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mlflow -d mlflow"]
interval: 5s
timeout: 3s
retries: 10
storage:
image: rustfs/rustfs:1.0.0-alpha.83
environment:
RUSTFS_ADDRESS: :9000
RUSTFS_SERVER_DOMAINS: storage:9000
RUSTFS_REGION: us-east-1
RUSTFS_ACCESS_KEY: ${AWS_ACCESS_KEY_ID}
RUSTFS_SECRET_KEY: ${AWS_SECRET_ACCESS_KEY}
RUSTFS_CONSOLE_ENABLE: "false"
volumes:
- storage-data:/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", 'curl -s http://127.0.0.1:9000/health | grep -q ''"status":"ok"''']
interval: 5s
timeout: 5s
retries: 10
start_period: 5s
create-bucket:
image: amazon/aws-cli:2.33.25
depends_on:
storage:
condition: service_healthy
entrypoint:
- /bin/sh
- -c
- |
set -e
echo "Waiting for S3 gateway..."
if aws --endpoint-url=http://storage:9000 s3api head-bucket --bucket mlflow 2>/dev/null; then
echo "Bucket mlflow already exists. Skipping creation."
else
echo "Creating bucket mlflow..."
aws --endpoint-url=http://storage:9000 s3api create-bucket --bucket mlflow --region us-east-1
fi
environment:
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
AWS_DEFAULT_REGION: us-east-1
AWS_S3_ADDRESSING_STYLE: path
restart: "no"
mlflow:
image: ghcr.io/mlflow/mlflow:v3.17.2-full
depends_on:
postgres:
condition: service_healthy
storage:
condition: service_healthy
create-bucket:
condition: service_completed_successfully
environment:
MLFLOW_BACKEND_STORE_URI: postgresql://mlflow:${POSTGRES_PASSWORD}@postgres:5432/mlflow
MLFLOW_S3_ENDPOINT_URL: http://storage:9000
MLFLOW_ARTIFACTS_DESTINATION: s3://mlflow
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
AWS_DEFAULT_REGION: us-east-1
MLFLOW_S3_IGNORE_TLS: "true"
MLFLOW_TRACKING_URI: ${MLFLOW_TRACKING_URI}
MLFLOW_SERVER_ALLOWED_HOSTS: ${MLFLOW_SERVER_ALLOWED_HOSTS}
command: >
mlflow server
--backend-store-uri postgresql://mlflow:${POSTGRES_PASSWORD}@postgres:5432/mlflow
--artifacts-destination s3://mlflow
--serve-artifacts
--host 0.0.0.0
--port 5000
--allowed-hosts "${MLFLOW_SERVER_ALLOWED_HOSTS}"
ports:
- "127.0.0.1:5000:5000"
restart: always
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://localhost:5000/health')",
]
interval: 5s
timeout: 5s
retries: 30
volumes:
pgdata:
storage-data:
Lint, codegen, breaking-change checks and dependency management in one Git-native CLI. Open source, no registry lock-in.
Show that MLflow is listed as an open-source alternative. Paste into your README:
Ubuntu 24.04 · Apple container, Docker 29.1.3, Compose 2.40.3+ds1-0ubuntu1~24.04.1. Images pulled in 369 s: rustfs/rustfs:1.0.0-alpha.83, amazon/aws-cli:2.33.25, ghcr.io/mlflow/mlflow:v3.17.2-full, postgres:15.
Based on the upstream setup. Official compose using ghcr.io/mlflow/mlflow:v3.17.2-full with bundled drivers, RustFS for artifact storage, and PostgreSQL backend; secrets and allowed hosts moved to .env; port 5000 bound to 127.0.0.1 because MLflow has no native authentication enabled by default.
postgres-1 | 2026-11-01T04:15:36.567633637Z The files belonging to this database system will be owned by user "postgres". postgres-1 | 2026-11-01T04:15:36.567674595Z This user must also own the server process. postgres-1 | 2026-11-01T04:15:36.567677720Z postgres-1 | 2026-11-01T04:15:36.570017762Z The database cluster will be initialized with locale "en_US.utf8". postgres-1 | 2026-11-01T04:15:36.570028553Z The default database encoding has accordingly been set to "UTF8". postgres-1 | 2026-11-01T04:15:36.570029470Z The default text search configuration will be set to "english". postgres-1 | 2026-11-01T04:15:36.570030137Z postgres-1 | 2026-11-01T04:15:36.570030678Z Data page checksums are disabled. postgres-1 | 2026-11-01T04:15:36.570031220Z postgres-1 | 2026-11-01T04:15:36.570031845Z fixing permissions on existing directory /var/lib/postgresql/data ... ok postgres-1 | 2026-11-01T04:15:36.570032428Z creating subdirectories ... ok postgres-1 | 2026-11-01T04:15:36.570157762Z selecting dynamic shared memory implementation ... posix postgres-1 | 2026-11-01T04:15:36.578849178Z selecting default max_connections ... 100 postgres-1 | 2026-11-01T04:15:36.590511595Z selecting default shared_buffers ... 128MB postgres-1 | 2026-11-01T04:15:36.594560553Z selecting default time zone ... Etc/UTC postgres-1 | 2026-11-01T04:15:36.595218803Z creating configuration files ... ok postgres-1 | 2026-11-01T04:15:36.644549845Z running bootstrap script ... ok postgres-1 | 2026-11-01T04:15:36.794055637Z performing post-bootstrap initialization ... ok postgres-1 | 2026-11-01T04:15:36.853487345Z initdb: warning: enabling "trust" authentication for local connections postgres-1 | 2026-11-01T04:15:36.853510512Z initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb. postgres-1 | 2026-11-01T04:15:36.853559970Z syncing data to disk ... ok postgres-1 | 2026-11-01T04:15:36.853748845Z postgres-1 | 2026-11-01T04:15:36.853750220Z postgres-1 | 2026-11-01T04:15:36.853751012Z Success. You can now start the database server using: postgres-1 | 2026-11-01T04:15:36.853751803Z postgres-1 | 2026-11-01T04:15:36.853752512Z pg_ctl -D /var/lib/postgresql/data -l logfile start postgres-1 | 2026-11-01T04:15:36.853753178Z postgres-1 | 2026-11-01T04:15:36.874076928Z waiting for server to start....2026-11-01 04:15:36.873 UTC [46] LOG: starting PostgreSQL 15.19 (Debian 15.19-1.pgdg13+2) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit postgres-1 | 2026-11-01T04:15:36.874933428Z 2026-11-01 04:15:36.874 UTC [46] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" postgres-1 | 2026-11-01T04:15:36.876940887Z 2026-11-01 04:15:36.876 UTC [49] LOG: database system was shut down at 2026-11-01 04:15:36 UTC postgres-1 | 2026-11-01T04:15:36.878934428Z 2026-11-01 04:15:36.878 UTC [46] LOG: database system is ready to accept connections postgres-1 | 2026-11-01T04:15:36.967038804Z done postgres-1 | 2026-11-01T04:15:36.967054095Z server started postgres-1 | 2026-11-01T04:15:37.017814970Z CREATE DATABASE postgres-1 | 2026-11-01T04:15:37.018291137Z postgres-1 | 2026-11-01T04:15:37.018546470Z postgres-1 | 2026-11-01T04:15:37.018560429Z /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/* postgres-1 | 2026-11-01T04:15:37.018561804Z postgres-1 | 2026-11-01T04:15:37.018943554Z waiting for server to shut down....2026-11-01 04:15:37.018 UTC [46] LOG: received fast shutdown request postgres-1 | 2026-11-01T04:15:37.019744929Z 2026-11-01 04:15:37.019 UTC [46] LOG: aborting any active transactions postgres-1 | 2026-11-01T04:15:37.021726095Z 2026-11-01 04:15:37.021 UTC [46] LOG: background worker "logical replication launcher" (PID 52) exited with exit code 1 postgres-1 | 2026-11-01T04:15:37.021779679Z 2026-11-01 04:15:37.021 UTC [47] LOG: shutting down storage-1 | 2026-11-01T04:15:36.551235137Z Initializing data directories: /data storage-1 | 2026-11-01T04:15:36.551392262Z Initializing log directory: /logs storage-1 | 2026-11-01T04:15:36.551395678Z Starting: /usr/bin/rustfs /data storage-1 | 2026-11-01T04:15:36.562314428Z RustFS Http API: http://172.18.0.2:9000 http://127.0.0.1:9000 storage-1 | 2026-11-01T04:15:36.562352553Z RustFS Start Time: 2026-11-01 04:15:36 storage-1 | 2026-11-01T04:15:36.578865303Z RustFS server version: refs/tags/1.0.0-alpha.83 started successfully at [::]:9000, current time: 2026-11-01T04:15:36.578770512Z[Etc/Unknown] postgres-1 | 2026-11-01T04:15:37.022345304Z 2026-11-01 04:15:37.022 UTC [47] LOG: checkpoint starting: shutdown immediate mlflow-1 | 2026-11-01T04:15:44.415105709Z Registry store URI not provided. Using backend store URI. mlflow-1 | 2026-11-01T04:15:44.458654375Z 2026/11/01 04:15:44 INFO mlflow.store.db.utils: Creating initial MLflow database tables... mlflow-1 | 2026-11-01T04:15:44.475446084Z 2026/11/01 04:15:44 INFO mlflow.store.db.utils: Updating database tables mlflow-1 | 2026-11-01T04:15:45.099245667Z [MLflow] Security middleware enabled. Allowed hosts: localhost, 127.0.0.1, localhost:5000, and 1 more. mlflow-1 | 2026-11-01T04:15:45.129418042Z 2026/11/01 04:15:45 INFO mlflow.server.jobs.executor_registry: Available backends after discovery: local mlflow-1 | 2026-11-01T04:15:45.129436459Z 2026/11/01 04:15:45 INFO mlflow.server.jobs.executor_registry: Job executor backend 'local' passed requirements check mlflow-1 | 2026-11-01T04:15:45.221853917Z 2026/11/01 04:15:45 INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit) mlflow-1 | 2026-11-01T04:15:45.221986501Z 2026/11/01 04:15:45 INFO: Started parent process [11] mlflow-1 | 2026-11-01T04:15:48.037606419Z /usr/local/lib/python3.11/site-packages/mlflow/server/fastapi_app.py:23: StarletteDeprecationWarning: starlette.middleware.wsgi is deprecated and will be removed in a future release. Please refer to https://github.com/abersheeran/a2wsgi as a replacement. mlflow-1 | 2026-11-01T04:15:48.037628044Z from starlette.middleware.wsgi import WSGIResponder, build_environ mlflow-1 | 2026-11-01T04:15:48.082917127Z /usr/local/lib/python3.11/site-packages/mlflow/server/fastapi_app.py:23: StarletteDeprecationWarning: starlette.middleware.wsgi is deprecated and will be removed in a future release. Please refer to https://github.com/abersheeran/a2wsgi as a replacement. mlflow-1 | 2026-11-01T04:15:48.082934669Z from starlette.middleware.wsgi import WSGIResponder, build_environ mlflow-1 | 2026-11-01T04:15:48.119105002Z /usr/local/lib/python3.11/site-packages/mlflow/server/fastapi_app.py:23: StarletteDeprecationWarning: starlette.middleware.wsgi is deprecated and will be removed in a future release. Please refer to https://github.com/abersheeran/a2wsgi as a replacement. mlflow-1 | 2026-11-01T04:15:48.119121377Z from starlette.middleware.wsgi import WSGIResponder, build_environ mlflow-1 | 2026-11-01T04:15:48.200210419Z /usr/local/lib/python3.11/site-packages/mlflow/server/fastapi_app.py:23: StarletteDeprecationWarning: starlette.middleware.wsgi is deprecated and will be removed in a future release. Please refer to https://github.com/abersheeran/a2wsgi as a replacement. mlflow-1 | 2026-11-01T04:15:48.200228460Z from starlette.middleware.wsgi import WSGIResponder, build_environ mlflow-1 | 2026-11-01T04:15:48.660606086Z 2026/11/01 04:15:48 INFO: Started server process [16] mlflow-1 | 2026-11-01T04:15:48.660685002Z 2026/11/01 04:15:48 INFO: Waiting for application startup. mlflow-1 | 2026-11-01T04:15:48.674079252Z 2026/11/01 04:15:48 INFO: Application startup complete. mlflow-1 | 2026-11-01T04:15:48.716657544Z 2026/11/01 04:15:48 INFO: Started server process [17] mlflow-1 | 2026-11-01T04:15:48.721866002Z 2026/11/01 04:15:48 INFO: Waiting for application startup. mlflow-1 | 2026-11-01T04:15:48.722718336Z 2026/11/01 04:15:48 INFO: Started server process [14] mlflow-1 | 2026-11-01T04:15:48.725866502Z 2026/11/01 04:15:48 INFO: Waiting for application startup. mlflow-1 | 2026-11-01T04:15:48.738283211Z 2026/11/01 04:15:48 INFO: Application startup complete. mlflow-1 | 2026-11-01T04:15:48.743424669Z 2026/11/01 04:15:48 INFO: Started server process [15] mlflow-1 | 2026-11-01T04:15:48.743715877Z 2026/11/01 04:15:48 INFO: Application startup complete. mlflow-1 | 2026-11-01T04:15:48.753919544Z 2026/11/01 04:15:48 INFO: Waiting for application startup. mlflow-1 | 2026-11-01T04:15:48.765982752Z 2026/11/01 04:15:48 INFO: Application startup complete. mlflow-1 | 2026-11-01T04:15:49.454434336Z 2026/11/01 04:15:49 INFO: 172.18.0.1:60678 - "GET /health HTTP/1.1" 200 OK mlflow-1 | 2026-11-01T04:15:49.610543711Z 2026/11/01 04:15:49 INFO: 172.18.0.1:60682 - "GET / HTTP/1.1" 200 OK mlflow-1 | 2026-11-01T04:15:50.144508503Z 2026/11/01 04:15:50 INFO: 172.18.0.1:60692 - "POST /api/2.0/mlflow/experiments/create HTTP/1.1" 200 OK mlflow-1 | 2026-11-01T04:15:50.150519003Z 2026/11/01 04:15:50 INFO: 172.18.0.1:60696 - "GET /api/2.0/mlflow/experiments/get?experiment_id=1 HTTP/1.1" 200 OK mlflow-1 | 2026-11-01T04:15:50.166584045Z 2026/11/01 04:15:50 INFO: 172.18.0.1:60710 - "POST /api/2.0/mlflow/runs/create HTTP/1.1" 200 OK mlflow-1 | 2026-11-01T04:15:50.173614628Z 2026/11/01 04:15:50 INFO: 172.18.0.1:60714 - "GET /api/2.0/mlflow/runs/get?run_id=4f1ff199c6884fe1a01aa73582494146 HTTP/1.1" 200 OK mlflow-1 | 2026-11-01T04:15:52.733906504Z 2026/11/01 04:15:52 INFO mlflow.server.jobs.utils: Registered online_scoring_scheduler periodic task (runs every 1 minute) mlflow-1 | 2026-11-01T04:15:52.734030421Z 2026/11/01 04:15:52 INFO mlflow.server.jobs.utils: Registered trace_archival_scheduler periodic task (polls every 1 minute and no-ops when trace archival is disabled or unconfigured) mlflow-1 | 2026-11-01T04:15:52.738198046Z 2026/11/01 04:15:52 INFO mlflow.server.jobs.utils: SQL trace rollup scheduler was not registered because rollups are disabled mlflow-1 | 2026-11-01T04:15:53.593251671Z 2026/11/01 04:15:53 INFO: 127.0.0.1:33386 - "GET /health HTTP/1.1" 200 OK mlflow-1 | 2026-11-01T04:15:58.656979507Z 2026/11/01 04:15:58 INFO: 127.0.0.1:44844 - "GET /health HTTP/1.1" 200 OK mlflow-1 | 2026-11-01T04:16:03.724972134Z 2026/11/01 04:16:03 INFO: 127.0.0.1:44854 - "GET /health HTTP/1.1" 200 OK mlflow-1 | 2026-11-01T04:16:08.825021845Z 2026/11/01 04:16:08 INFO: 127.0.0.1:40840 - "GET /health HTTP/1.1" 200 OK postgres-1 | 2026-11-01T04:15:37.046216220Z 2026-11-01 04:15:37.046 UTC [47] LOG: checkpoint complete: wrote 922 buffers (5.6%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.008 s, sync=0.014 s, total=0.025 s; sync files=301, longest=0.004 s, average=0.001 s; distance=4239 kB, estimate=4239 kB postgres-1 | 2026-11-01T04:15:37.048926595Z 2026-11-01 04:15:37.048 UTC [46] LOG: database system is shut down postgres-1 | 2026-11-01T04:15:37.119133762Z done postgres-1 | 2026-11-01T04:15:37.119148720Z server stopped postgres-1 | 2026-11-01T04:15:37.119826429Z postgres-1 | 2026-11-01T04:15:37.119834179Z PostgreSQL init process complete; ready for start up. postgres-1 | 2026-11-01T04:15:37.119835054Z postgres-1 | 2026-11-01T04:15:37.129806304Z 2026-11-01 04:15:37.129 UTC [1] LOG: starting PostgreSQL 15.19 (Debian 15.19-1.pgdg13+2) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit postgres-1 | 2026-11-01T04:15:37.130182637Z 2026-11-01 04:15:37.130 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 postgres-1 | 2026-11-01T04:15:37.130187679Z 2026-11-01 04:15:37.130 UTC [1] LOG: listening on IPv6 address "::", port 5432 postgres-1 | 2026-11-01T04:15:37.131498887Z 2026-11-01 04:15:37.131 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" postgres-1 | 2026-11-01T04:15:37.134241845Z 2026-11-01 04:15:37.134 UTC [62] LOG: database system was shut down at 2026-11-01 04:15:37 UTC postgres-1 | 2026-11-01T04:15:37.136143762Z 2026-11-01 04:15:37.136 UTC [1] LOG: database system is ready to accept connections create-bucket-1 | 2026-11-01T04:15:42.113569973Z Waiting for S3 gateway... create-bucket-1 | 2026-11-01T04:15:42.560020390Z Creating bucket mlflow...