Skip to content

Images, Volumes and Secrets

View Markdown
image = (
nodus.Image.from_registry("nodus/pytorch:2.8-cuda12.8")
.apt_install("git")
.pip_install("transformers==4.57.6", "peft")
.env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
.add_local_python_source("mylib")
)
image = nodus.Image.from_dockerfile("Dockerfile", context=".")
image = nodus.Image.debian_slim("3.12").uv_pip_install("numpy")

Nothing is built until an App that uses the image runs, or until you call image.build(), which streams the build log. An Image is named by the hash of its steps, so the same chain reuses the same build. add_local_file, add_local_dir and add_local_python_source upload local files into the image; uv_sync(".") installs a uv project from its lockfile.

vol = nodus.Volume.from_name("data", create_if_missing=True)
vol.put_file("./local.csv", "/train/local.csv")
print(vol.listdir("/train"))
with vol.batch_upload() as up:
up.put_directory("./dataset", "/train")
weights = nodus.Volume.import_from("llama", huggingface="meta-llama/Llama-3.1-8B", revision="0e9e39f")

A Volume made by create_if_missing=True is ReadWriteMany, with Modal’s semantics: each worker mounts the latest revision when it starts, vol.commit() publishes that worker’s changed files as a new revision (the last writer wins per path), and vol.reload() mounts the newest revision. Close open files before reload(). Uploads and downloads use the nodus CLI that the package installs.

examples/python/storage/app.py
"""Workers share a ReadWriteMany Volume: each commits its result, and a reload sees everyone's.
Run it with `nodus run examples/python/storage/app.py`.
"""
from pathlib import Path
import nodus
app = nodus.App("storage")
results = nodus.Volume.from_name("storage-example", create_if_missing=True)
token = nodus.Secret.from_dict({"GREETING": "hello"})
@app.function(volumes={"/results": results}, secrets=[token], max_workers=4, max_cost=1)
def work(i: int) -> str:
import os
Path(f"/results/{i}.txt").write_text(f"{os.environ['GREETING']} {i}\n")
results.commit() # publish this worker's files as a new revision
return f"{i}.txt"
@app.function(volumes={"/results": results}, max_cost=1)
def collect() -> list[str]:
results.reload() # mount the latest revision
return sorted(p.name for p in Path("/results").iterdir())
@app.local_entrypoint()
def main() -> None:
print(list(work.map(range(4))))
print(collect.remote())
hf = nodus.Secret.from_name("hf-token") # an existing Secret
cfg = nodus.Secret.from_dict({"API_TOKEN": "..."}) # created with the App and deleted with it
env = nodus.Secret.from_dotenv(".env")
nodus.Secret.create("hf-token", {"HF_TOKEN": "hf_..."}) # writes a new version if it exists

Every key arrives in the container as an environment variable and as a file under /run/secrets/<name>/<key>. Values are never returned by the API, and running containers keep the version they started with.