Python API in a container

Published

2026-06-29

Set up

Review the original lab 6 files here. I’ve created both Python and R versions, and you can access and run the API from the Python/api/ folder.

First, check the Python version:

which python3
/usr/bin/python3
python3 --version
Python 3.12.3

Create virtual environment using venv:

/usr/bin/python3 -m venv .venv 
source .venv/bin/activate  

Install libraries in the requirements.txt:

pip install -r requirements.txt

Build model (optional)

We don’t have to, but if we want to run model.py, we’ll also need palmerpenguins and duckdb.

pip install palmerpenguins
pip install duckdb

Now we can run the model.py script:

python3 model.py

This will create a new model in models/ (if something changed).

  species     island  bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g     sex  year
0  Adelie  Torgersen            39.1           18.7              181.0       3750.0    male  2007
1  Adelie  Torgersen            39.5           17.4              186.0       3800.0  female  2007
2  Adelie  Torgersen            40.3           18.0              195.0       3250.0  female  2007
R^2 0.8555368759537614
Intercept 2169.2697209393996
prototype_data      bill_length_mm  species_Chinstrap  species_Gentoo  sex_male
0              39.1              False           False      True
1              39.5              False           False     False
2              40.3              False           False     False
4              36.7              False           False     False
5              39.3              False           False      True
..              ...                ...             ...       ...
339            55.8               True           False      True
340            43.5               True           False     False
341            49.6               True           False      True
342            50.8               True           False      True
343            50.2               True           False     False

[333 rows x 4 columns]
Columns Index(['bill_length_mm', 'species_Chinstrap', 'species_Gentoo', 'sex_male'], dtype='object')
Coefficients [  32.53688677 -298.76553447 1094.86739145  547.36692408]
Model Cards provide a framework for transparent, responsible reporting. 
 Use the vetiver `.qmd` Quarto template as a place to start, 
 with vetiver.model_card()
Writing pin:
Name: 'penguin_model'
Version: 20260618T104900Z-91fdd

Run the API

We’re going to make a small change to mod-api.py in this lab, and its because we need the os.environ['PINS_ALLOW_PICKLE_READ'] at the top (before any imports).

# UPDATED mod-api.py

import os
os.environ['PINS_ALLOW_PICKLE_READ'] = '1'

import warnings
warnings.filterwarnings("ignore", message=".*urllib3 v2 only supports OpenSSL.*")

import vetiver
import pins
import uvicorn
import pandas as pd


model_path = os.environ.get("MODEL_PATH", "models/")
model_board = pins.board_folder(model_path)

sklearn_model = model_board.pin_read("penguin_model")
print(f":-] Successfully loaded model: {type(sklearn_model)}")

if hasattr(sklearn_model, 'feature_names_in_'):
    feature_names = sklearn_model.feature_names_in_
    print(f"Model expects features: {list(feature_names)}")
    
    def get_prototype_value(column_name):
        """Get appropriate default value for each column type"""
        if 'bill_length' in column_name:
            return 45.0  
        elif 'species_Gentoo' in column_name:
            return 1     
        elif 'sex_male' in column_name:
            return 1     
        else:
            return 0
    
    prototype_data = pd.DataFrame({
        name: [get_prototype_value(name)] for name in feature_names
    })
    
else:
    
    print("Model doesn't have feature_names_in_, using estimated prototype")
    prototype_data = pd.DataFrame({
        "bill_length_mm": [45.0],
        "species_Chinstrap": [0],
        "species_Gentoo": [1], 
        "sex_male": [1]
    })

print("Prototype data shape:", prototype_data.shape)
print("Prototype columns:", list(prototype_data.columns))
print("Sample data:")
print(prototype_data)

v = vetiver.VetiverModel(
    model=sklearn_model, 
    model_name="penguin_model",
    prototype_data=prototype_data
)
print(f":-] Created VetiverModel: {type(v)}")

vetiver_api = vetiver.VetiverAPI(v, check_prototype=True)
# can run with:
# vetiver_api.run(port = 8080)

# actual FastAPI application
app = vetiver_api.app

print(f"FastAPI app type: {type(app)}")

if __name__ == "__main__":
    print(":-] Starting Penguin Model API...")
    print(":-] API Documentation: http://127.0.0.1:8080/docs")
    print(":-] Health Check: http://127.0.0.1:8080/ping")
    print(":-] Model Info: http://127.0.0.1:8080/metadata")
    uvicorn.run(app, host="0.0.0.0", port=8080)
1
Set environment variable BEFORE importing pins
2
Install packages
3
load model from MODEL_PATH environment variable (default: local models/ for dev)
4
Read pinned model
5
Check model for the features it expects
6
Define prototype value logic (realistic penguin bill length, default to Gentoo species, default to male, default for other dummy variables)
7
Create prototype data directly
8
Fallback when model doesn’t have feature names
9
Wrap as VetiverModel
10
Create VetiverAPI and extract FastAPI app
11
Print API info

Finally, run the API using:

python3 mod-api.py

View the API using the following URLS:

http://127.0.0.1:8080/

http://127.0.0.1:8080/docs

Testing API (optional)

We can perform some terminal testing, too (in a new terminal).

Test health check:

curl http://127.0.0.1:8080/ping

Test prediction using the following:

curl -X POST "http://localhost:8080/predict" \
  -H "Content-Type: application/json" \
  -d '[{"bill_length_mm": 45.0, "species_Chinstrap": 0, "species_Gentoo": 1, "sex_male": 1}]'

vetiver typically expects data as a list of observation records. The [0] index wraps a single prediction so it becomes one row in the DataFrame.

Make sure to quit (Ctrl + c) the API before building the Docker image and running the model in the container.

Docker

Start docker:

systemctl --user start docker-desktop

Create a Dockerfile and enter the following:


FROM python:3.11-slim

WORKDIR /app

COPY requirements-api.txt .
RUN pip install --no-cache-dir -r requirements-api.txt

COPY mod-api.py .

ENV MODEL_PATH=/data/model
ENV PINS_ALLOW_PICKLE_READ=1

EXPOSE 8080

CMD ["python3", "mod-api.py"]
1
official Python 3.11 slim image as the base
2
working directory inside the container to /app
3
copies the API requirements file from the host into the container
4
installs Python dependencies
5
copies the FastAPI application script into the container
6
sets an environment variable pointing to where the model will be mounted
7
enables Vetiver to read pickle model files
8
exposes port 8080 so the API can be accessed externally
9
runs the FastAPI application when the container starts

The python:3.11-slim minimizes container size. The --no-cache-dir -r will perform a pip install without caching to reduce image size. The ENV lines are environment variables (path to data and pickle files), and EXPOSE will allow us to use a port (similar to mod-api.py).

Build the image

Build the image (run from Python/api/):

docker build -t penguin-model-py .

In the Terminal, you should see something like:

When the image is built, we can see it under Builds in Docker Desktop:

Run the container

Run the container, mounting the model directory from the host:

docker run --rm -d \
  -p 8080:8080 \
  --name penguin-model-py \
  -v "$(pwd)/models":/data/model \
  penguin-model-py

The -v flag mounts the local models/ folder into the container at /data/model, which is where MODEL_PATH points by default. This keeps the model outside the image so it can be updated without a rebuild.

In Docker Desktop, click on the Containers tab and you’ll see the container running:

Test the running container:

curl http://localhost:8080/ping

The response should be:

{"ping":"pong"}

To stop the container, run the following:

docker stop penguin-model-py

Since we used --rm in the run command, the container will be automatically removed when it stops.

If the graceful stop doesn’t work, you can force kill it:

docker kill penguin-model-py

Final project files

├── api.Rproj
├── Dockerfile
├── mod-api.py
├── model.py
├── models/
├── my-db.duckdb
├── README.md
├── requirements-api.txt
└── requirements.txt

5 directories, 12 files
1
Container definition to build and run the API in Docker.
2
FastAPI application that loads the ML model and exposes REST endpoints (ping, metadata, predict).
3
Script that trains a linear regression model on Palmer penguin data and saves it using Vetiver pins.
4
Directory containing timestamped model versions with .joblib model files and data.txt metadata.
5
DuckDB database file storing the Palmer penguins dataset.
6
Setup and usage instructions for running the API locally and in Docker.
7
Python dependencies for the FastAPI application.
8
Python dependencies for both model training and the API.

In the next lab, we’ll run the R api in a Docker container.