Container Deployments#
I Overview#
Pods are pre-built container deployment artifacts for different model serving scenarios in myModel.
Each pod is a self-contained directory under pods/ with a Containerfile, a compose config, and launch scripts.
They let you deploy models in containers without writing your own Dockerfiles.
If you are new to containers: a Containerfile (called a Dockerfile in Docker parlance) is a recipe for building a container image, and a compose file (compose.yaml) declares how to run that image, including GPU access, ports, and volumes. myModel uses podman (rootless, daemonless) instead of docker, but the file formats are compatible.
pods/
├── chat/ # vLLM inference server
├── read/ # Marker PDF/OCR server
├── tune/ # QLoRA training environment
├── dev/ # CUDA development environment
└── README.md # Container image reference
The four pods cover the most common workflows: serving a model for chat, processing documents, fine-tuning, and developing against the CUDA stack. You can mix and match them or build your own (see §VII).
II Container images reference#
myModel builds on upstream container images from NVIDIA and vLLM.
The table below is an expanded view of pods/README.md, listing the images used or referenced by the pods.
Image |
Variant |
Use case |
Notes |
|---|---|---|---|
|
CUDA devel |
Building GPU-dependent Python packages (read pod) |
Ubuntu 24.04, CUDA 13.0.2 |
|
cuDNN devel |
Same but with cuDNN pre-installed |
Required for Marker’s torchvision/torch stack |
|
First-party vLLM |
Running the OpenAI-compatible vLLM server |
|
|
Pinned first-party |
Chat pod default image |
Pinned for reproducibility |
|
NVIDIA’s vLLM |
Alternative vLLM image from NGC |
Ubuntu 24.04, CUDA 13.0.2, Python 3.12 |
Compatibility notes:
podman: The pods use
security_opt: [label=disable]anddevices: [nvidia.com/gpu=0], which are podman-compatible. NVIDIA CDI must be configured on the host.Platform: All images target
linux/amd64unless overridden. The chat pod’scompose.yamlpinsplatform: linux/arm64for the default configuration (adjust to your hardware).
III Chat pod: vLLM inference server#
The chat pod runs vLLM as an OpenAI-compatible HTTP server inside a container. This is the fastest path to serving a model: you provide a HuggingFace model ID and the container handles the rest.
The Containerfile#
pods/chat/Containerfile is a thin layer on top of docker.io/vllm/vllm-openai:v0.11.2.
The file is mostly commented-out scaffolding showing where you would add custom dependencies:
FROM docker.io/vllm/vllm-openai:v0.11.2
# Dependency installation example:
# RUN uv pip install --system vllm[audio]
Uncomment or add RUN lines to install extra Python packages.
For most deployments, the base image alone is sufficient.
The compose.yaml#
services:
chat-server:
image: docker.io/vllm/vllm-openai:v0.11.2
platform: linux/arm64
runtime: nvidia
ipc: host
ports:
- 8000:8000
environment:
- HF_TOKEN=${HF_TOKEN}
- MODEL_ID=${MODEL_ID:-google/gemma-3-12b-it}
volumes:
- $HF_HOME:/root/.cache/huggingface
security_opt:
- label=disable
command: ["--model", "${MODEL_ID:-google/gemma-3-12b-it}"]
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
Setting |
Purpose |
|---|---|
|
NVIDIA container runtime for GPU access |
|
Share host IPC namespace (vLLM recommendation for perf) |
|
Expose the OpenAI API on host port 8000 |
|
HuggingFace access token (for gated models) |
|
The HuggingFace model ID to serve (defaults to Gemma 3 12B) |
|
Bind-mount the HF cache so model weights persist |
|
Disable SELinux label enforcement (podman compatibility) |
|
Reserve all NVIDIA GPUs for the container |
The run script#
pods/chat/run.sh is a one-liner equivalent to the compose file for ad-hoc use:
podman run --runtime nvidia --gpus all \
-v ~/.cache/huggingface:/root/.cache/huggingface \
-p 8000:8000 \
--env "HF_TOKEN=${HF_TOKEN}" \
--security-opt label=disable \
docker.io/vllm/vllm-openai --model google/gemma-3-12b-it
Build and run#
cd pods/chat
podman build -t mymodel-chat .
export HF_TOKEN=hf_xxxxxxxxxxxx
podman compose -f compose.yaml up
# or: bash run.sh
Customize the model#
Set MODEL_ID before launching to serve a different model:
MODEL_ID=microsoft/Phi-3-mini-4k-instruct podman compose -f compose.yaml up
Test it#
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "google/gemma-3-12b-it", "messages": [{"role": "user", "content": "Hello!"}], "max_tokens": 64}'
The reference container file#
pods/chat/vLLM__reference.container is the full upstream vLLM Dockerfile (22 KB), included for reference.
It shows how the official image is built from scratch (multi-stage CUDA build, wheel compilation, DeepGEMM, EP kernels, FlashInfer).
You do not need to build from this file, but it is an invaluable reference if you need to patch vLLM or add custom kernels.
For more on serving models outside of containers, see Serving with vLLM.
IV Read pod: OCR/PDF processing#
The read pod runs a Marker server for converting PDF and image files into structured markdown. Unlike the chat pod, this pod builds a custom image from the NVIDIA CUDA base.
The marker-server.py#
pods/read/marker-server.py (11 KB) distributes document processing across multiple GPUs using Python multiprocessing.
Key features:
Multi-GPU parallelism: One
multiprocessing.Poolper GPU; each worker pins itself viaCUDA_VISIBLE_DEVICES.Configurable LLM backend: Optionally routes to OpenAI, Claude, Gemini, Vertex, Azure, or Ollama for validation and correction.
Modal integration: Designed to run on Modal (serverless GPU) with 4x T4 GPUs, but the core logic works locally on any CUDA-capable machine.
I/O: Files in a mounted intake directory (PDF, JPG, PNG, TIFF); output is extracted markdown text, images, and metadata per document.
The Containerfile#
pods/read/Containerfile is a multi-stage build from the NVIDIA CUDA cuDNN base:
FROM nvcr.io/nvidia/cuda:13.0.2-cudnn-devel-ubuntu24.04 AS base
RUN python3 -m pip install uv
# GPU packages: marker-pdf, torch, torchvision
FROM base as marker-base
RUN $UV_INSTALL marker-pdf pillow pytorch torchvision torchaudio
# Server packages: FastAPI, granian (ASGI server)
FROM marker-base as py-base
RUN $UV_INSTALL fastapi granian uvloop sqlmodel
# Final image
FROM py-base as final-read
COPY ./marker-server.py /usr/local/bin/marker_server
ENTRYPOINT ["marker_server", "--port", "8000"]
It uses uv for fast package resolution with cache mounts for reproducible, layer-cached builds.
The compose.yaml#
services:
my-reader:
build: .
image: mymodel-read:latest
ports:
- 8000:8000
devices:
- nvidia.com/gpu=0
security_opt:
- label=disable
env_file:
- ${MY_ENV_FILE}
volumes:
- ${MY_DATA}/ocr_intake:/data/intake:ro
- ${MY_DATA}/ocr_output:/data/output:rw
- datalab-cache:/root/.cache/datalab/:rw
Setting |
Purpose |
|---|---|
|
Build from the local |
|
Mount GPU 0 via NVIDIA CDI |
|
Intake (read-only), output (read-write), cache |
|
Load secrets (API keys, model paths) from an env file |
Build and run#
cd pods/read
podman build -t mymodel-read:latest .
export MY_ENV_FILE=.env
export MY_DATA=/path/to/your/data
podman compose -f compose.yaml up
Send documents for processing#
Place PDFs or images in your intake directory, then call the server:
curl http://localhost:8000/convert \
-H "Content-Type: application/json" \
-d '{"file": "/data/intake/document.pdf", "output_format": "markdown"}'
V Tune pod: QLoRA fine-tuning#
The tune pod provides a standalone training environment for QLoRA fine-tuning of LLMs.
It is a deliberately separate uv project because unsloth’s tight torch/transformers pins are unsatisfiable when co-resolved with my-model’s vLLM dependency under the repo’s exclude-newer guard.
The tune pod takes teacher-trace JSONL datasets and produces LoRA adapters that can be hot-loaded into a running vLLM server. It uses Unsloth for memory-efficient 4-bit quantized training with gradient checkpointing.
Separate uv project structure#
pods/tune/
├── pyproject.toml # Standalone uv project (my-model-tune)
├── README.md # Usage and adapter serving notes
└── train.py # QLoRA training entrypoint (7 KB)
pyproject.toml declares its own dependencies (datasets, trl, unsloth) and pins Python to >=3.13,<3.14.
It does not depend on my-model; instead, train.py bootstraps the sibling package via an explicit sys.path.insert:
REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT))
This lets train.py import the pure, unit-tested mymodel.tune modules (config, dataset schema, prechecks) without joining the two dependency trees.
The train.py script#
train.py is the QLoRA entrypoint: trace JSONL in, LoRA adapter out.
Key CLI arguments:
Argument |
Purpose |
|---|---|
|
Path to a |
|
Run a tiny synthetic 5-step QLoRA on Qwen3-0.6B (proves the loop) |
|
Override the config step cap |
|
Skip the busy-machine refusal (cloud/burst runs only) |
The script loads and validates the dataset, runs a machine precheck that refuses to start if the GPU or RAM is busy, loads the base model in 4-bit via Unsloth’s FastLanguageModel, applies LoRA to the standard Qwen/Llama attention and MLP modules, runs SFTTrainer from trl with bf16 training, and saves the adapter with a run-manifest.json.
How to use it#
# 1. Resolve the environment (first run downloads torch — big)
task tune:sync
# 2. Smoke test: 5-step QLoRA on Qwen3-0.6B with a synthetic dataset
task tune:smoke
# 3. Real training run
task tune:train -- --config runs/my-run.json
GPU requirements and memory#
The precheck refuses to start while the GPU or RAM is busy.
Real runs should carry a memory scope (house salt memory-guard rule) to protect the desktop:
systemd-run --user --scope -p MemoryMax=40G \
task tune:train -- --config runs/my-run.json
Serving the adapter#
Write adapters to ${HF_HOME:-~/local/models}/loras/<adapter> (the TuneConfig.output_dir default).
That path rides the HF-cache bind mount into the local vLLM container.
Start vLLM with the student base and LoRA enabled, then hot-load:
VLLM_MODEL=org/exact-base-checkpoint VLLM_TOOL_PARSER=hermes task vllm:up
curl -s http://localhost:8000/v1/load_lora_adapter \
-H "Authorization: Bearer $LOCAL_VLLM_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"lora_name": "example-adapter", "lora_path": "/root/.cache/huggingface/loras/example-adapter"}'
Requests then select the adapter by "model": "example-adapter".
See Fine-tuning for the full tuning guide.
VI Dev pod: CUDA development#
The dev pod provides a bare CUDA development environment for exploring the container ecosystem, building custom images, or testing GPU-dependent code without the vLLM or Marker overhead.
pods/dev/cuda.yaml launches the NVIDIA CUDA cuDNN devel image with GPU access and a bash shell:
services:
cuda-dev:
image: nvcr.io/nvidia/cuda:13.0.2-cudnn-devel-ubuntu24.04
devices:
- nvidia.com/gpu=0
security_opt:
- label=disable
command: ['/bin/bash']
When to use this vs the chat pod#
Use the dev pod when you need to build or compile GPU-dependent packages from source (flash-attention, custom CUDA kernels), explore the CUDA toolkit, or debug GPU issues in isolation. Use the chat pod when you just want to serve a model. The dev pod has no model server, no HTTP endpoint, and no pre-installed ML libraries.
How to use it#
cd pods/dev
podman compose -f cuda.yaml run --rm cuda-dev
This drops you into a bash shell inside the container with GPU access.
VII Building custom pods#
Creating your own pod follows the same three-file pattern used by the existing pods:
pods/my-pod/
├── Containerfile # How to build the image
├── compose.yaml # How to run the container
└── run.sh # (optional) Ad-hoc launch script
Minimal example#
Containerfile:
FROM nvcr.io/nvidia/cuda:13.0.2-cudnn-devel-ubuntu24.04
RUN python3 -m pip install uv
RUN uv pip install --system torch fastapi uvicorn
COPY server.py /app/server.py
WORKDIR /app
ENTRYPOINT ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
compose.yaml:
services:
my-server:
build: .
image: mymodel-mypod:latest
ports:
- 8000:8000
devices:
- nvidia.com/gpu=0
security_opt:
- label=disable
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
Build and run:
cd pods/my-pod
podman build -t mymodel-mypod:latest .
podman compose -f compose.yaml up
Wiring GPU access#
Podman with NVIDIA CDI (recommended):
devices:
- nvidia.com/gpu=0
CDI (Container Device Interface) is the modern, portable way to expose GPUs. Configure it on the host with:
nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
Docker / docker-compose with NVIDIA runtime:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
Exposing ports#
Map container ports to the host in the compose file:
ports:
- 8000:8000 # host:container
For multiple services, use different host ports to avoid conflicts.
Mounting model directories#
Bind-mount the HuggingFace cache so model weights persist across restarts:
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
For custom data directories:
volumes:
- /path/to/host/data:/data:ro # read-only intake
- /path/to/host/out:/out:rw # read-write output
VIII Podman vs Docker#
myModel prefers podman over docker.
Both use the same Containerfile and compose.yaml formats, but there are practical differences.
Rootless podman advantages#
No daemon: podman runs as a regular process. No
dockerdconsuming resources at idle.Rootless by default: containers run as your user, not root. Better security isolation.
Systemd integration:
podman generate systemdcreates unit files for managing containers as user services.Drop-in compatibility:
podman build,podman run, andpodman composemirror the docker CLI for most workflows.
NVIDIA CDI configuration#
Both podman and docker can use NVIDIA CDI for GPU access.
CDI is preferred over the legacy --gpus all flag because it is runtime-agnostic:
# Generate the CDI spec (once)
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
# Verify GPU access
podman run --rm --device nvidia.com/gpu=0 \
nvcr.io/nvidia/cuda:13.0.2-cudnn-devel-ubuntu24.04 nvidia-smi
Host networking considerations#
The gateway (see The LiteLLM Gateway) uses podman with host networking (--net host) so that the gateway and local vLLM servers share localhost.
This avoids port-mapping overhead and simplifies TLS termination.
When running containers on the default bridge network, use explicit port mappings: podman run -p 8000:8000 ....
Boot persistence for rootless containers#
Rootless podman containers do not survive reboot by default. To persist a container as a user service:
# Generate a systemd unit file
podman generate systemd --files --name my-container
# Install as a user service
mkdir -p ~/.config/systemd/user/
cp container-my-container.service ~/.config/systemd/user/
# Enable lingering so the service starts at boot
loginctl enable-linger $USER
systemctl --user daemon-reload
systemctl --user enable --now container-my-container.service
This ensures your model-serving container restarts automatically after a reboot.