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
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:
- Version confusion —
model_v2_final_v3.pklin one folder,model_v2_FINAL_really.pklin another. - No lineage — you can't trace which dataset, hyperparameters, or code commit produced an artifact.
- Collateral overwrites — a teammate saves their run over your best checkpoint, and the metrics are gone.
- Deployment drift — the artifact in staging differs from the one in production because files were copied ad hoc.
- 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:
- Set up the project structure — separate your API, storage, and Docker config.
- Define the metadata schema — decide what fields matter: model name, version, metrics, params, created_at, stage.
- Implement the API — use a lightweight framework like Flask or FastAPI to expose endpoints.
- Store artifacts persistently — mount a Docker volume so files survive container restarts.
- Dockerize the registry — write a
Dockerfilethat installs dependencies and runs the server. - 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 logsfor 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:/datais correct and that you're writing inside/datain 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_sizeif behind nginx. - Concurrent writes to
db.jsoncorrupt 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_pathis 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_productionendpoint returns the last item in the list, but if you don't call_write_dbafter mutation, changes are lost. Double-check persistence. - Port already in use — run
docker compose downto free ports or change the host port mapping to8001: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
- Replace the file-based metadata store with a PostgreSQL database and store artifacts in S3-compatible object storage for production use.
- Use MLflow's model registry directly as a drop-in, adding a UI and experiment tracking without building your own API.
- 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.