Slackbot
05/19/2026, 7:54 AMMarvin
05/19/2026, 7:54 AMMarvin
05/19/2026, 7:56 AMget_client()
Async client:
python
from prefect import get_client
async def main():
async with get_client() as client:
response = await client.hello()
print(response.json())
Sync client:
python
from prefect import get_client
def main():
with get_client(sync_client=True) as client:
response = client.hello()
print(response.json())
Important: use the client as a context manager:
- async with get_client() for async code
- with get_client(sync_client=True) for sync code
A closed client should not be reused.
Example: read flow runs
python
from prefect import get_client
from prefect.client.schemas.filters import FlowRunFilter
async def show_completed_runs():
async with get_client() as client:
runs = await client.read_flow_runs(
flow_run_filter=FlowRunFilter(
state={"type": {"any_": ["COMPLETED"]}}
),
limit=10,
)
for run in runs:
print(run.name, run.state.type)
Example: read deployments with pagination
python
from prefect import get_client
async def read_all_deployments():
deployments = []
offset = 0
page_size = 200
async with get_client() as client:
while True:
page = await client.read_deployments(limit=page_size, offset=offset)
if not page:
break
deployments.extend(page)
if len(page) < page_size:
break
offset += page_size
return deployments
When should you use the client?
- Querying flow/task runs
- Creating or inspecting deployments
- Managing work pools / queues
- Reading or setting variables, artifacts, blocks
- Performing lower-level orchestration actions not covered by decorators alone
Common caveats:
- Async and sync clients are different; don’t mix with / async with
- Results are often paginated; use `limit`/`offset` if you need everything
- get_client() may reuse a client inside the same active context
- Auth is usually picked up from your Prefect settings/environment automatically
If you want, I can also give you:
1. a cheatsheet of the most useful client methods, or
2. a real example for a specific use case like listing deployments, cancelling runs, or triggering a deployment.Marvin
05/19/2026, 8:10 AMMuhammad Ridwan
05/19/2026, 8:11 AMMarvin
05/19/2026, 8:11 AMMarvin
05/19/2026, 8:11 AMprefect CLI command is not provided by the prefect-client package.
In Prefect 3.x:
- prefect package → includes the SDK and the prefect CLI
- prefect-client package → client library for interacting with the API, but not the full CLI
So if you install only prefect-client, you should not expect the prefect shell command to be available.
If you want the CLI, install `prefect`:
bash
pip install prefect
If helpful:
- Prefect docs: docs.prefect.io
- Python client API: Prefect client API
If you want, I can also explain the practical difference between prefect vs prefect-client packages.Marvin
05/19/2026, 8:12 AMprefect.settings is available in prefect-client in Prefect 3.x.
That means if you're using the lighter prefect-client package, you can still do things like:
python
from prefect.settings import PREFECT_API_URL, get_current_settings
print(PREFECT_API_URL.value())
settings = get_current_settings()
print(settings.api.url)
The client package itself uses prefect.settings internally for things like:
- API URL
- API key
- retry behavior
- Cloud/server connection settings
So settings support is not just present — it’s part of how the client works.
Useful docs:
- `prefect.settings` API
- Prefect client API
Example with temporary settings override:
python
from prefect.settings import PREFECT_API_URL, temporary_settings
from prefect import get_client
async def main():
with temporary_settings(updates={PREFECT_API_URL: "<http://localhost:4200/api>"}):
async with get_client() as client:
response = await client.hello()
print(response.json())
A couple caveats:
- Some settings namespaces may exist even if the related feature is not especially useful in prefect-client alone
- The CLI is not included in prefect-client, even though settings APIs are
- Settings sources still work normally: env vars, profiles, .env, prefect.toml, etc.
So the short answer is: yes, prefect.settings is available and supported in prefect-client.
If you want, I can also show:
- the minimum imports needed for prefect-client only, or
- the difference between from prefect import get_client vs from prefect.client.orchestration import get_client in this setup.Muhammad Ridwan
05/19/2026, 8:53 AMMarvin
05/19/2026, 8:54 AMMarvin
05/19/2026, 8:55 AMcreate_flow_run_from_deployment()
Usually the flow is:
1. Look up the deployment
2. Get its id
3. Create a flow run from that deployment
Docs:
- Prefect client API
Async example
python
from prefect.client.orchestration import get_client
async def trigger():
async with get_client() as client:
deployment = await client.read_deployment_by_name("my-flow/my-deployment")
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id,
parameters={"x": 1, "y": 2},
name="manual-trigger",
tags=["ad-hoc", "api"],
idempotency_key="manual-trigger-001",
)
print(flow_run.id)
Sync example
python
from prefect.client.orchestration import get_client
def trigger():
with get_client(sync_client=True) as client:
deployment = client.read_deployment_by_name("my-flow/my-deployment")
flow_run = client.create_flow_run_from_deployment(
deployment_id=deployment.id,
parameters={"x": 1, "y": 2},
name="manual-trigger",
tags=["ad-hoc", "api"],
idempotency_key="manual-trigger-001",
)
print(flow_run.id)
If you already know the deployment UUID
python
from uuid import UUID
from prefect.client.orchestration import get_client
async def trigger():
async with get_client() as client:
flow_run = await client.create_flow_run_from_deployment(
deployment_id=UUID("12345678-1234-5678-1234-567812345678"),
parameters={"x": 1},
)
print(flow_run.id)
Optional arguments you can pass
- parameters — override deployment parameters
- name — custom flow run name
- tags — extra tags for this run
- idempotency_key — avoid duplicate run creation on retries
- state — provide an initial state, e.g. scheduled later
- work_queue_name — override the queue
- job_variables — override infrastructure/job settings
- labels — attach labels
Example scheduling it for later
python
from datetime import datetime, timedelta
from prefect.client.orchestration import get_client
from prefect.states import Scheduled
async def trigger_later():
async with get_client() as client:
deployment = await client.read_deployment_by_name("my-flow/my-deployment")
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id,
state=Scheduled(scheduled_time=datetime.utcnow() + timedelta(minutes=30)),
name="run-later",
)
print(flow_run.id)
A few caveats:
- You need a deployment ID for create_flow_run_from_deployment()
- If you only know the name, use read_deployment_by_name("flow-name/deployment-name")
- This triggers a run of an existing deployment, so it uses the deployment’s configured infrastructure / worker setup
- This is different from calling a flow function directly, which just runs Python code and does not create a deployment-backed run in the same way
If you want, I can also show a minimal example using only prefect-client imports specifically, without the broader prefect package style imports.Muhammad Ridwan
05/19/2026, 9:12 AMMarvin
05/19/2026, 9:12 AMMarvin
05/19/2026, 9:14 AMget_client() pick them up automatically
You usually want to configure at least:
- PREFECT_API_URL
- PREFECT_API_KEY for Prefect Cloud
- or PREFECT_API_AUTH_STRING for self-hosted server with basic auth
Relevant docs:
- Prefect settings API
- Prefect client API
- Prefect docs
1. If you use Prefect Cloud
Set:
bash
PREFECT_API_URL=<https://api.prefect.cloud/api/accounts/<account-id>/workspaces/<workspace-id>>
PREFECT_API_KEY=pnu_xxxxx
Example `.env`:
bash
PREFECT_API_URL=<https://api.prefect.cloud/api/accounts/ACCOUNT_ID/workspaces/WORKSPACE_ID>
PREFECT_API_KEY=pnu_your_api_key_here
PREFECT_API_REQUEST_TIMEOUT=60
2. If you use self-hosted Prefect server
Set:
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
If your server is protected with basic auth:
bash
PREFECT_API_AUTH_STRING=username:password
Example `.env`:
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
PREFECT_API_AUTH_STRING=admin:password
PREFECT_API_REQUEST_TIMEOUT=60
Important caveat: for self-hosted setups, do not also set PREFECT_API_KEY unless you specifically mean to use token auth supported by your environment. If both are set, auth behavior can be confusing, and PREFECT_API_KEY may take precedence.
3. How .env works
Prefect settings are loaded from multiple places, including:
- environment variables
- .env
- prefect.toml
- pyproject.toml
- profiles
In practice for microservices, the cleanest approach is:
- local development: .env
- containers/Kubernetes: real environment variables or secrets injection
So your code can stay minimal:
python
from prefect.client.orchestration import get_client
async def main():
async with get_client() as client:
response = await client.hello()
print(response.json())
No need to manually pass URL/key if the environment is configured correctly.
4. Recommended microservices pattern
Each service should receive its own config through:
- Docker Compose environment:
- Kubernetes ConfigMap + Secret
- ECS task env vars / secrets
- your secret manager
Docker Compose example
yaml
services:
my-service:
image: my-service:latest
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
PREFECT_API_AUTH_STRING: admin:password
Kubernetes-style pattern
- PREFECT_API_URL in a ConfigMap
- PREFECT_API_KEY or PREFECT_API_AUTH_STRING in a Secret
That is much better than baking credentials into the image or committing .env to git.
5. Other useful settings
Depending on your network/security setup, these can help:
- PREFECT_API_REQUEST_TIMEOUT
- PREFECT_API_TLS_INSECURE_SKIP_VERIFY=true (dev only)
- PREFECT_API_SSL_CERT_FILE=/path/to/ca.pem
- PREFECT_CLIENT_CUSTOM_HEADERS='{"X-Service-Name":"orders"}'
Example:
bash
PREFECT_API_URL=<https://prefect.internal/api>
PREFECT_API_SSL_CERT_FILE=/etc/ssl/certs/internal-ca.pem
PREFECT_API_REQUEST_TIMEOUT=90
PREFECT_CLIENT_CUSTOM_HEADERS={"X-Service-Name":"billing"}
6. Suggested setup by environment
Local development
bash
# .env
PREFECT_API_URL=<http://localhost:4200/api>
PREFECT_API_AUTH_STRING=admin:password
Production Cloud
bash
PREFECT_API_URL=<https://api.prefect.cloud/api/accounts/ACCOUNT/workspaces/WORKSPACE>
PREFECT_API_KEY=${INJECTED_SECRET}
Production self-hosted
bash
PREFECT_API_URL=<https://prefect.mycompany.internal/api>
PREFECT_API_AUTH_STRING=${INJECTED_SECRET}
PREFECT_API_SSL_CERT_FILE=/etc/ssl/certs/company-root.pem
7. Best practices
- Use .env only for local dev
- Use secret injection for productionMarvin
05/19/2026, 9:14 AMPREFECT_API_URL if they target the same Prefect API
- Do not commit API keys to source control
- Keep Cloud auth (PREFECT_API_KEY) and self-hosted basic auth (PREFECT_API_AUTH_STRING) separate
If you want, I can give you a ready-to-use example for your exact stack:
- Docker Compose
- Kubernetes
- FastAPI microservice
- Node-to-Python sidecar
- self-hosted Prefect server vs Prefect CloudMuhammad Ridwan
05/19/2026, 9:28 AMMarvin
05/19/2026, 9:28 AMMarvin
05/19/2026, 9:31 AMpython:<version>-slim images.
In fact, for Prefect 3.x, slim images are generally a good choice. The big exception is usually not “slim vs standard”, but rather:
- slim works
- alpine often causes trouble
There’s even a recent issue showing Prefect working with python:3.12-slim while python:3.12-alpine crashes with a segmentation fault:
- Segmentation fault on Alpine, slim works
Also relevant:
- Prefect Docker guide
- Serve flows with Docker
So why might python:slim fail for you?
Usually it’s because the extra OS packages your app or dependencies need are missing, not because Prefect itself requires the full Python image.
Common missing pieces in slim images:
- git — needed if you pull code from git repos
- build-essential — needed if any package must compile native extensions
- curl — sometimes used in startup scripts/healthchecks
- DB/system libs like libpq-dev — if using some Postgres drivers from source
- graphviz — only if you use flow visualization
- certificates / SSL packages — if you connect to internal HTTPS endpoints with custom certs
Important distinction:
- python:3.12-slim → Debian-based, usually fine
- python:3.12 → larger Debian image, more tools preinstalled
- python:3.12-alpine → musl-based, much more likely to break
A good baseline Dockerfile for Prefect on slim
dockerfile
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "your_script.py"]
If you want a worker image, a common pattern is:
dockerfile
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/prefect
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["prefect", "worker", "start", "--pool", "my-pool"]
Even better: if you want a Prefect-first base image, start from Prefect’s published image:
dockerfile
FROM prefecthq/prefect:3-latest
That can save you some dependency headaches.
When slim is usually safe
- client-only microservices using prefect-client
- flow containers with pure Python dependencies
- workers that don’t need special system libraries
- cloud SDK integrations that provide wheels
When slim often needs extra work
- ML / data science stacks
- packages with native extensions
- browser automation
- database drivers with compiled dependencies
- custom internal cert/network setups
My guess for your case
If “standard Python works, slim doesn’t”, the likely root cause is one of:
1. missing git
2. missing compiler/build tools
3. missing shared library for one of your dependencies
4. using Alpine somewhere, not slim
5. a dependency without a prebuilt wheel for your Python version / architecture
Best way to debug
Build the slim image and inspect the actual install error:
bash
docker build . --progress=plain
If the build succeeds but runtime fails:
bash
docker run --rm -it your-image bash
python -c "import prefect; print('ok')"
python -c "import your_other_dependency"
That will usually reveal the package causing the issue.
If you want, paste your current Dockerfile and requirements.txt / pyproject.toml, and I can tell you exactly what is missing for slim.Muhammad Ridwan
05/20/2026, 2:47 AMMarvin
05/20/2026, 2:47 AMMarvin
05/20/2026, 2:49 AMflow_run_id
- subflow logs live under each child/subflow flow_run_id
So if you want all task logs from subflows, you need to query each subflow run’s logs.
### Basic pattern
python
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import LogFilter
async def get_logs_for_flow_run(flow_run_id):
async with get_client() as client:
logs = await client.read_logs(
log_filter=LogFilter(
flow_run_id={"any_": [flow_run_id]}
)
)
return logs
That gets logs for one flow run only.
---
## How to retrieve subflow logs
The relationship is tracked through parent_task_run_id on the subflow run. In practice, the easiest approach is:
- read the parent flow run
- find child flow runs whose parent_task_run_id belongs to a task run from the parent flow
- query logs for those child flow runs
### Example: parent + direct subflows
python
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
FlowRunFilter,
TaskRunFilter,
LogFilter,
)
async def get_parent_and_subflow_logs(parent_flow_run_id):
async with get_client() as client:
all_logs = []
# 1. Parent flow logs
parent_logs = await client.read_logs(
log_filter=LogFilter(
flow_run_id={"any_": [parent_flow_run_id]}
)
)
all_logs.extend(parent_logs)
# 2. Get task runs in the parent flow
parent_task_runs = await client.read_task_runs(
flow_run_filter=FlowRunFilter(
id={"any_": [parent_flow_run_id]}
)
)
parent_task_run_ids = [tr.id for tr in parent_task_runs]
# 3. Find child flow runs (subflows) started by those task runs
subflow_runs = await client.read_flow_runs(
flow_run_filter=FlowRunFilter(
parent_task_run_id={"any_": parent_task_run_ids}
)
)
# 4. Read logs for each subflow
for subflow_run in subflow_runs:
subflow_logs = await client.read_logs(
log_filter=LogFilter(
flow_run_id={"any_": [subflow_run.id]}
)
)
all_logs.extend(subflow_logs)
return all_logs
---
## If you want all nested subflows recursively
If subflows can themselves launch subflows, use recursion:
```python
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import FlowRunFilter, LogFilter
async def get_all_logs_recursive(flow_run_id, client):
logs = await client.read_logs(
log_filter=LogFilter(
flow_run_id={"any_": [flow_run_id]}
)
)
task_runs = await client.read_task_runs(
flow_run_filter=FlowRunFilter(
id={"any_": [flow_run_id]}
)
)
task_run_ids = [tr.id for tr in task_runs]
if not task_run_ids:
return logs
subflow_runs = await client.read_flow_runs(
flow_run_filter=FlowRunFilter(
parent_task_run_id={"any_": task_run_ids}
)
)
for subflow_run in subflow_runs:
child_logs = await get_all_logs_recursive(subflow_run.id, client)
logs.extend(child_logs)
return logs
async def fetch_everything(root_flow_run_id):
async with get_client() as client:Marvin
05/20/2026, 2:49 AM---
## Important caveats
- *Logs are separate from flow run records* — `read_flow_runs()` does not include them
- `read_logs()` is usually *paginated / limited*, so if you have lots of logs, use `limit` + `offset`
- querying only the parent flow run *will not* return subflow task logs
- for large trees of nested subflows, recursive querying can be expensive
### Pagination example
```python
from prefect.client.schemas.sorting import LogSort
async def read_all_logs_for_run(flow_run_id, client, page_size=200):
offset = 0
all_logs = []
while True:
page = await client.read_logs(
log_filter=LogFilter(
flow_run_id={"any_": [flow_run_id]}
),
limit=page_size,
offset=offset,
sort=LogSort.TIMESTAMP_ASC,
)
if not page:
break
all_logs.extend(page)
if len(page) < page_size:
break
offset += page_size
return all_logs
---
## CLI note
If you just need logs for one flow run, the CLI can help:
bash
prefect flow-run logs <FLOW_RUN_ID>
But for parent + all subflows, the Python client is the better option because you need multiple queries.
If you want, I can give you a ready-to-run utility function that returns:
- all logs grouped by subflow
- all logs flattened and sorted by timestamp
- or all task-only logs excluding flow-level logsMuhammad Ridwan
05/20/2026, 3:29 AMMarvin
05/20/2026, 3:29 AMMarvin
05/20/2026, 3:29 AMcreate_flow_run_from_deployment() knows which deployment to execute because you explicitly tell it via the deployment_id you pass in.
So this call:
python
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id
)
means:
- find the deployment record with that exact UUID
- create a new flow run using that deployment’s configuration
- schedule it for execution
It does not infer the deployment automatically from your code or current flow.
### What the deployment contains
A deployment stores the information Prefect needs to launch a run, such as:
- which flow it belongs to
- parameters/default parameters
- work pool / queue routing
- infrastructure/job settings
- code location / entrypoint
- schedules, tags, and related metadata
So when you call create_flow_run_from_deployment(deployment_id=...), Prefect creates a flow run based on that deployment definition.
### Typical flow
Usually you do one of these first:
Lookup by name
python
from prefect.client.orchestration import get_client
async with get_client() as client:
deployment = await client.read_deployment_by_name("my-flow/my-deployment")
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id
)
Or use a known UUID directly
python
from uuid import UUID
flow_run = await client.create_flow_run_from_deployment(
deployment_id=UUID("12345678-1234-5678-1234-567812345678")
)
### So how does execution actually happen?
create_flow_run_from_deployment() itself does not run the code directly. It:
1. creates a flow run associated with that deployment
2. stores it in the API
3. sets it up for workers to pick up
Then a worker polls the appropriate work pool / queue and executes the run according to the deployment configuration.
### Short answer
It knows which deployment to execute because of the deployment_id argument — that ID points to one specific deployment record in the Prefect API.
Reference:
- Prefect client API
If you want, I can also explain:
- how Prefect maps flow/deployment-name to deployment_id, or
- what happens internally after the flow run is created.Muhammad Ridwan
05/20/2026, 3:43 AMMarvin
05/20/2026, 3:43 AMMarvin
05/20/2026, 3:44 AMstate.
Async example
python
from prefect.client.orchestration import get_client
from uuid import UUID
async def get_flow_run_status(flow_run_id: str):
async with get_client() as client:
flow_run = await client.read_flow_run(UUID(flow_run_id))
return flow_run.state.name, flow_run.state.type
Sync example
python
from prefect.client.orchestration import get_client
from uuid import UUID
def get_flow_run_status(flow_run_id: str):
with get_client(sync_client=True) as client:
flow_run = client.read_flow_run(UUID(flow_run_id))
return flow_run.state.name, flow_run.state.type
If you print it:
python
state_name, state_type = await get_flow_run_status("YOUR-FLOW-RUN-ID")
print(state_name) # e.g. "Completed"
print(state_type) # e.g. "COMPLETED"
### Common values
- Scheduled
- Pending
- Running
- Completed
- Failed
- Cancelled
- Crashed
### If you want to poll until completion
python
import asyncio
from prefect.client.orchestration import get_client
from uuid import UUID
async def wait_for_flow_run(flow_run_id: str, interval: int = 5):
async with get_client() as client:
while True:
flow_run = await client.read_flow_run(UUID(flow_run_id))
state = flow_run.state
print(f"{flow_run.id}: {state.name}")
if state.is_final():
return state
await asyncio.sleep(interval)
Docs:
- Prefect client API
If you want, I can also show:
- how to get the status of all subflows too
- how to get status by deployment name
- how to get status via raw REST API instead of the Python clientMuhammad Ridwan
05/20/2026, 3:57 AMMarvin
05/20/2026, 3:57 AMMarvin
05/20/2026, 3:58 AMBaseSettings object in your app.
## Short answer
If you have something like:
python
from pydantic_settings import BaseSettings
class AppSettings(BaseSettings):
PREFECT_API_URL: str
that alone does not make Prefect use it.
You must do one of these:
1. set a real environment variable PREFECT_API_URL
2. put PREFECT_API_URL in a .env file that Prefect loads
3. bridge your app setting into Prefect with temporary_settings(...)
---
## How Prefect actually loads settings
Prefect 3.x reads settings from its own settings stack, roughly in this order:
1. programmatic overrides via Prefect settings context
2. environment variables
3. .env
4. prefect.toml
5. pyproject.toml
6. Prefect profiles
7. defaults
So the Prefect client does things like:
- read PREFECT_API_URL
- read PREFECT_API_KEY
- build the API client from those values
Docs:
- Prefect settings API
- Prefect client API
---
## Case 1: using your own Pydantic settings class
Example:
python
from pydantic_settings import BaseSettings
class AppSettings(BaseSettings):
prefect_api_url: str
This is just your app config. Prefect will not inspect this object unless you explicitly pass its values into Prefect’s settings system.
### Correct bridge with temporary_settings
python
from pydantic_settings import BaseSettings
from prefect.settings import PREFECT_API_URL, temporary_settings
from prefect.client.orchestration import get_client
class AppSettings(BaseSettings):
prefect_api_url: str
settings = AppSettings()
async def main():
with temporary_settings(
updates={PREFECT_API_URL: settings.prefect_api_url}
):
async with get_client() as client:
response = await client.hello()
print(response.json())
That works because now you are explicitly telling Prefect’s settings system what value to use.
---
## Case 2: using .env
If your .env contains:
bash
PREFECT_API_URL=<http://localhost:4200/api>
and Prefect is running in a directory where that .env is loaded, then:
python
from prefect.client.orchestration import get_client
async def main():
async with get_client() as client:
response = await client.hello()
print(response.json())
will just work.
### Mechanism difference vs your own Pydantic settings
With `.env`:
- Prefect reads the file as part of its own settings loading
- no bridge code needed
- the setting is globally available to Prefect in that process
With your app’s `BaseSettings`:
- only your code knows about that object
- Prefect does not automatically consult it
- you must copy the value into Prefect settings yourself
---
## Environment variable vs .env
These are related but not identical.
### Real environment variable
bash
export PREFECT_API_URL=<http://localhost:4200/api>
This is an OS/process environment variable. Prefect reads it directly.
### .env file
bash
PREFECT_API_URL=<http://localhost:4200/api>
This is just a file. It matters only if something loads it. Prefect can load .env as part of its settings system.
### Precedence
If both exist:
- the real environment variable usually wins over .env
So this:
bash
export PREFECT_API_URL=<http://prod-server/api>
with `.env`:
bash
PREFECT_API_URL=<http://dev-server/api>
usually means Prefect will use:
bash
<http://prod-server/api>
---
## Best practice options
### Option A: let Prefect use env / .env
Best when you want standard deployment behavior.
bash
PREFECT_API_URL=<http://localhost:4200/api>
PREFECT_API_KEY=...
Then just:
```pythonMarvin
05/20/2026, 3:58 AM### Option B: bridge from your app settings
Best when your app centrally manages config.
```python
from prefect.settings import PREFECT_API_URL, PREFECT_API_KEY, temporary_settings
Then inject those values into Prefect at runtime.
---
## Example with API URL and API key
python
from pydantic_settings import BaseSettings
from prefect.settings import PREFECT_API_URL, PREFECT_API_KEY, temporary_settings
from prefect.client.orchestration import get_client
class AppSettings(BaseSettings):
prefect_api_url: str
prefect_api_key: str | None = None
settings = AppSettings()
async def main():
updates = {
PREFECT_API_URL: settings.prefect_api_url,
}
if settings.prefect_api_key:
updates[PREFECT_API_KEY] = settings.prefect_api_key
with temporary_settings(updates=updates):
async with get_client() as client:
response = await client.hello()
print(response.json())
---
## Practical difference in behavior
### .env approach
- declarative
- process-wide
- easy for containers/microservices
- no Python bridge code needed
### temporary_settings approach
- explicit and local to a code block
- useful when different requests/services need different Prefect targets
- great if your app config is already managed with Pydantic
---
## Recommendation
For microservices, I’d usually recommend:
- production: real environment variables / secrets injection
- local dev: .env
- only use temporary_settings if you specifically want to map app-level config into Prefect dynamically
## Minimal examples
### .env
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
PREFECT_API_KEY=your_key_here
python
from prefect.client.orchestration import get_client
async def main():
async with get_client() as client:
...
### Pydantic settings bridge
python
from pydantic_settings import BaseSettings
from prefect.settings import PREFECT_API_URL, temporary_settings
class Settings(BaseSettings):
prefect_api_url: str
python
settings = Settings()
with temporary_settings(updates={PREFECT_API_URL: settings.prefect_api_url}):
...
If you want, I can give you a complete FastAPI example showing:
- BaseSettings
- .env
- Prefect client
- dependency injection
- Cloud vs self-hosted configMuhammad Ridwan
05/20/2026, 4:26 AMMarvin
05/20/2026, 4:26 AMMarvin
05/20/2026, 4:27 AM.env file in the app directory.
## Short answer
- If the .env file is in another directory, Prefect will not automatically treat it the same as a .env in the current working directory unless that file is explicitly loaded.
- If you set PREFECT_API_URL, PREFECT_API_KEY, etc. in Docker Compose under environment:, Prefect will read them just fine.
- From Prefect’s perspective, real environment variables from Docker Compose are even more direct than .env file values.
---
## 1. What if .env is in another directory?
Prefect’s .env loading depends on what file is available in the process working context that its settings loader checks.
So if your project looks like:
text
project/
services/
api/
app.py
config/
.env
and you run your service from services/api, Prefect will not automatically know to read config/.env just because it exists elsewhere.
### That means this may not work automatically:
text
config/.env
unless you do something explicit like:
- load it yourself before creating the Prefect client
- or expose those values as actual environment variables
- or copy/mount it into the runtime working directory
---
## 2. Docker Compose environment: absolutely works
Example:
yaml
services:
my-service:
image: my-service:latest
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
PREFECT_API_KEY: ${PREFECT_API_KEY}
Inside the container, those become real environment variables.
So Prefect client code like:
python
from prefect.client.orchestration import get_client
async def main():
async with get_client() as client:
response = await client.hello()
print(response.json())
will pick them up automatically.
---
## 3. Is Docker Compose env the same as .env in the app directory?
Effectively for Prefect client usage: yes, and usually stronger.
Why:
- .env is just a file that must be loaded
- environment: in Docker Compose creates actual process environment variables
- Prefect reads environment variables directly
- environment variables typically have higher precedence than .env
So if your question is “can Docker Compose make it work like a local .env?” the answer is:
Yes — and usually more reliably.
---
## 4. Docker Compose .env vs env_file vs environment
These are easy to mix up.
### A. environment:
yaml
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
This directly injects env vars into the container.
Prefect sees these immediately.
### B. env_file:
yaml
env_file:
- ./config/prefect.env
This tells Docker Compose to read that file and inject those values into the container environment.
Prefect also sees these fine.
Example:
yaml
services:
my-service:
image: my-service:latest
env_file:
- ./config/prefect.env
`./config/prefect.env`:
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
PREFECT_API_KEY=pnu_xxx
This is a great solution if your env file lives elsewhere.
### C. Compose project .env
Docker Compose also has its own .env file for variable substitution in the compose file itself. That is a different concept.
Example:
yaml
environment:
PREFECT_API_URL: ${PREFECT_API_URL}
Here Compose reads ${PREFECT_API_URL} from its .env or your shell, then injects it into the container.
That still works — just remember this .env is for Compose interpolation, not directly for Prefect.
---
## 5. Best options if your .env is elsewhere
### Option 1: use env_file in Docker Compose
Best if you already have a separate env file.
yaml
services:
my-service:
image: my-service:latest
env_file:
- ./config/prefect.env
### Option 2: use environment:
Best if values come from secrets or CI/CD.
```yaml
services:
my-service:
image: my-service:latestMarvin
05/20/2026, 4:27 AM### Option 3: load the file in Python yourself
Only if you really need app-managed loading.
For example with `pydantic-settings` or `python-dotenv`, then bridge into Prefect if needed.
But if you’re already in containers, I’d recommend letting Docker inject the env instead.
---
## 6. Recommended pattern for containers
For Docker/microservices, I recommend:
- *don’t rely on Prefect discovering a `.env` in some nested path*
- use Docker Compose `environment:` or `env_file:`
- inject `PREFECT_API_URL`, `PREFECT_API_KEY`, `PREFECT_API_AUTH_STRING` directly into the container
Example:
```yaml
services:
orders-service:
build: .
env_file:
- ./config/prefect.env
./config/prefect.env
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
PREFECT_API_AUTH_STRING=admin:password
Then Prefect client code needs no extra config.
---
## 7. Practical summary
- .env in another directory: not automatically guaranteed to be loaded by Prefect
- Docker Compose `environment:`: yes, works
- Docker Compose `env_file:`: yes, also works
- For containers, environment: / env_file: is usually the preferred approach
Useful docs:
- Prefect settings API
- Prefect client API
If you want, I can show you the exact Docker Compose setup for:
- Prefect Cloud
- self-hosted Prefect server
- microservice + worker + server on the same Compose networkMuhammad Ridwan
05/20/2026, 4:34 AMMarvin
05/20/2026, 4:34 AMMarvin
05/20/2026, 4:34 AMyaml
services:
app:
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
PREFECT_API_KEY: ${PREFECT_API_KEY}
then inside the container, those are normal environment variables. Prefect can read them from anywhere in your project:
python
from prefect.client.orchestration import get_client
async def main():
async with get_client() as client:
...
It does not matter that your Python file is in a subdirectory like:
text
/app/src/services/prefect_client/client.py
because environment variables are attached to the process/container environment, not to a specific file path.
## In other words
This works:
text
container env
PREFECT_API_URL=<http://prefect-server:4200/api>
even if your code is here:
text
/app/project/subdir/my_prefect_code.py
because Prefect is not looking for “a variable near that file”; it is reading from the process environment.
---
## What matters
What matters is:
- the env var exists in the container
- your Python process is running in that same container
- Prefect client is created in that process
What does not matter:
- where in the project tree the file lives
- whether that file sits next to .env
- whether the code is in a subdirectory
---
## Simple example
### Docker Compose
yaml
services:
my-service:
build: .
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
### Project layout
text
project/
src/
services/
prefect_code.py
### prefect_code.py
python
from prefect.client.orchestration import get_client
async def test():
async with get_client() as client:
response = await client.hello()
print(response.json())
That works because PREFECT_API_URL is in the container environment.
---
## Quick way to verify inside container
You can confirm with:
bash
docker exec -it <container-name> env | grep PREFECT
or:
bash
docker exec -it <container-name> printenv PREFECT_API_URL
If you see the value there, Prefect should be able to use it.
You can also verify in Python:
python
import os
print(os.environ.get("PREFECT_API_URL"))
and with Prefect settings:
python
from prefect.settings import PREFECT_API_URL
print(PREFECT_API_URL.value())
---
## Short answer
Yes: if the variable is injected into the container, then your Prefect client can use it from code in a project subdirectory, because env vars are process-wide, not directory-scoped.
If you want, I can give you a tiny test script to verify your container is correctly seeing PREFECT_API_URL and PREFECT_API_KEY.Muhammad Ridwan
05/20/2026, 6:47 AMMarvin
05/20/2026, 6:47 AMMarvin
05/20/2026, 6:48 AMprefect server start --host 0.0.0.0 starts the Prefect API/UI server.
uv run python main.py sounds like your application / flow-serving / worker process.
In a containerized setup, these should usually be separate services:
- one container for Prefect server
- one container for your app / worker / deployment-serving code
## Recommended architecture
### Service 1: Prefect server
yaml
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
### Service 2: your app / worker
yaml
services:
my-app:
build: .
command: uv run python main.py
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
depends_on:
- prefect-server
Your app connects to the Prefect server via PREFECT_API_URL.
---
## Why not run both in one container?
Because both commands are typically long-running foreground processes:
- uv run python main.py keeps the container busy
- prefect server start also keeps the container busy
A container normally has one main process. If you start one, the second won’t run unless you use a process manager or shell tricks.
Also, running server + app in one container is usually harder to manage, debug, and scale.
---
## If you really must run both in one container
It’s possible, but not recommended.
You’d need a shell script that starts one process in the background, then starts the other in the foreground.
Example:
bash
#!/bin/sh
set -e
prefect server start --host 0.0.0.0 &
uv run python main.py
wait
or the reverse:
bash
#!/bin/sh
set -e
uv run python main.py &
prefect server start --host 0.0.0.0
wait
But this raises important questions:
- Which one is the primary process?
- What happens if one crashes?
- How do signals/shutdown work?
- Is main.py supposed to exit, or run forever?
This is why separate containers are better.
---
## If main.py must wait for Prefect server
If your main.py depends on the server being available, use:
- separate containers
- depends_on
- ideally a healthcheck or startup wait loop
Example:
yaml
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
healthcheck:
test: ["CMD", "curl", "-f", "<http://localhost:4200/api/health>"]
interval: 10s
timeout: 5s
retries: 12
my-app:
build: .
command: uv run python main.py
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
depends_on:
prefect-server:
condition: service_healthy
---
## Important distinction: server vs worker vs deployed flow
Sometimes users mix these up:
- prefect server start → runs Prefect backend/API/UI
- prefect worker start → polls work pools and executes runs
- python main.py / flow.serve() → may register/serve deployments depending on your code
If your goal is just to execute deployments, you probably want:
- Prefect server running in one container
- a worker running in another container
- your deployments created separately
For example, instead of uv run python main.py, maybe what you really want is:
yaml
command: uv run prefect worker start --pool my-pool
or your main.py may be creating deployments.
---
## Likely best solution for your case
If you are self-hosting Prefect:
### Container A
yaml
command: prefect server start --host 0.0.0.0
### Container B
yaml
command: uv run python main.py
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
Then main.py can talk to the server after it comes up.
---
## Short answer
If you want to run prefect server start --host 0.0.0.0 after uv run python main.py, that suggests both are in one container — which is usually the wrong pattern.
Best practice: run them in separate containers/services.Marvin
05/20/2026, 6:48 AMdocker-compose.yml
- Dockerfile
- startup script
for either:
1. self-hosted Prefect server + app container, or
2. Prefect server + worker + deployment containerMuhammad Ridwan
05/20/2026, 7:18 AMMarvin
05/20/2026, 7:18 AMMarvin
05/20/2026, 7:18 AMPREFECT_API_URL.
## Typical architecture
### 1. Prefect server container
This runs:
bash
prefect server start --host 0.0.0.0
It provides:
- the REST API
- the orchestration logic
- the UI
- the state database layer
This is the central coordinator.
### 2. FastAPI app container
This uses PREFECT_API_URL to connect to the Prefect server.
Typical use cases:
- trigger deployment runs
- query flow run status
- read logs
- manage deployments programmatically
Example:
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
### 3. Prefect worker container
This also uses PREFECT_API_URL to connect to the same Prefect server.
It:
- polls work pools / queues
- receives scheduled flow runs
- executes them
Example:
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
---
## So yes: both usually have PREFECT_API_URL
For example:
yaml
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
fastapi-app:
build: .
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
prefect-worker:
image: prefecthq/prefect:3-latest
command: prefect worker start --pool my-pool
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
---
## Role of each component
### FastAPI → Prefect server
The FastAPI app is a client of Prefect server.
It might do things like:
python
await client.create_flow_run_from_deployment(...)
await client.read_flow_run(...)
await client.read_logs(...)
So FastAPI sends requests to the server.
### Worker → Prefect server
The worker is also a client of Prefect server.
It asks:
- is there any work for my pool?
- what run should I execute?
- what state updates should I send back?
So the worker also talks to the server.
### Prefect server
The server sits in the middle and coordinates both sides:
- receives requests from FastAPI
- stores flow run records
- schedules/orchestrates runs
- gives work to workers
- receives state/log updates from workers
---
## Important distinction
The Prefect server does not directly execute flows.
It orchestrates them.
The worker does the actual execution.
So if your FastAPI app triggers a deployment:
1. FastAPI sends request to Prefect server
2. Prefect server creates a flow run
3. Worker polls the server
4. Worker sees the run and executes it
5. Worker reports status/logs back to the server
6. FastAPI can later query the server for results/status
---
## Summary flow
text
FastAPI app ---> Prefect Server <--- Prefect Worker
|
v
orchestration state
More concretely:
text
FastAPI app
-> create deployment run
-> get run status
-> fetch logs
Prefect Server
-> stores run metadata
-> schedules/orchestrates
-> exposes API/UI
Worker
-> polls server
-> runs deployments
-> sends logs/state back
---
## Short answer
Yes:
- FastAPI container should usually have PREFECT_API_URL
- worker container should usually have PREFECT_API_URL
- Prefect server container is the orchestrator/API that both connect to
If you want, I can draw you a complete Docker Compose example with:
- FastAPI
- Prefect server
- Prefect worker
- optional Postgres
wired correctly together.Muhammad Ridwan
05/20/2026, 7:26 AMMarvin
05/20/2026, 7:26 AMMarvin
05/20/2026, 7:27 AMprefect worker start --pool my-pool starts a worker
- python main.py runs your Python application/code
A Prefect worker does not contain your flow logic by magic. It only:
- polls the Prefect API for scheduled flow runs
- receives run instructions
- launches execution in the environment defined by the deployment
## Short version
Use:
- python main.py when you want to directly run your code
- prefect worker start --pool my-pool when you want a worker that executes deployment runs
---
## Why use prefect worker start --pool my-pool?
Because in Prefect’s deployment model, the worker is the component responsible for picking up and executing deployment-backed flow runs.
If your FastAPI app does:
python
await client.create_flow_run_from_deployment(deployment_id=...)
that only creates a run in Prefect.
It does not execute the flow immediately by itself.
A worker must be running and watching the correct work pool so it can pick up that run.
---
## Then how does the worker know the actual flow logic?
It knows because the deployment tells it where the flow code is and how to run it.
A deployment contains things like:
- flow entrypoint
- code location
- parameters
- work pool / queue
- infrastructure configuration
- image / environment information
So the worker does not need your code baked into the worker process itself unless your deployment model expects that.
It learns what to run from the deployment.
---
## Two common patterns
### Pattern A: worker + deployment + code in image
The deployment points to code that exists in the execution environment/image.
For example, your deployment may reference:
text
flows/main.py:my_flow
and the worker executes a run in a container/image that already contains that file.
In this pattern:
- worker starts
- worker polls the pool
- Prefect tells it to run deployment X
- the execution environment has your code
- Prefect imports the flow from the configured entrypoint
### Pattern B: directly running Python
If you do:
bash
python main.py
then you are just running the script directly.
That bypasses the worker/pool orchestration model unless main.py itself does something like:
- define and serve deployments
- start a long-lived process
- register flows
---
## Why not replace worker with python main.py?
Because python main.py usually does not poll the work pool.
A worker is specifically built to:
- ask Prefect server for available work
- claim runs
- manage execution lifecycle
- report states/logs back
Your main.py usually does not do that unless you wrote custom orchestration logic.
---
## Example mental model
### prefect worker start --pool my-pool
Means:
“I am an execution agent. Give me runs from###and I will execute them.”my-pool
python main.py
Means:
“Run this script now.”These are not equivalent. --- ## Where does the flow code come from? That depends on deployment configuration. Common options: - code baked into the Docker image - code pulled from git / remote storage - local mounted volume - image specified by deployment/job variables The deployment tells Prefect what the entrypoint is. For example:
text
entrypoint = "flows/orders.py:process_orders"
Then the worker execution environment imports:
- module/file flows/orders.py
- flow function process_orders
---
## Example
Suppose you have:
python
from prefect import flow
@flow
def my_flow(name: str = "world"):
print(f"Hello {name}")
and deploy it.
The deployment stores metadata like:
- flow name
- deployment name
- entrypoint
- target work pool
Then:
python
await client.create_flow_run_from_deployment(deployment_id=...)
creates a run.
The worker watching that pool eventually picks it up and executes the entrypoint defined by the deployment.
---
## So when would you still use python main.py?Marvin
05/20/2026, 7:27 AMpython main.py when that file is doing one of these:
### 1. local execution
python
if __name__ == "__main__":
my_flow()
This just runs the flow directly.
### 2. deployment registration / creation
python
if __name__ == "__main__":
my_flow.from_source(...).deploy(...)
This creates a deployment, but is not the worker.
### 3. custom service behavior
Your app might use the Prefect client inside main.py to trigger runs, check status, etc.
Again, that is not the worker.
---
## Key distinction
### Worker
Responsible for executing scheduled deployment runs
### Your code / app
Responsible for defining flows, creating deployments, triggering runs, business logic
---
## In your architecture
You likely want:
### FastAPI container
Runs:
bash
uv run python main.py
if main.py is your FastAPI app
This app can trigger Prefect deployments via the client.
### Worker container
Runs:
bash
prefect worker start --pool my-pool
This picks up the deployment runs and executes them.
### Prefect server container
Runs:
bash
prefect server start --host 0.0.0.0
This orchestrates everything.
---
## Short answer
You use prefect worker start --pool my-pool because a worker is what actually listens for and executes deployment runs.
It knows the flow logic because the deployment metadata tells it:
- what flow to run
- where the code lives
- what environment/image to use
- what entrypoint to import
python main.py just runs your app/script directly; it is not a worker unless you intentionally wrote it to behave like one.
If you want, I can show you a concrete end-to-end example:
1. define a flow
2. create a deployment
3. start a worker
4. trigger it from FastAPI
5. explain exactly where the code is loaded from.Muhammad Ridwan
05/20/2026, 7:45 AMMarvin
05/20/2026, 7:45 AMMarvin
05/20/2026, 7:45 AMmain.py currently does:
python
main_pipeline.serve(name="pipeline-auto")
then you are using the serve pattern, not the server + worker deployment pattern.
Those are related, but not the same.
## What serve(...) is doing
flow.serve(...) creates a long-running process that:
- registers a deployment-like runner for that flow
- stays alive
- handles scheduled/ad-hoc execution for that served flow
So your app process itself is acting as the thing that makes that flow runnable.
That’s why it feels different from the worker model.
---
# If you want to move to Prefect server + worker
You generally want to change from:
python
if __name__ == "__main__":
main_pipeline.serve(name="pipeline-auto")
to a pattern where:
1. your flow is still defined in code
2. you create a deployment for it
3. a worker executes runs from a work pool
4. your FastAPI app or client triggers the deployment via Prefect API
---
## Conceptual change
### Current
text
main.py with .serve()
= long-running flow-serving process
### Target
text
Prefect server container
= orchestrator
Prefect worker container
= execution agent
your code container / image
= contains flow code
deployment
= tells worker what flow to run and where
---
# How to alter it
## Step 1: keep the flow definition, remove .serve()
Instead of this:
python
from prefect import flow
@flow
def main_pipeline():
print("running pipeline")
if __name__ == "__main__":
main_pipeline.serve(name="pipeline-auto")
change to:
python
from prefect import flow
@flow
def main_pipeline():
print("running pipeline")
Now this file just defines the flow.
---
## Step 2: create a deployment
In Prefect 3.x, the preferred deployment flow is to deploy from source, or use prefect deploy if you have a prefect.yaml.
If your flow lives in your repo, one common pattern is to create the deployment in a separate script.
Example:
python
from flows.main import main_pipeline
if __name__ == "__main__":
main_pipeline.deploy(
name="pipeline-auto",
work_pool_name="my-pool",
)
But depending on your code source strategy, the more modern / flexible 3.x approach is often:
python
from prefect import flow
@flow
def main_pipeline():
print("running pipeline")
if __name__ == "__main__":
main_pipeline.from_source(
source=".",
entrypoint="main.py:main_pipeline",
).deploy(
name="pipeline-auto",
work_pool_name="my-pool",
)
This creates a deployment that the worker can later execute.
Important: don’t use removed 2.x methods like Deployment.build_from_flow().
---
## Step 3: run a worker in a separate container
Your worker container runs:
bash
prefect worker start --pool my-pool
This worker polls Prefect server for runs assigned to my-pool.
Before suggesting CLI syntax, I should verify it. The correct command in Prefect 3.x is indeed prefect worker start --pool <name>.
A worker does not need .serve() in your app.
---
## Step 4: your FastAPI app triggers the deployment
Your FastAPI app can then do:
python
from prefect.client.orchestration import get_client
async def trigger_pipeline():
async with get_client() as client:
deployment = await client.read_deployment_by_name(
"main-pipeline/pipeline-auto"
)
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id
)
return str(flow_run.id)
Now:
- FastAPI calls Prefect server
- Prefect server creates a run
- worker picks it up
- worker executes the flow
---
# The big question: where does the worker get the code?
This is the most important migration detail.
When you used .serve(), the process already had the flow in memory.
When using workers, the worker needs a way to access the flow code.
There are a few common options:Marvin
05/20/2026, 7:45 AMdockerfile
FROM prefecthq/prefect:3-latest
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
Then the deployment entrypoint can point to:
text
main.py:main_pipeline
and the worker can import it because the code is inside the container image.
## Option B: code pulled from git / remote source
The deployment references a source repository and the worker pulls it at runtime.
## Option C: shared mounted volume
Less common in production, but possible.
---
# A concrete migration example
## Before
python
from prefect import flow
@flow
def main_pipeline():
print("running pipeline")
if __name__ == "__main__":
main_pipeline.serve(name="pipeline-auto")
## After: flow module
python
from prefect import flow
@flow
def main_pipeline():
print("running pipeline")
## After: deployment script
python
from main import main_pipeline
if __name__ == "__main__":
main_pipeline.from_source(
source=".",
entrypoint="main.py:main_pipeline",
).deploy(
name="pipeline-auto",
work_pool_name="my-pool",
)
Then run that once to register the deployment.
---
# Container roles
## Prefect server container
Runs:
bash
prefect server start --host 0.0.0.0
## Worker container
Runs:
bash
prefect worker start --pool my-pool
and has:
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
## FastAPI container
Runs your API app and also has:
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
The FastAPI app triggers runs via the client.
---
# Docker Compose sketch
yaml
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
prefect-worker:
build: .
command: prefect worker start --pool my-pool
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
depends_on:
- prefect-server
fastapi-app:
build: .
command: uv run python api.py
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
depends_on:
- prefect-server
If the worker needs your flow code, make sure the worker image includes it.
---
# Important migration note
serve() is convenient for:
- one service
- one long-running process
- simpler local/dev patterns
The worker model is better for:
- decoupled orchestration
- scaling workers independently
- FastAPI triggering deployments
- cleaner self-hosted architecture
So your migration is basically:
- remove .serve()
- create a deployment instead
- run a worker
- trigger the deployment through the API/client
---
# What to change in main.py
If main.py is currently both:
- defining the flow
- serving it
split responsibilities.
### flows/main.py
python
from prefect import flow
@flow
def main_pipeline():
print("running pipeline")
### deploy.py
python
from flows.main import main_pipeline
if __name__ == "__main__":
main_pipeline.from_source(
source=".",
entrypoint="flows/main.py:main_pipeline",
).deploy(
name="pipeline-auto",
work_pool_name="my-pool",
)
### api.py
python
from fastapi import FastAPI
from prefect.client.orchestration import get_client
app = FastAPI()
@app.post("/run")
async def run_pipeline():
async with get_client() as client:
deployment = await client.read_deployment_by_name(
"main-pipeline/pipeline-auto"
)
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id
)
return {"flow_run_id": str(flow_run.id)}
---
# One more subtle point
If you deploy from source using source=".", be sure the worker’s runtime filesystem matches what the deployment expects, or use a more explicit source strategy like a git repo.
---Marvin
05/20/2026, 7:45 AM.serve()
3. create a deployment for main_pipeline
4. run a dedicated worker container on a work pool
5. let FastAPI trigger the deployment using the Prefect client
6. point both worker and FastAPI to the same PREFECT_API_URL
---
If you want, I can build you a complete working example for your case with:
- main.py
- deploy.py
- api.py
- Dockerfile
- docker-compose.yml
for a self-hosted Prefect server + worker + FastAPI setup.Muhammad Ridwan
05/20/2026, 8:54 AMPREFECT_API_URL setting for your current profile doesn't match the address of the server that's running. You need to set it to communicate with the server.? How would you like to proceed? [Use arrows to move; enter to select]
How do I avoid this in my prefect-server container? once I prefect server start --host 0.0.0.0Marvin
05/20/2026, 8:55 AMMarvin
05/20/2026, 8:57 AMprefect server start is trying to help configure the CLI profile for talking to the server it just started.
In a container, you usually want to disable that interactive behavior.
## Best way to avoid it
Set:
bash
PREFECT_CLI_PROMPT=false
in the server container.
So your server container should look like:
yaml
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
environment:
PREFECT_CLI_PROMPT: "false"
That prevents the interactive prompt.
## Important point
Your server container usually does not need PREFECT_API_URL at all.
Use this mental model:
- server container → hosts the Prefect API/UI
- worker container → needs PREFECT_API_URL=<http://prefect-server:4200/api>
- FastAPI/app container → also needs PREFECT_API_URL=<http://prefect-server:4200/api>
So typically:
### Server container
yaml
environment:
PREFECT_CLI_PROMPT: "false"
### Worker/app containers
yaml
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
PREFECT_CLI_PROMPT: "false"
## Why the message happens
When you run:
bash
prefect server start --host 0.0.0.0
Prefect computes the server address and compares it to the active CLI profile’s PREFECT_API_URL. If they don’t match, it asks whether you want to update the profile.
That is useful on a laptop, but annoying in containers.
## Recommended Docker Compose pattern
yaml
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
environment:
PREFECT_CLI_PROMPT: "false"
ports:
- "4200:4200"
prefect-worker:
image: prefecthq/prefect:3-latest
command: prefect worker start --pool my-pool
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
PREFECT_CLI_PROMPT: "false"
depends_on:
- prefect-server
fastapi-app:
build: .
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
PREFECT_CLI_PROMPT: "false"
depends_on:
- prefect-server
## Important hostname caveat
Inside Docker Compose:
- localhost inside the worker/app container means that same container
- to reach the server container, use the service name, e.g. prefect-server
So this is correct for other containers:
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
not:
bash
PREFECT_API_URL=<http://localhost:4200/api>
unless the code is running inside the server container itself.
## Optional note
prefect server start --help shows there is no dedicated --no-prompt flag on this command, so PREFECT_CLI_PROMPT=false is the correct way to suppress the prompt behavior.
Reference:
- Prefect settings API
If you want, I can give you a complete working docker-compose.yml for:
- Prefect server
- Prefect worker
- FastAPI app
- Postgres backendMuhammad Ridwan
05/20/2026, 9:55 AMMarvin
05/20/2026, 9:55 AMMarvin
05/20/2026, 9:57 AMLogFilter.task_run_id if you want task logs only.
That’s the key distinction:
- flow logs → have flow_run_id but no task_run_id
- task logs → have both flow_run_id and task_run_id
So if you filter on task_run_id, you’ll get task-associated logs instead of flow-level logs.
## Example: logs for one task run
python
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import LogFilter, LogFilterTaskRunId
async def get_task_logs(task_run_id):
async with get_client() as client:
logs = await client.read_logs(
log_filter=LogFilter(
task_run_id=LogFilterTaskRunId(any_=[task_run_id])
)
)
return logs
---
## If you want all task logs for a flow run
You usually need to:
1. get the task runs for that flow run
2. collect their IDs
3. query logs with task_run_id=...
Example:
python
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
LogFilter,
LogFilterTaskRunId,
TaskRunFilter,
TaskRunFilterFlowRunId,
)
async def get_all_task_logs_for_flow(flow_run_id):
async with get_client() as client:
task_runs = await client.read_task_runs(
task_run_filter=TaskRunFilter(
flow_run_id=TaskRunFilterFlowRunId(any_=[flow_run_id])
)
)
task_run_ids = [task_run.id for task_run in task_runs]
if not task_run_ids:
return []
logs = await client.read_logs(
log_filter=LogFilter(
task_run_id=LogFilterTaskRunId(any_=task_run_ids)
)
)
return logs
## Why not just filter by flow_run_id?
Because flow_run_id includes:
- flow-level logs
- task logs inside that flow
So flow_run_id alone does not exclude the flow’s own logs.
## Useful combination: task logs + error level
python
import logging
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
LogFilter,
LogFilterTaskRunId,
LogFilterLevel,
)
from prefect.client.schemas.sorting import LogSort
async def get_error_task_logs(task_run_id):
async with get_client() as client:
logs = await client.read_logs(
log_filter=LogFilter(
task_run_id=LogFilterTaskRunId(any_=[task_run_id]),
level=LogFilterLevel(ge_=logging.ERROR),
),
sort=LogSort.TIMESTAMP_ASC,
)
return logs
## Caveat
There isn’t a simple client-side “give me logs where task_run_id is not null” helper. So if your goal is:
all logs for this flow, but exclude flow logsthe safest pattern is: - first query task runs - then query logs by those `task_run_id`s ## References - Prefect client API - REST API overview If you want, I can also give you: 1. a version that includes subflow task logs too, or 2. a version that returns logs grouped by
task_run_id.Muhammad Ridwan
05/21/2026, 2:14 AMMarvin
05/21/2026, 2:14 AMMarvin
05/21/2026, 2:15 AMclient.read_logs(...) returns a list of log objects.
So this code:
python
logs = await client.read_logs(
log_filter=LogFilter(
task_run_id=LogFilterTaskRunId(any_=[task_run_id])
)
)
means:
“Give me all logs whose## Whatmatches this specific task run.”task_run_id
logs looks like
logs is typically a list like:
python
[
Log(...),
Log(...),
Log(...),
]
Each item represents one log entry emitted during that task run.
Conceptually, each log object contains fields like:
- id
- name
- level
- message
- timestamp
- flow_run_id
- task_run_id
- worker_id / metadata fields depending on context
## Example output shape
If you printed each log in a readable way:
python
for log in logs:
print({
"timestamp": str(log.timestamp),
"level": log.level,
"message": log.message,
"flow_run_id": str(log.flow_run_id),
"task_run_id": str(log.task_run_id),
"name": log.name,
})
you might see something like:
python
{
"timestamp": "2026-05-21 10:15:01.123456+00:00",
"level": 20,
"message": "Beginning task run 'extract-data'",
"flow_run_id": "11111111-1111-1111-1111-111111111111",
"task_run_id": "22222222-2222-2222-2222-222222222222",
"name": "prefect.task_runs"
}
{
"timestamp": "2026-05-21 10:15:03.567890+00:00",
"level": 20,
"message": "Fetched 150 records from source API",
"flow_run_id": "11111111-1111-1111-1111-111111111111",
"task_run_id": "22222222-2222-2222-2222-222222222222",
"name": "prefect.task_runs"
}
{
"timestamp": "2026-05-21 10:15:05.999999+00:00",
"level": 40,
"message": "Request failed with timeout, retrying...",
"flow_run_id": "11111111-1111-1111-1111-111111111111",
"task_run_id": "22222222-2222-2222-2222-222222222222",
"name": "prefect.task_runs"
}
## What the fields mean
### timestamp
When that log line was created.
Example:
python
"2026-05-21 10:15:03.567890+00:00"
### level
Python logging level as an integer:
- 10 = DEBUG
- 20 = INFO
- 30 = WARNING
- 40 = ERROR
- 50 = CRITICAL
So in the example:
python
20
means INFO.
### message
The actual log text.
Example:
python
"Fetched 150 records from source API"
### flow_run_id
The parent flow run this task belongs to.
### task_run_id
The exact task run that emitted the log.
Because you filtered on task_run_id, all returned logs should share the same task run ID.
### name
The logger name, often something Prefect-related or your app logger depending on how logs are emitted.
---
## Practical example
Suppose you have a task:
python
from prefect import task, flow
from prefect.logging import get_run_logger
@task
def my_task():
logger = get_run_logger()
<http://logger.info|logger.info>("Starting task")
<http://logger.info|logger.info>("Doing work")
logger.error("Something went wrong")
If you retrieve logs for that task run, the output might correspond to those three log lines.
For example:
python
for log in logs:
print(log.timestamp, log.level, log.message)
might print:
python
2026-05-21 10:15:01.123456+00:00 20 Starting task
2026-05-21 10:15:02.456789+00:00 20 Doing work
2026-05-21 10:15:03.789012+00:00 40 Something went wrong
---
## What if there are no logs?
Then:
python
logs == []
Possible reasons:
- that task emitted no logs
- you passed the wrong task_run_id
- logs are not available/retained in your environment
- the task has not run yet
---
## A more useful formatting example
python
for log in logs:
print(f"[{log.timestamp}] {log.level} - {log.message}")
Example output:
text
[2026-05-21 10:15:01.123456+00:00] 20 - Starting task
[2026-05-21 10:15:02.456789+00:00] 20 - Doing work
[2026-05-21 10:15:03.789012+00:00] 40 - Something went wrong
Or convert the numeric level:
```python
import logging
for log in logs:Marvin
05/21/2026, 2:15 AMOutput:
```text
[2026-05-21 10:15:01.123456+00:00] INFO - Starting task
[2026-05-21 10:15:02.456789+00:00] INFO - Doing work
[2026-05-21 10:15:03.789012+00:00] ERROR - Something went wrong
## Summary
That code returns:
- a list of log records
- all belonging to the given task_run_id
- each record includes timestamp, level, message, and related run IDs
If you want, I can also show you:
- the actual Prefect Log object schema
- how to serialize the logs to JSON for FastAPI response
- how to retrieve only the latest log lineMuhammad Ridwan
05/21/2026, 2:53 AMMarvin
05/21/2026, 2:53 AMMarvin
05/21/2026, 2:53 AMname argument on create_flow_run_from_deployment().
If your Pydantic model includes a custom run name, you can pass it directly when you submit the deployment run.
## Example
python
from pydantic import BaseModel
from prefect.client.orchestration import get_client
class DeploymentRequest(BaseModel):
deployment_name: str
parameters: dict = {}
flow_run_name: str | None = None
async def submit_deployment(req: DeploymentRequest):
async with get_client() as client:
deployment = await client.read_deployment_by_name(req.deployment_name)
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id,
parameters=req.parameters,
name=req.flow_run_name,
)
return flow_run
So if the request is:
python
DeploymentRequest(
deployment_name="my-flow/my-deployment",
parameters={"x": 1},
flow_run_name="customer-sync-job-001",
)
then the created flow run will be named:
text
customer-sync-job-001
---
## If you are using FastAPI
python
from fastapi import FastAPI
from pydantic import BaseModel
from prefect.client.orchestration import get_client
app = FastAPI()
class DeploymentRequest(BaseModel):
deployment_name: str
parameters: dict = {}
flow_run_name: str | None = None
@app.post("/run")
async def run_deployment(req: DeploymentRequest):
async with get_client() as client:
deployment = await client.read_deployment_by_name(req.deployment_name)
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id,
parameters=req.parameters,
name=req.flow_run_name,
)
return {
"flow_run_id": str(flow_run.id),
"flow_run_name": flow_run.name,
"state": flow_run.state.name if flow_run.state else None,
}
---
## Example request body
json
{
"deployment_name": "main-pipeline/pipeline-auto",
"parameters": {
"customer_id": 123
},
"flow_run_name": "pipeline-customer-123"
}
---
## Notes
- name= sets the flow run name, not the deployment name
- if you omit name, Prefect generates a random human-readable run name
- this happens at creation time, so the flow run is created with that name immediately
## Relevant client method
The method is:
- create_flow_run_from_deployment(deployment_id=..., name=...)
Reference:
- Prefect client API
If you want, I can also show how to:
- enforce a naming convention from your Pydantic model
- append timestamps/UUIDs automatically
- update a flow run name after creation if neededMuhammad Ridwan
05/21/2026, 3:56 AMMarvin
05/21/2026, 3:56 AMMarvin
05/21/2026, 3:59 AMget_run_logger()
- after task completion, query those task logs with the Prefect client
- parse / aggregate them
- emit a new flow-level log with get_run_logger() inside the flow
That way the summary appears in the flow logs.
---
## Example
### Task emits logs
python
from prefect import flow, task
from prefect.logging import get_run_logger
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import LogFilter, LogFilterTaskRunId
from prefect.client.schemas.sorting import LogSort
import re
@task
def compute():
logger = get_run_logger()
<http://logger.info|logger.info>("value=10")
<http://logger.info|logger.info>("value=20")
<http://logger.info|logger.info>("value=30")
return "done"
### Flow queries task logs, processes them, forwards summary to flow logs
python
from prefect import flow
from prefect.logging import get_run_logger
@flow
async def my_flow():
flow_logger = get_run_logger()
task_state = await compute(return_state=True)
task_run_id = task_state.state_details.task_run_id
async with get_client() as client:
task_logs = await client.read_logs(
log_filter=LogFilter(
task_run_id=LogFilterTaskRunId(any_=[task_run_id])
),
sort=LogSort.TIMESTAMP_ASC,
)
values = []
for log in task_logs:
matches = re.findall(r"value=(\d+)", log.message)
values.extend(int(m) for m in matches)
if values:
total = sum(values)
avg = total / len(values)
<http://flow_logger.info|flow_logger.info>(
f"Task log summary: count={len(values)}, total={total}, avg={avg}"
)
else:
<http://flow_logger.info|flow_logger.info>("Task log summary: no matching values found")
This creates a flow log entry like:
text
Task log summary: count=3, total=60, avg=20.0
---
## Why this works
- logs written inside the task stay as task logs
- logs written inside the flow stay as flow logs
- so your “forwarding” step is really:
- read task logs
- transform them
- write a new summary from the flow context
---
## If you want per-task summaries
python
@flow
async def pipeline():
flow_logger = get_run_logger()
task_states = []
for _ in range(3):
state = await compute(return_state=True)
task_states.append(state)
async with get_client() as client:
for i, task_state in enumerate(task_states, start=1):
task_run_id = task_state.state_details.task_run_id
logs = await client.read_logs(
log_filter=LogFilter(
task_run_id=LogFilterTaskRunId(any_=[task_run_id])
)
)
values = []
for log in logs:
matches = re.findall(r"value=(\d+)", log.message)
values.extend(int(m) for m in matches)
<http://flow_logger.info|flow_logger.info>(
f"Task {i} summary: count={len(values)}, sum={sum(values) if values else 0}"
)
---
## Caveats
### 1. task logs are separate records
Prefect does not merge them into flow logs for you.
### 2. you may need to query after task completion
If you query too early, logs may not all be available yet.
### 3. for many tasks, this adds API calls
If you have lots of tasks, batch carefully.
### 4. use the flow logger for the forwarded summary
That is what makes the result show up in flow logs.
---
## Alternative: avoid log scraping entirely
If your goal is mathematical processing, often the better pattern is:
- return structured values from tasks
- aggregate in the flow
- log the summary in the flow
Example:
```python
@task
def compute():
return [10, 20, 30]
@flowMarvin
05/21/2026, 3:59 AMsummarize_task_logs(task_run_id)
2. a version that works for subflows and all tasks inside them
3. a FastAPI-ready JSON version of the summarized logsMuhammad Ridwan
05/21/2026, 7:10 AMMarvin
05/21/2026, 7:10 AMMarvin
05/21/2026, 7:11 AMcannot import name 'BeforeValidator' from 'pydantic'
2. cannot import name 'Console' from 'rich.console'
Those imports should normally exist in compatible versions of pydantic and rich.
## What this usually means
Your worker environment likely has one of these issues:
- incompatible pydantic version
- incompatible rich version
- a broken install caused by dependency resolution
- local files/modules shadowing installed packages
- Python 3.13 compatibility issue with one of your pinned packages/plugins
---
## Most suspicious issue: dependency mismatch
### BeforeValidator error
BeforeValidator is associated with Pydantic v2.
So if some Prefect plugin or dependency expects Pydantic v2, but your environment ended up with Pydantic v1, you’ll get:
text
cannot import name 'BeforeValidator' from 'pydantic'
That strongly suggests:
- something expects pydantic>=2
- but your environment may have pydantic<2
### rich.Console error
This is also abnormal. Console should be importable from rich.console.
So this may mean:
- rich is corrupted
- wrong version installed
- or you have a local module/file named rich interfering
---
## The traceback suggests plugin initialization is also failing
This line is important:
text
Failed to initialize plugins
So a plugin may be importing something that drags in incompatible dependencies.
That could mean:
- a Prefect integration package
- your own package
- another installed library exposing entrypoints/plugins
---
## Most likely causes in order
### 1. Pydantic v1/v2 conflict
Very likely.
Check what is installed:
bash
uv run python -c "import pydantic; print(pydantic.__version__)"
If it is v1.x, that is probably the issue.
### 2. Broken rich install
Check:
bash
uv run python -c "from rich.console import Console; print(Console)"
If that fails, your rich install is broken or shadowed.
Also check version:
bash
uv run python -c "import rich; print(rich.__version__)"
### 3. Local module shadowing
Make sure you do not have files like:
text
rich.py
pydantic.py
prefect.py
or directories like:
text
rich/
pydantic/
inside your project that could shadow real packages.
### 4. Python 3.13 edge compatibility
You are using:
text
/app/.venv/lib/python3.13/...
Python 3.13 is still new enough that some dependency stacks can be shaky.
Even if Prefect itself supports it in many cases, some plugins/integrations may not.
If you can, I would strongly recommend testing with Python 3.11 or 3.12 first.
---
## What I would check first
Inside the worker container, run:
bash
uv run python -c "import sys; print(sys.version)"
uv run python -c "import pydantic; print(pydantic.__version__)"
uv run python -c "import rich; print(getattr(rich, '__version__', 'no version attr'))"
uv run python -c "from rich.console import Console; print(Console)"
uv run python -c "from pydantic import BeforeValidator; print(BeforeValidator)"
## What the results would mean
### If this fails:
bash
from pydantic import BeforeValidator
then your Pydantic version is wrong for the installed code.
### If this fails:
bash
from rich.console import Console
then either:
- rich is corrupted
- wrong version
- or module shadowing is happening
---
## Likely fix
### Prefer Python 3.12
If possible, switch your base image from 3.13 to 3.12.
For example:
dockerfile
FROM <http://ghcr.io/astral-sh/uv:python3.12-bookworm-slim|ghcr.io/astral-sh/uv:python3.12-bookworm-slim>
### Rebuild dependencies cleanly
If using uv, rebuild without stale cache:
bash
uv sync --reinstall
or rebuild the image with no cache.
### Pin compatible dependencies
Make sure you are not forcing old versions of:
- pydantic
- rich
If you explicitly pin them, that may be the source of the issue.
---Marvin
05/21/2026, 7:11 AMprefect, pydantic, and rich
3. check for local files named pydantic.py, rich.py, or prefect.py
4. rebuild the venv cleanly
If you want, paste your:
- Dockerfile
- pyproject.toml or requirements.txt
- Prefect version
and I can pinpoint the exact mismatch.Muhammad Ridwan
05/21/2026, 7:58 AMMarvin
05/21/2026, 7:58 AMMarvin
05/21/2026, 7:58 AMbash
prefect worker start --pool worker-pool
then that worker registers itself with Prefect as a worker for the work pool named worker-pool.
It is not discovered automatically from your microservices architecture.
## What happens internally
The worker container needs two things:
1. where the Prefect API is
via PREFECT_API_URL
2. which work pool to poll
via --pool worker-pool
So the worker starts up and effectively says:
“Connect to this Prefect server, and poll for runs assigned to the--- ## In microservices architecture The microservices part doesn’t change the rule. Your worker container still needs: - network access to the Prefect server -work pool.”worker-pool
PREFECT_API_URL
- a pool name passed in the command
Example:
yaml
prefect-worker:
build: .
command: prefect worker start --pool worker-pool
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
In this setup:
- PREFECT_API_URL tells it which Prefect server
- --pool worker-pool tells it which work pool on that server
---
## How does this connect to deployments?
A deployment is configured with a work pool, for example:
yaml
work_pool:
name: worker-pool
or in Python deployment code:
python
flow.from_source(
source=".",
entrypoint="flows/main.py:my_flow",
).deploy(
name="my-deployment",
work_pool_name="worker-pool",
)
When you create a run from that deployment, Prefect knows:
“This run belongs toThen any worker polling.”worker-pool
worker-pool can pick it up.
---
## So the matching works like this
### Deployment
Says:
text
I should run on work pool = worker-pool
### Worker
Says:
text
I poll work pool = worker-pool
### Prefect server
Matches them together.
---
## Important point
The worker container does not infer the pool name from:
- Docker Compose service name
- container name
- FastAPI service
- deployment code automatically
- environment structure
It only knows from the startup configuration you give it.
---
## Typical architecture example
yaml
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
prefect-worker:
image: prefecthq/prefect:3-latest
command: prefect worker start --pool worker-pool
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
fastapi-app:
build: .
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
And your deployment is created with:
python
my_flow.from_source(
source=".",
entrypoint="flows/main.py:my_flow",
).deploy(
name="pipeline-auto",
work_pool_name="worker-pool",
)
Now the worker knows what to poll because you told it --pool worker-pool.
---
## If you want to make it configurable
You can inject the pool name with an env var at the shell level:
yaml
prefect-worker:
command: sh -c "prefect worker start --pool $PREFECT_WORK_POOL"
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
PREFECT_WORK_POOL: worker-pool
That can be useful in microservices setups where different environments use different pool names.
---
## Short answer
The worker container knows the work pool name because you pass it explicitly in:
bash
prefect worker start --pool worker-pool
Microservices architecture does not change that; the worker still needs:
- PREFECT_API_URL to find the server
- --pool ... to know which work pool to poll
If you want, I can also show you:
- how to create the work pool automatically in Docker Compose
- how FastAPI-triggered deployments get routed to that pool
- or an end-to-end self-hosted example with server + worker + appMuhammad Ridwan
05/21/2026, 8:19 AMMarvin
05/21/2026, 8:19 AMMarvin
05/21/2026, 8:20 AMprefect server yourself.
Instead:
- FastAPI talks to Prefect Cloud
- workers or cloud-executed infrastructure also talk to Prefect Cloud
- your Docker Compose does not include a Prefect server container
This is the recommended way if you still want:
- deployments
- flow run records
- logs
- retries
- observability
- work pools / workers
- API-driven triggering
### Architecture
text
FastAPI container ---> Prefect Cloud <--- Worker / execution environment
In this model, your Compose might only contain:
- FastAPI app
- optional worker
- your app dependencies
No self-hosted Prefect API container.
### What you configure
For FastAPI and worker:
bash
PREFECT_API_URL=<https://api.prefect.cloud/api/accounts/<account>/workspaces/<workspace>>
PREFECT_API_KEY=pnu_xxx
Then FastAPI can still do:
python
await client.create_flow_run_from_deployment(...)
and workers can still execute those runs.
This is the cleanest “no orchestrator container in Compose” setup.
---
## Option 2: No Prefect orchestrator at all
This means your FastAPI app directly runs Python code or calls another container/service directly.
At that point, you are not really using Prefect deployments/workers/orchestration anymore.
You may still use Prefect as a Python workflow library by doing something like:
python
from my_flows import my_flow
result = my_flow(...)
or calling a subprocess/container yourself.
But then:
- no deployment orchestration
- no create_flow_run_from_deployment
- no worker polling
- no Prefect-managed scheduling
- no central Prefect API state unless you use ephemeral/local behavior
This is basically direct app-to-code execution, not normal Prefect orchestration.
---
# Important clarification
You said:
“communication between FastAPI and Prefect flow run code container will be direct”If by that you mean: - FastAPI sends an HTTP request to another service/container - that service/container runs the flow code directly then that is possible, but that is your own microservice execution model, not the usual Prefect deployment model. Example:
text
FastAPI ---> Flow Runner Service ---> executes Python flow function
That can work, but Prefect is then mostly being used inside that runner service as a library, not as an orchestrated platform.
---
# If you still want Prefect features, use Prefect Cloud
If you want “serverless architecture” and still want Prefect’s orchestration features, the best answer is:
Use Prefect Cloud instead of self-hosted Prefect server.
Then your Compose does not include:
- prefect server
- postgres for Prefect backend
- Prefect API/UI container
You just configure clients/workers to talk to Cloud.
---
## Example architecture with Prefect Cloud
### FastAPI container
yaml
services:
fastapi-app:
build: .
environment:
PREFECT_API_URL: <https://api.prefect.cloud/api/accounts/ACCOUNT/workspaces/WORKSPACE>
PREFECT_API_KEY: ${PREFECT_API_KEY}
### Worker container
yaml
services:
prefect-worker:
image: prefecthq/prefect:3-latest
command: prefect worker start --pool worker-pool
environment:
PREFECT_API_URL: <https://api.prefect.cloud/api/accounts/ACCOUNT/workspaces/WORKSPACE>
PREFECT_API_KEY: ${PREFECT_API_KEY}
Then your FastAPI app can trigger deployments, and the worker executes them.
---
# If you want direct FastAPI → flow container communication
Then you should think of it as:Marvin
05/21/2026, 8:20 AMhttp
POST /run-flow
FastAPI sends:
json
{
"flow_name": "main_pipeline",
"parameters": {...}
}
Then inside that service:
python
from flows.main import main_pipeline
main_pipeline(**parameters)
This can use Prefect decorators, but it is not using deployment orchestration.
### Pros
- simple topology
- no Prefect server
- direct service-to-service communication
### Cons
- you lose most of Prefect’s orchestration advantages
- you must build your own run tracking / queueing / retries / status APIs
- deployment APIs like create_flow_run_from_deployment no longer make sense
---
# So which approach should you choose?
## Choose Prefect Cloud if you want:
- no self-hosted orchestrator
- still use Prefect properly
- deployments, work pools, workers, logs, retries, UI
## Choose direct FastAPI → runner service if you want:
- no orchestrator at all
- simple internal execution
- you are okay building orchestration behavior yourself
---
# Practical recommendation
Based on your earlier questions about:
- Prefect client
- deployments
- workers
- flow run names
- logs
- statuses
it sounds like you do want orchestration features.
So I would recommend:
Use Prefect Cloud rather than removing the orchestrator entirely.
That gives you “serverless” from your infrastructure point of view, while keeping the Prefect model intact.
---
# Example: FastAPI + Prefect Cloud
## FastAPI trigger
python
from prefect.client.orchestration import get_client
async def trigger():
async with get_client() as client:
deployment = await client.read_deployment_by_name(
"main-pipeline/pipeline-auto"
)
run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id,
name="api-triggered-run",
)
return str(run.id)
## Env
bash
PREFECT_API_URL=<https://api.prefect.cloud/api/accounts/<account>/workspaces/<workspace>>
PREFECT_API_KEY=pnu_xxx
No self-hosted server container needed.
---
# Short answer
If you want serverless Prefect, the correct pattern is usually:
- remove self-hosted Prefect server from Compose
- use Prefect Cloud as the orchestrator
- keep FastAPI and workers talking to Cloud
If you want direct FastAPI → flow container communication with no orchestrator at all, that is possible, but then you are mostly not using Prefect’s deployment/worker orchestration model anymore.
If you want, I can give you one of these two concrete blueprints:
1. FastAPI + Prefect Cloud + worker
2. FastAPI directly calling a flow-runner container with no Prefect orchestratorMuhammad Ridwan
05/21/2026, 8:45 AMMarvin
05/21/2026, 8:45 AMMarvin
05/21/2026, 8:45 AMprefect-client in a project, but not if that project contains and runs Prefect flow source code in the normal way.
## Short version
- prefect = full Prefect SDK
- prefect-client = API client subset
If your project defines flows like:
python
from prefect import flow, task
then you need prefect, not just prefect-client.
---
## Main difference
### prefect
The full package. It includes:
- flow/task decorators
- runtime engine
- workers/CLI
- deployment functionality
- settings
- orchestration client
Typical use cases:
- defining flows
- running flows
- deploying flows
- starting workers
- using the CLI
Install:
bash
pip install prefect
---
### prefect-client
A lighter package focused on the API client side.
It includes:
- client/orchestration code
- settings support
- ability to call Prefect API programmatically
But it does not include the full flow execution SDK/CLI surface you’d typically use to define and run flows.
So it’s suited for things like:
- FastAPI backend triggering deployments
- querying flow runs
- reading logs
- status/reporting services
Install:
bash
pip install prefect-client
---
## Can I use prefect-client in my flow source code project?
### Yes, if that project is only acting as a client
For example:
- a service that triggers deployments
- a service that checks flow run states
- a backend that reads Prefect logs
Then prefect-client is enough.
Example:
python
from prefect.client.orchestration import get_client
That is a valid prefect-client-only style use case.
---
### No, if the project defines or runs flows
If your project contains code like:
python
from prefect import flow, task
@flow
def my_flow():
...
then you want the full prefect package.
That’s because the flow decorators and execution engine come from prefect, not just prefect-client.
---
## Good rule of thumb
### Use prefect-client for:
- API-only microservices
- FastAPI apps that trigger deployments
- monitoring/reporting apps
- services that never define @flow or @task
### Use prefect for:
- any repository that defines flows
- any worker image
- any deployment image that executes flows
- any environment using prefect worker ...
- any environment using prefect deploy
- any environment importing from prefect import flow, task
---
## In your architecture
### FastAPI service
If it only triggers runs and checks status:
- prefect-client can be enough
### Flow source code project
If it defines actual Prefect flows:
- use prefect
### Worker container
- use prefect
---
## Examples
### Client-only service
python
from prefect.client.orchestration import get_client
This can work with prefect-client.
### Flow code project
python
from prefect import flow, task
@task
def hello():
print("hi")
@flow
def my_flow():
hello()
This needs full prefect.
---
## Also important
The prefect package already includes client functionality, so if you install prefect, you do not need to separately prefer prefect-client for the same environment.
So in a flow project, there is usually no benefit to using prefect-client instead of prefect.
---
## Short answer
- Yes, prefect-client can be used in a project
- but not as a replacement for prefect in a flow source code project
- if the project defines/runs flows, use prefect
- if the project only talks to the Prefect API, prefect-client is fine
References:
- Prefect docs
- Prefect client API
If you want, I can also give you a package split recommendation for your architecture:
- FastAPI app dependencies
- worker dependencies
- flow code dependenciesMuhammad Ridwan
05/21/2026, 8:49 AMMarvin
05/21/2026, 8:49 AMMarvin
05/21/2026, 8:49 AMcommand: is for the single command the container should run.
This part:
yaml
command:
- prefect server start --host 0.0.0.0
- prefect work-pool create --type docker work=pool
does not mean “run these two commands in sequence”.
It becomes an invalid command form for what you want.
Also, the work-pool command itself looks wrong:
bash
prefect work-pool create --type docker work=pool
The correct syntax is:
bash
prefect work-pool create <POOL_NAME> --type docker
So for example:
bash
prefect work-pool create worker-pool --type docker
Verified CLI help:
- prefect server start
- prefect work-pool create NAME --type docker
---
## If you want to do both
You need a shell wrapper that:
1. starts the server
2. waits for it to be available
3. creates the work pool
But there’s an important issue:
- prefect server start is a long-running foreground process
- so if you run it first, the second command never executes unless you background it
## Example with shell
yaml
prefect-server:
image: prefecthq/prefect:3-latest
command: >
sh -c "
prefect server start --host 0.0.0.0 &
sleep 10 &&
prefect work-pool create worker-pool --type docker &&
wait
"
environment:
PREFECT_CLI_PROMPT: "false"
ports:
- "4200:4200"
That can work, but it’s a bit fragile because sleep 10 is just a guess.
---
## Better approach
Usually it’s cleaner to separate concerns:
### Option A: create the work pool from a one-off init container
Example pattern:
- prefect-server starts
- another short-lived container runs:
bash
prefect work-pool create worker-pool --type docker
### Option B: create the pool manually once
If the pool is persistent, you may only need to create it one time.
### Option C: startup script with a health check
More reliable than fixed sleep.
---
## Corrected command syntax
If you only want the work-pool command syntax fixed:
bash
prefect work-pool create worker-pool --type docker
not:
bash
prefect work-pool create --type docker work=pool
---
## Recommended Compose pattern
yaml
services:
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
environment:
PREFECT_CLI_PROMPT: "false"
ports:
- "4200:4200"
prefect-init:
image: prefecthq/prefect:3-latest
command: >
sh -c "
sleep 10 &&
prefect work-pool create worker-pool --type docker
"
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
PREFECT_CLI_PROMPT: "false"
depends_on:
- prefect-server
This is cleaner because:
- server container runs only the server
- init container creates the pool
## Important note
The container creating the pool needs:
bash
PREFECT_API_URL=<http://prefect-server:4200/api>
because it is acting as a client to the server.
The server container itself usually does not need PREFECT_API_URL.
---
## Short answer
No, that exact YAML is not valid.
Use either:
- one command only for the server container, or
- a shell script if you really want sequential behavior, or
- better, a separate init container for prefect work-pool create
If you want, I can write a full valid Docker Compose file for:
- Prefect server
- work-pool init
- Prefect worker
- FastAPI appMuhammad Ridwan
05/21/2026, 9:35 AMMarvin
05/21/2026, 9:35 AMMarvin
05/21/2026, 9:35 AMdatabase is locked)
---
## 1) Discovered type 'docker' ... necessary dependencies
This means the work pool is of type docker, but the worker container does not have the Docker worker dependencies installed.
For Docker work pools, don’t use plain prefect only. Install/run Prefect with Docker extras.
### Recommended worker image
Use a worker image with Docker integration available.
If building your own image:
dockerfile
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --extra docker
Or if using pip:
dockerfile
RUN pip install "prefect[docker]"
The key is: the worker for a Docker pool needs prefect[docker].
If you run commands with uv, same idea applies in ephemeral command verification:
bash
uv run --with 'prefect[docker]' prefect worker start --pool work-pool
---
## 2) sqlite3.OperationalError: database is locked
This is a classic sign that your self-hosted Prefect server is using SQLite under concurrent load.
SQLite is okay for light/local/dev usage, but it is not a good backend for multi-container orchestration with:
- server
- worker(s)
- API clients
- concurrent state/log writes
For anything beyond trivial local testing, switch Prefect server to PostgreSQL.
### Recommended fix: use Postgres
Set PREFECT_API_DATABASE_CONNECTION_URL on the server/services containers.
Example:
yaml
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: prefect
POSTGRES_PASSWORD: prefect
POSTGRES_DB: prefect
volumes:
- postgres_data:/var/lib/postgresql/data
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
environment:
PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
PREFECT_CLI_PROMPT: "false"
ports:
- "4200:4200"
depends_on:
- postgres
This will eliminate the SQLite locking problem in normal setups.
---
## What’s probably happening in your setup
### Worker side
Your work pool is docker, so the worker tries to initialize Docker worker code, but your image only has base Prefect installed.
### Server side
Your server likely has no PREFECT_API_DATABASE_CONNECTION_URL set, so it falls back to SQLite. Under worker/server activity, writes contend and SQLite locks.
---
## Practical next steps
### Fix worker dependencies
If your worker uses Docker work pool, install:
bash
prefect[docker]
### Fix backend database
Move from SQLite to Postgres by setting:
bash
PREFECT_API_DATABASE_CONNECTION_URL=postgresql+asyncpg://...
---
## Example Compose sketch
yaml
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: prefect
POSTGRES_PASSWORD: prefect
POSTGRES_DB: prefect
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
environment:
PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
PREFECT_CLI_PROMPT: "false"
ports:
- "4200:4200"
depends_on:
- postgres
prefect-worker:
build: .
command: prefect worker start --pool work-pool
environment:
PREFECT_API_URL: <http://prefect-server:4200/api>
PREFECT_CLI_PROMPT: "false"
depends_on:
- prefect-server
And in the worker image, ensure:
dockerfile
RUN pip install "prefect[docker]"
---
## One more important Docker-pool caveat
If you use a Docker work pool, the worker typically needs access to the Docker daemon too, often via:
```yaml
volumes:Marvin
05/21/2026, 9:35 AMprefect[docker]
- server problem: SQLite backend locking → switch to PostgreSQL
If you want, paste your current docker-compose.yml and Dockerfile, and I can rewrite them into a working Prefect server + Postgres + Docker worker setup.Muhammad Ridwan
05/21/2026, 9:42 AMMarvin
05/21/2026, 9:42 AMMarvin
05/21/2026, 9:42 AMpython:<version> image.
So if you use:
dockerfile
FROM prefecthq/prefect:3-latest
you are already getting a relatively slim Prefect base image.
## If you want the smallest practical approach
You have two options:
### Option 1: use the official Prefect image directly
dockerfile
FROM prefecthq/prefect:3-latest
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
This is the easiest option.
---
### Option 2: build your own slimmer image from python:slim
If you want tighter control, start from a slim Python image and install only what you need.
Example:
dockerfile
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
And in `requirements.txt`:
text
prefect
or if you need Docker worker support:
text
prefect[docker]
---
## If your question is “can I make prefecthq/prefect:3-latest even slimmer?”
Not really in a simple official toggle sense. There isn’t a separate “super-slim” official tag you can switch to by just appending something obvious.
If image size matters a lot, the common pattern is:
- use python:<version>-slim
- install only the exact Prefect extras you need
- avoid unnecessary OS packages
That often gives you a leaner custom image than a general-purpose official one.
---
## Best practice by use case
### FastAPI client-only service
Use custom slim image:
dockerfile
FROM python:3.12-slim
RUN pip install --no-cache-dir prefect-client
### Flow/worker image
Use custom slim image:
dockerfile
FROM python:3.12-slim
RUN pip install --no-cache-dir "prefect[docker]"
### Quick start / less maintenance
Use:
dockerfile
FROM prefecthq/prefect:3-latest
---
## Important note
Use slim, not alpine.
python:3.x-slim is generally fine with Prefect.
python:3.x-alpine is much more likely to cause dependency/runtime issues.
---
## Short answer
- the official Prefect base image is already effectively slim-based
- if you want maximum control/minimal size, build from python:<version>-slim and install prefect or prefect-client yourself
Useful docs:
- Prefect Docker guide
- Serve flows with Docker
If you want, I can give you:
- a minimal Dockerfile for prefect-client only
- a minimal Dockerfile for a Docker worker
- a size-optimized multi-stage Dockerfile for PrefectMuhammad Ridwan
05/22/2026, 2:06 AMMarvin
05/22/2026, 2:06 AMMarvin
05/22/2026, 2:06 AMsqlite3.OperationalError: database is locked is not just noisy logging — it means your Prefect server database is actually failing to handle concurrent writes.
So the right fix is not “hide the traceback”, but:
- reduce concurrency temporarily, or preferably
- move off SQLite to PostgreSQL
## Short answer
If you’re running self-hosted Prefect with workers / multiple services, SQLite is the wrong backend.
Use Postgres instead.
Why this happens: SQLite locks the whole DB much more aggressively, and Prefect orchestration generates concurrent reads/writes for:
- state updates
- logs
- scheduling
- worker polling
That’s why you see:
text
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) database is locked
---
## The real solution: use PostgreSQL
Set:
bash
PREFECT_API_DATABASE_CONNECTION_URL=<postgresql+asyncpg://user:password@postgres:5432/prefect>
Example in Docker Compose:
yaml
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: prefect
POSTGRES_PASSWORD: prefect
POSTGRES_DB: prefect
prefect-server:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
environment:
PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@postgres:5432/prefect>
PREFECT_CLI_PROMPT: "false"
ports:
- "4200:4200"
depends_on:
- postgres
This is the proper fix.
---
## If you only want to suppress the traceback
There isn’t a good supported “disable SQLite lock traceback” setting in Prefect that makes the problem harmless.
You could try to reduce log verbosity, but that only hides symptoms and the failures still happen.
For example, lowering SQLAlchemy/logging output won’t prevent:
- failed writes
- stuck orchestration
- worker instability
So I would not recommend it.
---
## If you absolutely need a temporary workaround
You can sometimes reduce how often it happens by reducing concurrency:
- fewer workers
- fewer simultaneous runs
- fewer background services
- lighter logging volume
But this still doesn’t solve the root issue.
### Temporary mitigation ideas
- run only one worker
- avoid bursty task/log writes
- don’t run many services against the same SQLite DB
- avoid production use of self-hosted SQLite
---
## Why “just disabling it” is dangerous
Because then you’d stop seeing evidence of a real backend failure while Prefect may still:
- fail to update states
- fail to persist logs
- fail to schedule runs correctly
So even if hidden, your orchestrator would still be unhealthy.
---
## Recommended conclusion
If you are using:
- Prefect server
- worker(s)
- FastAPI client
- Docker-based orchestration
then switch to PostgreSQL now.
That is the intended path for this kind of setup.
Relevant docs:
- Run a Prefect server
- Docker Compose self-hosted setup
If you want, I can rewrite your current docker-compose.yml to replace SQLite with Postgres cleanly.Muhammad Ridwan
05/22/2026, 2:48 AMMarvin
05/22/2026, 2:48 AMMarvin
05/22/2026, 2:48 AMwork-pool, and because no explicit worker type was resolved from an existing pool, it defaulted to a process worker and created the pool itself.
So in your case, this happened:
1. worker started with something like:
bash
prefect worker start --pool work-pool
2. Prefect checked whether work-pool exists
3. it did not exist
4. worker defaulted to a process worker
5. worker created work-pool
That is why you see:
text
Work pool 'work-pool' created.
## Important caveat
This only means it created a pool for the worker type it is running as.
From your log:
text
Starting a process worker...
So the created pool is effectively a process work pool, not Docker/Kubernetes/etc.
That matters a lot.
---
## What this means practically
### Yes, you can rely on worker-side creation
If you are okay with:
- the worker deciding/using its own type
- the pool being created as that worker type
then yes, the worker can bootstrap the pool.
### But be careful
If you intended to use:
- docker pool
- kubernetes pool
- another specific worker type
then letting the worker auto-create it as process may be not what you want.
---
## In your log, the worker created a process pool
So if your deployment expects a Docker pool, this is a mismatch.
For example:
- deployment configured for Docker execution
- worker auto-created a process pool
- runs may not behave the way you expect
---
## Best practice
### If you want a specific pool type, create it explicitly
For example:
bash
prefect work-pool create work-pool --type docker
or:
bash
prefect work-pool create work-pool --type process
Then start the matching worker.
### If you are fine with process execution, worker auto-creation is acceptable
In that case:
bash
prefect worker start --pool work-pool
is enough, and the worker can create the pool if missing.
---
## Recommended rule
- For dev/simple setups: worker-side auto-creation is okay
- For production/intentional infra: explicitly create the pool with the desired type
---
## Why your earlier Docker worker issue matters
Previously, you had logs about Docker worker type/dependencies. If now the worker is auto-creating a process pool, then it may be because:
- the Docker pool did not exist
- or the worker could not initialize Docker type
- so it fell back to process
That could explain inconsistent behavior.
---
## Short answer
Yes, the worker is capable of creating the work pool from its side.
But in your log it created a process pool, because it says:
text
Starting a process worker...
So this is only safe if you actually want a process work pool.
If you want, I can help you determine whether your setup should use:
- process pool, or
- docker pool
based on how your containers are arranged.Muhammad Ridwan
05/22/2026, 2:54 AMMarvin
05/22/2026, 2:54 AMMarvin
05/22/2026, 2:54 AMprocess
- docker
- kubernetes
- other worker/integration-specific types when available
---
# 1. What a work pool does
A work pool is responsible for:
- grouping runs for a certain execution backend
- matching those runs to compatible workers
- storing infrastructure/job configuration defaults
- telling Prefect how jobs should be launched
So when a deployment is created with:
python
work_pool_name="my-pool"
Prefect expects that a worker polling my-pool knows how to execute that deployment according to the pool type.
---
# 2. Why the type matters
The type determines the launch mechanism.
## process work pool
Runs the flow as a local subprocess in the worker environment.
### How it works
- the worker polls the Prefect API
- when it gets a run, it starts a local process
- the code executes directly in that worker container/VM environment
### Good for
- local development
- simple self-hosted setups
- cases where the worker already has the flow code and dependencies
### Requirements
- worker must have your flow code available
- worker must have all required Python dependencies installed
### Mental model
text
worker container
-> runs flow directly inside itself
### Pros
- simple
- less moving parts
- easy to debug
### Cons
- weaker isolation
- dependencies for all flows often need to exist in the worker image
- scaling/isolation is limited compared to container-per-run approaches
---
## docker work pool
Runs each flow run as a Docker container.
### How it works
- worker polls Prefect API
- when it gets a run, it launches a Docker container for that run
- that container executes the flow
### Good for
- stronger isolation per run
- different images per deployment
- container-native infra
### Requirements
- worker must have Docker worker dependencies (prefect[docker])
- worker must be able to access Docker daemon
- deployment/job config must specify a suitable image or environment
### Mental model
text
worker container
-> talks to Docker daemon
-> launches another container for each flow run
### Pros
- better isolation
- easier per-deployment image control
- nice for containerized workloads
### Cons
- more setup complexity
- Docker socket/daemon access required
- nested container patterns can be tricky
---
## kubernetes work pool
Runs each flow run as a Kubernetes job/pod.
### How it works
- worker polls Prefect API
- worker submits Kubernetes jobs/pods for flow runs
### Good for
- cloud-native / cluster-based infra
- scalable production workloads
- teams already on Kubernetes
### Requirements
- Kubernetes worker dependencies
- cluster credentials / RBAC / namespace setup
- proper job templates/images
### Pros
- strong isolation
- scalable
- aligned with K8s operations
### Cons
- most operational complexity
- requires Kubernetes platform knowledge
---
# 3. Process vs Docker vs Kubernetes: the practical difference
## Process
Run inside the worker itself.
If your worker container has:
- app code
- Python deps
- correct env vars
then the run can execute there directly.
## Docker
Run in a new container launched by the worker.
The worker is more like a launcher/controller.
## Kubernetes
Run in a K8s job/pod launched by the worker.
The worker is even more remote from the actual runtime.
---
# 4. Why your pool type must match your worker
A worker must support the type of the pool it polls.
For example:Marvin
05/22/2026, 2:54 AMbash
prefect worker start --pool work-pool
and the pool does not exist, Prefect may create it automatically based on the worker’s own type.
Example:
text
Work pool 'work-pool' does not exist ... Starting a process worker...
Work pool 'work-pool' created.
That means the worker created a process pool.
This is convenient, but can surprise you if you intended Docker or Kubernetes.
---
# 6. Which work pool type should you choose?
## Choose process if:
- you want the simplest setup
- flows can run in the same environment as the worker
- you are doing local/dev/small self-hosted deployments
- you don’t need one-container-per-run isolation
This is often the easiest starting point.
## Choose docker if:
- you want each run isolated in its own container
- you already manage images for your flows
- your environment is Docker-centric
- you want different deployments to use different images
This is a common production-friendly self-hosted pattern.
## Choose kubernetes if:
- your platform already runs on Kubernetes
- you want scalable, isolated job execution
- you need cluster-native scheduling and control
This is the most infrastructure-heavy option.
---
# 7. How deployments interact with work pools
A deployment is associated with one work pool.
Example:
python
my_flow.from_source(
source=".",
entrypoint="flows/main.py:my_flow",
).deploy(
name="daily-job",
work_pool_name="work-pool",
)
When a run is created from that deployment:
- Prefect records it under work-pool
- a worker polling work-pool picks it up
- the pool type determines how the run is launched
---
# 8. What job variables/base job templates do
Work pools can also hold infrastructure defaults.
For example, in Docker/Kubernetes pools, the pool may define defaults like:
- image
- environment variables
- network settings
- memory/cpu settings
- job template details
Then deployments can override some of these with job_variables.
This matters much more in docker and kubernetes pools than in process pools.
---
# 9. How this maps to your architecture
Given your questions, here’s the practical mapping:
## FastAPI app
Usually does not care about pool type directly.
It just triggers deployments.
## Prefect server / Prefect Cloud
Stores the pool and routes runs logically.
## Worker
Must support the pool type.
## Flow code
Must be available according to the pool type:
- `process`: in worker environment
- `docker`: in run image/container
- `kubernetes`: in pod/job image or mounted/pulled source
---
# 10. Common mistakes
## Mistake 1: creating a Docker pool but starting a plain process worker
This leads to worker startup or execution mismatch.
## Mistake 2: using Docker pool without Docker dependencies
You need prefect[docker].
## Mistake 3: using process pool when worker image does not contain the flow code
Then the worker can’t import/run the flow.
## Mistake 4: letting the worker auto-create a process pool when you intended Docker
This happens if you start the worker without explicitly creating the desired pool type first.
---
# 11. Recommended decision tree
## Start with process if:
- you are still learning the architecture
- your worker and code live together
- you want fewer moving parts
## Move to docker if:
- you need per-run isolation
- you want deployment-specific images
- you are comfortable with Docker daemon setup
## Use kubernetes if:
- you already have a K8s platform
- you want cluster scheduling/isolation at scale
---
# 12. Example scenariosMarvin
05/22/2026, 2:54 AMbash
prefect work-pool create my-process-pool --type process
Create a docker pool:
bash
prefect work-pool create my-docker-pool --type docker
Create a kubernetes pool:
bash
prefect work-pool create my-k8s-pool --type kubernetes
Then start a matching worker:
bash
prefect worker start --pool my-process-pool
or:
bash
prefect worker start --pool my-docker-pool
---
# 14. Best advice for your current setup
Given your Docker Compose / self-hosted / FastAPI questions:
- if you want simple and stable first: use a process pool
- if you want container-per-run isolation: use a docker pool and install Docker worker extras properly
- don’t let auto-created pool type surprise you — create the pool explicitly if type matters
---
# Final summary
A work pool type tells Prefect how to execute flow runs:
- process → run inside the worker process environment
- docker → launch a Docker container per run
- kubernetes → launch a Kubernetes job/pod per run
The pool type must match:
- your worker capabilities
- your infrastructure
- where your flow code and dependencies live
If you want, I can next give you a comparison table specifically for process vs docker for your microservices setup, and tell you which one fits your architecture best.Muhammad Ridwan
05/22/2026, 3:13 AM{
"detail": "Invalid deployment name format: desktop-study-auto. Expected format: <FLOW_NAME>/<DEPLOYMENT_NAME>"
}Marvin
05/22/2026, 3:13 AMMarvin
05/22/2026, 3:13 AMtext
desktop-study-auto
but Prefect expects:
text
<FLOW_NAME>/<DEPLOYMENT_NAME>
So it wants something like:
text
my-flow/desktop-study-auto
## Why this happens
When using methods like:
python
client.read_deployment_by_name(...)
Prefect does not accept just the deployment name by itself.
It expects the fully qualified deployment name:
- flow name
- slash /
- deployment name
### Example
If your flow is named:
text
main-pipeline
and your deployment is named:
text
desktop-study-auto
then the correct input is:
text
main-pipeline/desktop-study-auto
---
## So what is wrong with your input?
You used only:
text
desktop-study-auto
That is only the deployment name.
Prefect needs:
text
<flow-name>/desktop-study-auto
---
## Why it may show as a bad gateway in your app
The real underlying application error is the invalid deployment name format.
Your FastAPI / reverse proxy / gateway may be wrapping it as a “bad gateway” style response, but the meaningful part is this:
json
{
"detail": "Invalid deployment name format: desktop-study-auto. Expected format: <FLOW_NAME>/<DEPLOYMENT_NAME>"
}
So the core problem is not networking — it is incorrect input.
---
## Fix
If your code currently does:
python
deployment = await client.read_deployment_by_name("desktop-study-auto")
change it to something like:
python
deployment = await client.read_deployment_by_name("my-flow/desktop-study-auto")
---
## Example
python
from prefect.client.orchestration import get_client
async def get_deployment():
async with get_client() as client:
deployment = await client.read_deployment_by_name(
"main-pipeline/desktop-study-auto"
)
return deployment
---
## How to know the flow name
The flow name is the name Prefect registered for the flow, often:
- the function name by default, or
- an explicitly set flow name
Example:
python
from prefect import flow
@flow(name="main-pipeline")
def my_flow():
pass
Then the deployment name might be:
text
desktop-study-auto
So full deployment name becomes:
text
main-pipeline/desktop-study-auto
---
## Summary
Your input:
text
desktop-study-auto
Expected:
text
<FLOW_NAME>/<DEPLOYMENT_NAME>
Example:
text
main-pipeline/desktop-study-auto
Reference:
- Prefect client API
If you want, I can help you figure out the exact full deployment name from your flow code or deployment definition.Muhammad Ridwan
05/22/2026, 3:29 AMMarvin
05/22/2026, 3:29 AMMarvin
05/22/2026, 3:29 AMpython
f"{body.deployment_name}/{os.environ["DEPLOYMENT_NAME"]}"
because the inner " closes the outer string.
Use single quotes inside:
python
f"{body.deployment_name}/{os.environ['DEPLOYMENT_NAME']}"
---
## 2. You may have flow name vs deployment name reversed
read_deployment_by_name(...) expects:
text
<FLOW_NAME>/<DEPLOYMENT_NAME>
But your code currently builds:
python
f"{body.deployment_name}/{os.environ['DEPLOYMENT_NAME']}"
That suggests:
- body.deployment_name is being used as the flow name
- DEPLOYMENT_NAME env var is being used as the deployment name
That may be correct only if body.deployment_name actually contains the flow name.
But from the name body.deployment_name, it sounds like it probably contains the deployment name already.
If so, this is wrong conceptually.
---
## 3. You are naming the flow run with body.deployment_name
This part is valid syntactically:
python
name=body.deployment_name
but semantically it means the flow run name will be the same as whatever body.deployment_name contains.
That may be okay, but usually you probably want a dedicated run name field, like:
python
name=body.flow_run_name
rather than reusing the deployment name.
---
# Likely intended structure
You probably want something like:
- body.flow_name
- body.deployment_name
- body.parameters
- optionally body.flow_run_name
## Better example
python
import os
from prefect.client.orchestration import get_client
async def trigger_flow(body: TriggerRequest):
async with get_client() as client:
deployment = await client.read_deployment_by_name(
f"{body.flow_name}/{body.deployment_name}"
)
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id,
parameters=body.parameters.model_dump(),
name=body.flow_run_name if body.flow_run_name else None,
)
print(flow_run.id)
return flow_run
---
# If you really want env var + request body
If your environment variable stores the deployment name, and the body stores the flow name, then:
python
deployment = await client.read_deployment_by_name(
f"{body.flow_name}/{os.environ['DEPLOYMENT_NAME']}"
)
That is valid.
But I would rename fields to make the meaning clearer.
---
# 4. Potential issue with model_dump()
This is okay only if:
python
body.parameters
is a Pydantic model instance.
If body.parameters is already a plain dict, then:
python
body.parameters.model_dump()
will fail.
So make sure the type supports model_dump().
If it might already be a dict, use it directly:
python
parameters=body.parameters
---
# 5. Missing return value
You print the flow run id:
python
print(flow_run.id)
but don’t return anything.
If this is a FastAPI handler, you probably want:
python
return {"flow_run_id": str(flow_run.id)}
---
# Corrected version
Assuming:
- body.flow_name = flow name
- body.deployment_name = deployment name
- body.flow_run_name optional
- body.parameters is a Pydantic model
python
from prefect.client.orchestration import get_client
async def trigger_flow(body: TriggerRequest):
async with get_client() as client:
deployment = await client.read_deployment_by_name(
f"{body.flow_name}/{body.deployment_name}"
)
flow_run = await client.create_flow_run_from_deployment(
deployment_id=deployment.id,
parameters=body.parameters.model_dump(),
name=body.flow_run_name if body.flow_run_name else None,
)
return {"flow_run_id": str(flow_run.id), "flow_run_name": flow_run.name}
---
# If you want to keep env var deployment name
```python
import os
from prefect.client.orchestration import get_clientMarvin
05/22/2026, 3:29 AM---
# Biggest likely mistake
The *biggest likely problem* is that you are using `body.deployment_name` in the first half of:
```python
<flow-name>/<deployment-name>
which is probably wrong if that field truly contains a deployment name.
If you want, I can help you redesign the TriggerRequest model so the field names correctly match Prefect’s expectations.