MLflow is an open source platform for tracking machine learning experiments: it records parameters, metrics and artifacts for each training run, and its model registry keeps versioned models your team can promote and load by name. A shared tracking server gives everyone one place to compare runs. In this tutorial you will deploy MLflow on Ubuntu 24.04 with PostgreSQL as the metadata store and a local directory for artifacts, run it under systemd, publish it through Nginx with HTTPS and basic authentication, and log a model from a Python client.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 2 GB of RAM and enough disk space for your model artifacts.
- A non-root user with
sudoprivileges. - A domain or subdomain with a DNS
Arecord pointing to the server. This guide usesmlflow.your_domain. - Ports 80 and 443 open to the Internet.
- On your workstation, Python 3.10 or newer to run the client example.
Step 1 - Setting up PostgreSQL
MLflow stores experiments, runs, parameters, metrics and the model registry in a database. PostgreSQL handles concurrent writers from several users much better than the default SQLite file. Install it:
sudo apt update
sudo apt install postgresql
Create a database role and a database owned by it. Replace your_db_password with a strong password:
sudo -u postgres psql -c "CREATE ROLE mlflow WITH LOGIN PASSWORD 'your_db_password';"
sudo -u postgres psql -c "CREATE DATABASE mlflow OWNER mlflow;"
Confirm that the new role can connect over TCP, as MLflow will:
psql "postgresql://mlflow:[email protected]:5432/mlflow" -c "SELECT current_user;"
current_user
--------------
mlflow
(1 row)
PostgreSQL on Ubuntu listens only on localhost by default, which is exactly what you want here.
Step 2 - Installing MLflow
Create a system user to run the server, with its home in /opt/mlflow, and a directory for artifacts:
sudo useradd --system --create-home --home-dir /opt/mlflow --shell /usr/sbin/nologin mlflow
sudo mkdir -p /var/lib/mlflow/artifacts
sudo chown -R mlflow:mlflow /var/lib/mlflow
Install MLflow and the PostgreSQL driver in a virtual environment owned by that user:
sudo apt install python3-venv
sudo -u mlflow python3 -m venv /opt/mlflow/venv
sudo -u mlflow /opt/mlflow/venv/bin/pip install --upgrade pip
sudo -u mlflow /opt/mlflow/venv/bin/pip install mlflow psycopg2-binary
Check the version:
sudo -u mlflow /opt/mlflow/venv/bin/mlflow --version
mlflow, version 3.4.0
Step 3 - Running MLflow as a systemd service
Keep the database URI, which contains the password, in a separate environment file that only root can read:
sudo nano /etc/mlflow.env
MLFLOW_BACKEND_STORE_URI=postgresql://mlflow:[email protected]:5432/mlflow
sudo chmod 600 /etc/mlflow.env
Now create the service unit:
sudo nano /etc/systemd/system/mlflow.service
[Unit]
Description=MLflow tracking server
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=mlflow
Group=mlflow
EnvironmentFile=/etc/mlflow.env
WorkingDirectory=/opt/mlflow
ExecStart=/opt/mlflow/venv/bin/mlflow server \
--host 127.0.0.1 \
--port 5000 \
--backend-store-uri ${MLFLOW_BACKEND_STORE_URI} \
--artifacts-destination /var/lib/mlflow/artifacts \
--serve-artifacts
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
The important options:
--host 127.0.0.1keeps MLflow off the public network; Nginx will be the only entry point.--backend-store-uripoints at PostgreSQL. On first start, MLflow creates its tables in the empty database.--serve-artifactswith--artifacts-destinationmakes the server proxy artifact uploads and downloads. Clients only need HTTP access to MLflow, never direct access to the storage.
systemd reads the environment file as root before dropping privileges, so the mlflow user does not need to read it. Start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now mlflow
Verify that it is running and answering on its health endpoint:
sudo systemctl status mlflow --no-pager
curl http://127.0.0.1:5000/health
● mlflow.service - MLflow tracking server
Active: active (running) since Thu 2026-09-25 12:04:51 UTC; 8s ago
OK
If it fails, sudo journalctl -u mlflow -n 50 shows the error, usually a wrong database password in /etc/mlflow.env.
Step 4 - Publishing MLflow with Nginx, HTTPS and authentication
MLflow's tracking server has no login by default: anyone who can reach it can read and delete experiments. Nginx in front of it adds TLS and HTTP basic authentication. Install Nginx and the htpasswd tool:
sudo apt install nginx apache2-utils
Create the first user; htpasswd asks for the password. Omit -c when adding more users, or it overwrites the file:
sudo htpasswd -c /etc/nginx/mlflow.htpasswd alice
Create the site configuration:
sudo nano /etc/nginx/sites-available/mlflow
server {
listen 80;
listen [::]:80;
server_name mlflow.your_domain;
# Model artifacts can be large
client_max_body_size 2G;
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 300;
proxy_send_timeout 300;
}
}
Enable it, test the configuration and reload:
sudo ln -s /etc/nginx/sites-available/mlflow /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Allow web traffic through UFW if it is active:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
Basic authentication is only safe over HTTPS. Request a Let's Encrypt certificate and let Certbot redirect HTTP to HTTPS:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d mlflow.your_domain
Check from the server that authentication is enforced:
curl -s -o /dev/null -w "%{http_code}\n" https://mlflow.your_domain/
curl -s -o /dev/null -w "%{http_code}\n" -u alice https://mlflow.your_domain/health
401
Enter host password for user 'alice':
200
Open https://mlflow.your_domain in a browser, log in, and the MLflow UI shows the Default experiment.
Step 5 - Logging a run from Python
On your workstation, create a virtual environment with the MLflow client and scikit-learn:
python3 -m venv mlflow-client
source mlflow-client/bin/activate
pip install mlflow scikit-learn
The client sends HTTP basic credentials when these environment variables are set. Export them, together with the server URL:
export MLFLOW_TRACKING_URI=https://mlflow.your_domain
export MLFLOW_TRACKING_USERNAME=alice
export MLFLOW_TRACKING_PASSWORD='your_password'
Create a training script:
nano train.py
import mlflow
import mlflow.sklearn
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
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)
params = {"n_estimators": 100, "max_depth": 5}
with mlflow.start_run(run_name="random-forest"):
mlflow.log_params(params)
model = RandomForestClassifier(**params, random_state=42)
model.fit(X_train, y_train)
accuracy = accuracy_score(y_test, model.predict(X_test))
mlflow.log_metric("accuracy", accuracy)
# Upload the model and register it as a new version of "iris-classifier"
mlflow.sklearn.log_model(
model,
name="model",
input_example=X_test[:2],
registered_model_name="iris-classifier",
)
print(f"accuracy={accuracy:.3f}")
Run it:
python train.py
accuracy=1.000
Successfully registered model 'iris-classifier'.
Created version '1' of model 'iris-classifier'.
In the UI, open the iris-classification experiment: the run shows its parameters, the accuracy metric and the model files. On the server, the artifacts are stored under /var/lib/mlflow/artifacts.
Step 6 - Using the model registry
Registered models are referenced by name and version. Instead of the old stages (Staging, Production), current MLflow versions use aliases: movable labels such as champion that point at one version. Set one:
nano promote.py
from mlflow import MlflowClient
client = MlflowClient()
client.set_registered_model_alias("iris-classifier", "champion", 1)
print(client.get_model_version_by_alias("iris-classifier", "champion").version)
python promote.py
1
Any application can now load whichever version currently holds the alias, without knowing its number:
python -c 'import mlflow; m = mlflow.sklearn.load_model("models:/iris-classifier@champion"); print(m.predict([[5.1, 3.5, 1.4, 0.2]]))'
[0]
When you train a better model, register version 2 and move the champion alias to it; consumers pick it up on their next load.
Troubleshooting
502 Bad Gateway from Nginx. MLflow is not running or not listening on 127.0.0.1:5000. Check sudo systemctl status mlflow and sudo journalctl -u mlflow -n 50.
The service fails with password authentication failed for user "mlflow". The password in /etc/mlflow.env does not match the database role. Test the URI with psql as in Step 1, fix the file and run sudo systemctl restart mlflow.
The UI or client returns an "Invalid Host header" error. Recent MLflow releases validate the Host header to protect against DNS rebinding. Allow your domain by adding --allowed-hosts "mlflow.your_domain,localhost,127.0.0.1" to ExecStart (check mlflow server --help for the option in your version), then reload systemd and restart the service.
Large model uploads fail with 413 Request Entity Too Large. Raise client_max_body_size in the Nginx site file and reload Nginx.
The client gets 401 errors. MLFLOW_TRACKING_USERNAME and MLFLOW_TRACKING_PASSWORD are not set in the shell running the script, or they do not match an entry in /etc/nginx/mlflow.htpasswd.
Conclusion
You now have a shared MLflow tracking server on Ubuntu 24.04 backed by PostgreSQL, storing artifacts on the server, running under systemd and reachable only through Nginx with HTTPS and authentication. Your team can log runs, compare them in the UI and promote models with registry aliases. Next, schedule pg_dump backups of the mlflow database together with /var/lib/mlflow/artifacts, move artifacts to S3-compatible object storage as they grow, or serve a registered model with mlflow models serve.
