Including Local Files and Source Code
Copy local files into an image
To bake local application files into a Flyte image, start with a named, extendable image and add source layers with with_source_file() or with_source_folder():
from pathlib import Path
import flyte
from flyte import Image
image = (
Image.from_debian_base(registry="localhost", name="my-image")
.with_source_file(Path("config.yaml"))
.with_source_folder(Path("local_package"), dst="/root/local_package")
)
env = flyte.TaskEnvironment(name="my-environment", image=image)
Image is a frozen dataclass. Each with_* call returns a new image rather than modifying the preceding one. Image.from_debian_base() creates an extendable base image; the explicit registry and name above also make the destination image identity explicit. Every source-copy call appends a CopyConfig layer.
Copy one file or several files
with_source_file() accepts either one Path or a list of Path objects. The destination defaults to ".":
from pathlib import Path
from flyte import Image
image = Image.from_debian_base(registry="localhost", name="test-image").with_source_file(
[Path("my_code.py"), Path("helper.py")]
)
A list creates one CopyConfig layer per file. The method rejects duplicate basenames in the same destination immediately, because files such as pkg_a/my_code.py and pkg_b/my_code.py would otherwise target the same filename:
from pathlib import Path
from flyte import Image
image = Image.from_debian_base(registry="localhost", name="test-image").with_source_file(
[Path("my_code.py"), Path("sub/my_code.py")]
)
The second example raises ValueError with an overwrite message. To copy files to a particular directory, pass that directory as dst; the value is stored on each CopyConfig layer and is also included in the image hash.
CopyConfig validates the source when Image.validate() is called (or when the image builder processes the image), not when the layer is created. A nonexistent path raises ValueError; a directory passed to with_source_file() and a non-directory passed to with_source_folder() also fail validation. The source contents participate in the image hash, including the contents of a single file:
from pathlib import Path
from flyte import Image
file = Path("version.md")
file.write_text("first")
base = Image.from_debian_base(registry="localhost", name="test-image")
h1 = base.with_source_file(file)._get_hash_digest()
file.write_text("second")
h2 = base.with_source_file(file)._get_hash_digest()
assert h1 != h2
Copy a directory
with_source_folder(src, dst=".", copy_contents_only=False) creates a directory CopyConfig layer. By default, the source directory itself is placed below the destination using its name. Thus, with_source_folder(Path("src")) uses ./src as the destination rather than copying the contents directly into ..
Use copy_contents_only=True when the directory's contents should go directly into the destination:
from pathlib import Path
from flyte import Image
image = (
Image.from_debian_base(registry="localhost", name="test-image")
.with_source_folder(Path("src"), dst="/root", copy_contents_only=True)
)
With copy_contents_only=True, dst remains /root; with the default False, the method changes a "." destination to ./src (where src is Path.name). Directory hashing and copying use Dockerfile-style ignore matching when an ignore matcher applies.
A sibling package in the uv monorepo example uses this explicit directory copy in addition to dependency installation:
import pathlib
import flyte
MY_APP_ROOT = pathlib.Path(__file__).parent.parent.parent
MY_LIB_PKG = MY_APP_ROOT.parent / "my_lib" / "src" / "my_lib"
env = flyte.TaskEnvironment(
name="my_app",
image=flyte.Image.from_debian_base()
.with_uv_project(pyproject_file=MY_APP_ROOT / "pyproject.toml")
.with_source_folder(MY_LIB_PKG)
.with_code_bundle(),
)
The example comments explain that the sibling package is an editable path dependency during the image build, while its package files are also baked into the image so runtime imports do not rely on the build-stage-only editable-install .pth path.
Use a base image that is not created by from_debian_base
Image.from_base() represents an already-existing base URI, but it has no name and is not extendable by default. Clone it before adding source layers:
from pathlib import Path
from flyte import Image
image = Image.from_base("python:3.12-slim").clone(
registry="localhost",
name="my-image",
extendable=True,
).with_source_folder(Path("src"))
Adding a layer requires a name. Image.clone() also rejects adding a layer to a non-extendable image. Dockerfile-based images created with Image.from_dockerfile() are non-extendable because Flyte does not parse the Dockerfile to determine how additional layers should be applied.
Let Flyte bundle project source code
Use with_code_bundle() when source inclusion depends on the runner's copy_style rather than on a fixed local path:
import flyte
env = flyte.TaskEnvironment(
name="full_build",
image=flyte.Image.from_debian_base().with_code_bundle(),
)
if __name__ == "__main__":
flyte.init_from_config()
run = flyte.with_runcontext(copy_style="none", version="x").run(main, n=10)
print(run.url)
The main task in the full-build example is defined in the same file. with_code_bundle() defaults to copy_style="loaded_modules" and dst="."; it creates a deferred CodeBundleLayer, whose root_dir is not known when the image expression is constructed. The supported styles are:
Runner copy_style | Resolution of with_code_bundle() |
|---|---|
"loaded_modules" or "all" | The deferred layer is removed because source is bundled separately. |
"none" with copy_style="all" | The layer becomes a directory CopyConfig for the resolved project root_dir. |
"none" with copy_style="loaded_modules" | The CodeBundleLayer remains, with root_dir set; imported module files are selected from sys.modules. |
Deployment image construction and local run planning resolve these layers before building or hashing images. Calling CodeBundleLayer.update_hash() before resolution raises ValueError: root_dir not set for CodeBundleLayer; application code should configure the layer through with_code_bundle() and let the deployment/run flow resolve it.
For example, the full-build example deliberately selects copy_style="none":
flyte.init_from_config()
run = flyte.with_runcontext(copy_style="none", version="x").run(main, n=10)
With a runner copy style other than "none", the same with_code_bundle() call is a no-op after resolution, because Flyte bundles the source separately.
Apply .dockerignore rules
Attach an explicit ignore file with with_dockerignore() when the image's copied source should use a particular set of patterns:
from pathlib import Path
import flyte
image = (
flyte.Image.from_debian_base(install_flyte=False)
.with_source_folder(Path("src"))
.with_dockerignore(Path(".dockerignore"))
)
env = flyte.TaskEnvironment(name="t1", image=image)
with_dockerignore(path) adds a frozen DockerIgnore layer. The builder reads non-empty, non-comment patterns from the selected file and applies them to copied directories and code/project content. The ignore file is rooted at its own location, so a project-root pattern such as src/data.bin can match a file below a copied src directory.
An explicit DockerIgnore layer takes precedence over the initialized project-root fallback. If there is no explicit layer, Flyte looks for .dockerignore under root_dir from initialization. If more than one explicit ignore layer is present, the last non-empty path wins. The same selection affects image hashing: DockerIgnore.update_hash() hashes the referenced file contents when it exists, and otherwise hashes the literal path string. Consequently, changing ignore-file contents changes the image hash and URI, while content excluded by the active rules does not change the URI. For example, the image with src/data.bin ignored has the same URI as an otherwise equivalent image whose copied src directory never contained that file.
Understand image identity and layer inspection
The source APIs are backed by concrete layer types:
with_source_file()createsCopyConfig(path_type=0, src=..., dst=...).with_source_folder()createsCopyConfig(path_type=1, src=..., dst=...).with_code_bundle()createsCodeBundleLayer(copy_style=..., dst=...)until runtime resolution.with_dockerignore()createsDockerIgnore(path=...).
Image.validate() delegates to each layer's validate() method. CopyConfig checks the path type and existence, while CodeBundleLayer.validate() does nothing at construction time because its root directory is deferred. _get_hash_digest() combines the base image or Dockerfile contents with layer data, source contents, destinations, and applicable ignore state. A cloned image's uri is formed as <registry>/<name>:<content-hash> (or <name>:<content-hash> without a registry); an unmodified image returns its original base URI.
This also means that changing a destination, adding a source layer, or changing non-ignored source content produces a different image identity. The immutable chain makes related images easy to derive without changing an earlier environment:
from pathlib import Path
from flyte import Image
image1 = Image.from_debian_base(name="base-image", python_version=(3, 12)).with_pip_packages("flyte", "ty")
image2 = image1.with_apt_packages("vim")
image3 = image2.with_commands(["pwd", "ls -al"]).with_requirements(Path("requirements.txt"))
image4 = image3.with_source_file(Path("Makefile")).with_env_vars({"hello": "world3"})
Troubleshoot source-copy failures
TypeErrorwhen creating an image: do not callImage(...)directly.Image.__post_init__()rejects direct construction; use afrom_*constructor.- “Cannot add additional layer…”: give the image a
nameand make it extendable withclone(..., extendable=True).Image.from_base()does not do this automatically. - Missing or wrong-kind source path:
CopyConfig.validate()runs throughimage.validate(), so create or correct the path before validation/build processing. - Duplicate filenames in a file list: use unique basenames for files copied to the same
dst, or make separate calls with different destinations.with_source_file()rejects duplicate basenames in one list. - Unexpected folder location: the default folder destination is
./<src.name>. Setcopy_contents_only=Trueto place its contents directly indst. - Code is not in the image: check runner
copy_style.with_code_bundle()only activates image copying for"none";"loaded_modules"and"all"cause the deferred layer to be stripped after source is bundled separately. - Unresolved code-bundle hashing: do not hash a newly created
CodeBundleLayerdirectly. Runtime resolution must setroot_dirfirst. - Ignore pattern appears ineffective: patterns are interpreted relative to the ignore file's directory, and the explicit
with_dockerignore()layer overrides initialization's<root_dir>/.dockerignore. Verify both the selected file and the pattern's project-root-relative path.