Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/container-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Build and Push Container Images

# Builds the CPU and GPU serving images and pushes them to GHCR.
# - CPU keeps the unsuffixed tag (`:<version>`, `:latest`) for backward compatibility
# with existing consumers (e.g. publish(image="ghcr.io/nubison/nubison-model:0.0.7")).
# - GPU is published under the `-gpu` suffix (`:<version>-gpu`, `:latest-gpu`).
# Floating tags (:latest, :cpu, :gpu, :latest-gpu) are only moved on a published release,
# not on manual workflow_dispatch runs.

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Image version tag (e.g. 0.0.8). Defaults to the short commit SHA."
required: false
type: string

permissions:
contents: read
packages: write

env:
IMAGE: ghcr.io/nubison/nubison-model

jobs:
build:
name: Build ${{ matrix.variant }} image
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- variant: cpu
dockerfile: container/Dockerfile
- variant: gpu
dockerfile: container/Dockerfile.gpu
steps:
- uses: actions/checkout@v4

- name: Resolve version
id: ver
run: |
if [ "${{ github.event_name }}" = "release" ]; then
V="${{ github.event.release.tag_name }}"
elif [ -n "${{ inputs.version }}" ]; then
V="${{ inputs.version }}"
else
V="$(git rev-parse --short HEAD)"
fi
V="${V#v}" # strip a leading v (v0.0.8 -> 0.0.8)
echo "version=$V" >> "$GITHUB_OUTPUT"

- name: Compute image tags
id: tags
run: |
V="${{ steps.ver.outputs.version }}"
IS_RELEASE="${{ github.event_name == 'release' }}"
if [ "${{ matrix.variant }}" = "cpu" ]; then
TAGS="${IMAGE}:${V},${IMAGE}:${V}-cpu"
[ "$IS_RELEASE" = "true" ] && TAGS="${TAGS},${IMAGE}:latest,${IMAGE}:cpu"
else
TAGS="${IMAGE}:${V}-gpu"
[ "$IS_RELEASE" = "true" ] && TAGS="${TAGS},${IMAGE}:gpu,${IMAGE}:latest-gpu"
fi
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./container
file: ${{ matrix.dockerfile }}
push: true
tags: ${{ steps.tags.outputs.tags }}
cache-from: type=gha,scope=${{ matrix.variant }}
cache-to: type=gha,mode=max,scope=${{ matrix.variant }}
10 changes: 10 additions & 0 deletions container/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
FROM python:3.12.8-slim-bookworm

# System deps for the common ML stack so a model's requirements.txt installs cleanly:
# build tools (C/C++ ext without wheels), git (git+ requirements), ffmpeg/libsndfile1
# (audio/video), libsm6/libxext6 (opencv/X runtime libs). Kept in parity with Dockerfile.gpu.
RUN apt-get update && apt-get install -y \
bash \
git \
build-essential \
pkg-config \
ffmpeg \
libsndfile1 \
libsm6 \
libxext6 \
python3-opencv \
&& rm -rf /var/lib/apt/lists/*

Expand Down
71 changes: 71 additions & 0 deletions container/Dockerfile.gpu
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# GPU (CUDA) variant of the nubison-model serving image.
#
# Mirrors container/Dockerfile (CPU) but sits on an NVIDIA CUDA runtime base so a
# model's CUDA-enabled framework wheels (e.g. torch) can use the GPU at serve time.
#
# Torch/TF are intentionally NOT baked here. Exactly like the CPU variant,
# start_server.sh downloads the model's requirements.txt from MLflow and builds the
# serving venv with uv at runtime, so the CUDA build of torch comes from the model's
# own requirements. This base only provides the CUDA runtime + cuDNN system libraries
# and the NVIDIA container-runtime hooks (NVIDIA_VISIBLE_DEVICES / _DRIVER_CAPABILITIES
# are preset by the nvidia/cuda base image) needed for those wheels to reach the GPU.
#
# CUDA 12.8 tracks the torch 2.7 line used by the notebook images. The host GPU driver
# must be new enough for this CUDA version; torch wheels ship their own CUDA libraries.
FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04

ENV DEBIAN_FRONTEND=noninteractive

# System deps for the common ML stack so a model's requirements.txt installs cleanly:
# - build-essential/pkg-config: build C/C++ extension packages that ship no wheel
# - git: pip installs from `git+https://...` requirements
# - ffmpeg/libsndfile1: audio/video libs (torchaudio, librosa, moviepy, decord)
# - libsm6/libxext6: complete the opencv/X-linked runtime libraries
# NOTE: source-compiled CUDA extensions (nvcc) are still NOT covered — that needs the
# `-cudnn-devel-` base (~2x size). Prefer models that use prebuilt CUDA wheels.
RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
ca-certificates \
git \
build-essential \
pkg-config \
ffmpeg \
libsndfile1 \
libsm6 \
libxext6 \
python3 \
python3-pip \
python3-opencv \
&& rm -rf /var/lib/apt/lists/*

# start_server.sh invokes a bare `python`; map it to the system python3.
RUN ln -sf /usr/bin/python3 /usr/local/bin/python

SHELL ["bash", "-c"]
WORKDIR /app

LABEL org.opencontainers.image.title="nubison-model-gpu" \
org.opencontainers.image.description="GPU (CUDA) container image for nubison-model." \
org.opencontainers.image.source="https://github.com/nubison/nubison-model"

COPY start_server.sh /app/start_server.sh
RUN chmod +x /app/start_server.sh

ENV MLFLOW_TRACKING_URI=""
ENV MLFLOW_MODEL_URI=""
ENV MLFLOW_MODEL_NAME=""
ENV MLFLOW_MODEL_VERSION=""
ENV DEBUG=""
ENV PORT=3000
ENV NUM_WORKERS=2

EXPOSE ${PORT}

# Same orchestrator toolchain as the CPU image: uv + mlflow-skinny drive dependency
# download and venv creation. pip is upgraded first because Ubuntu 22.04 ships an old
# pip; the base python only needs mlflow-skinny + pyyaml + uv.
RUN python3 -m pip install --no-cache-dir --upgrade pip && \
python3 -m pip install --no-cache-dir uv==0.5.10 "mlflow-skinny>=3.12.0,<3.14.0" pyyaml && \
uv python install 3.9 3.10 3.11 3.12

ENTRYPOINT [ "/app/start_server.sh" ]
39 changes: 36 additions & 3 deletions container/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,51 @@
# Container for nubison-model
It is a container for the nubison-model. It is based on `python:3.12-slim-bookworm` with `uv` and `mlflow-skinny`. It will run the nubison-model server for a given model URI.
It is a container for the nubison-model. It runs the nubison-model server for a given model URI, using `uv` and `mlflow-skinny` to download the model's dependencies and build the serving venv at runtime.

Two variants are published:

| Variant | Dockerfile | Base image | Tags |
|---------|------------|------------|------|
| CPU (default) | `Dockerfile` | `python:3.12-slim-bookworm` | `:<version>`, `:<version>-cpu`, `:latest`, `:cpu` |
| GPU (CUDA) | `Dockerfile.gpu` | `nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04` | `:<version>-gpu`, `:latest-gpu`, `:gpu` |

CPU keeps the **unsuffixed** tag (`:<version>`, `:latest`) for backward compatibility with existing consumers. The GPU variant is identical except for the CUDA base — torch/TF are **not** baked into either image; `start_server.sh` installs the model's own `requirements.txt` with `uv` at runtime. The GPU base only supplies the CUDA runtime + cuDNN system libraries and the NVIDIA container-runtime hooks so a CUDA-enabled torch wheel from the model's requirements can reach the GPU (the container must be scheduled with the NVIDIA runtime / a `nvidia.com/gpu` resource).

## Build

```bash
docker build -t ghcr.io/nubison/nubison-model:latest .
# CPU
docker build -t ghcr.io/nubison/nubison-model:latest -f Dockerfile .
# GPU
docker build -t ghcr.io/nubison/nubison-model:latest-gpu -f Dockerfile.gpu .
```

## Push

```bash
docker push ghcr.io/nubison/nubison-model:latest
docker push ghcr.io/nubison/nubison-model:latest # CPU
docker push ghcr.io/nubison/nubison-model:latest-gpu # GPU
```

Both variants are normally built and pushed by CI (`.github/workflows/container-image.yml`) on a published GitHub release (matrix over `cpu`/`gpu`), or on demand via `workflow_dispatch`.

## Library coverage

The image does **not** bake ML frameworks; each model's `requirements.txt` is installed at
serve time. To let that install succeed for the common ML stack, both images include a
build toolchain (`build-essential`, `pkg-config`), `git` (for `git+https://` requirements),
`ffmpeg` + `libsndfile1` (audio/video), and the opencv/X runtime libraries.

Known gaps (a model needing these must handle it itself):

- **Source-compiled CUDA extensions** (e.g. building flash-attn / apex / mmcv from source)
need `nvcc` and CUDA headers, which are only in the `-cudnn-devel-` base (~2× size). The GPU
image uses the `-runtime-` base, so prefer models that install **prebuilt CUDA wheels**.
- **CUDA driver / torch wheel compatibility (GPU):** the GPU base targets CUDA 12.8
(`NVIDIA_REQUIRE_CUDA=cuda>=12.8`). A model's CUDA framework wheel must match the **node
driver** — e.g. a torch wheel built for CUDA 13.0 (`+cu130`) fails on a node whose driver
only supports CUDA 12.8. Pin torch to a `cu126`/`cu128` build (or update the node driver).
- Exotic system libraries not listed above must be added to the image (or vendored by the model).

## Run

```bash
Expand Down
Loading