Skip to main content

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_styleResolution 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() creates CopyConfig(path_type=0, src=..., dst=...).
  • with_source_folder() creates CopyConfig(path_type=1, src=..., dst=...).
  • with_code_bundle() creates CodeBundleLayer(copy_style=..., dst=...) until runtime resolution.
  • with_dockerignore() creates DockerIgnore(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​

  • TypeError when creating an image: do not call Image(...) directly. Image.__post_init__() rejects direct construction; use a from_* constructor.
  • “Cannot add additional layer…”: give the image a name and make it extendable with clone(..., extendable=True). Image.from_base() does not do this automatically.
  • Missing or wrong-kind source path: CopyConfig.validate() runs through image.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>. Set copy_contents_only=True to place its contents directly in dst.
  • 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 CodeBundleLayer directly. Runtime resolution must set root_dir first.
  • 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.