Mounting local files and directories
When you run your code on Modal, it executes in a containerized environment separate from your local machine.
There are two ways to make local files available to your Modal app:
Mounting: Mounting is the process of making local files and directories accessible to your Modal function or application during runtime. Mounting is intended for files that change frequently during development. It allows you to modify your code locally and rerun it on Modal without needing to rebuild the container image each time. This can significantly speed up your development iteration cycle.
Adding files to the container image: For files that don’t change often, you can add them directly to your Modal container image during the build process with
copy_local_file
orcopy_local_dir
. This is suitable for dependencies and static assets that remain relatively constant throughout your development process.
This page is concerned with mounting. To use local files and packages within your Modal app via mounting, they either need to be automounted or explicitly mounted.
Automount
By default, with
automount=True
,
Modal mounts local Python packages that you have used (imported) in your code
but are not installed globally on your system (like those in site-packages
,
where globally installed packages reside).
For example, if you have a local module deps.py
that contains a function you
would like to import, dependency
. You import it as follows:
from deps import dependency
app = modal.App()
Modal will automatically mount deps.py
, and all of its dependencies not in
site-packages
.
All Python packages that are installed in site-packages
will be excluded from
automounting. This includes packages installed in virtual environments.
Non-Python files will be automounted only if they are located in the same
directory or subdirectory of a Python package. Note that the directory where
your Modal entrypoint is located is considered a package if it contains a
__init__.py
file and is being called as a package.
Editable-mode exclusion
If local packages that you thought were installed in site-packages
are
being automounted, it’s possible that those packages were installed in
editable-mode.
When you install a package in editable-mode (also known as “development mode”),
instead of copying the package files to the site-packages
directory, a link
(symbolic link or .egg-link file) is placed there. This link points to the
actual location of the package files, which are typically in your project
directory or a separate source directory. As a result, they may be automounted.
Automounts take precedence over PyPI packages
Automounts take precedence over PyPI packages, so in the case where
you pip_install
or otherwise include a package by building it into your image,
the automount will still trigger and shadow the site-packages
installed
version. An example of when this would happen is if you have binary parts of
local modules such that they need to be built as part of the image build.
Example #1: Simple directory structure
Let’s look at an example directory structure:
mountingexample1
├── __init__.py
├── data
│ └── my_data.jsonl
└── entrypoint.py
And let’s say your entrypoint.py
code looks like this:
import modal
app = modal.App()
@app.function()
def app_function():
print("app function")
When you run modal run entrypoint.py
from inside the mountingexample1
directory, you will see the following items mounted:
✓ Created objects.
├── 🔨 Created mount /Users/yirenlu/modal-scrap/mountingexample1/entrypoint.py
└── 🔨 Created app_function.
The data
directory is not auto-mounted, because mountingexample1
is not
being treated like a package in this case.
Now, let’s say you run cd .. && modal run mountingexample1.entrypoint
. You
should see the following items mounted:
✓ Created objects.
├── 🔨 Created mount PythonPackage:mountingexample1.entrypoint
├── 🔨 Created mount PythonPackage:mountingexample1
└── 🔨 Created app_function.
The entire mountingexample1
package is mounted, including the data
subdirectory.
This is because the mountingexample1
directory is being treated as a package.
Example #2: Global scope imports
Oftentimes when you are building on Modal, you will be migrating an existing codebase that is spread across multiple files and packages. Let’s say your directory looks like this:
mountingexample2
├── __init__.py
├── data
│ └── my_data.jsonl
├── entrypoint.py
└── package
├── __init__.py
├── package_data
│ └── library_data.jsonl
└── package_function.py
And your entrypoint code looks like this:
import modal
from package.package_function import package_dependency
app = modal.App()
@app.function()
def app_function():
package_dependency()
When you run the entrypoint code with modal run mountingexample2.entrypoint
,
you will see the following items mounted:
✓ Created objects.
├── 🔨 Created mount PythonPackage:mountingexample2.entrypoint
├── 🔨 Created mount PythonPackage:mountingexample2
└── 🔨 Created app_function.
The entire contents of mountingexample2
is mounted, including the /data
directory and the package
package inside of it.
Finally, let’s check what happens when you remove the package
import from your
entrypoint code and run it with modal run entrypoint.py
.
✓ Created objects.
├── 🔨 Created mount /Users/yirenlu/modal-scrap/mountingexample2/entrypoint.py
└── 🔨 Created app_function.
Only the entrypoint file is mounted, and nothing else.
Mounting files manually
If something that you want to have mounted is not included in an automount, you have a few options:
- Specify local files and directories through
Mount
objects. - Include local Python modules with the function
Mount.from_local_python_packages()
. - Refactor your directory structure so that the relevant files and directories are automounted as part of packages.