images.md
275 lines · skills/modal/references/images.md at 5f2bc03
Modal Container Images
Reviewed against SDK 1.6.0 and official Images and existing Images guides. Image definitions were checked locally; builds and workload dependency compatibility were not executed. Package versions below are illustrative pins, not a tested ML stack.
Raw lines 1 to 7Table of Contents
- Overview
- Base Images
- Installing Packages
- System Packages
- Shell Commands
- Running Python During Build
- Adding Local Files
- Environment Variables
- Dockerfiles
- Alternative Package Managers
- Image Caching
- Handling Remote-Only Imports
Overview
Every Modal function runs inside a container built from an Image. By default, Modal uses a Debian Linux image with the same Python minor version as your local interpreter.
Defining an Image is lazy and performs no remote build. Modal resolves/builds it when running or deploying the App (before inputs execute), or when creating a Sandbox. Layers are cached for fast rebuilds.
Raw lines 23 to 30Base Images
# Default: Debian slim with your local Python version
image = modal.Image.debian_slim()
# Specific Python version
image = modal.Image.debian_slim(python_version="3.11")
# From Docker Hub
image = modal.Image.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.11")
# From a Dockerfile
image = modal.Image.from_dockerfile("./Dockerfile")Raw lines 31 to 46Installing Packages
Raw lines 47 to 48uv (Recommended)
uv_pip_install uses the uv package manager for fast, reliable installs:
image = (
modal.Image.debian_slim(python_version="3.11")
.uv_pip_install(
"torch==2.12.0",
"transformers==5.9.0",
"accelerate==1.13.0",
"scipy==1.17.1",
)
)Pin versions for reproducibility. uv resolves dependencies faster than pip.
Raw lines 49 to 66pip (Fallback)
image = modal.Image.debian_slim(python_version="3.11").pip_install(
"numpy==1.26.0",
"pandas==2.1.0",
)Raw lines 67 to 75From requirements.txt
image = modal.Image.debian_slim().pip_install_from_requirements("requirements.txt")For a uv-managed project, modal.Image.debian_slim().uv_sync(".") reads its pyproject.toml and uv.lock (frozen=True by default). Keep the lockfile in sync.
Private Packages
image = (
modal.Image.debian_slim()
.pip_install_private_repos(
"github.com/org/private-repo",
git_user="username",
secrets=[modal.Secret.from_name("github-token")],
)
)For GitHub, the named Secret must contain GITHUB_TOKEN; do not embed tokens in URLs.
System Packages
Install Linux packages via apt:
image = (
modal.Image.debian_slim()
.apt_install("ffmpeg", "libsndfile1", "git", "curl")
.uv_pip_install("librosa", "soundfile")
)Raw lines 100 to 111Shell Commands
Run arbitrary commands during image build:
image = (
modal.Image.debian_slim()
.run_commands("mkdir -p /opt/data")
)Raw lines 112 to 122With GPU
Some build steps require GPU access (e.g., compiling CUDA kernels):
image = (
modal.Image.debian_slim()
.uv_pip_install("torch")
.run_commands("python -c 'import torch; assert torch.cuda.is_available()'", gpu="A100")
)Raw lines 123 to 134Running Python During Build
Execute Python functions as build steps — useful for downloading model weights:
def download_model():
from huggingface_hub import snapshot_download
snapshot_download("openai-community/gpt2", local_dir="/models/gpt2")
image = (
modal.Image.debian_slim(python_version="3.11")
.uv_pip_install("huggingface_hub", "torch", "transformers")
.run_function(download_model, secrets=[modal.Secret.from_name("huggingface")])
)The resulting filesystem (including downloaded files) is snapshotted into the image. For gated models, accept the model license and pass an HF_TOKEN Secret explicitly. Pin the model revision for reproducible production builds.
Adding Local Files
Raw lines 155 to 156Local Directories
image = modal.Image.debian_slim().add_local_dir(
local_path="./config",
remote_path="/root/config",
)By default, files are added at container startup (not baked into the image layer). Use copy=True to bake them in.
Local Python Modules
image = modal.Image.debian_slim().add_local_python_source("my_module")This uses Python's import system to find and include the module.
As of v1.0, Modal no longer "automounts" imported local modules. You must explicitly include local dependencies withRaw lines 168 to 181add_local_python_source(the App's own source is still included automatically; setinclude_source=Falseon the App/Function to opt out). The deprecatedmodal.Mountobject and themount=/context_mount=parameters have been replaced by theseImage.add_local_*methods.
Individual Files
image = modal.Image.debian_slim().add_local_file(
local_path="./model_config.json",
remote_path="/root/config.json",
)Raw lines 182 to 190Environment Variables
image = (
modal.Image.debian_slim()
.env({
"TOKENIZERS_PARALLELISM": "false",
"HF_HOME": "/cache/huggingface",
})
)Names and values must be strings.
Raw lines 191 to 204Dockerfiles
Build from existing Dockerfiles:
image = modal.Image.from_dockerfile("./Dockerfile")The build context is now inferred automatically from the Dockerfile's commands. The old context_mount= parameter — along with the modal.Mount object it relied on — is deprecated and was enforced as removed in v1.0; do not pass it.
Alternative Package Managers
Raw lines 217 to 218Micromamba / Conda
For packages requiring coordinated system and Python package installs:
image = (
modal.Image.micromamba(python_version="3.11")
.micromamba_install("numpy", "scipy", channels=["conda-forge"])
)Raw lines 219 to 229Image Caching
Modal caches images per layer (per method call). Breaking the cache on one layer cascades to all subsequent layers.
Raw lines 230 to 233Optimization Tips
- Order layers by change frequency: Put stable dependencies first, frequently changing code last
- Pin versions: Unpinned versions may resolve differently and break cache
- Separate large installs: Put heavy packages (torch, tensorflow) in early layers
Force Rebuild
# Single layer
image = modal.Image.debian_slim().apt_install("git", force_build=True)# All images in a run
MODAL_FORCE_BUILD=1 modal run script.py
# Rebuild without updating cache
MODAL_IGNORE_CACHE=1 modal run script.pyRaw lines 240 to 254Handling Remote-Only Imports
When packages are only available in the container (not locally), use conditional imports:
@app.function(image=image)
def process():
import torch # Only available in the container
return torch.cuda.device_count()For module-level imports shared across functions, use the Image.imports() context manager:
with image.imports():
import torch
import transformersThis prevents ImportError locally while making the imports available in the container. It does not install dependencies or make an unavailable module usable during local execution.