System Packages and Custom Commands
Add system packages and build commands
To build a Debian-based Flyte image that contains operating-system tools and performs setup during the image build, start with Image.from_debian_base() and add with_apt_packages() and with_commands() layers:
import flyte
from flyte import Image
image = (
Image.from_debian_base(name="base-image", python_version=(3, 12))
.with_pip_packages("flyte", "ty")
.with_apt_packages("vim", "curl", "git")
.with_commands(["pwd", "ls -al"])
)
env = flyte.TaskEnvironment(name="tools", image=image)
Image is a frozen dataclass with a fluent construction pattern. from_debian_base() creates the base image, and each with_* call returns a new image. The original image remains available for another environment. The complete progression in examples/image/multi_image.py demonstrates this directly:
from pathlib import Path
import flyte
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(__file__).parent / "requirements.txt")
image4 = image3.with_source_file(Path(__file__).parent.parent.parent / "Makefile").with_env_vars({"hello": "world3"})
env1 = flyte.TaskEnvironment(name="t1", image=image1)
env2 = flyte.TaskEnvironment(name="t2", image=image2, depends_on=[env1])
env3 = flyte.TaskEnvironment(name="t3", image=image3, depends_on=[env2])
env4 = flyte.TaskEnvironment(name="t4", image=image4, depends_on=[env3])
Internally, with_apt_packages() appends an AptPackages layer and with_commands() appends a Commands layer through Image.clone(). Layers retain their order, so the Docker builder processes package, source, and command setup in the order in which they were added.
Install system packages
Pass package names as separate positional arguments:
image = (
flyte.Image.from_debian_base(name="sglang-app-image", install_flyte=False)
.with_apt_packages(
"libnuma-dev",
"wget",
"curl",
"openssl",
"pkg-config",
"libssl-dev",
"build-essential",
)
)
This is the pattern used by examples/genai/sglang/sglang_app.py. Do not pass a list as one package argument. Use .with_apt_packages("git", "curl"), not .with_apt_packages(["git", "curl"]). The public method accepts *packages, while the underlying AptPackages layer requires tuple-valued, hashable fields; a list-valued package field is rejected by the shared Layer validation.
For the local Docker builder, an AptPackages layer is rendered using an apt cache mount and one installation block equivalent to:
RUN --mount=type=cache,sharing=locked,mode=0777,target=/var/cache/apt,id=apt \
apt-get update && apt-get install -y --no-install-recommends \
curl git
The exact template is APT_INSTALL_COMMAND_TEMPLATE in src/flyte/_internal/imagebuild/docker_builder.py. Because it emits apt-get directly, use an apt-compatible Debian or Ubuntu base image and package names available from its configured repositories. The generated command uses --no-install-recommends; the cache mount is generated by the builder and is not an Image option.
AptPackages stores the package tuple and optional secret mounts. Its update_hash() contributes the concatenated package names and secret representations to the image hash. Consequently, changing a package name, package order, or the order of image layers changes the cloned image identity and its hash-derived tag.
Run custom build commands
Supply a list of command text to with_commands():
import flyte
from flyte import Image
image = (
Image.from_debian_base(
registry="localhost:30000",
name="img_with_more",
install_flyte=False,
)
.with_pip_packages("requests")
.with_commands(["echo hello world", "echo hello world again"])
)
This is the image used by test_image_folders_commands() in tests/flyte/imagebuild/test_docker_builder.py. with_commands() normalizes the supplied list or tuple to a tuple and creates a Commands layer.
Do not include RUN in the command strings. The CommandsHandler in src/flyte/_internal/imagebuild/docker_builder.py prepends RUN to every command:
image = flyte.Image.from_debian_base(name="tools").with_commands(["echo hello world", "echo hello world again"])
produces two Dockerfile instructions, one for each item:
RUN echo hello world
RUN echo hello world again
Commands are inserted as raw shell text. The builder does not escape or combine them, so the command text must contain its own quoting, chaining, downloads, and cleanup behavior. Since each item becomes a separate RUN, shell state from one item is not automatically shared with the next. Combine operations that require shared shell state into one command with shell operators when appropriate.
A nontrivial setup sequence from examples/genai/sglang/sglang_app.py installs a CUDA repository and toolchain, then configures Rust:
image = (
flyte.Image.from_debian_base(name="sglang-app-image", install_flyte=False)
.with_apt_packages("libnuma-dev", "wget", "curl", "openssl", "pkg-config", "libssl-dev", "build-essential")
.with_commands(
[
"wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb",
"dpkg -i cuda-keyring_1.1-1_all.deb",
"apt-get update",
"apt-get install -y cuda-toolkit-12-8",
]
)
.with_commands(["curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && . $HOME/.cargo/env"])
.with_env_vars({"CUDA_HOME": "/usr/local/cuda-12.8", "PATH": "/root/.cargo/bin:/usr/local/cuda-12.8/bin:$PATH"})
)
The separate with_commands() calls append separate ordered Commands layers. The four CUDA commands therefore become four RUN instructions, and the Rust command becomes a later instruction.
Commands.update_hash() includes the raw command text and secret representations in the image hash. Editing a command or changing its position in the layer sequence therefore produces a different image identity.
Use build secrets
Both methods accept secret_mounts. A string is treated as a secret key, while a Secret object can describe the group, key, target mount, or environment-variable name:
import flyte
from flyte import Image
image = (
Image.from_debian_base(install_flyte=False)
.with_apt_packages("vim", secret_mounts=["apt-secret"])
.with_commands(
["echo configuring the private tool"],
secret_mounts=["key"],
)
)
The repository's examples/image/image_secret.py also shows a secret object with an explicit environment variable. The equivalent layer setup can use the string form when the secret key is sufficient:
from flyte import Image
image = Image.from_debian_base(install_flyte=False).with_apt_packages(
"vim",
secret_mounts=["apt-secret"],
)
For local Docker builds, the builder resolves each secret from an environment variable derived from its group and key. Secret(group="group", key="key") maps to GROUP_KEY; Secret(key="flyte") maps to FLYTE. If that variable is absent, the builder looks for /etc/secrets/<group>_<key>. A missing source raises FileNotFoundError with a message beginning Secret not found. The secret is a build secret mounted for the relevant RUN instruction; it is not automatically made available to the running task.
A Secret may specify as_env_var for an environment variable inside the build command or mount for a target path. Remote builds collect and deduplicate secret arguments, although each AptPackages or Commands layer still records its own secret reference for hashing. The integration test test_image_with_secrets() sets FLYTE and GROUP_KEY before building an image with apt, pip, and command secret mounts.
Image construction and extension constraints
Create images through a from_* constructor. Direct construction is rejected by Image.__post_init__:
# Use a constructor such as this one instead of Image(...).
image = Image.from_debian_base(name="tools", install_flyte=False)
A from_base() image represents an existing base URI and is not automatically layerable. Give it a name and mark the clone extendable before adding managed layers:
image = (
Image.from_base("python:3.12-slim")
.clone(name="tools", extendable=True)
.with_apt_packages("git", "curl")
)
Image.clone() rejects an added layer when the current image is not extendable, and it requires a name when adding a layer. It also rejects added layers on images created with from_dockerfile(): the SDK does not parse a user Dockerfile to determine how additional layers should be applied. Put apt installation and custom commands directly in that Dockerfile instead.
Code-generation and sandbox integrations use the same API: detected system packages are translated to with_apt_packages(), and sandbox additional_commands are translated to with_commands(). The sandbox can also add gcc, g++, and make when compiler support is needed. These integrations still produce the same ordered AptPackages and Commands layers and are subject to the same base-image and secret requirements.
Troubleshooting
| Symptom | Cause and correction |
|---|---|
TypeError: Direct instantiation of Image not allowed... | Image must be created with a from_* constructor, such as Image.from_debian_base() or Image.from_base(). |
Adding a layer to Image.from_base(...) raises a non-extendable-image ValueError | Clone the base with a name and extendable=True before calling with_apt_packages() or with_commands(). |
| Adding a layer reports that the image has no name | clone(addl_layer=...) requires a name for a cloned image. Supply name= in the initial clone or use Image.from_debian_base(name=...). |
with_apt_packages(["git", "curl"]) fails layer validation | Package arguments are positional strings. Write with_apt_packages("git", "curl"). |
The build contains RUN RUN ... or an invalid command | with_commands() expects command text without the Dockerfile keyword. Write with_commands(["echo hello"]), not with_commands(["RUN echo hello"]). |
A build fails with Secret not found | Set the derived environment variable, such as FLYTE or GROUP_KEY, or provide the corresponding /etc/secrets/<group>_<key> file for a local Docker build. |
apt-get cannot locate a package | AptPackages uses the base image's apt repositories. Check that the base is Debian/Ubuntu-compatible and that the package name exists there. |
| A Dockerfile-based image cannot accept these methods | from_dockerfile() images cannot receive Flyte-managed layers. Add the system packages and commands to the Dockerfile itself. |