# Images, Volumes and Secrets

> Build container images from Python, share files between workers with Volumes, and pass credentials with Secrets.

Source: https://nodus-platform-site.pages.dev/docs/guides/python/storage/
Build revision: 211ad9f836655b1c3a2668c4693e442471f28614

## Images

```python
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.

## Volumes

```python
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

```python
"""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())
```

## Secrets

```python
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.
