Deploy an MLflow Tracking Server on AWS

Deploy a production MLflow server on AWS with EC2, RDS, S3, and Nginx auth.

Introduction

30 Second Summary

When data scientists train machine learning models, they constantly tweak settings and compare results. Without a central place to track those experiments, teams lose track of what worked, what didn't, and which model is actually the best.

In this project, you will deploy a production-grade MLflow tracking server on AWS that logs ML experiments from your local machine to the cloud. You'll set up the same architecture that real ML teams use to manage their models.

What You'll Build

A cloud-hosted MLflow tracking server running on AWS with a database for experiment metadata, object storage for model artifacts, and a reverse proxy enforcing authentication.

By the end of this project, you'll have:

  • A running MLflow tracking server on EC2, accessible via your browser.
  • An RDS PostgreSQL database storing experiment metadata and an S3 bucket storing trained model artifacts.
  • An Nginx reverse proxy with HTTP Basic Authentication protecting your server.
  • A local Python script that trains a model and logs everything to your remote server.
  • Secret Mission: Add TLS encryption with a self-signed certificate so your MLflow server runs over HTTPS.

Are there any prerequisites?

You should have basic Python experience and be comfortable training a simple model from a dataset (e.g. using scikit-learn). No prior AWS experience is required.

Set Up AWS Account and Launch EC2

Your MLflow tracking server needs a home in the cloud. In this project, that home is an EC2 instance on AWS where you will install and run the tracking server software.

Before any of that can happen, you need an AWS account, a running server, and a way to connect to it from your local machine. In this step, you will create an AWS Free Tier account, launch your first EC2 instance, and connect to it using the VS Code Remote-SSH extension.

In this step, get ready to:
  • Create an AWS Free Tier account.
  • Launch an EC2 instance with the correct AMI, instance type, key pair, and security group.
  • Connect to your EC2 instance from VS Code using the Remote-SSH extension.
Create your AWS Free Tier account and install the Remote-SSH extension

AWS offers a Free plan for new accounts that gives you $100 in credits at sign-up. This project's resources (EC2, RDS, S3) are all covered by those credits, so you will not be charged.

  • Open your browser and navigate to https://aws.amazon.com.
  • Click Create an AWS Account (or Create a free account if displayed).
  • Follow the registration steps: enter your email, create a password, and provide your payment details.
  • When asked to select a support plan, choose Free plan.
  • Complete verification and sign into the AWS Management Console.

What is the AWS Free plan?

The Free plan ensures you will not incur any charges until you upgrade to a paid plan. It expires after 6 months or when you exhaust your credits, whichever comes first.

All resources in this project fit within those credits. You can monitor your usage in the Billing dashboard at any time.

Next, install the VS Code extension you will use to connect to your EC2 instance.

  • Open VS Code.
  • Press Ctrl+Shift+X to open the Extensions view.
  • Type remote ssh in the search bar.
  • Find Remote - SSH (published by Microsoft) and click Install.

What is Remote-SSH?

The Remote-SSH extension lets you open a terminal and edit files on a remote machine directly from VS Code. You get the same editor experience as if the files were local, but everything runs on the remote server.

Launch an EC2 instance

An EC2 instance is a virtual server in the cloud. You will launch one running Amazon Linux 2023 and configure it so you can connect via SSH and serve web traffic.

  • In the AWS Management Console, type EC2 in the top search bar and select EC2 from the results.
  • Click Launch instance from the EC2 dashboard.

You are now in the launch instance wizard. Configure each section as follows:

  • Under Name and tags, enter mlflow-server as the instance name.
  • Under Application and OS Images (Amazon Machine Image), choose Quick Start. Select Amazon Linux. Confirm the AMI shows Amazon Linux 2023 AMI (it should be selected by default).
  • Under Instance type, select t3.micro. It should be labeled Free tier eligible.
  • Under Key pair (login), click Create new key pair.

A dialog appears for creating your key pair. Configure it:

  • Enter mlflow-key as the key pair name.
  • Set the key pair type to RSA.
  • Set the private key file format to .pem.
  • Click Create key pair. The .pem file downloads to your computer automatically.

Save your .pem file somewhere safe

Move the downloaded mlflow-key.pem file to a known location (for example, your Downloads folder or a dedicated .ssh folder). You will need the full path to this file shortly.

Now configure the network settings to control who can connect to your instance:

  • Under Network settings, click Edit.
  • Under Firewall (security groups), select Create security group.
  • You should see a default SSH rule (port 22). Change its Source type to My IP. This restricts SSH access to only your current IP address.
  • Click Add security group rule to add a second rule.
  • Set the Type to HTTP (port 80). Set Source type to My IP.

Why restrict access to My IP?

Choosing My IP means only your current network can reach the instance on those ports. This is far safer than allowing traffic from anywhere (0.0.0.0/0), which would expose your server to the entire internet.

  • Leave all other settings at their defaults.
  • In the Summary panel on the right, review your configuration and click Launch instance.

You should see a success notification. Click the instance ID link to open the Instances page. Wait until the Instance state column shows Running.

Instance stuck in Pending or shows Terminated?

If your instance immediately terminates, your account may not have completed verification. Check your email for a verification link from AWS and try again after verifying.

If the instance stays in Pending for more than a few minutes, refresh the page. EC2 instances typically take 30-60 seconds to start.

help me troubleshoot my EC2 instance that is not reaching the Running state

  • Once the instance is running, select it and look for the Public IPv4 address in the details panel at the bottom. Copy this address. You will need it next.
Connect to your EC2 instance via VS Code Remote-SSH

Now that your instance is running, you will configure VS Code to connect to it over SSH using the key pair you downloaded.

  • In VS Code, press Ctrl+Shift+P to open the Command Palette.
  • Type Remote-SSH: Open Configuration File... and select it.
  • Choose the first config file in the list (typically C:\Users\YourName\.ssh\config).

The config file opens in the editor. Add the following entry at the bottom of the file:

Host mlflow-server
    HostName [[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]
    User ec2-user
    IdentityFile [[PEMPATH="C:\\Users\\YourName\\Downloads\\mlflow-key.pem"]]

What does this config do?

  • Host mlflow-server gives this connection a friendly name you can reference later.
  • HostName is the public IP address of your EC2 instance.
  • User ec2-user is the default username for Amazon Linux instances.
  • IdentityFile points to the .pem private key file you downloaded when creating the key pair.
  • Save the config file.
  • Open the Command Palette again with Ctrl+Shift+P.
  • Type Remote-SSH: Connect to Host... and select it.
  • Select mlflow-server from the list.

VS Code opens a new window and connects to your EC2 instance. You may see a prompt asking you to confirm the fingerprint of the remote host. Select Continue.

  • If prompted to select the platform of the remote host, choose Linux.
  • Wait for VS Code to finish initializing the connection. The bottom-left status bar indicator will show SSH: mlflow-server when the connection is ready.
  • Open a terminal in the connected window by pressing Ctrl+` (backtick).

Verify the connection by running this command in the remote terminal:

whoami

You should see ec2-user printed in the terminal. This confirms you are connected to your EC2 instance.

Connection timed out or permission denied?

  • If you see a timeout error, double-check that your EC2 security group allows SSH (port 22) from your IP. Your IP may have changed if you switched networks.
  • If you see "Permission denied", verify the IdentityFile path in your SSH config points to the correct .pem file. Make sure the file name matches what you downloaded.
  • If the host key verification fails, you may have previously connected to a different instance at the same IP. Remove the old entry from your known_hosts file and try again.

help me debug my VS Code Remote-SSH connection to EC2

Your cloud server is up and connected. Next, you will create the S3 bucket, RDS database, and IAM role that your MLflow tracking server needs for storage and permissions.

Create S3 Bucket, RDS PostgreSQL, and IAM Role

Your EC2 instance is up and connected, but it needs supporting services to run a real tracking server.

MLflow needs two things beyond the server itself: a database for experiment metadata (parameters, metrics, run info) and object storage for large artifacts (trained models, plots). In this step, you will create both of these in AWS and grant your EC2 instance permission to access them.

In this step, get ready to:
  • Create an S3 bucket for storing model artifacts.
  • Create an RDS PostgreSQL database for experiment metadata.
  • Create an IAM role granting your EC2 instance access to the S3 bucket.
Create an S3 bucket for artifacts

Your MLflow server needs a place to store large files like trained models. S3 is AWS's object storage service, perfect for this job.

  • In the AWS Management Console, search for S3 in the top search bar and open the S3 service.
  • Click Create bucket.
  • For Bucket name, enter a globally unique name like mlflow-artifacts-yourname (replace with your own name).
  • Confirm that Block all public access is checked (it should be by default).
  • Leave all other settings at their defaults.
  • Scroll down and click Create bucket.

Why block all public access?

Your S3 bucket stores trained ML models. Blocking public access ensures only your EC2 instance (via its IAM role) can read or write artifacts. This follows the principle of least privilege.

You should see your new bucket listed on the S3 Buckets page.

Bucket name already exists?

S3 bucket names are globally unique across all AWS accounts. If you see an error about the name already being taken, try adding random numbers or your initials to the name.

help me pick a unique S3 bucket name

Create an RDS PostgreSQL database

MLflow stores experiment metadata (run parameters, metrics, tags) in a relational database. RDS is AWS's managed database service that handles backups, patching, and scaling for you.

  • In the AWS Console search bar, search for RDS and open the RDS service.
  • In the left navigation pane, click Databases.
  • Click Create database.
  • Select Standard create.
  • For Engine type, choose PostgreSQL.
  • For Templates, choose Free tier (or Sandbox if you see that instead).
  • In the Settings section, enter a DB instance identifier (for example, mlflow-db).
  • For Master username, enter a username you will remember (for example, mlflowadmin).
  • For Credentials management, select Self managed.
  • Turn off Auto generate a password.
  • Enter a Master password and confirm it. Write this down somewhere safe.
  • In the Instance configuration section, select Burstable classes (includes t classes) and choose db.t3.micro.
  • In the Connectivity section, make sure the database is in the same VPC as your EC2 instance.
  • Set Public access to No.
  • Expand the Additional configuration section at the bottom.
  • For Initial database name, enter mlflow.
  • Click Create database.

What is RDS?

Amazon RDS (Relational Database Service) manages the infrastructure for your database. It handles hardware provisioning, OS patching, backups, and failover. You just connect and use it like any PostgreSQL database.

The db.t3.micro instance class is covered by your Free Tier credits. It provides enough capacity for experiment tracking in a learning environment.

The database takes 5-10 minutes to provision. Its status will show as Creating until it is ready.

Use the wait time productively

While RDS provisions, continue to the next substep to create your IAM role. You will come back to check the RDS status and configure its security group once it is available.

Create an IAM role and configure RDS security

Your EC2 instance needs permission to write model artifacts to S3. Instead of storing AWS access keys on the server, you will create an IAM role. The EC2 instance assumes this role automatically and receives temporary credentials.

  • In the AWS Console search bar, search for IAM and open the IAM service.
  • In the left navigation pane, click Roles.
  • Click Create role.
  • For Trusted entity type, choose AWS service.
  • Under Use case, select EC2, then click Next.
  • On the Add permissions page, click Create policy (this opens a new tab).

In the new policy tab, you will define exactly which S3 actions your EC2 instance can perform.

  • Select the JSON tab in the policy editor.
  • Clear the default content and paste the following policy:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::[[BUCKET_NAME=\"mlflow-artifacts-yourname\"]]",
                "arn:aws:s3:::[[BUCKET_NAME=\"mlflow-artifacts-yourname\"]]/*"
            ]
        }
    ]
}

What does this policy do?

This policy grants four specific S3 permissions on your artifact bucket only:

  • s3:PutObject allows uploading model artifacts.
  • s3:GetObject allows downloading artifacts to view them.
  • s3:DeleteObject allows removing old artifacts.
  • s3:ListBucket allows listing the contents of the bucket.

The two Resource lines scope these permissions to your specific bucket and its contents. This follows the principle of least privilege.

  • Replace the bucket name in both Resource lines with your actual bucket name from earlier.
  • Click Next.
  • Enter a policy name (for example, mlflow-s3-artifact-access).
  • Click Create policy.
  • Close this tab and return to the Create role tab.
  • Click the refresh button on the policy list, then search for your new policy name (mlflow-s3-artifact-access).
  • Select the checkbox next to it and click Next.
  • For Role name, enter mlflow-ec2-s3-role.
  • Click Create role.

Why an IAM role instead of access keys?

IAM roles provide temporary, automatically rotated credentials to your EC2 instance. This means you never need to store long-lived AWS access keys on the server. If the instance is compromised, the credentials expire automatically.

Now attach the role to your running EC2 instance.

  • Navigate to the EC2 service in the AWS Console.
  • Click Instances in the left navigation.
  • Select your MLflow instance.
  • Click Actions, then Security, then Modify IAM role.
  • For IAM role, select mlflow-ec2-s3-role from the dropdown.
  • Click Update IAM role.

Now configure the RDS security group so your EC2 instance can connect to the database.

  • Navigate back to the RDS service.
  • Click Databases and wait until your database status shows Available.
  • Click on your database name to open its details.
  • In the Connectivity & security section, note down the Endpoint value. It looks like mlflow-db.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com. You will need this later.
  • Click the link to the security group listed under VPC security groups.
  • Select the security group, then click Edit inbound rules.
  • Click Add rule.
  • For Type, select PostgreSQL (this sets port 5432 automatically).
  • For Source, choose the security group attached to your EC2 instance (start typing your EC2 security group name to find it).
  • Click Save rules.

Why reference the EC2 security group as the source?

By setting the source to your EC2 security group (instead of an IP address), any instance in that group can reach the database. This is more maintainable than hardcoding IPs and follows AWS networking best practices.

RDS still showing Creating after 10 minutes?

RDS provisioning can occasionally take up to 15 minutes depending on region load. Refresh the page and wait. If it stays in Creating for more than 20 minutes, check that you selected the Free tier template (larger instances take longer).

help me troubleshoot my RDS instance that is stuck in Creating status

Your infrastructure backbone is in place. You have object storage for artifacts, a managed database for metadata, and secure permissions connecting everything. Next, you will install MLflow on your EC2 instance and connect it to these services.

Install and Start MLflow Tracking Server

Your AWS infrastructure is in place. You have an EC2 instance, an RDS PostgreSQL database for metadata, and an S3 bucket for artifacts.

Now it is time to install MLflow on your EC2 instance and connect all three pieces together. The MLflow tracking server will use RDS as its backend store (where it records parameters, metrics, and run info) and S3 as its artifact store (where it keeps trained models and large files).

In this step, get ready to:
  • Install Python 3.11 and MLflow with its dependencies on EC2.
  • Start the MLflow tracking server connected to RDS and S3.
  • Verify the MLflow UI is accessible in your browser.
Install Python 3.11 and MLflow dependencies

Amazon Linux 2023 ships with Python 3.9 as the system default, but MLflow requires Python 3.10 or higher. You will install Python 3.11 alongside the system Python without replacing it.

  • In the VS Code terminal connected to your EC2 instance, install Python 3.11 and pip by running this command:
sudo dnf install python3.11 python3.11-pip -y

What does this command do?

  • sudo dnf install uses the DNF package manager (Amazon Linux 2023's default) to install system packages.
  • python3.11 installs the Python 3.11 runtime at /usr/bin/python3.11, separate from the system Python 3.9.
  • python3.11-pip installs pip3.11, the package installer for Python 3.11.
  • Verify the installation by running:
python3.11 --version

You should see a version number starting with Python 3.11.

Not seeing Python 3.11?

Make sure you included the -y flag so DNF did not prompt for confirmation and skip the install. Run the install command again if needed.

Help me troubleshoot my Python 3.11 installation on EC2.

  • Install MLflow and its required dependencies by running:
pip3.11 install mlflow psycopg2-binary boto3

What are these packages?

  • mlflow is the experiment tracking framework itself.
  • psycopg2-binary is the PostgreSQL database adapter that lets MLflow talk to your RDS instance.
  • boto3 is the AWS SDK for Python, enabling MLflow to read and write artifacts to S3.

The installation takes a few minutes. You should see a success message once all packages are installed.

Seeing permission errors?

If you see a permissions error, try running the command with sudo prepended: sudo pip3.11 install mlflow psycopg2-binary boto3. On a fresh EC2 instance, pip may require elevated permissions to write to the system site-packages.

Help me fix permission errors installing Python packages on EC2.

Start the MLflow tracking server

The MLflow server command connects your three AWS resources together. It takes your RDS PostgreSQL connection string as the backend store and your S3 bucket as the artifact destination.

  • Start the MLflow server by running this command (replace the placeholders with your actual values):
mlflow server --host 0.0.0.0 --backend-store-uri postgresql://[[RDS_USERNAME="your RDS master username"]]:[[RDS_PASSWORD="your RDS master password"]]@[[RDS_ENDPOINT="your-rds-endpoint.region.rds.amazonaws.com"]]/mlflow --artifacts-destination s3://[[S3_BUCKET="mlflow-artifacts-yourname"]]

What does this command do?

  • --host 0.0.0.0 makes the server listen on all network interfaces, not just localhost.
  • --backend-store-uri tells MLflow to store experiment metadata (parameters, metrics, run info) in your RDS PostgreSQL database.
  • --artifacts-destination tells MLflow to proxy artifact uploads to your S3 bucket. The server acts as a proxy so clients never need direct S3 credentials.

Where do I find my RDS endpoint?

Return to the AWS Console. Open the RDS service and click on your database instance. The endpoint is listed under Connectivity & security. It looks something like mydb.abc123xyz.us-east-1.rds.amazonaws.com.

The terminal should show MLflow starting up with log output. The server runs in the foreground, so this terminal stays occupied.

Seeing a connection refused error?

If MLflow cannot connect to RDS, check that your RDS security group allows inbound traffic on port 5432 from your EC2 security group. Also double-check your username, password, and endpoint for typos.

Help me fix MLflow's connection to my RDS PostgreSQL database.

Verify the MLflow UI in your browser

MLflow is running on port 5000, but your EC2 security group only allows traffic on ports 22 and 80 right now. You will temporarily open port 5000 to confirm the UI works, then close it again.

  • Return to the AWS Console and open the EC2 service.
  • Click Security Groups in the left sidebar under Network & Security.
  • Select the security group attached to your EC2 instance.
  • Click the Inbound rules tab, then click Edit inbound rules.
  • Click Add rule.
  • Set Type to Custom TCP.
  • Set Port range to 5000.
  • Set Source to My IP.
  • Click Save rules.
  • Open a new browser tab and visit your EC2 public IP on port 5000:
http://[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]:5000

You should see the MLflow UI with an empty experiment list. This confirms your tracking server is running and connected to RDS and S3.

MLflow UI not loading?

Make sure you are using http:// (not https). Check that the port 5000 rule was saved correctly and your source is set to your current IP. If your IP changed since the initial setup, update the rule.

Also confirm the MLflow server is still running in your EC2 terminal. If it crashed, scroll up for error messages.

Help me get the MLflow UI to load in my browser.

  • Now remove the temporary port 5000 rule. Return to Security Groups in the AWS Console.
  • Click Edit inbound rules again.
  • Click the Delete button (X icon) next to the port 5000 rule you just added.
  • Click Save rules.

Why remove port 5000?

In the next step, you will set up Nginx as a reverse proxy on port 80 with authentication. Exposing MLflow directly on port 5000 bypasses authentication entirely. Removing this rule ensures all traffic goes through Nginx.

Your MLflow tracking server is live. Next up, you will secure it behind Nginx with authentication so only authorized users can access the UI and API.

Secure MLflow with Nginx Reverse Proxy and Basic Auth

Your MLflow server is running, but it's exposed directly on port 5000 with no access control. Anyone who knows your EC2 IP address could view your experiments, download your models, or even delete your data.

In this step, you'll place Nginx in front of MLflow as a reverse proxy. Nginx will handle all incoming requests on port 80, require a username and password, and only forward authenticated traffic to MLflow on port 5000. This is the same pattern used in production systems across the industry.

In this step, get ready to:
  • Install Nginx and create a password file for HTTP Basic Authentication.
  • Write an Nginx configuration that proxies port 80 to MLflow with authentication enforced.
  • Start Nginx and verify the login prompt works in your browser.
Install Nginx and create a password file

Before Nginx can protect your server, you need two things: the Nginx web server itself, and a password file that stores your login credentials. The htpasswd tool (from the httpd-tools package) creates this password file in a secure hashed format.

  • In your VS Code terminal connected to EC2, install Nginx and httpd-tools by running:
sudo dnf install nginx httpd-tools -y
  • Create the password file with a username by running:
sudo htpasswd -c /etc/nginx/.htpasswd mlflow-user

You will be prompted to enter and confirm a password. Choose something memorable.

What do these commands do?

The first command installs Nginx (the web server) and httpd-tools (which provides the htpasswd utility).

The htpasswd -c command creates a new password file at /etc/nginx/.htpasswd with the username mlflow-user. The -c flag means "create" the file. Your password is stored as a hash, not in plain text.

  • Verify the password file was created by running:
cat /etc/nginx/.htpasswd

You should see a line showing mlflow-user followed by a long hashed string.

Command not found?

If htpasswd is not found, the httpd-tools package may not have installed correctly. Re-run the install command and check for errors.

Help me fix the htpasswd command not found error.

Create the Nginx configuration file

Now you need to tell Nginx how to handle incoming traffic. Your configuration will listen on port 80, require authentication, and forward requests to MLflow on port 5000.

  • First, remove the default server block that ships with Nginx so it does not conflict with your configuration. Run:
sudo sed -i '/^\s*server {/,/^\s*}/d' /etc/nginx/nginx.conf

Why remove the default server block?

The default /etc/nginx/nginx.conf on Amazon Linux 2023 includes a server block listening on port 80 that serves the Nginx welcome page. If you leave it in place, it will compete with your new configuration file for traffic on port 80.

  • Create the MLflow Nginx configuration file by running:
sudo tee /etc/nginx/conf.d/mlflow.conf > /dev/null << 'EOF'
server {
    listen 80;
    server_name _;

    client_max_body_size 500M;

    auth_basic "MLflow Restricted Access";
    auth_basic_user_file /etc/nginx/.htpasswd;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
        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;
    }
}
EOF

What does this configuration do?

  • listen 80 tells Nginx to accept all HTTP traffic on port 80.
  • client_max_body_size 500M allows large model uploads (trained models can be hundreds of megabytes).
  • auth_basic and auth_basic_user_file enforce username/password authentication using the file you just created.
  • proxy_pass http://127.0.0.1:5000 forwards authenticated requests to MLflow running on port 5000.
  • The proxy_set_header lines pass the original client information through to MLflow so logging stays accurate.
  • Test that your Nginx configuration has no syntax errors by running:
sudo nginx -t

You should see a message saying the syntax is OK and the test is successful.

Seeing a syntax error?

Check that your mlflow.conf file has matching braces and semicolons at the end of each directive line. You can view the file with cat /etc/nginx/conf.d/mlflow.conf to spot any issues.

Help me fix my Nginx configuration syntax error.

✔️ Awesome, I've got everything!

Great. Double check you have a single file at /etc/nginx/conf.d/mlflow.conf and nginx -t passes.

ⓧ I'd like to double check the full code

Here is the complete Nginx configuration file:

server {
    listen 80;
    server_name _;

    client_max_body_size 500M;

    auth_basic "MLflow Restricted Access";
    auth_basic_user_file /etc/nginx/.htpasswd;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
        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;
    }
}
Start Nginx and test access

Everything is configured. Time to start Nginx and confirm the authentication prompt appears in your browser.

  • Start Nginx and enable it to run on boot by running:
sudo systemctl start nginx
sudo systemctl enable nginx

What does enable do?

The start command launches Nginx right now. The enable command tells the system to automatically start Nginx whenever your EC2 instance reboots.

  • Open your browser and visit your EC2 public IP address (just http:// followed by the IP, no port number needed).
  • You should see a login prompt asking for a username and password.
  • Enter mlflow-user as the username and the password you set earlier.

After authenticating, the MLflow UI should load showing your empty experiment list.

Not seeing the login prompt?

Make sure your EC2 security group allows inbound traffic on port 80 (HTTP) from your IP address. You configured this in Step 1.

Also confirm Nginx is running by checking its status:

sudo systemctl status nginx

If Nginx failed to start, check the error log for details:

sudo cat /var/log/nginx/error.log

Help me troubleshoot why I can't see the login prompt.

Why use a reverse proxy instead of exposing MLflow directly?

MLflow itself does not include robust authentication for production use. Its documentation recommends placing a reverse proxy like Nginx in front to add TLS encryption and authentication.

This pattern is transferable. The same Nginx-as-reverse-proxy approach secures Jupyter notebooks, Grafana dashboards, and internal APIs across production environments.

Your MLflow server is now secured behind authentication. Next up, you'll connect to this server from your local machine and log a real ML experiment.

Log a Scikit-Learn Experiment from Your Local Machine

Your MLflow server is secured and accessible through a browser. Now it's time to use it for its real purpose: logging ML experiments from your development machine.

In this step, you'll install the MLflow client library on your local Windows machine and run a scikit-learn training script that sends all parameters, metrics, and model artifacts to your remote server. This is the workflow that data science teams use every day.

In this step, get ready to:
  • Install MLflow and scikit-learn on your local machine.
  • Write a Python script that trains a model and logs everything to your remote MLflow server.
  • Verify the experiment appears in the MLflow UI with parameters, metrics, and a model artifact.
Install MLflow and scikit-learn on your local machine

Your EC2 instance has MLflow installed as the server. Your local machine needs MLflow installed as the client so it can communicate with that server.

  • Open a new VS Code window for local work. Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the Command Palette.
  • Type New Window and select File: New Window.
  • This opens a fresh VS Code window connected to your local machine (not EC2).
  • Open the integrated terminal in this new window by pressing Ctrl+` (both macOS and Windows).
  • Install the MLflow client and scikit-learn by running this command:
pip install mlflow scikit-learn

What does this command do?

This installs two packages. mlflow provides the client library for communicating with your remote tracking server. scikit-learn provides the machine learning tools you'll use to train a model.

  • Confirm the installation succeeded by running:
pip show mlflow
  • You should see package metadata including the version number (3.14.0 or similar) and the install location.

Seeing 'pip not recognized'?

On Windows, try pip3 instead of pip. If neither works, make sure Python is added to your system PATH. You can also try python -m pip install mlflow scikit-learn as an alternative.

Help me troubleshoot pip not being recognized on Windows.

Create the experiment logging script

This script will train a Random Forest classifier on the Iris dataset, then send all experiment data to your remote MLflow server. The server stores metadata in RDS and model artifacts in S3.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create a new file called log_experiment.py by running:
touch log_experiment.py
  • Confirm the file exists by running:
ls
  • You should see log_experiment.py in the output.
  • Open log_experiment.py in VS Code by running:
code log_experiment.py
  • Add the imports and MLflow configuration by pasting this code into the file:
import os
import mlflow
import mlflow.sklearn
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score

# Configure MLflow tracking
os.environ["MLFLOW_TRACKING_USERNAME"] = "mlflow-user"
os.environ["MLFLOW_TRACKING_PASSWORD"] = "your-password-here"

mlflow.set_tracking_uri("http://[[EC2_PUBLIC_IP=\"your EC2 public IPv4 address\"]]")
mlflow.set_experiment("iris-classification")

What does this code do?

The imports bring in MLflow for experiment tracking, scikit-learn for model training, and the os module for setting environment variables.

MLFLOW_TRACKING_USERNAME and MLFLOW_TRACKING_PASSWORD are environment variables that the MLflow client reads automatically when authenticating with a server protected by Basic Auth. set_tracking_uri points the client at your EC2 instance's public IP (where Nginx is listening on port 80).

Replace the placeholder values

Replace your-password-here with the actual password you created with htpasswd in the previous step. Replace the EC2 IP with your instance's public IPv4 address from the AWS console.

  • Below the configuration block, add the data loading code:
# Load and split data
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.2, random_state=42
)

What does this code do?

load_iris() loads a classic ML dataset with 150 flower samples and 4 features. train_test_split splits it into 80% training data and 20% test data so you can evaluate the model on samples it has never seen.

  • Below the data loading, add the training and logging block:
# Train and log with MLflow
with mlflow.start_run(run_name="random-forest-experiment"):
    n_estimators = 100
    max_depth = 5

    mlflow.log_param("n_estimators", n_estimators)
    mlflow.log_param("max_depth", max_depth)
    mlflow.log_param("test_size", 0.2)
    mlflow.log_param("random_state", 42)

    model = RandomForestClassifier(
        n_estimators=n_estimators,
        max_depth=max_depth,
        random_state=42
    )
    model.fit(X_train, y_train)

    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred, average="weighted")

    mlflow.log_metric("accuracy", accuracy)
    mlflow.log_metric("f1_score", f1)

    mlflow.sklearn.log_model(model, artifact_path="model")

    print(f"Run logged successfully!")
    print(f"Accuracy: {accuracy:.4f}")
    print(f"F1 Score: {f1:.4f}")
    print(f"View at: {mlflow.get_tracking_uri()}")

What does this code do?

mlflow.start_run() creates a new experiment run. Inside it, log_param records the hyperparameters you chose, and log_metric records performance scores. These all go to RDS PostgreSQL via your tracking server.

mlflow.sklearn.log_model serializes the trained model and uploads it as an artifact. Because your server uses --artifacts-destination pointing to S3, the model file is stored in your S3 bucket automatically.

  • Save log_experiment.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved log_experiment.py before continuing.

ⓧ I'd like to double check the full code

Here is the complete log_experiment.py file for comparison:

import os
import mlflow
import mlflow.sklearn
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score

# Configure MLflow tracking
os.environ["MLFLOW_TRACKING_USERNAME"] = "mlflow-user"
os.environ["MLFLOW_TRACKING_PASSWORD"] = "your-password-here"

mlflow.set_tracking_uri("http://your-ec2-public-ip")
mlflow.set_experiment("iris-classification")

# Load and split data
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.2, random_state=42
)

# Train and log with MLflow
with mlflow.start_run(run_name="random-forest-experiment"):
    n_estimators = 100
    max_depth = 5

    mlflow.log_param("n_estimators", n_estimators)
    mlflow.log_param("max_depth", max_depth)
    mlflow.log_param("test_size", 0.2)
    mlflow.log_param("random_state", 42)

    model = RandomForestClassifier(
        n_estimators=n_estimators,
        max_depth=max_depth,
        random_state=42
    )
    model.fit(X_train, y_train)

    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred, average="weighted")

    mlflow.log_metric("accuracy", accuracy)
    mlflow.log_metric("f1_score", f1)

    mlflow.sklearn.log_model(model, artifact_path="model")

    print(f"Run logged successfully!")
    print(f"Accuracy: {accuracy:.4f}")
    print(f"F1 Score: {f1:.4f}")
    print(f"View at: {mlflow.get_tracking_uri()}")
Run the script and verify in the MLflow UI

With the script saved and your placeholder values replaced, you can now send your first experiment to the remote tracking server.

  • Run the experiment script by executing this command in your local terminal:
python log_experiment.py

What should I see?

Your terminal should print output similar to this:

Run logged successfully!
Accuracy: 1.0000
F1 Score: 1.0000
View at: http://your-ec2-public-ip

The exact accuracy and F1 values may differ slightly. The important thing is that no errors appeared and the run logged successfully.

  • Open your browser and navigate to http:// followed by your EC2 public IP address.
  • Enter your mlflow-user username and password when prompted.
  • In the MLflow UI, click on the iris-classification experiment in the left sidebar.
  • Click on the run named random-forest-experiment to open the run details.
  • Verify you can see the Parameters section showing n_estimators, max_depth, test_size, and random_state.
  • Verify the Metrics section shows accuracy and f1_score values.
  • Click the Artifacts tab to confirm the model artifact is stored (this lives in your S3 bucket).

Seeing a connection error when running the script?

Make sure your EC2 public IP in the script matches the current IP shown in the AWS console. If you stopped and restarted your instance, the public IP may have changed.

Also verify that your EC2 security group still allows inbound HTTP traffic on port 80 from your IP address.

Help me debug a connection error when running my MLflow experiment script against a remote server.

Seeing a 401 Unauthorized error?

Double check that MLFLOW_TRACKING_USERNAME is set to mlflow-user (the exact username you created with htpasswd) and that MLFLOW_TRACKING_PASSWORD matches the password you entered during that setup.

Help me fix a 401 Unauthorized error when my MLflow client connects to a Nginx-protected server.

Secret mission

Add TLS with a Self-Signed Certificate

Your MLflow server is secured with a username and password, but the traffic between your browser and the server travels unencrypted. Anyone on the network could intercept your credentials or experiment data in transit. In this secret mission, you will add HTTPS encryption to your MLflow server using a self-signed TLS certificate.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. All resources in this project are covered by your AWS Free Tier credits ($100 at sign-up). Monitor your credit usage in the AWS Billing dashboard to avoid unexpected charges when credits are exhausted.

Cost warning

Your AWS Free plan expires after 6 months or when you exhaust your credits, whichever comes first. If you leave your EC2 and RDS instances running after credits expire, you will start incurring charges. We recommend pausing or deleting your resources if you are not actively using them.

Resources you used:

  • EC2 instance (t3.micro) running MLflow, Nginx, and the tracking server.
  • RDS PostgreSQL instance (db.t3.micro) storing experiment metadata.
  • S3 bucket storing model artifacts from experiment runs.
  • IAM role with S3 access policy attached to the EC2 instance.
  • Local Python scripts (log_experiment.py and log_experiment_tls.py) on your Windows machine.

Keep everything running

No action needed. Choose this if you are still actively experimenting with MLflow and want to log more runs.

  • Your EC2, RDS, and S3 resources remain active and continue consuming credits.
  • Monitor your credit balance in the AWS Billing dashboard under **Credits**.
  • The VS Code Remote-SSH extension, local Python packages (mlflow, scikit-learn), and your local scripts remain on your machine.

Pause - I'll come back to this later

Shut down running instances to stop consuming credits while keeping your data intact. You can restart them later.

  • In the EC2 console, select your instance, choose **Instance state**, then **Stop instance**.
  • In the RDS console, select your database instance, choose **Actions**, then **Stop temporarily**.
  • Your S3 bucket remains available with minimal storage cost against your credits.
  • To resume later, start both the EC2 and RDS instances from their respective consoles. Note that your EC2 public IP will change after a stop/start cycle. Update your log_experiment.py tracking URI with the new IP.

Delete - I don't want to use this again

Remove all project resources and start fresh. This ensures zero ongoing cost against your credits.

Follow these steps in order:

  • Terminate the EC2 instance: Open the EC2 console. In the navigation pane, choose **Instances**. Select your instance, choose **Instance state**, then **Terminate (delete) instance**. Choose **Terminate (delete)** when prompted.
  • Delete the RDS instance: Open the RDS console at https://console.aws.amazon.com/rds/. In the navigation pane, choose **Databases**. Select your MLflow database instance, choose **Actions**, then **Delete**. Deselect **Create final snapshot**, enter delete me in the confirmation box, and choose **Delete**.
  • Empty and delete the S3 bucket: Open the S3 console at https://console.aws.amazon.com/s3/. In **General purpose buckets**, select your artifacts bucket and choose **Empty**. Type permanently delete to confirm, then choose **Empty**. After emptying completes, select the bucket again, choose **Delete**, type the bucket name to confirm, and choose **Delete bucket**.
  • Delete the IAM role: Open the IAM console at https://console.aws.amazon.com/iam/. In the navigation pane, choose **Roles**. Select the checkbox next to your EC2 S3 access role, choose **Delete** at the top. Enter the role name to confirm and choose **Delete**.
  • Delete your local scripts: Remove log_experiment.py and log_experiment_tls.py from your local machine if you no longer need them.

That's a Wrap!

That's a Wrap!

Nice work! You just deployed a production-grade MLflow tracking server on AWS that mirrors how real ML teams manage experiments.

You've learned how to:

  • Deployed an MLflow tracking server on EC2 with a production architecture separating RDS PostgreSQL for metadata and S3 for artifact storage.
  • Secured the server with an Nginx reverse proxy and HTTP Basic Authentication, following the same pattern used across production web services.
  • Logged a complete ML experiment from your local machine to the remote server, including parameters, metrics, and a trained model artifact.
  • Secret Mission: Added HTTPS encryption with a self-signed TLS certificate so your MLflow server communicates securely.

Ready to quiz yourself?