MLFlow + KitOps: The Missing Link in ML Reproducibility
How to package your MLFlow experiments into tamper-proof, deployable artifacts MLFlow solves experiment tracking. KitOps solves artifact packaging. Together, they solve the real problem
The Problem With MLFlow Alone
MLFlow is excellent for tracking experiments. You log metrics, parameters, and models. But when deployment time comes, you’re stuck with:
A scattered artifact store with no versioning guarantees. Model files are separate from their dependencies. No cryptographic proof that what you trained is what you’re deploying—zero integration with OCI registries that your DevOps team actually uses.
KitOps fills this gap by packaging MLFlow outputs into ModelKits—tamper-proof, versioned containers that work with existing infrastructure.
The Architecture: How It Fits Together
Here’s the system architecture for the MLFlow + KitOps integration:
MLFlow Layer: Handles experiment tracking, model logging, and artifact storage. Your data scientists work here, logging models as they always have.
KitOps ModelKitManager: Acts as the bridge between MLFlow’s artifact location and OCI registries. It reads from MLFlow’s output directory, packages everything into a ModelKit, and pushes to a registry.
Registry Layer: Stores ModelKits in OCI-compatible registries like Jozu Hub, Docker Hub, or AWS ECR. Your deployment pipelines pull from here.
Deployment: Kubernetes, Docker, or any OCI-compatible runtime unpacks the ModelKit and deploys the model with all its dependencies intact.
The beauty? Data scientists keep using MLFlow. DevOps teams get OCI artifacts. Nobody changes their workflow.
The Code: Line-by-Line Breakdown
Let’s walk through exactly how to implement this integration. Every line explained.
Step 1: Import the KitOps Components
Python
from kitops.modelkit.manager import ModelKitManager
from kitops.modelkit.user import UserCredentials
from kitops.cli import kitLine 1: ModelKitManager is your main interface. It handles the entire lifecycle—packing, pushing, and managing ModelKits. Think of it as the orchestrator.
Line 2: UserCredentials manages authentication to OCI registries. It supports username/password, tokens, and environment variable injection for security.
Line 3: kit is the CLI interface exposed as a Python API? You’ll use kit.init() to create Kitfiles programmatically instead of manually writing YAML.
Step 2: Set Up Authentication
python
# A password can be read from Environment Variables or .env files: JOZU_PASSWORD=<secret password>
creds = UserCredentials(username=username, registry=”jozu.ml”)User Credentials Setup: Note that it only requires the username and password. The password? That’s pulled from environment variables (JOZU_PASSWORD) or .env files.
Why this matters: You never hardcode credentials. CI/CD pipelines inject JOZU_PASSWORD as a secret. Local development reads from .env. Production uses managed secrets.
Registry parameter: Can be any OCI registry—jozu.ml, docker.io, your private ECR endpoint. KitOps doesn’t lock you into specific infrastructure.
Step 3: Initialize the ModelKitManager
python
manager = ModelKitManager(
working_directory=artifact_location,
user_credentials=creds,
modelkit_tag=modelkit_tag
)working_directory=artifact_location: This points to MLFlow’s artifact storage location. Usually something like mlruns/0/run_id/artifacts/. The manager will scan this directory for model files, metadata, and dependencies.
user_credentials=creds: Injects the authentication we just configured. The manager will use these credentials when pushing to the registry.
modelkit_tag=modelkit_tag: Your version tag. Convention is semantic versioning like 1.0.0 or latest. This becomes part of the OCI artifact reference: jozu.ml/username/modelname:modelkit_tag.
Key insight: The manager doesn’t copy files around. It works directly with MLFlow’s artifact location, reducing storage overhead and avoiding duplication.
Step 4: Authenticate to Registry
python
manager.login()Simple but critical. This executes docker login equivalent using your credentials, establishing the session with the OCI registry.
Under the hood, it:
Validates credentials against the registry
Stores auth tokens temporarily
Returns immediately if already authenticated
Best practice: Call this once at the start of your pipeline, not for every ModelKit operation.
Step 5: Create the Kitfile
python
kit.init(
directory=artifact_location,
name=name,
description=”my cool project”,
author=username
)This is where KitOps creates the Kitfile—the manifest that describes what goes into your ModelKit.
directory=artifact_location: The manager scans this directory and auto-generates a Kitfile based on what it finds. Model weights, preprocessing scripts, requirements files—everything gets cataloged.
name=name: Your model’s identifier. Combined with username and tag, this creates the full reference: jozu.ml/username/name:tag.
description: Human-readable context. Shows up in registry UIs and helps teams understand what this ModelKit contains.
author=username: Attribution and access control. Registries use this for permissions and audit trails.
What gets added to the Kitfile automatically:
Model files from MLFlow artifacts
Any
requirements.txtorconda.yamlin the artifact locationPreprocessing code if present
Metadata from MLFlow run (parameters, metrics, tags)
The generated Kitfile looks like this:
yaml
manifestVersion: v1.0.0
package:
name: my-model
version: 1.0.0
description: my cool project
authors: username
model:
name: my-model
framework: sklearn # or tensorflow, pytorch - detected automatically
path: ./model
code:
- path: ./preprocessing.py
- path: ./requirements.txtStep 6: Pack and Push
python
manager.pack_and_push_modelkit(with_login_and_logout=False)This is the heavy lifting—packaging the ModelKit and pushing it to the registry.
pack_and_push_modelkit(): Combines two operations. First, it packs all files referenced in the Kitfile into an OCI artifact (same format as Docker images). Then it pushes this artifact to the registry specified in credentials.
with_login_and_logout=False: Since we already called manager.login(), we don’t need to authenticate again. Setting this to True would login, push, and logout in one operation—useful for one-off scripts but unnecessary here.
What happens during packing:
Files are compressed and layered (same as Docker layers)
Each layer gets a cryptographic hash (SHA256)
Metadata is attached (model framework, author, timestamp)
The complete artifact is signed for tamper detection
What happens during push:
Layers are uploaded to registry (with resume support for large files)
Manifest is registered with all hashes
Registry validates integrity
Tag is updated to point to new artifact
The pushed ModelKit is now available at: jozu.ml/username/name:modelkit_tag
The Complete Integration Pattern
Here’s how this fits into a complete MLFlow training pipeline:
python
import mlflow
from kitops.modelkit.manager import ModelKitManager
from kitops.modelkit.user import UserCredentials
from kitops.cli import kit
# Standard MLFlow experiment
mlflow.set_experiment(”my-experiment”)
with mlflow.start_run() as run:
# Your training code
model = train_model(data)
# Log to MLFlow as usual
mlflow.sklearn.log_model(model, “model”)
mlflow.log_params({”learning_rate”: 0.01})
mlflow.log_metrics({”accuracy”: 0.95})
# Get MLFlow’s artifact location
artifact_location = mlflow.get_artifact_uri()
# Now package into ModelKit
creds = UserCredentials(
username=”your-username”,
registry=”jozu.ml”
)
manager = ModelKitManager(
working_directory=artifact_location,
user_credentials=creds,
modelkit_tag=f”v{run.info.run_id[:8]}” # Use run ID as version
)
manager.login()
kit.init(
directory=artifact_location,
name=”my-model”,
description=f”Accuracy: 0.95, Run: {run.info.run_id}”,
author=”your-username”
)
manager.pack_and_push_modelkit(with_login_and_logout=False)
print(f”ModelKit pushed: jozu.ml/your-username/my-model:v{run.info.run_id[:8]}”)Why This Integration Matters
For Data Scientists: Keep using MLFlow exactly as before. The KitOps layer is transparent—you’re just adding 10 lines of code at the end of your training script.
For MLOps Engineers: Finally get tamper-proof artifacts in OCI registries. No more “did this model file get corrupted?” questions. Cryptographic hashes prove integrity.
For DevOps Teams: Deploy models using the same tools you use for containers. kubectl apply, Helm charts, FluxCD—everything that works with OCI artifacts now works with models.
For Organizations: Complete audit trail from experiment to deployment. Every ModelKit includes the MLFlow run ID, making it trivial to trace back to training conditions.
Production Considerations
Storage efficiency: ModelKits use content-addressable storage. If two models share preprocessing code, that code is stored once and referenced twice. Just like Docker layers.
Security: Set JOZU_PASSWORD via secrets management (Vault, AWS Secrets Manager). Never commit credentials. The UserCredentials class respects the security boundaries you establish.
Versioning strategy: Use semantic versioning for releases (1.0.0, 1.1.0) and MLFlow run IDs for experiments (v8a3f2d1). This gives you both stable releases and full traceability.
Registry choice: Jozu Hub is optimized for ML artifacts (better UIs for models, built-in model cards). But any OCI registry works—use what your organization already runs.
CI/CD integration: This code runs anywhere Python runs. Jenkins, GitLab CI, GitHub Actions, Airflow—just inject credentials as environment variables.
The Bottom Line
MLFlow tracks your experiments. KitOps makes them deployable.
The integration is intentionally minimal—you’re not replacing MLFlow’s workflow, you’re extending it. Ten lines of code turn experimental artifacts into production-ready ModelKits that work with your existing infrastructure.
Data scientists stay in notebooks. DevOps teams get OCI artifacts. Everyone speaks the same language: versioned, tamper-proof containers.
That’s the power of bridging experiment tracking with artifact management. One workflow, from training to deployment, with zero compromises.
Want to try this? Install with pip install kitops mlflow and point artifact_location at your MLFlow runs directory. The integration is production-ready and actively maintained.
Do you have questions about integrating this into your MLOps pipeline? Drop a comment—I’m working through these patterns in production right now.

