Skip to main content
Modal Images define the container environment where your code runs. They let you install Python packages, system dependencies, and configure the runtime environment for your Functions and Classes.

Creating images

Modal provides several base images to start from:
import modal

# Debian-based image (default)
image = modal.Image.debian_slim()

# Micromamba-based image for conda packages
image = modal.Image.micromamba()

# Use a custom Docker image
image = modal.Image.from_registry("python:3.11-slim")

Default image

If you don’t specify an image, Modal uses modal.Image.debian_slim() by default:
app = modal.App()

@app.function()  # Uses debian_slim by default
def my_function():
    pass

Installing Python packages

Add Python dependencies using .pip_install():
image = modal.Image.debian_slim().pip_install(
    "numpy",
    "pandas",
    "scikit-learn"
)
Install from a requirements file:
image = modal.Image.debian_slim().pip_install_from_requirements("requirements.txt")
Install from pyproject.toml:
image = modal.Image.debian_slim().pip_install_from_pyproject("pyproject.toml")
image = modal.Image.debian_slim().pip_install("numpy", "pandas")

Installing system packages

Install system-level dependencies with .apt_install():
image = modal.Image.debian_slim().apt_install(
    "ffmpeg",
    "libpq-dev",
    "curl"
)
For complex setup, use .run_commands():
image = modal.Image.debian_slim().run_commands(
    "apt-get update",
    "apt-get install -y build-essential",
    "curl -O https://example.com/installer.sh",
    "bash installer.sh"
)
Commands in .run_commands() are executed during image build time, not at runtime. Use them for installation and setup, not for executing application logic.

Conda and Micromamba

Use Micromamba for conda package management:
image = modal.Image.micromamba().micromamba_install(
    "pytorch",
    "torchvision",
    channels=["pytorch", "conda-forge"]
)
Combine conda and pip packages:
image = (
    modal.Image.micromamba()
    .micromamba_install("cudatoolkit", "cudnn", channels=["conda-forge"])
    .pip_install("transformers", "datasets")
)

Custom Dockerfiles

Build from a Dockerfile:
image = modal.Image.from_dockerfile("./Dockerfile")
With build arguments:
image = modal.Image.from_dockerfile(
    "./Dockerfile",
    build_args={"PYTHON_VERSION": "3.11"}
)

Registry images

Use pre-built images from Docker registries:
# Docker Hub
image = modal.Image.from_registry("nvidia/cuda:12.0.0-base-ubuntu22.04")

# Private registry
image = modal.Image.from_registry(
    "myregistry.com/myimage:latest",
    secret=modal.Secret.from_name("registry-credentials")
)
When using registry images, Modal adds the Modal runtime dependencies automatically. This ensures Functions and Classes can communicate with Modal’s infrastructure.

Copying files

Add local files to the image:
image = modal.Image.debian_slim().copy_local_file(
    "./config.yaml",
    "/root/config.yaml"
)
Copy entire directories:
image = modal.Image.debian_slim().copy_local_dir(
    "./models",
    "/root/models"
)

Environment variables

Set environment variables in the image:
image = modal.Image.debian_slim().env({
    "PYTHONUNBUFFERED": "1",
    "MODEL_PATH": "/root/models",
    "LOG_LEVEL": "INFO"
})

Working directory

Set the default working directory:
image = modal.Image.debian_slim().workdir("/app")

Python version

Specify a Python version:
# Use Python 3.11
image = modal.Image.debian_slim(python_version="3.11")

# Use a specific micro version
image = modal.Image.debian_slim(python_version="3.11.7")
Supported Python versions depend on the image builder version. Modal currently supports Python 3.10, 3.11, 3.12, and 3.13.

Image builder versions

Modal periodically releases new image builder versions with updated dependencies:
image = modal.Image.debian_slim(builder_version="2024.10")
Available versions:
  • "2023.12" (deprecated)
  • "2024.04"
  • "2024.10" (current default)
  • "2025.06"
  • "PREVIEW" (experimental)
Newer builder versions include updated system packages and Python dependencies. Use consistent builder versions across your images for reproducibility.

Chaining operations

Image methods return new Image objects, so you can chain operations:
image = (
    modal.Image.debian_slim(python_version="3.11")
    .apt_install("git", "curl")
    .pip_install("numpy", "pandas")
    .env({"PYTHONUNBUFFERED": "1"})
    .workdir("/app")
)

Using images with functions

Assign images to Functions:
image = modal.Image.debian_slim().pip_install("requests")

app = modal.App()

@app.function(image=image)
def fetch_data(url: str):
    import requests
    return requests.get(url).json()
Set a default image for the entire App:
image = modal.Image.debian_slim().pip_install("numpy", "pandas")

app = modal.App(image=image)

@app.function()  # Uses the app's default image
def process_data():
    import numpy as np
    import pandas as pd
    pass

GPU images

For GPU workloads, Modal automatically includes CUDA libraries:
image = (
    modal.Image.debian_slim()
    .pip_install("torch", "torchvision")
)

@app.function(image=image, gpu="A100")
def train_model():
    import torch
    print(torch.cuda.is_available())  # True
For conda-based GPU setups:
image = (
    modal.Image.micromamba()
    .micromamba_install(
        "pytorch",
        "torchvision",
        "pytorch-cuda=12.1",
        channels=["pytorch", "nvidia"]
    )
)

Image caching

Modal caches built images to speed up subsequent deployments. Images are rebuilt when:
  • The image definition changes
  • Dependencies are updated
  • Base images are updated
Structure your image builds with frequently-changing dependencies last to maximize cache hits:
image = (
    modal.Image.debian_slim()
    .apt_install("ffmpeg")              # Rarely changes
    .pip_install("numpy", "pandas")     # Stable dependencies
    .pip_install_from_requirements("requirements.txt")  # Your app deps
)

Force rebuilds

Force a complete image rebuild:
image = modal.Image.debian_slim().pip_install("package", force_build=True)

Best practices

Minimize image size

Smaller images deploy faster:
# Good: Only install what you need
image = modal.Image.debian_slim().pip_install("requests")

# Avoid: Installing unnecessary packages
image = modal.Image.debian_slim().pip_install(
    "requests", "numpy", "pandas", "scipy", "matplotlib"
)  # Only if all are needed

Pin dependency versions

Pin versions for reproducibility:
image = modal.Image.debian_slim().pip_install(
    "numpy==1.24.3",
    "pandas==2.0.2",
    "scikit-learn==1.3.0"
)

Use requirements files for complex dependencies

For projects with many dependencies, use requirements.txt:
image = modal.Image.debian_slim().pip_install_from_requirements(
    "requirements.txt"
)

Leverage image caching

Order operations from least to most frequently changing:
image = (
    modal.Image.debian_slim()
    .apt_install("system-packages")     # Changes rarely
    .pip_install("stable-dependencies")  # Changes occasionally  
    .copy_local_dir("./src", "/app")   # Changes frequently
)

Reuse images across functions

Define images once and reuse them:
ml_image = (
    modal.Image.debian_slim()
    .pip_install("torch", "transformers", "datasets")
)

@app.function(image=ml_image)
def train():
    pass

@app.function(image=ml_image)
def evaluate():
    pass

Build docs developers (and LLMs) love