Romain Lefebvre
10/10/2025, 2:26 PMMarvin
10/10/2025, 2:26 PMMarvin
10/10/2025, 2:31 PMfrom_source
) and run pip install -r requirements-extras.txt
before Prefect runs your flow. See Docker infra guidance: Docker deployments (Prefect 3.x docs) and workers overview: Workers (concepts).
- Kubernetes worker: use an initContainer or a custom command on the main container to install the extras, then start the flow. See Kubernetes deployments (Prefect 3.x docs). A common pattern is:
- Mount a writable volume (e.g., /opt/site-packages)
- Init container: pip install --target /opt/site-packages -r /workspace/extra-requirements.txt
- Main container: set PYTHONPATH=/opt/site-packages
before starting the flow
- Process worker (or anywhere): install at the top of the flow, then import. If you can’t bake the deps into the image, you can install to a local directory and add it to sys.path
. This avoids needing to restart the interpreter.
Example: install-on-the-fly inside your flow
from prefect import flow, task, get_run_logger
import sys, subprocess, tempfile
from pathlib import Path
@task
def ensure_dynamic_deps(packages: list[str]) -> str:
target = Path(tempfile.gettempdir()) / "prefect_extra_deps"
target.mkdir(parents=True, exist_ok=True)
cmd = [
sys.executable, "-m", "pip", "install",
"--no-input", "--no-cache-dir",
"--target", str(target),
*packages,
]
subprocess.check_call(cmd)
# make installed packages importable in this process
if str(target) not in sys.path:
sys.path.insert(0, str(target))
return str(target)
@flow
def my_flow():
# pin versions for reproducibility
ensure_dynamic_deps(["somepkg==1.2.3", "otherpkg>=0.9"])
import somepkg # now available
# ... rest of your flow logic ...
if __name__ == "__main__":
my_flow()
Notes and tradeoffs:
- Task runner: if you use a ProcessPoolTaskRunner, modifying sys.path
in one process won’t affect others. Prefer the default or a ThreadPoolTaskRunner for this pattern, or run the install step in the flow function before any tasks that import those packages.
- Speed/caching: consider using a persistent pip cache (mount PIP_CACHE_DIR
in Docker/K8s), hosting wheels internally, or using uv/mamba for faster installs.
- Security/reproducibility: pin versions and treat dynamic installs as a last resort. When possible, keep a base image with common deps and layer dynamic extras on top at runtime.
- Custom worker: for fully standardized pre-start behavior, you can build a custom worker template that runs a pre-flight script to install extras before the flow process starts. See Developing a custom worker.
If you’re actually on Prefect 2.x, you can still use pull steps to run shell commands like pip install -r ...
as part of the deployment steps, but we recommend moving to 3.x. Reference for 2.x: Prefect 2.x deployment guide.
Can you share:
- Which Prefect version you’re on (2.x or 3.x)?
- Your worker/infrastructure (Docker worker, Kubernetes worker, or Process worker)?
- Whether you can add a small bootstrap command to your container entrypoint?
With that, I can give you a concrete snippet (Docker/K8s job template or flow code) tailored to your setup.Romain Lefebvre
10/10/2025, 2:41 PMMarvin
10/10/2025, 2:41 PMMarvin
10/10/2025, 2:43 PMvolumes:
- name: pydeps
emptyDir: {}
initContainers:
- name: install-dynamic-deps
image: YOUR_FLOW_IMAGE # match Python/pip with the main container
command: ["/bin/sh","-lc"]
args:
- |
set -e
if [ -n "$EXTRA_PY_DEPS" ]; then
echo "Installing dynamic deps: $EXTRA_PY_DEPS"
python - <<'PY'
import os, sys, subprocess
pkgs = os.environ.get("EXTRA_PY_DEPS","").split()
if pkgs:
subprocess.check_call([sys.executable, "-m", "pip", "install",
"--no-cache-dir",
"--target", "/opt/pydeps", *pkgs])
PY
else
echo "No EXTRA_PY_DEPS set; skipping"
fi
env:
- name: EXTRA_PY_DEPS
value: "" # override per-deployment via job variables
volumeMounts:
- name: pydeps
mountPath: /opt/pydeps
containers:
- name: prefect-job
env:
- name: PYTHONPATH
value: /opt/pydeps:$(PYTHONPATH)
volumeMounts:
- name: pydeps
mountPath: /opt/pydeps
How to pass the packages list per flow
- Easiest: set an env var (EXTRA_PY_DEPS) in the deployment’s job variables so each flow’s deployment can specify different packages. For example: “pydantic==2.9.2 httpx<1.0 numpy==1.26.4”.
- If you prefer Prefect Variables:
- Option A (template at deploy-time): Reference the workspace variable in the deployment/job variables so it’s rendered into EXTRA_PY_DEPS. See variables and templating: Variables (Prefect 3.x docs).
- Option B (fetch at run-time in initContainer): Change the initContainer command to fetch the variable directly:
python - <<'PY'
import os, subprocess, sys
from prefect import variables
pkgs = variables.get("YOUR_VARIABLE_NAME") or ""
if pkgs.strip():
subprocess.check_call([sys.executable, "-m", "pip", "install",
"--no-cache-dir", "--target", "/opt/pydeps", *pkgs.split()])
PY
Note: this requires the pod’s initContainer to have Prefect and API env configured (the Kubernetes worker usually injects PREFECT_API_URL and auth env into the pod; if not, add them to the initContainer env).
Tips
- Use your own package index/cache for speed (PIP_INDEX_URL, PIP_EXTRA_INDEX_URL). You can add these as env vars in the job template and mount a pip cache volume if desired.
- Keep versions pinned in the variable to ensure reproducibility.
- Use the same base image for init and main container to avoid Python/pip ABI mismatches.
DocsMarvin
10/10/2025, 2:43 PMRomain Lefebvre
10/10/2025, 2:50 PMcommand
of the image to first install dynamically deps and then run the flow ?Marvin
10/10/2025, 2:50 PMMarvin
10/10/2025, 2:51 PM#!/bin/sh
set -e
if [ -n "$EXTRA_PY_DEPS" ]; then
echo "Installing dynamic deps: $EXTRA_PY_DEPS"
python - <<'PY'
import os, sys, subprocess
pkgs = os.environ.get("EXTRA_PY_DEPS","").split()
if pkgs:
subprocess.check_call([sys.executable, "-m", "pip", "install",
"--no-cache-dir",
"--target", "/opt/pydeps", *pkgs])
PY
export PYTHONPATH="/opt/pydeps:${PYTHONPATH}"
fi
exec "$@"
Dockerfile:
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh && mkdir -p /opt/pydeps
ENTRYPOINT ["/entrypoint.sh"]
# do not hardcode CMD; let Prefect set it per run
InitContainer pattern (K8s-native)
- Use this when you prefer strict separation and no changes to the main image command:
volumes:
- name: pydeps
emptyDir: {}
initContainers:
- name: install-dynamic-deps
image: YOUR_FLOW_IMAGE
command: ["/bin/sh","-lc"]
args:
- |
set -e
if [ -n "$EXTRA_PY_DEPS" ]; then
echo "Installing dynamic deps: $EXTRA_PY_DEPS"
python - <<'PY'
import os, sys, subprocess
pkgs = os.environ.get("EXTRA_PY_DEPS","").split()
if pkgs:
subprocess.check_call([sys.executable, "-m", "pip", "install",
"--no-cache-dir",
"--target", "/opt/pydeps", *pkgs])
PY
else
echo "No EXTRA_PY_DEPS set; skipping"
fi
env:
- name: EXTRA_PY_DEPS
value: "" # set per deployment via job variables or template
volumeMounts:
- name: pydeps
mountPath: /opt/pydeps
containers:
- name: prefect-job
env:
- name: PYTHONPATH
value: /opt/pydeps:$(PYTHONPATH)
volumeMounts:
- name: pydeps
mountPath: /opt/pydeps
Feeding the package listMarvin
10/10/2025, 2:51 PMfrom prefect import variables; variables.get("YOUR_VAR")
and install those packages. Docs: Variables (Prefect 3.x docs)
- Pin versions for reproducibility. For speed, consider internal wheels or a cache (PIP_INDEX_URL, PIP_EXTRA_INDEX_URL, PIP_CACHE_DIR).
Avoid
- Kubernetes postStart lifecycle hooks for this purpose; they run concurrently with app start and are racey.
- Modifying system site-packages in a read-only/non-root container. Use --target
or a user directory + PYTHONPATH.
Docs
- K8s deployments: Kubernetes deployments (Prefect 3.x docs)
- Workers: Workers (concepts)
- Custom worker templates: Developing a custom worker
If you share how you manage your K8s work pool base job template (UI or as JSON/YAML) and the variable name holding the package list, I can provide an exact snippet you can paste in.