Problem
A machine learning model that runs only on the developer's machine is not deployable. It depends on a specific Python version, a specific set of installed libraries, and a specific directory structure — none of which another person, server, or cloud environment is guaranteed to have. The traditional answer is documentation and hope: "install these packages, make sure your numpy matches, good luck." Docker eliminates that entirely. The challenge in this project was learning how to package not just a trained model but an entire ML inference stack — training environment, model artifacts, and FastAPI service — into reproducible containers that anyone can pull and run with a single command, anywhere.
Solution
This project trained a Random Forest classifier on the Titanic dataset inside a Jupyter Docker container, resolved a numpy/pandas binary incompatibility that crashed the serving layer, and packaged the FastAPI prediction API into a Docker image published to Docker Hub. The result is a fully containerized, publicly pullable ML service: docker pull thetechlearner/titanic-survival-api:v1.0 runs a production-ready API that accepts passenger features and returns survival probabilities with structured confidence levels — no local Python environment required.
Skills Acquired
- Docker — the core tool of the project. Used to containerize both the training environment (Jupyter + scikit-learn) and the serving environment (FastAPI + model artifacts). Every artifact produced in this project is reproducible via Docker commands.
- Docker Hub — the public container registry where the final image was pushed and tagged as both
v1.0andlatest. Anyone can pullthetechlearner/titanic-survival-apiand run it locally without a Python environment. - FastAPI — the serving framework wrapping the trained model. Exposes four endpoints with auto-generated Swagger documentation at
/docs. - scikit-learn — used to train the Random Forest classifier on the Titanic dataset. Feature engineering (age groups, fare levels, family size) was applied at both training and inference time to ensure consistent representations.
- Dependency management — diagnosed and resolved a numpy/pandas binary incompatibility ("numpy.dtype size changed, may indicate binary incompatibility") by pinning
numpy==1.24.3alongsidepandas==2.0.3inrequirements.txt. - Docker image tagging — learned the distinction between tagging with a username (
thetechlearner/) versus a local alias, and how registry authorization works when pushing to Docker Hub. - MLOps workflow — experienced the full containerization pipeline: train → serialize → serve → containerize → push → document.
Deep Dive
There is a well-known gap between "it works on my machine" and "it works in production." Docker exists to close that gap. Before this project, I understood containerization conceptually — after it, I had shipped a containerized ML service to a public registry that anyone in the world can pull and run.
This project built a two-container ML system: one container for training (Jupyter + scikit-learn on the Titanic dataset), and a second, leaner container for serving predictions via FastAPI. The serving container was pushed to Docker Hub as thetechlearner/titanic-survival-api:v1.0 — a publicly verifiable, reviewer-approved deployment artifact.
Why This Project?
This was the Docker and Containerization sprint of my TripleTen AI and Machine Learning Bootcamp. The assignment: train a model from scratch inside a Docker container, build a serving API, containerize it, and publish the image to Docker Hub for others to pull and run. The Titanic survival dataset was the chosen domain — a classic binary classification problem that put the focus on the containerization workflow rather than model complexity.
I treated every step as a real MLOps exercise — not just making things run but making them reproducible. The numpy/pandas incompatibility that crashed the first serving attempt was a real production-class dependency management problem, not a notebook error.
What You'll Learn from This
- Why Docker containers solve the "works on my machine" problem for ML systems — and what "reproducible environment" actually means in practice
- How to structure a two-container ML project: training environment separate from serving environment
- Why pinning numpy alongside pandas matters — and how binary incompatibilities between compiled packages manifest at runtime
- The correct workflow for tagging and pushing Docker images to Docker Hub, including why the username prefix must match your registry account
- How FastAPI's auto-generated Swagger UI gives you a live, testable API explorer with zero additional documentation work
- What a reviewer looks for when validating a containerized ML deployment — from image pullability to endpoint contract compliance
The Service
The API predicts Titanic passenger survival from 7 features: passenger class, sex, age, siblings/spouses aboard, parents/children aboard, fare, and embarkation port. The trained Random Forest model applies feature engineering at inference time — computing age group, fare level, family size, and travel-alone status — before making a prediction.
| Endpoint | Method | Purpose |
|---|---|---|
/ | GET | Health check — confirms API is running |
/predict | POST | Single passenger prediction with survival probability |
/predict-batch | POST | Multiple passenger predictions in one request |
/model-info | GET | Model algorithm, accuracy, and feature metadata |
/docs | GET | Auto-generated Swagger UI (FastAPI built-in) |
The prediction response includes not just survived and survival_probability but also a prediction_confidence label (High / Medium / Low) and a full passenger_profile object — showing the feature-engineered representation the model actually used.
# Live test result — Class 1 adult female curl -X POST "http://localhost:8000/predict" \ -H "Content-Type: application/json" \ -d '{"Pclass": 1, "Sex": "female", "Age": 30.0, "SibSp": 0, "Parch": 0, "Fare": 50.0, "Embarked": "S"}' # Response { "survived": 1, "survival_probability": 0.9152319739084446, "prediction_confidence": "High", "passenger_profile": { "class": "Class 1", "gender": "female", "age_group": "Adult", "family_size": 1, "traveling_alone": true, "fare_level": "High", "embarkation_port": "Southampton" } }
How I Built It
Phase 1
Training Environment — Jupyter in Docker
The model was trained inside a jupyter/base-notebook Docker container mounted to the local project directory. Running docker exec titanic-trainer python train_model.py executed the full training pipeline — data loading, feature engineering (5 new features: FamilySize, IsAlone, Title, AgeGroup, FareGroup), label encoding, Random Forest fitting, and artifact serialization — producing titanic_model.joblib, label_encoders.joblib, and model_metadata.json in a models/ directory.
The first training attempt failed with a FileNotFoundError because the models/ directory did not exist inside the container. Fix: docker exec titanic-trainer mkdir -p models. A minor but realistic production detail — containers start from a clean image with no persistent directories.
Phase 2
Dependency Debugging — numpy/pandas Incompatibility
The first serving container crashed immediately with:
ValueError: numpy.dtype size changed, may indicate binary incompatibility. Expected 96 from C header, got 88 from PyObject
The root cause: numpy was not pinned in requirements.txt, so pip resolved it to a version compiled against a different ABI than pandas expected. The fix was adding numpy==1.24.3 — the version compatible with pandas==2.0.3. Without an explicit pin, pip can resolve any numpy version, and the compiled C extensions between packages can silently break.
# requirements.txt — before (missing numpy pin) fastapi==0.104.1 uvicorn==0.24.0 pandas==2.0.3 scikit-learn==1.3.0 joblib==1.3.1 python-multipart==0.0.6 # requirements.txt — after (numpy pinned to compatible version) fastapi==0.104.1 uvicorn==0.24.0 numpy==1.24.3 pandas==2.0.3 scikit-learn==1.3.0 joblib==1.3.1 python-multipart==0.0.6
Phase 3
Publishing to Docker Hub
The serving image was tagged and pushed to Docker Hub under the correct account username. An initial push to vietnguyen/titanic-survival-api failed because the Docker Hub account is thetechlearner — the image tag must match the registry account. After retagging and creating the repository on Docker Hub first, both v1.0 and latest pushed successfully.
# Retag with correct Docker Hub username docker tag titanic-survival-api thetechlearner/titanic-survival-api:v1.0 docker tag titanic-survival-api thetechlearner/titanic-survival-api:latest # Push both tags docker push thetechlearner/titanic-survival-api:v1.0 docker push thetechlearner/titanic-survival-api:latest # Anyone can now pull and run the API docker pull thetechlearner/titanic-survival-api:v1.0 docker run -p 8000:8000 thetechlearner/titanic-survival-api:v1.0
Model Performance
The Random Forest (100 estimators, max_depth=10) was trained on 712 samples and evaluated on 179 held-out samples using a stratified 80/20 split.
| Metric | Value | Notes |
|---|---|---|
| Algorithm | RandomForestClassifier | 100 estimators, max_depth=10 |
| Training set | 712 passengers | stratified split, random_state=42 |
| Test set | 179 passengers | 20% held out |
| Test Accuracy | 81.56% | accuracy_score on test set |
| Precision (survived) | 0.79 | class 1, 69 support |
| Recall (survived) | 0.71 | caught 71% of actual survivors |
| F1 (survived) | 0.75 | harmonic mean of precision/recall |
| Live Test (Class 1, female, 30) | 91.5% survival probability | High confidence |
Top 10 feature importances from the trained model:
| Rank | Feature | Importance | Type |
|---|---|---|---|
| 1 | Sex | 28.1% | raw input |
| 2 | Fare | 17.1% | raw input |
| 3 | Title | 12.7% | engineered from Name |
| 4 | Age | 12.3% | raw input (median-imputed) |
| 5 | Pclass | 9.2% | raw input |
| 6 | FamilySize | 5.1% | SibSp + Parch + 1 |
| 7 | FareGroup | 4.3% | quartile bucketing of Fare |
| 8 | SibSp | 3.0% | raw input |
| 9 | Embarked | 2.7% | raw input |
| 10 | AgeGroup | 2.3% | age bucket (Child/Teen/Adult/Middle/Senior) |
Reviewer Approval
The project received full approval from TripleTen reviewer Victor Camargo. Every checkpoint was verified against the official evaluation criteria.
| Checkpoint | Status |
|---|---|
| Image reference is usable and well-formed | APPROVED ✓ |
| Container starts successfully | APPROVED ✓ |
| Runtime logs look healthy | APPROVED ✓ |
| GET / endpoint | APPROVED ✓ |
| POST /predict endpoint | APPROVED ✓ |
| POST /predict-batch endpoint | APPROVED ✓ |
| GET /model-info endpoint | APPROVED ✓ |
| Image can be pulled and run locally | APPROVED ✓ |
| API endpoints respond per expected contract | APPROVED ✓ |
| Documentation and usability sufficient | APPROVED ✓ |
Key Takeaways
- Two-container architecture — training and serving are separate concerns, each with their own Dockerfile and dependencies
- Dependency pinning is non-negotiable — missing a numpy pin caused a binary incompatibility that would fail silently in a notebook but crashes a container immediately
- Registry account must match image tag —
vietnguyen/andthetechlearner/are different namespaces; the image tag must match the Docker Hub account you're authenticated to - Create the Docker Hub repo before pushing — the registry won't auto-create a repository on push; it must exist first
- Feature engineering at inference time — all 5 engineered features (FamilySize, IsAlone, Title, AgeGroup, FareGroup) are computed inside the container, ensuring consistency with training regardless of caller inputs
- Full reviewer validation — all 10 checkpoints approved by TripleTen reviewer, confirming the deployment artifact meets production standards
What I Learned & Why It Matters to Employers
Before this sprint, I understood Docker conceptually — images, containers, layers. After it, I had debugged a real binary dependency incompatibility, navigated Docker Hub authentication, and pushed a publicly pullable ML service to a container registry. The distinction matters in interviews: I can talk about containerization from the inside, not just from the documentation. Any ML Engineer role that deploys models to production uses Docker or Kubernetes — and the ability to go from training notebook to versioned, publicly runnable image is exactly the MLOps skill that bridges data science and engineering.
Conclusion & Reflections
The most durable lesson from this sprint: containerization is not a deployment detail — it is a reproducibility guarantee. Every time someone runs docker pull thetechlearner/titanic-survival-api:v1.0, they get the exact same Python version, the exact same library versions, and the exact same model weights that the reviewer validated. No setup instructions. No "works on my machine."
That guarantee is what makes containers the standard unit of deployment for ML systems in production. It is also what makes a containerized portfolio project more credible than a GitHub notebook: the artifact is verifiable, pullable, and runnable by anyone with Docker installed — including a technical interviewer.
| Project Requirement | Status |
|---|---|
| Model trained inside a Docker container | YES — docker exec titanic-trainer python train_model.py ✓ |
| Serving API containerized separately | YES — titanic-survival-api image ✓ |
| Dependency incompatibility resolved | YES — numpy pinned to 1.24.3 ✓ |
| Image published to Docker Hub | YES — thetechlearner/titanic-survival-api:v1.0 ✓ |
| All 4 API endpoints implemented | YES — /, /predict, /predict-batch, /model-info ✓ |
| Full reviewer approval | YES — 10/10 checkpoints approved ✓ |
Want to Pull and Run the API?
The Docker image is publicly available on Docker Hub — pull it and test it yourself.