--- title: Cache and Manage Container Images sidebar_label: Cache images --- import { Download, Archive, RefreshCw, Lock, Trash2 } from 'react-feather'; # Cache and Manage Container Images Pulling a multi-gigabyte image on every job wastes time and hits registry rate limits. This page covers the two ways to avoid that — **squash files on shared storage** and **named containers** — plus private registries and cleanup. }, { href: '#import-once-to-a-squash-file', label: 'Squash files', icon: }, { href: '#named-containers', label: 'Named containers', icon: }, { href: '#private-registries', label: 'Private registries', icon: }, { href: '#housekeeping', label: 'Housekeeping', icon: }, ]} /> --- ## The Problem Worth Solving When you pass a registry reference to `--container-image`, Enroot imports the image on **each node that runs a task**, into that node's own Enroot cache. That means: - A 4-node job may pull the same image four times. - A 16-task job array may pull it 16 times. - A node restart or a rolling image update clears what was cached there. - Registries rate-limit, and a large sweep can trip that limit mid-run. :::note Registry pulls go out through the cluster's security group The pull happens **from the node running the job**, so it is outbound traffic subject to the [security group attached to the cluster](/docs/tir/SlurmCluster/manage/network-security#security-groups). The default SSH group allows it; a group that restricts egress blocks pulls, and the failure looks like a plain timeout. Importing once to a squash file avoids the question entirely. ::: :::warning There is no automatic shared image cache TIR does not configure a cluster-wide squash cache for you. Sharing an image across nodes and jobs is something you set up, once, by putting a squash file on shared storage — the pattern below. If you have read elsewhere that images are automatically converted and shared, that is not how this platform behaves. ::: ### Find out where Enroot caches on your cluster Enroot's cache and data paths come from the cluster image, and they are usually under your `$HOME`. Check them on your own cluster before assuming: ```bash ssh root@ ``` ```bash cat /etc/enroot/enroot.conf 2>/dev/null enroot start --help | head -40 echo "HOME=$HOME" ``` This matters because **`$HOME` differs depending on how you log in**: | How you log in | Where `$HOME` lives | Consequence | |----------------|---------------------|-------------| | `ssh root@` (default) | Inside the login pod's own filesystem | Not shared with compute nodes; lost when the login service restarts | | A [persistent login user](/docs/tir/SlurmCluster/connect/login-user-management) | On the shared filesystem you nominated | Shared with every node and durable across restarts | Either way, the recommendation is the same: **store the artefacts you care about at an explicit path on PFS/SFS**, not wherever a default happens to point. --- ## Import Once to a Squash File This is the pattern to standardise on. ### 1. Import the image on the login node ```bash ssh root@ ``` ```bash mkdir -p /pfs/images cd /pfs/images enroot import -o pytorch-25.09.sqsh docker://nvcr.io/nvidia/pytorch:25.09-py3 ``` `enroot import` pulls the layers and writes a single squashfs file. Expect this to take a few minutes and a few GB. ```bash ls -lh /pfs/images/ ``` ### 2. Point jobs at the squash file ```bash srun --container-image=/pfs/images/pytorch-25.09.sqsh \ --container-mounts=/pfs:/pfs \ python train.py ``` Every node reads the same file from shared storage. No registry traffic, no per-node duplication, and start-up is consistent. :::tip Why this works on TIR PFS, SFS and Weka volumes are mounted from the same filesystem on the login node **and** on every worker, at the path you chose. A squash file written on the login node is immediately readable by every node in the allocation — which is exactly what a shared image cache needs. ::: ### 3. Keep a naming convention Squash files are opaque once you have a few. Encode the source tag in the filename: ```bash /pfs/images/pytorch-25.09-py3.sqsh /pfs/images/nemo-25.07.sqsh /pfs/images/vllm-0.11.0.sqsh /pfs/images/internal-trainer-v42.sqsh ``` And keep a note of provenance next to them: ```bash cat >> /pfs/images/README.md <<'EOF' pytorch-25.09-py3.sqsh docker://nvcr.io/nvidia/pytorch:25.09-py3 imported 2026-09-10 internal-trainer-v42.sqsh docker://registry.example.com/ml/trainer:v42 imported 2026-09-10 EOF ``` :::danger Never write a squash file to a dataset mount Dataset volumes are mounted **read-only**. Use PFS, SFS or Weka. Do not use `/tmp`, `/dev/shm` or any node-local path — those are per-node and defeat the entire purpose. ::: ### Refreshing an image Squash files do not update themselves. When the upstream tag moves, re-import to a new filename and switch your job scripts over — that way an in-flight run is never disturbed: ```bash enroot import -o /pfs/images/pytorch-25.11-py3.sqsh docker://nvcr.io/nvidia/pytorch:25.11-py3 ``` --- ## Named Containers `--container-name` gives an imported container a name on that node, so subsequent steps and jobs on the same node start instantly. ```bash srun --container-image=/pfs/images/pytorch-25.09.sqsh \ --container-name=pytorch \ --container-mounts=/pfs:/pfs \ python step_one.py # Same node, same allocation — no re-import srun --container-name=pytorch --container-mounts=/pfs:/pfs python step_two.py ``` | Use it for | Not for | |------------|---------| | Several steps in one job script | Sharing a container **between nodes** — the name is node-local | | Repeated jobs landing on the same node | Long-term storage of a modified environment; use a squash file | :::note A name is per node A container created as `pytorch` on `slinky-0` does not exist on `slinky-1`. For multi-node jobs, combine `--container-name` with a **squash file** as the image source so each node has a cheap local name backed by the same shared artefact. ::: You can also manage containers directly with the Enroot CLI: ```bash enroot list # containers on this node enroot create --name pytorch /pfs/images/pytorch-25.09.sqsh enroot remove pytorch ``` --- ## Private Registries To pull from a registry that needs credentials, Enroot reads a per-user credentials file. The exact location follows Enroot's configuration on your cluster — check it first: ```bash cat /etc/enroot/enroot.conf 2>/dev/null | grep -i config ``` The credentials file uses one `machine` line per registry: ```bash mkdir -p ~/.config/enroot cat > ~/.config/enroot/.credentials <<'EOF' machine nvcr.io login $oauthtoken password machine registry.example.com login password EOF chmod 600 ~/.config/enroot/.credentials ``` Then import as usual: ```bash enroot import -o /pfs/images/internal-trainer-v42.sqsh docker://registry.example.com/ml/trainer:v42 ``` :::warning Credentials and shared home directories If your `$HOME` is on shared storage — which it is for [persistent login users](/docs/tir/SlurmCluster/connect/login-user-management) — then a credentials file there is readable by every node under your identity. Keep it at mode `600`, use a scoped read-only registry token rather than a personal password, and rotate it on the schedule your organisation requires. Never put registry credentials in a job script, a [prolog script](/docs/tir/SlurmCluster/slurm-configuration/prolog-epilog), or a [lifecycle script](/docs/tir/SlurmCluster/getting-started/create-cluster#lifecycle-script-optional) — those are stored with the cluster configuration. ::: The recommended pattern is to **import once on the login node** with your credentials and have jobs reference the resulting squash file. Job scripts then need no credentials at all. :::info Kubernetes image pull secrets are unrelated The cluster's own components (controller, workers, login node) are Kubernetes pods and use their own image pull configuration. That has nothing to do with the registry your **job** containers come from. ::: --- ## Housekeeping Squash files are large and shared storage is billed. Audit them periodically. ```bash du -sh /pfs/images/* ``` ```bash # Remove a squash file no job references any more rm /pfs/images/pytorch-24.03-py3.sqsh ``` ```bash # Clear a node-local Enroot container enroot list enroot remove ``` | Check | Why | |-------|-----| | Old squash files | Each is often 5–20 GB; a year of tags adds up fast | | Duplicate imports of the same tag | Usually two people importing independently — agree one shared path | | `/pfs/images` growth against your PFS quota | A full filesystem fails jobs in confusing ways, including checkpoint writes | :::tip Watch the storage, not just the cluster Storage is billed separately from the cluster and **keeps billing after the cluster is terminated**. See [Billing](/docs/tir/SlurmCluster/billing#storage-is-billed-separately). ::: --- ## Related Resources - [Containers overview](/docs/tir/SlurmCluster/containers/) - [Run containers in jobs](/docs/tir/SlurmCluster/containers/run-containers) - [Multi-node container training](/docs/tir/SlurmCluster/containers/multi-node-training) - [Troubleshoot container jobs](/docs/tir/SlurmCluster/troubleshoot/containers) - [Storage and volumes](/docs/tir/SlurmCluster/manage/storage)