From Trained Model to Production API: A Practical Guide to Deploying Machine Learning Models with…
From Trained Model to Production API: A Practical Guide to Deploying Machine Learning Models with FastAPI, Docker, GitHub and Render
Training a machine learning model is only part of the job.
You can have a model with excellent validation accuracy, save it as a .keras, .h5, .pt, or .onnx file, and still have something that nobody can actually use.
A farmer shouldn’t need to open your Jupyter Notebook to classify a pest.
A mobile application shouldn’t need access to your training code.
A frontend application shouldn’t need to know how your neural network works internally.
This is where model deployment comes in.
In a recent workshop, I walked through the process of taking a trained pest-classification model and turning it into a usable inference service using:
- Python
- TensorFlow/Keras
- FastAPI
- Docker
- GitHub
- Render
- A frontend application
The goal was simple:
Take a trained model, wrap it in an API, containerize it, deploy it, and make it accessible to other applications.

This article puts the entire process together as a practical reference.
PS: If you were with us during the live session and you’re mainly here for the link to other resources, you can skip the walkthrough and jump straight to the Resources & References section at the end of this article. I’ve put everything there for you.
1. First, What Exactly Is Model Deployment?
Let’s start with the big picture.
Machine learning can be thought of as a process where we learn patterns from data and use those patterns to make predictions.
For example, suppose we want to build a pest classifier.
We might have images like:

Beetle
The model learns patterns from many labeled images. During training. After training, we save the learned model. That saved model is commonly called a model artifact. For example: model.h5, model.keras, model.pt, model.onnx
The important thing is that the model is now something that can be loaded later for inference.
2. Training, Validation and Inference Are Different
One concept that is important to understand before deployment is the difference between training, validation and inference.
Training
During training, the model learns patterns from training data.
Training data
↓
Model
↓
Learned parameters
Validation
During validation, we evaluate how well the learned model performs on data it has not seen during training.
Unseen validation data
↓
Model
↓
Prediction
↓
Compare with actual label
This helps us determine whether the model is good enough to use.
Inference
Inference happens when the trained model is used to make predictions on new data.
For example:
Farmer takes picture
↓
API
↓
Model
↓
Prediction: Caterpillar
This is what we ultimately want to expose to an application.
3. The Model Is Just an Artifact
After training, the model isn’t magically an application.
It is essentially a saved artifact containing the learned parameters and the structure required to perform inference.
For example:
Models/
└── Model_0001.h5
But having:
Model_0001.h5
doesn’t automatically give us a usable application.
We need something that can:
- Receive an input.
- Validate the input.
- Preprocess it correctly.
- Load the model.
- Run inference.
- Convert the model output into something understandable.
- Return the result.
This is where an API becomes useful.
4. Why Do We Need an API?
Imagine we have a website where a user uploads an image. The website needs somewhere to send that image. We don’t want the frontend to directly manipulate the Python model.
Instead, we can expose an endpoint such as:
POST /predict
The frontend sends an image:
Browser
↓
POST /predict
↓
FastAPI
↓
Model
↓
Prediction
↓
JSON response
For example:
{
"prediction": "caterpillar",
"confidence": 0.91
}
Now any application capable of making an HTTP request can use our model.
That application could be:
- A website
- A mobile application
- An IoT device
- Another backend
- A desktop application
The API becomes the bridge between the model and the rest of the world.
5. Deployment Doesn’t Have One Universal Answer
One thing I wanted participants to understand from the workshop is that deployment isn’t simply “put the model on the cloud.”

Before deploying, ask:
Where should inference run?
There are several possibilities.
Each option has trade-offs.
For example, if we are building a website, we could potentially run inference:
In the browser: The model is downloaded to the user’s browser and inference happens locally.
In the cloud: The browser sends the image to our API, and the model runs on a remote server.
Browser
|
| HTTP request
↓
Cloud API
|
↓
Model
|
↓
Prediction
There is also a hybrid approach where different parts of the system run in different places.
6. Cloud vs On-Device Inference
A simple way to think about the difference is:

Neither option is automatically better.
The correct choice depends on the application.
For our workshop, we chose cloud inference because the goal was to build a reusable API that different applications could consume.
7. If We Choose Cloud, How Should We Serve the Model?
Once you’ve decided to run inference in the cloud, another question appears:
How will the model be served?
There are several approaches.

Custom API
You build the inference service yourself using something like: FastAPI, Flask, Django.
You control the entire pipeline.
Request
↓
Validation
↓
Preprocessing
↓
Model
↓
Postprocessing
↓
Response
Model Server
You can use specialized model-serving infrastructure such as: TensorFlow Serving, NVIDIA Triton Inference Server. These systems are designed specifically for serving machine learning models.
Managed ML Endpoint
Cloud providers also offer managed machine-learning infrastructure, such as: Amazon SageMaker, Google Vertex AI, Azure Machine Learning. These can handle much of the infrastructure for you.
For this workshop, we intentionally chose the custom API approach because it allows us to understand what is actually happening underneath.
8. Our Architecture
The architecture for the project was:

The trained model already existed. Our job was to build everything around it.
9. Project Structure
The project looked approximately like this:
pest-classifier-api/
│
├── app/
│ └── main.py
│
├── Models/
│ └── Model_0001.h5
│
├── requirements.txt
├── Dockerfile
├── render.yaml
└── .gitignore
The important files have different responsibilities.
main.py: Contains the API and inference pipeline.
Models/: Contains the trained model artifact.
requirements.txt: Contains the Python dependencies required by the application.
Dockerfile: Defines how the application should be packaged into a Docker image.
render.yaml: Contains deployment configuration for Render.
.gitignore: Prevents files such as virtual environments and secrets from being pushed to GitHub.
10. The Model Contract

One of the most important ideas when deploying a model is the model contract.
The preprocessing used during inference must match what the model expects.
Suppose the model was trained using: 224 × 224 × 3
That means the input image needs to be transformed into the format the model expects before prediction.
For example:
Uploaded image
↓
Convert to RGB
↓
Resize to 224 × 224
↓
Convert to NumPy array
↓
Add batch dimension
↓
Model
If training preprocessing and inference preprocessing don’t match, the model can behave unexpectedly.
This is one of the easiest deployment mistakes to make.
11. Image Preprocessing

The API receives an uploaded image.
Conceptually, our preprocessing function does something like:
image = Image.open(...)
image = image.convert("RGB")
image = image.resize((224, 224))
array = np.asarray(image)
array = np.expand_dims(array, axis=0)
The important idea isn’t memorizing these lines. It’s understanding why they exist. The model was trained with a specific input format.
Therefore:
The inference pipeline must respect the model’s expected input contract.
12. Loading the Model

We also don’t want to load the model from disk every time somebody sends a request.
Imagine receiving:
Request 1 → Load model → Predict
Request 2 → Load model → Predict
Request 3 → Load model → Predict
Request 4 → Load model → Predict
That would be unnecessarily expensive. Instead, we load the model and keep it available in memory.
Conceptually:
Application starts
↓
Load model
↓
Keep model in memory
↓
Request → Predict
Request → Predict
Request → Predict
For the workshop’s model, caching the loaded model was sufficient because the model was small enough for this deployment setup.
13. The Prediction Endpoint
The core of our API is the prediction endpoint.
Conceptually:
POST /predict
The request contains an image.
The API then performs:
Validate file
↓
Read image
↓
Preprocess image
↓
Load/reuse model
↓
Run prediction
↓
Find predicted class
↓
Calculate confidence
↓
Return JSON
The endpoint can therefore be represented as:
Image
↓
Preprocessing
↓
Tensor
↓
Model
↓
Probabilities
↓
Class index
↓
Class name
↓
JSON response
14. Understanding Classification Output
The model doesn’t necessarily return “caterpillar” directly.
For a multi-class classifier, the model can return a probability for each class.
For example:
ants → 0.05
bees → 0.10
B2 → 0.03
caterpillar → 0.78
The class with the highest probability becomes the predicted class. This is where argmax is useful.
Conceptually:
prediction_index = np.argmax(probabilities)
Then we use that index to retrieve the corresponding class name.
For example:
0 → ants
1 → bees
2 → B2
3 → caterpillar
So if:
prediction_index = 3
we map that back to “caterpillar”
The probability associated with that class can also be used as the confidence score.
15. Don’t Forget Postprocessing
The model’s raw output isn’t always what we want to show to a user.
Our API can transform it into something more useful.
For example:
{
"prediction": "caterpillar",
"confidence": 0.78,
"model_version": "1.0"
}
We can also return additional information if our application requires it.
In our example, we experimented with adding a short Wikipedia-based summary for the predicted class.
The important principle is:
The API doesn’t only run the model. It turns model output into something useful for an application.
16. Testing Locally
Before deploying anything, test locally.
This is extremely important.
You want to catch problems before they become deployment problems.
Start the FastAPI application locally and open:
http://localhost:8000/docs
FastAPI automatically provides an interactive Swagger interface.
You can upload an image directly from the browser and test the /predict endpoint.
This gives us:
Browser
↓
localhost:8000
↓
FastAPI
↓
Model
If the API doesn’t work locally, putting it on a cloud platform won’t magically fix it.
17. Docker: Why Do We Need It?
At this point, the API works on our computer.
But there is another problem.
Your computer has a particular:
- Python version
- TensorFlow version
- Operating system
- Installed packages
- Environment configuration
Another computer may have something different.
Docker helps us package the application and its environment into a container.
The idea is:
“Take my application and package everything it needs to run in a reproducible environment.”
18. Understanding the Dockerfile

Our Dockerfile is approximately:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
COPY Models ./Models
EXPOSE 8000
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]
Let’s break it down.
- Choose the Python runtime
FROM python:3.11-slim
We start from a lightweight Python image.
- Create the application directory
WORKDIR /app
The application will run from /app.
- Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
We copy the dependency list and install it.
- Copy the API
COPY app ./app
This puts our FastAPI code inside the container.
- Copy the model
COPY Models ./Models
This puts the trained model inside the container as well.
- Document the port
EXPOSE 8000
This tells us which port the application uses.
- Start the server
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]
This starts Uvicorn and runs the FastAPI application.
19. So Where Is the Model?
This is a question worth answering clearly.
If we have:
Models/
└── Model_0001.h5
and our Dockerfile contains:
COPY Models ./Models
then the model becomes part of the Docker image.
Conceptually:
Repository
│
│ docker build
▼
Docker Image
│
▼
/app/Models/Model_0001.h5
│
▼
FastAPI loads model
For this workshop, we deliberately bundled the model inside the image because the model was relatively small and it made the deployment simple and reproducible.
For larger models, you may want to consider alternatives such as object storage or a model registry.
20. Build and Run the Container Locally
We can build the image with:
docker build -t pest-classifier-api:v1 .
Then run it:
docker run --rm -p 8000:8000 pest-classifier-api:v1
Or, if using Docker Compose:
docker compose up --build
Then open:
http://localhost:8000/docs
Notice something important.
The endpoint didn’t change.
It is still:
/predict
The difference is that our application is now running inside a container.
21. Understanding Port Mapping
This command:
docker run --rm -p 8000:8000 pest-classifier-api:v1
can be understood as:
8000:8000
│ │
│ └── Container port
└──────── Host port
So when we visit:
localhost:8000
Docker forwards the request to port 8000 inside the container.
22. Push the Project to GitHub
Once the project works locally, we can put it in a GitHub repository.
Before pushing, make sure you have a .gitignore.
For example:
.venv/
__pycache__/
.env
We don’t want to accidentally push:
- Virtual environments
- Python cache files
- Secrets
- Environment variables containing credentials
Then we can initialize the repository:
git init
git add .
git commit -m "Build pest classifier inference API"
git branch -M main
git remote add origin <repository-url>
git push -u origin main
The repository becomes the source from which our deployment platform can build the application.
23. GitHub → Render → Public API
Now the deployment pipeline looks like this:
Local Project
↓
GitHub
↓
Render
↓
Read Dockerfile
↓
Build Docker Image
↓
Start Container
↓
Public HTTPS URL
This is the point where our local application becomes an internet-accessible service.
24. Deploying to Render
For this project, we use Render as the hosting platform.
The deployment process is roughly:
- Open Render.
- Create a new service/Blueprint.
- Connect your GitHub account.
- Select the API repository.
- Configure the service.
- Specify the Docker runtime.
- Start the deployment.
- Monitor the build logs.
- Wait for the container to start.
- Open the generated public URL.
The important thing is that Render isn’t just copying our Python files somewhere.
It reads our deployment configuration and Dockerfile, builds the image, and starts the resulting container.
25. Render Configuration
A simplified render.yaml can look like:
services:
- type: web
name: pest-classifier-api
runtime: docker
plan: free
dockerfilePath: ./Dockerfile
dockerContext: .
The important configuration pieces include:
Docker runtime: runtime: docker Render knows that we’re deploying a Docker-based service.
Dockerfile path: dockerfilePath: ./Dockerfile This tells Render where the Dockerfile is.
Docker context: dockerContext: .This tells Docker which directory should be used as the build context.
26. The PORT Variable
One thing to be aware of when deploying to platforms such as Render is the application port.
Locally, we may think: 8000
But cloud platforms can provide the port dynamically through an environment variable such as: PORT
That’s why our Docker command uses:
${PORT:-8000}
This means:
Use the platform-provided PORT if it exists; otherwise use 8000.
This small detail can save you from confusing deployment failures.
27. Verify the Live API
Once deployment finishes, Render provides a public URL.
For example:
https://<service>.onrender.com
Our endpoints then become:
https://<service>.onrender.com/health
https://<service>.onrender.com/docs
https://<service>.onrender.com/predict
The /docs endpoint is especially useful because FastAPI gives us an interactive interface for testing the deployed API.
28. Testing the Production Endpoint
We can test the health endpoint:
curl.exe https://<service>.onrender.com/health
And we can send an image for prediction:
curl.exe -X POST `
"https://<service>.onrender.com/predict" `
-F "file=@C:\images\pest.jpg"
The important part is that the request is no longer going to:
localhost
It is going over the internet to our deployed service.
The architecture is now:
Your Laptop
│
│ Internet
▼
Render API
│
▼
Docker Container
│
▼
FastAPI
│
▼
Model
And the prediction travels back to the client.
29. Connecting the API to a Frontend
Now comes one of my favorite parts.
We have an API. But how does a website actually use it?
The frontend doesn’t need to know anything about TensorFlow.
It only needs to know the API URL and the endpoint.
For example, we can configure:
VITE_API_URL=https://<service>.onrender.com
Then the frontend can send an image:
const formData = new FormData();
formData.append("file", image);
const response = await fetch(`${API_URL}/predict`, {
method: "POST",
body: formData,
});
That’s it.
The frontend sends the image.
The API handles:
Image
↓
Validation
↓
Preprocessing
↓
Model
↓
Prediction
and returns the result.
30. The Big Idea: The Frontend Doesn’t Need the Model
This is one of the most important architectural ideas in the entire workshop.
We don’t need to put:
- TensorFlow
- Model
- Preprocessing code
- Inference logic
inside our React application.
Instead:
Internet
│
▼
┌─────────────┐ ┌────────────────┐
│ Frontend │ ─────► │ FastAPI │
│ React │ │ + Model │
└─────────────┘ └────────────────┘
The frontend only knows:
POST /predict
This separation makes the system easier to maintain and allows other clients to use the same API.
31. Bonus: API-Based Inference vs Browser-Based Inference

There is another interesting deployment strategy worth mentioning.
Instead of sending the image to a cloud API, we can potentially run a compatible model directly inside the browser.
That gives us two architectures.
API-based inference
Browser
↓
Internet
↓
FastAPI
↓
Model
↓
Prediction
Browser-based inference
Browser
↓
JavaScript inference runtime
↓
Model
↓
Prediction
Browser-based inference can reduce network round trips and can allow data to remain in the browser.
However, the model usually needs to be converted into a browser-compatible format/runtime.
For example, depending on the model and tooling, technologies such as TensorFlow.js, LiteRT.js, or ONNX Runtime Web may be relevant.
This was not the main deployment path for the workshop, but it is an important alternative to know about.
32. Which Deployment Strategy Should You Choose?
There isn’t one answer that works for every project.
A useful way to think about it is:
Where should inference happen?
↓
Cloud / Browser / Mobile / Edge
↓
If Cloud:
How should it be served?
↓
Custom API / Model Server / Managed Endpoint
↓
Where should the model artifact live?
↓
Container / Object Storage / Model Registry
Your choice depends on things like:
- Model size
- Latency requirements
- Privacy
- Internet availability
- Infrastructure requirements
- Number of users
- Cost
- Update frequency
- Hardware constraints
Deployment is therefore an engineering decision, not just a button you press.
33. A Complete Mental Model
If you remember nothing else from this article, remember this pipeline:
TRAINED MODEL
↓
MODEL ARTIFACT
↓
INFERENCE CODE
↓
FASTAPI
↓
LOCAL TESTING
↓
DOCKER
↓
GITHUB
↓
RENDER
↓
PUBLIC API
↓
FRONTEND / MOBILE / OTHER CLIENT
Each stage solves a different problem.
- Model: Knows how to make predictions.
- Inference code: Knows how to prepare inputs and interpret outputs.
- FastAPI: Provides an interface other applications can call.
- Docker: Packages the application into a reproducible environment.
- GitHub: Stores the source code and provides the deployment source.
- Render: Builds and runs the container.
- Frontend: Consumes the API.
34. What We Actually Built
At the end of this process, we didn’t simply “upload a model.”
We built a complete inference service.
Trained Model
↓
Artifact Contract
↓
FastAPI Service
↓
Local Testing
↓
Docker Image
↓
GitHub
↓
Render
↓
Public HTTPS API
↓
Independent Clients
The final system allows an application to send an image and receive a prediction without knowing anything about how the model was trained.
That is the real value of deployment.
35. Resources & References
Workshop Resources
You can follow the complete project using the repositories below.
- Pest Classifier — Frontend Repo: This repository contains the frontend application used to interact with the deployed classifier.
- Backend API: This repository contains the FastAPI inference service, model artifacts, Docker configuration, and deployment configuration.
- Pest Classifier — Live Demo: The deployed application demonstrates how an independent frontend can consume the model through the API.
Further Reading
Here are some resources worth exploring further.
- Aurélien Géron, Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow: I strongly recommend this book. In particular, Chapter 19: Training and Deploying TensorFlow Models at Scale is highly relevant to the ideas discussed in this workshop.
- Scikit-learn: For classical machine learning workflows, preprocessing, model training and evaluation. [Scikit-learn Documentation]
- TensorFlow: For building and training neural networks and working with saved models.[TensorFlow Documentation]
- FastAPI: For building Python APIs and inference services. [FastAPI Documentation]
- Docker: For packaging applications and their dependencies into containers. [Docker Documentation]
- TensorFlow Serving: For learning about dedicated model-serving infrastructure.[TensorFlow Serving Documentation]
- Render: For deploying the Dockerized API. [Render Documentation]
Final Thoughts
One of the biggest lessons from building machine learning systems is that training the model is not the finish line.
A model sitting inside a notebook is useful for experimentation.
A model exposed through a reliable interface can become part of a real application.
The journey looks something like this:
Experiment
↓
Train
↓
Evaluate
↓
Save model
↓
Build inference pipeline
↓
Expose API
↓
Containerize
↓
Deploy
↓
Connect clients
And once you understand this pipeline, you can start thinking beyond individual models. You start thinking about AI systems. That’s where machine learning engineering becomes really interesting.
Iremide ~ The AI Guy…
메타데이터
- post_id
- 2c6104cf166d
- slug
- from-trained-model-to-production-api-a-practical-guide-to-deploying-machine-learning-models-with-2c6104cf166d
- url
- https://medium.com/@stephrex602_25261/from-trained-model-to-production-api-a-practical-guide-to-deploying-machine-learning-models-with-2c6104cf166d
- canonical_url
- https://medium.com/@stephrex602_25261/from-trained-model-to-production-api-a-practical-guide-to-deploying-machine-learning-models-with-2c6104cf166d
- author_url
- https://medium.com/@stephrex602_25261
- status
- ok
- fetched_at
- 2026-08-31 10:09:44