Vishnu Sajan
11/27/2025, 4:36 AMMarvin
11/27/2025, 4:36 AMMarvin
11/27/2025, 4:43 AMFlow.deploy() is reading a local API URL from your active settings context, so your os.environ["PREFECT_API_URL"] change isn’t taking effect when the client is created to look up the work pool.
In Prefect 3.x, the API URL used by Flow.deploy() comes from the current Prefect settings in this precedence order:
1) temporary_settings() context
2) Environment variables (PREFECT_API_URL, PREFECT_API_KEY)
3) .env / prefect.toml / pyproject.toml in CWD
4) Active profile (~/.prefect/profiles.toml)
5) Defaults
Reliable ways to force a remote API URL
Option A — Explicit in code (recommended)
Wrap your deploy in a temporary settings override. This takes highest precedence and guarantees the value used by get_client() inside .deploy().
from prefect import flow
from prefect.settings import PREFECT_API_URL, temporary_settings
@flow
def my_flow():
pass
if __name__ == "__main__":
with temporary_settings({PREFECT_API_URL: "<https://your-remote-host/api>"}):
print("API:", PREFECT_API_URL.value()) # sanity check
my_flow.deploy(
name="my-deployment",
work_pool_name="your-remote-pool",
)
Option B — Set env vars before starting Python
Set variables in your shell (or CI) so Prefect reads them when creating settings for the process.
export PREFECT_API_URL=<https://your-remote-host/api>
export PREFECT_API_KEY=YOUR_KEY_IF_NEEDED
python deploy_script.py
Option C — Use a Prefect profile
Create/switch to a profile that points at your remote server, then run your script.
prefect profile create remote
prefect profile use remote
prefect config set PREFECT_API_URL=<https://your-remote-host/api>
prefect config set PREFECT_API_KEY=YOUR_KEY_IF_NEEDED
python deploy_script.py
Quick sanity check
Before calling .deploy(), print the resolved value:
from prefect.settings import PREFECT_API_URL
print("Resolved API URL:", PREFECT_API_URL.value())
If it’s still <http://localhost:4200/api>, something else in your settings stack is overriding your env var (e.g., an active profile, .env, or a prior settings context). Using temporary_settings(...) will override all of those.
A few extra tips
- Make sure the work pool exists on the remote server (and not only locally).
- If your remote is Prefect Cloud or a self-hosted server with auth, also set PREFECT_API_KEY.
- If you prefer profiles in code, you can also do:
from prefect.context import use_profile
with use_profile("remote"):
my_flow.deploy(name="...", work_pool_name="...")
Docs links
- Settings & profiles (precedence, env, profiles): Settings and profiles
- Manage settings: How to manage settings
- Deploy via Python: Deploy via Python
If this still points to localhost, can you share:
- Where in your script you set os.environ["PREFECT_API_URL"]
- A minimal snippet around your flow.deploy(...)
- Output of print(PREFECT_API_URL.value()) right before the deploy
I’ll help pinpoint what’s overriding it.Vishnu Sajan
11/27/2025, 4:55 AMMarvin
11/27/2025, 4:55 AMMarvin
11/27/2025, 4:56 AMfrom prefect import flow
from prefect.client.orchestration import get_client
from prefect.settings import PREFECT_API_URL, PREFECT_API_KEY, temporary_settings
@flow
def my_flow():
pass
if __name__ == "__main__":
with temporary_settings({
PREFECT_API_URL: "<https://your-remote-host/api>",
# include if your server requires auth
# PREFECT_API_KEY: "pnu_..."
}):
# Sanity check
print("Resolved API URL:", PREFECT_API_URL.value())
# Any client created here will use the remote URL
async def check():
async with get_client() as client:
print("Client API URL:", client.api)
import anyio
anyio.run(check)
# Deploy will also use the same settings
my_flow.deploy(
name="my-deployment",
work_pool_name="your-remote-pool",
)
If you prefer env vars, set them before starting Python (not inside the script), then start a fresh process so a new client is created with the right settings:
export PREFECT_API_URL=<https://your-remote-host/api>
export PREFECT_API_KEY=YOUR_KEY_IF_NEEDED
python deploy_script.py
Or use a profile and switch before running your script:
prefect profile create remote
prefect profile use remote
prefect config set PREFECT_API_URL=<https://your-remote-host/api>
prefect config set PREFECT_API_KEY=YOUR_KEY_IF_NEEDED
python deploy_script.py
Debugging tips:
- See what is setting the URL:
prefect config view --show-sources
- Print the resolved setting right before you create the client or call `.deploy()`:
from prefect.settings import PREFECT_API_URL
print("Resolved API URL:", PREFECT_API_URL.value())
- If you created a PrefectClient before changing settings, discard it and create a new one after the change (or let .deploy() create its own via get_client() inside a temporary_settings(...) block).
Docs:
- Settings and profiles (precedence, env, profiles): Settings and profiles
- Manage settings: How to manage settings
- Deploy via Python: Deploy via Python
If you’re still seeing localhost, can you share:
- A minimal snippet showing where you set PREFECT_API_URL and where you create/use the client
- Output of print(PREFECT_API_URL.value()) right before your .deploy()
- Output of prefect config view --show-sources | grep -A2 api.url
I’ll help pinpoint what’s overriding it.Vishnu Sajan
11/27/2025, 6:35 AMMarvin
11/27/2025, 6:35 AMMarvin
11/27/2025, 6:38 AMfrom_source(...) so the pull step embeds them, or
- give the worker an auth mechanism it can use on its own (SSH key, env, GIT_ASKPASS), or
- define credentials via pull steps in prefect.yaml.
Here are the reliable patterns for private Git repos with Flow.from_source in Prefect 3.x.
Option 1 — Pass a credentials block to GitRepository (recommended)
This serializes credentials into the deployment’s pull step so the worker can clone with a token at runtime.
from prefect import flow
from prefect.runner.storage import GitRepository
from prefect_github import GitHubCredentials # pip install prefect-github
# Save once (UI or code)
# GitHubCredentials(token="ghp_...").save("my-github-creds", overwrite=True)
repo = GitRepository(
url="<https://github.com/org/private-repo.git>",
branch="main",
credentials=GitHubCredentials.load("my-github-creds"),
)
flow.from_source(
source=repo,
entrypoint="path/to/flow_file.py:my_flow",
).deploy(
name="my-deployment",
work_pool_name="my-docker-pool",
)
- At run time, the worker runs the pull step, which formats the URL like https://<token>@github.com/org/private-repo.git and clones successfully.
- Use the provider-specific collection that implements credentials formatting:
- GitHub: prefect-github
- GitLab: prefect-gitlab
- Bitbucket: prefect-bitbucket
Docs:
- Store flow code (Git auth, pull steps): Store flow code
- GitHub integration: prefect-github
Option 2 — Define pull steps in prefect.yaml
If you prefer yaml:
pull:
- prefect.deployments.steps.git_clone:
repository: <https://github.com/org/private-repo.git>
credentials: "{{ prefect.blocks.github-credentials.my-github-creds }}"
branch: main
deployments:
- name: my-deployment
entrypoint: path/to/flow_file.py:my_flow
work_pool:
name: my-docker-pool
Option 3 — SSH URL and mount SSH keys into worker
Use an SSH clone URL and give the worker a key.
- Use SSH URL in your repo definition:
repo = GitRepository(
url="git@github.com:org/private-repo.git",
branch="main",
)
- In your docker compose for the worker, mount keys and known_hosts:
services:
prefect-worker:
image: prefecthq/prefect:3-latest
environment:
PREFECT_API_URL: <http://your-prefect-server:4200/api>
volumes:
- ~/.ssh:/root/.ssh:ro
- /etc/ssh/ssh_config:/etc/ssh/ssh_config:ro # optional
command: prefect worker start --pool my-docker-pool
- Ensure the public key is added to the GitHub account and known_hosts includes github.com (first connection will add it, or seed known_hosts).
Option 4 — Env-based token with GIT_ASKPASS (less recommended)
If you can’t use blocks, you can configure GIT_ASKPASS or embed token via environment:
- Provide a script inside the worker image that echoes the token, then set:
environment:
GIT_ASKPASS: /opt/git-askpass.sh
GITHUB_TOKEN: ${GITHUB_TOKEN}
And your `/opt/git-askpass.sh`:
#!/bin/sh
echo "$GITHUB_TOKEN"
- Or embed the token in the URL in the pull step (be careful with logs and security).
Why your current setup fails
- Using GitHubCredentials only in the “scheduling/deploy” script doesn’t help at run time unless those credentials are attached to the GitRepository passed to from_source (or defined in yaml pull steps). The worker runs in a different environment and needs its own way to authenticate at execution time.
- Workers started via docker compose on another server have no access to your local env unless you mount or pass it explicitly.Marvin
11/27/2025, 6:38 AMfrom_source to use a GitRepository with a credentials block as shown above, and redeploy.
2) Ensure your worker has network access to github.com and correct time/CA certs.
3) If using SSH, mount ~/.ssh into the worker container and use the SSH URL.
4) Avoid putting tokens directly in the repo URL in code; prefer credentials blocks or SSH.
If you share:
- Your current from_source(...) snippet
- Your docker-compose service for the worker
- Whether you prefer PAT or SSH
…I can tailor an exact working example for your setup.Vishnu Sajan
11/27/2025, 6:43 AMMarvin
11/27/2025, 6:43 AMMarvin
11/27/2025, 6:44 AMFlow.from_source(...), Prefect includes those credentials in the deployment’s pull step so the worker can authenticate at runtime. If your worker can’t clone, it’s usually a runtime environment issue rather than the block not being attached.
Common causes and fixes
1) Work pool type and where the pull happens
- Docker work pool: the pull step runs inside the job container image, not in the worker container. That image must contain git (and CA certs) and have the integration installed if needed.
- Process/local work pool: the pull step runs on the worker host/container, so that host/container must have git and proper certs.
2) Ensure you’re attaching credentials to the GitRepository used by from_source
- Good pattern:
from prefect import flow
from prefect.runner.storage import GitRepository
from prefect_github import GitHubCredentials # pip install prefect-github
repo = GitRepository(
url="<https://github.com/org/private-repo.git>",
branch="main",
credentials=GitHubCredentials.load("my-github-creds"),
)
flow.from_source(
source=repo,
entrypoint="path/to/flow.py:my_flow",
).deploy(
name="my-deployment",
work_pool_name="my-docker-pool",
# if using Docker work pool, specify an image that has git installed:
image="my-registry/prefect-git:latest",
)
3) Your runtime image must include git (and likely the integration package)
- If you’re using a Docker work pool, build an image with git and ca-certificates installed:
# Dockerfile
FROM prefecthq/prefect:3-latest
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir "prefect-github>=0.3.0"
- Push this image and use it in .deploy(image=...).
4) Use HTTPS URL with GitHubCredentials
- The GitHubCredentials formatter expects an https repo URL. If you want SSH, don’t use the credentials block—use an SSH URL and mount SSH keys into the runtime environment instead.
5) Token scopes and submodules
- Ensure the token has repo scope and access to any private submodules.
- If your repo uses private submodules, consider enabling submodule support in your GitRepository and ensure the token can access them.
6) Verify the deployment captured the credentials
- In the UI, open the deployment and look at the Pull section. You should see a git_clone step with credentials (token will be redacted).
- If it’s missing, revisit your from_source(..., source=GitRepository(credentials=...)) code and redeploy.
7) Network and SSL
- Confirm outbound egress to github.com from the job container.
- Install CA certificates (as above) to avoid TLS errors.
How to debug what the worker/job is doing
- Increase logging:
- Set PREFECT_LOGGING_LEVEL=DEBUG on the worker (and for docker jobs, in the job environment/image).
- Reproduce inside the job image:
- Run an interactive shell in the same image the deployment uses:
docker run -it --rm my-registry/prefect-git:latest bash
git --version
# Try a tokened URL (replace <TOKEN> and repo)
git ls-remote https://<TOKEN>@github.com/org/private-repo.git
- This confirms git and certs are present and the token works.
- Check the Pull section logs in the flow run details (UI) to see the error (403 vs DNS vs TLS).
- If you need step-by-step debugging of flow code, run locally:
- Switch to a process/local pool and run the worker on your dev machine so you can attach a debugger.
- Or run the same code locally (with the same from_source(...)) to reproduce before deploying.
- You can also add a temporary shell pull step (if you move to prefect.yaml) that prints git --version and git config -l to verify the environment. With pure .deploy() you can’t add custom pull steps in Python, so environment verification is best done by testing the job image interactively as above.
Next steps / quick checklistMarvin
11/27/2025, 6:44 AMcredentials=GitHubCredentials.load("...") into GitRepository.
Docs
- Store flow code (Git auth, pull steps): Store flow code
- Docker work pools: Docker work pools
- GitHub integration: prefect-github
Share the error text and your deploy snippet + work pool type, and I’ll give you an exact fix for your setup.