Model Registry with Docker

Learn to build a model registry with Docker in this Applied AI engineering tutorial. Step-by-step guidance, hands-on exercises, and troubleshooting tips for developers.

Focus: build a model registry with docker

Sponsored

You've trained a model that nails your validation set, but now a colleague asks which version is running in production — and you can't answer. Worse, you've overwritten the only save file with a slightly different experiment, and your evaluation results no longer match the artifact. This chaos is the norm when model files live on shared drives and in chat messages. Building a model registry with Docker gives you a single, versioned, auditable source of truth for every model you produce — and it's the missing piece between your training code and your deployed service.

The problem this lesson solves

Without a registry, model management breaks down in a few predictable ways:

  1. Version confusionmodel_v2_final_v3.pkl in one folder, model_v2_FINAL_really.pkl in another.
  2. No lineage — you can't trace which dataset, hyperparameters, or code commit produced an artifact.
  3. Collateral overwrites — a teammate saves their run over your best checkpoint, and the metrics are gone.
  4. Deployment drift — the artifact in staging differs from the one in production because files were copied ad hoc.
  5. No rollback path — when a new model underperforms, you have no quick way to promote the previous champion.

The pain point: your model is only as trustworthy as your ability to reproduce and audit it. If you can't answer "which artifact is canonical?" in under a minute, you're flying blind.

A model registry solves this by giving every artifact a unique ID, requiring metadata (metrics, parameters, dataset hash), and enforcing a lifecycle stage (staging, production, archived). Docker makes the registry portable, reproducible, and deployable anywhere — your laptop, a GPU server, or a cloud cluster.

Core concept / mental model

Think of a model registry as a bank vault for AI artifacts. Training runs deposit model files like cash; the vault stamps each with a serial number (version ID), records the transaction (metadata), and tracks whether the cash is in circulation (staging), at the teller (production), or retired (archived). Docker is the vault building — a consistent environment that runs the vault software (the registry server) and its storage.

In code, a minimal registry is a service with three pieces:

  • Storage — a directory or bucket where model binary files (.pkl, .onnx, .pt) live.
  • Metadata store — a database (or JSON file) mapping IDs to metrics, params, timestamps, and stage.
  • API — HTTP endpoints to register, fetch, list, and stage models.

Here's a mental picture:

Training script ──> POST /models ──> Registry (Docker container)
     │                                  │
     └── sends: file + metadata         ├── ./artifacts/  (files)
                                        └── ./db.json     (metadata)

Deployment ──> GET /models/production  ──> returns artifact + info

Every model gets a version number (auto-incremented) and a stage — you can transition a version from staging to production with one call.

How it works step by step

Building your own Dockerized registry is about wiring those three pieces together. Here's the flow:

  1. Set up the project structure — separate your API, storage, and Docker config.
  2. Define the metadata schema — decide what fields matter: model name, version, metrics, params, created_at, stage.
  3. Implement the API — use a lightweight framework like Flask or FastAPI to expose endpoints.
  4. Store artifacts persistently — mount a Docker volume so files survive container restarts.
  5. Dockerize the registry — write a Dockerfile that installs dependencies and runs the server.
  6. Run and interact — start the container, register models from training scripts, and consume them in deployments.

The key design decision is whether to store artifacts and metadata in simple files or in a database. For learning and small teams, a file-based approach keeps it simple; for production, you'd use PostgreSQL and blob storage. Docker abstracts away the host differences — that's the magic.

Hands-on walkthrough

Let's build a minimal but functional model registry with Docker. You'll need Docker installed and Python 3.10+ on your host for the client script.

Step 1: Project structure

model-registry/
├── app.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
└── storage/          # mounted as volume
    └── artifacts/

Step 2: The registry API (FastAPI)

app.py — the core service.

import json
import os
import shutil
import uuid
from datetime import datetime, timezone
from pathlib import Path
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request, Query
from fastapi.responses import FileResponse, JSONResponse

ARTIFACT_DIR = Path("/data/artifacts")
DB_PATH = Path("/data/db.json")

app = FastAPI(title="Model Registry")


def _init_db():
    if not DB_PATH.exists():
        DB_PATH.write_text(json.dumps({"models": []}))
        ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)


def _read_db():
    return json.loads(DB_PATH.read_text())


def _write_db(db):
    DB_PATH.write_text(json.dumps(db, indent=2))


@app.on_event("startup")
def startup():
    _init_db()


@app.post("/models/{name}/versions", status_code=201)
async def register_model(name: str, file: UploadFile = File(...), accuracy: float = Form(...), params: str = Form("")):
    """Register a new model version."""
    db = _read_db()
    version = len(db["models"]) + 1
    model_id = str(uuid.uuid4())

    # Save artifact
    ext = Path(file.filename).suffix
    artifact_name = f"{name}_v{version}{ext}"
    artifact_path = ARTIFACT_DIR / artifact_name
    with artifact_path.open("wb") as f:
        shutil.copyfileobj(file.file, f)

    # Record metadata
    entry = {
        "model_id": model_id,
        "name": name,
        "version": version,
        "accuracy": accuracy,
        "params": json.loads(params) if params else {},
        "stage": "staging",
        "created_at": datetime.now(timezone.utc).isoformat(),
        "artifact_path": str(artifact_path),
    }
    db["models"].append(entry)
    _write_db(db)
    return entry


@app.get("/models/{name}/versions")
def list_versions(name: str):
    db = _read_db()
    return [m for m in db["models"] if m["name"] == name]


@app.get("/models/{name}/versions/{version}/download")
def download_model(name: str, version: int):
    db = _read_db()
    entry = next((m for m in db["models"] if m["name"] == name and m["version"] == version), None)
    if not entry:
        raise HTTPException(status_code=404, detail="Version not found")
    return FileResponse(entry["artifact_path"], filename=f"{name}_v{version}.pkl")


@app.post("/models/{name}/versions/{version}/transition")
def transition_stage(name: str, version: int, stage: str = Query(..., enum=["staging", "production", "archived"])):
    db = _read_db()
    for m in db["models"]:
        if m["name"] == name and m["version"] == version:
            m["stage"] = stage
            _write_db(db)
            return m
    raise HTTPException(status_code=404, detail="Version not found")


@app.get("/models/{name}/production")
def get_production(name: str):
    db = _read_db()
    prod = [m for m in db["models"] if m["name"] == name and m["stage"] == "production"]
    if not prod:
        raise HTTPException(status_code=404, detail="No production version")
    return prod[-1]  # last promoted wins

Step 3: Dockerize

Dockerfile:

FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN mkdir -p /data/artifacts && touch /data/db.json
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

requirements.txt:

fastapi==0.115.0
uvicorn[standard]==0.30.6
python-multipart==0.0.9

docker-compose.yml — persistent volume:

version: "3.9"
services:
  registry:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - ./storage:/data

Step 4: Run and test

Build and start the service:

cd model-registry
docker compose up --build

In another terminal, use a Python client to register a model:

import requests

# Simulate a trained model file
with open("my_model.pkl", "wb") as f:
    f.write(b"\x80\x04\x95...important artifact...")

files = {'file': ('model.pkl', open('my_model.pkl', 'rb'), 'application/octet-stream')}
data = {'accuracy': '0.94', 'params': '{"hidden":128,"lr":0.01}'}
r = requests.post("http://localhost:8000/models/spam-detector/versions", files=files, data=data)
print(r.json())

Expected output (IDs will vary):

{"model_id":"63e7...","name":"spam-detector","version":1,"accuracy":0.94,"params":{"hidden":128,"lr":0.01},"stage":"staging","created_at":"2025-01-01T12:34:56+00:00","artifact_path":"/data/artifacts/spam-detector_v1.pkl"}

Now promote it to production:

curl -X POST "http://localhost:8000/models/spam-detector/versions/1/transition?stage=production"

Then fetch it in a deployment script:

# fetch_production.py
import requests

info = requests.get(
    "http://localhost:8000/models/spam-detector/production"
).json()
print(f"Downloading version {info['version']} (acc={info['accuracy']})")
art = requests.get(
    f"http://localhost:8000/models/spam-detector/versions/{info['version']}/download"
)
with open("deployed_model.pkl", "wb") as f:
    f.write(art.content)

Pro tip: Always store the full training metadata — dataset hash, git commit, hyperparameters — not just accuracy. That turns your registry into a lineage tracker.

Compare options / when to choose what

You might not need to build your own registry. Here's a comparison:

Option Pros Cons Best for
Self-built (this lesson) Full control, educational, fits custom workflows You maintain it, lacks advanced features Learning, internal experiments, small teams
MLflow Mature, open-source, UI, experiment tracking built-in Heavier, requires separate tracking server Teams wanting a production-ready registry without locking in
DVC (Data Version Control) Tracks files in Git, good for data pipelines Not a dedicated registry, no lifecycle API Reproducible research, data-centric projects
Cloud services (SageMaker, Vertex AI) Managed, integrated with training Vendor lock-in, cost Enterprises with cloud AI platforms

When to choose self-built: you need to understand the internals, have unusual storage needs, or want a lightweight registry for a single project. When to choose MLflow: you need a UI, experiment tracking, and multi-user support without building from scratch.

Troubleshooting & edge cases

  • Container exits immediately — check docker compose logs for missing module or port bind. Ensure you're not binding port 8000 on the host already.
  • Data lost on container restart — verify the volume mount ./storage:/data is correct and that you're writing inside /data in the API. Never write to the container's local filesystem.
  • File upload fails with 413 — FastAPI doesn't cap by default, but proxies might. Increase client_max_body_size if behind nginx.
  • Concurrent writes to db.json corrupt it — use SQLite or PostgreSQL. For a single-user learning registry, it's fine, but wrap writes in a thread lock or use a proper DB.
  • Model file is empty when downloaded — check that artifact_path is correct and the file wasn't overwritten by another version with the same name. Always use a unique ID plus version in the filename.
  • Promotion doesn't take effect — the get_production endpoint returns the last item in the list, but if you don't call _write_db after mutation, changes are lost. Double-check persistence.
  • Port already in use — run docker compose down to free ports or change the host port mapping to 8001:8000.

Pro tip: Use a health-check endpoint (/health) to confirm the registry is up before running automated scripts.

What you learned & what's next

You now understand the core idea behind a model registry with Docker: a versioned, staged, metadata-rich artifact store wrapped in a portable container. You completed a practical exercise where you built a FastAPI registry, containerized it, registered a model, promoted it to production, and downloaded it — exactly the workflow you'd use for real deployments.

You also learned how to compare self-built registries versus frameworks like MLflow, and how to troubleshoot common Docker and persistence issues.

Next lesson — you'll explore model serving with ONNX Runtime and learn how to deploy the artifacts from your registry into a low-latency inference endpoint. That's where the registry pays off: you'll pull the exact production version and serve it consistently.

Keep this registry as your baseline — and remember, the discipline of versioning is more valuable than the tool itself.

Practice recap

Create a second model version (simulate re-training) with different parameters and promote it to production. Then write a script that fetches the production model and prints its accuracy — this verifies your registry correctly returns the latest champion. Bonus: implement a /health endpoint and add it to your troubleshooting toolset.

Common mistakes

  • Not persisting storage: writing artifacts inside the container's filesystem loses everything on docker compose down — always mount a volume.
  • Using a JSON file for metadata under concurrent load → corrupted reads/writes. Use SQLite or PostgreSQL if more than one process writes.
  • Ignoring metadata such as dataset hash and git commit → you can't reproduce results later.
  • Overwriting artifact files when registering a new version → always include version in the filename to keep history intact.

Variations

  1. Replace the file-based metadata store with a PostgreSQL database and store artifacts in S3-compatible object storage for production use.
  2. Use MLflow's model registry directly as a drop-in, adding a UI and experiment tracking without building your own API.
  3. Add an S3 gateway or a blob storage abstraction so the registry works on-prem or in any cloud with minimal changes.

Real-world use cases

  • An ML team registers every trained model with its evaluation metrics, then CI/CD promotes the best version to staging for manual QA review.
  • A fraud-detection service queries the registry's production endpoint at startup to load the latest champion model, enabling instant rollback if accuracy degrades.
  • A data scientist uses the registry to share experiment artifacts across a distributed team, avoiding the 'model_v2_final' chaos in shared drives.

Key takeaways

  • A model registry centralizes artifacts, metadata, and lifecycle stages, solving version and lineage problems.
  • Docker makes the registry portable and reproducible — containerize the API and mount persistent storage for artifacts.
  • Every artifact should be versioned with a unique ID and stored immutably; never overwrite existing files.
  • Metadata (metrics, parameters, dataset hash) is as important as the model binary — capture it at registration time.
  • Promote models through stages (staging → production) via a single API call, enabling controlled rollouts and rollbacks.
  • You can build a minimal registry with FastAPI and Docker in under 100 lines — start simple, then adopt mature tools like MLflow as needs grow.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.