+91 80401 38000[email protected]24/7 Expert Support
[email protected]Client Portal →
ServerGurus
← All posts
DevOpsAI/MLTutorial

How to Set Up a CI/CD Pipeline for ML Models Using GitHub Actions and Docker

By ServerGurus12 July 20265 min read
How to Set Up a CI/CD Pipeline for ML Models Using GitHub Actions and Docker

Most ML teams build great models that never make it to production. The model gets trained, the notebook looks impressive, and then it sits in a folder while the team argues about how to deploy it. Meanwhile, the business is not getting any value from the work.

A CI/CD pipeline for ML solves this. Every time you push code, your model gets tested, containerized, and deployed automatically. Here is how to build one using GitHub Actions, Docker, and a dedicated GPU server. No Kubernetes cluster required.

What You Will Build

By the end of this guide, you will have a pipeline that:

  • Runs your model tests on every push
  • Builds a Docker image with your model and dependencies
  • Pushes the image to a container registry
  • Deploys it to a GPU server as a running inference API

You can run this on any infrastructure. We tested it on a dedicated L40S GPU server, but it works on any box with Docker and an NVIDIA GPU.

Step 1: Structure Your Project

Your ML project should look like this:

ml-model/
  model.py           # your model code
  requirements.txt   # Python dependencies
  Dockerfile         # container definition
  tests/
    test_model.py    # model tests
  .github/
    workflows/
      deploy.yml     # GitHub Actions pipeline

Keep it simple. The model is just a Python file. The tests verify it loads and produces output. No fancy frameworks. This is production, not a research notebook.

Step 2: Write the Dockerfile

FROM nvidia/cuda:12.4-runtime-ubuntu22.04

RUN apt-get update && apt-get install -y python3 python3-pip
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY model.py .

EXPOSE 8000
CMD ["python3", "-m", "uvicorn", "model:app", "--host", "0.0.0.0", "--port", "8000"]

This uses the NVIDIA CUDA base image so your model can access the GPU. The requirements file should pin your exact versions:

torch==2.5.0
transformers==4.45.0
fastapi==0.115.0
uvicorn==0.30.0

Step 3: Create the Model API

Your model.py exposes a FastAPI endpoint:

from fastapi import FastAPI
from pydantic import BaseModel
import torch

app = FastAPI()

# Load model once at startup (not on every request)
model = torch.load("model.pt", map_location="cuda")
model.eval()

class PredictRequest(BaseModel):
    text: str

class PredictResponse(BaseModel):
    label: str
    confidence: float

@app.post("/predict", response_model=PredictResponse)
def predict(req: PredictRequest):
    with torch.no_grad():
        output = model(req.text)
        pred = output.argmax().item()
        confidence = output.softmax(dim=-1)[pred].item()
    return PredictResponse(label=str(pred), confidence=confidence)

@app.get("/health")
def health():
    return {"status": "ok"}

Two endpoints. One for predictions. One for health checks. That is all you need.

Step 4: Write Tests

Your tests should verify the model loads and produces output of the right shape:

from model import app
from fastapi.testclient import TestClient

client = TestClient(app)

def test_health():
    response = client.get("/health")
    assert response.status_code == 200

def test_predict():
    response = client.post("/predict", json={"text": "sample input"})
    assert response.status_code == 200
    data = response.json()
    assert "label" in data
    assert "confidence" in data
    assert 0 <= data["confidence"] <= 1

No GPU needed for unit tests. Tests run on the GitHub Actions runner, which has no GPU. This is by design - you test the API logic, not the GPU hardware. GPU validation happens in staging.

Step 5: The GitHub Actions Pipeline

Create .github/workflows/deploy.yml:

name: ML Model CI/CD

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install -r requirements.txt
      - run: pip install pytest httpx
      - run: pytest tests/ -v

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker image
        run: docker build -t my-model:latest .
      - name: Deploy to GPU server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.GPU_HOST }}
          username: ${{ secrets.GPU_USER }}
          key: ${{ secrets.GPU_SSH_KEY }}
          script: |
            docker pull my-model:latest
            docker stop model-api 2>/dev/null || true
            docker run -d --name model-api \
              --gpus all -p 8000:8000 \
              --restart unless-stopped \
              my-model:latest
            sleep 5
            curl -f http://localhost:8000/health

The pipeline runs tests first. If they pass, it builds the Docker image and deploys it to your GPU server via SSH. The health check after deployment ensures the model is actually serving before the pipeline reports success.

Step 6: Deploy and Verify

Push your code to GitHub. Watch the Actions tab. In about three minutes, your model will be running on your GPU server.

Test it:

curl -X POST http://your-gpu-server:8000/predict \
  -H "Content-Type: application/json" \
  -d '{"text": "this is great"}'

You should get back a label and confidence score. Your model is live.

When You Need More Than One GPU

This pipeline works for a single model on a single GPU server. If you have multiple models or need auto-scaling, you will eventually want Kubernetes. But start simple. Most teams never outgrow a single dedicated GPU server for inference. The ones that do usually know they need more before they start.

If you are serving multiple models, you can run separate Docker containers on different ports. Use a reverse proxy like nginx to route requests by model name. Add Prometheus metrics to track latency and throughput. The pipeline stays the same. You just add more deploy jobs.

What About AWS SageMaker?

SageMaker is a great product for teams already deep in the AWS ecosystem. It handles model registry, endpoint management, and auto-scaling out of the box. But SageMaker endpoints cost money even when idle, and the per-hour GPU pricing is higher than a dedicated server.

For teams just starting with ML deployments, a single GPU server with this CI/CD pipeline is simpler, cheaper, and easier to understand. You can always add SageMaker later when you need its orchestration features. The Docker image you built in Step 2 deploys to SageMaker with zero changes - just point it at an ECR repository instead of your GPU server.

How ServerGurus Helps

Our GPU cloud gives you dedicated NVIDIA GPUs with root access. You own the server, you run your Docker containers, you set up your CI/CD pipeline however you want. No per-hour billing anxiety. No cold starts. No noisy neighbors.

Every GPU plan includes a dedicated IP, NVMe storage, and private networking. If you need help setting up the pipeline in this guide, our DevOps team can have you running in under an hour. That is the difference between buying infrastructure and having a partner who helps you use it.

Check GPU cloud pricing or talk to our DevOps team.

Ready to build your infrastructure?

Get a quote from our Hyderabad-based team - Tier IV datacenter, real support, INR or USD billing.

View pricingRequest a quoteWhatsApp sales