Mohamed Zaki
08/28/2025, 6:44 AMMarvin
08/28/2025, 6:44 AMMarvin
08/28/2025, 6:49 AMPREFECT_API_AUTH_STRING
to satisfy your proxy.
Recommended production pattern
- Do not expose Prefect services directly. Put all Prefect containers (API, services, DB, workers) on a private network. Only the reverse proxy is internet‑facing.
- Terminate TLS at the proxy (Let’s Encrypt/ACME or your corp certs). Prefer TLS 1.2+.
- Enforce authentication/authorization at the proxy:
- OIDC/SSO with oauth2‑proxy (Okta, Azure AD, Google, Auth0, etc.) for browser/UI and programmatic access
- or mTLS for service‑to‑service/programmatic access
- or Basic/JWT auth or an API gateway (Kong/Envoy/NGINX Plus) that validates tokens
- Lock down the database (require strong password, optionally TLS), do not expose it.
- For internal clients (workers, CLI, your apps), set:
- PREFECT_API_URL
to your external API base (e.g., `https://prefect.example.com/api`)
- PREFECT_API_AUTH_STRING
to the Authorization header your proxy expects (e.g., Bearer <jwt>
or Basic <base64>
)
- If you use a private CA, set PREFECT_API_SSL_CERT_FILE
to the CA bundle path mounted in your containers.
- For the UI behind a proxy, if you’re changing host/path, set the UI envs to point to your external URLs.
Key settings you’ll use
- PREFECT_API_URL
- PREFECT_API_AUTH_STRING
(Prefect will forward this verbatim as the HTTP Authorization header)
- PREFECT_API_SSL_CERT_FILE
(trust a private CA)
- PREFECT_UI_URL
and, if needed, PREFECT_UI_API_URL
(so the browser UI calls the right API over your proxy)
Example: secure reverse proxy with oauth2‑proxy (OIDC)
- Reverse proxy enforces login and only forwards authenticated requests to Prefect’s API/UI.
- Prefect clients/CLI/workers use a bearer token (or a service account flow) and set PREFECT_API_AUTH_STRING="Bearer <token>"
.
Minimal Nginx snippet (conceptual)
server {
listen 443 ssl http2;
server_name <http://prefect.example.com|prefect.example.com>;
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;
# UI
location / {
# If using oauth2-proxy:
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_pass <http://prefect-server:4200/;> # Prefect UI/API (adjust to your container/ports)
}
# API path if you want to split it
location /api/ {
auth_request /oauth2/auth;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_pass <http://prefect-server:4200/api/;>
}
# oauth2-proxy endpoints
location /oauth2/ {
proxy_pass <http://oauth2-proxy:4180/;>
}
}
Docker Compose pointers
- Keep Prefect API/services/DB on an internal network; expose only the proxy’s 443.
- Configure client/worker env vars in Compose:
environment:
- PREFECT_API_URL=<https://prefect.example.com/api>
- PREFECT_API_AUTH_STRING=Bearer ${PREFECT_SERVICE_TOKEN}
- PREFECT_API_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt
- If the UI is being proxied on a different host/path, add to the Prefect server/UI container:
environment:
- PREFECT_UI_URL=<https://prefect.example.com>
- PREFECT_UI_API_URL=<https://prefect.example.com/api>
CLI/SDK usage
- Configure your local profile once:
prefect config set PREFECT_API_URL=<https://prefect.example.com/api>
prefect config set PREFECT_API_AUTH_STRING="Bearer <your-service-token>"
- Or set env vars in your app:
```
import os
os.environ["PREFECT_API_URL"] = "https://prefect.example.com/api"
os.environ["PREFECT_API_AUTH_STRING"] = "Bearer <your-service-token>"Marvin
08/28/2025, 6:49 AMPREFECT_API_SSL_CERT_FILE
- Rate limit/throttle at the proxy
- Use strict-Allow/Origin policies if you split UI/API hosts
- Rotate any service tokens; prefer short‑lived JWTs or mTLS for automation
- Back up your PostgreSQL; enable TLS to Postgres if across networks
References
- Self-hosted via Docker Compose (official): Docker Compose guide
- Settings reference (API/UI envs): Settings reference
- Security overview and guidance: Security settings
- Community reverse proxy example: Compose with reverse proxy example
A couple of clarifying questions to tailor this for you:
- Which identity provider do you use (Okta, Azure AD, Google, etc.) and do you prefer OIDC with oauth2‑proxy?
- Will the API be publicly accessible, or only inside a VPN/private network?
- Do you want browser SSO only, or also token-based auth for programmatic access? I can share a concrete Compose + oauth2‑proxy config once I know these details.Mohamed Zaki
08/28/2025, 7:14 AMMarvin
08/28/2025, 7:14 AMMarvin
08/28/2025, 7:15 AMservices:
oauth2-proxy:
image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7.6.0|quay.io/oauth2-proxy/oauth2-proxy:v7.6.0>
environment:
# Replace with your IdP details
- OAUTH2_PROXY_PROVIDER=oidc
- OAUTH2_PROXY_CLIENT_ID=${OIDC_CLIENT_ID}
- OAUTH2_PROXY_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
- OAUTH2_PROXY_COOKIE_SECRET=${OAUTH2_COOKIE_SECRET} # 32-byte base64
- OAUTH2_PROXY_OIDC_ISSUER_URL=${OIDC_ISSUER_URL} # e.g. <https://login.microsoftonline.com/<tenant>/v2.0>
- OAUTH2_PROXY_REDIRECT_URL=<https://prefect.example.com/oauth2/callback>
- OAUTH2_PROXY_WHITELIST_DOMAINS=<http://prefect.example.com|prefect.example.com>
- OAUTH2_PROXY_EMAIL_DOMAINS=*
- OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
# Pass user info to Nginx if needed
- OAUTH2_PROXY_SET_XAUTHREQUEST=true
networks:
- internal
nginx:
image: nginx:1.27
depends_on:
- oauth2-proxy
# - prefect-server # your existing service
ports:
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./secrets/tls/fullchain.pem:/etc/ssl/certs/fullchain.pem:ro
- ./secrets/tls/privkey.pem:/etc/ssl/private/privkey.pem:ro
- ./secrets/htpasswd:/etc/nginx/.htpasswd:ro
networks:
- internal
networks:
internal:
driver: bridge
2) Nginx config (two vhosts: UI+OAuth2 and API+Basic)
Put this at ./nginx/nginx.conf.
```
worker_processes 1;
events { worker_connections 1024; }
http {
# Basic hardening, tune as needed
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
upstream prefect_backend {
# Match your container name and port
server prefect-server:4200;
}
# ---------- UI host (OAuth2 for browser + cookie session) ----------
server {
listen 443 ssl http2;
server_name prefect.example.com;
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;
# oauth2-proxy endpoints
location /oauth2/ {
proxy_pass http://oauth2-proxy:4180/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme https;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
}
# Auth gate (subrequest to oauth2-proxy)
location = /oauth2/auth {
proxy_pass http://oauth2-proxy:4180/oauth2/auth;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
# UI and API for browser
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Upgrade headers help with SSE/long-pollingMarvin
08/28/2025, 7:15 AM3) Configure Prefect UI to point at your external URLs
Set these on your Prefect server container so the UI renders the correct endpoints:
PREFECT_UI_URL=https://prefect.example.com
PREFECT_UI_API_URL=https://prefect.example.com/api
4) Create Basic auth credentials
Create the htpasswd file used by the API host:
- With Docker (no local htpasswd needed):
docker run --rm httpd:2-alpine htpasswd -nbB myuser 'mypassword' > ./secrets/htpasswd
- Or on your machine:
htpasswd -cB ./secrets/htpasswd myuser # you will be prompted for a password
5) Programmatic clients (CLI/workers/app code)
Point them at the Basic-protected API host and include the Basic header via PREFECT_API_AUTH_STRING.
- One-time CLI profile:
prefect config set PREFECT_API_URL=https://api.prefect.example.com/api
prefect config set PREFECT_API_AUTH_STRING="Basic $(printf '%s' 'myuser:mypassword' | base64)"
- In code:
import os
os.environ["PREFECT_API_URL"] = "https://api.prefect.example.com/api"
os.environ["PREFECT_API_AUTH_STRING"] = "Basic <base64 user:password>"
from prefect.client.orchestration import get_client
async def main():
async with get_client() as client:
flows = await client.read_flows()
print(len(flows))
```
Notes and tips
- Keep Prefect backend and DB on the internal network; only expose Nginx:443.
- If you use a private CA, distribute it and set PREFECT_API_SSL_CERT_FILE
on workers/clients.
- If you put API and UI on different hosts (as above), the UI will call its own host’s /api path (OAuth2 cookie), while programmatic access uses the API host (Basic). No CORS needed for the UI path.
- Rotate Basic credentials regularly; consider mTLS or short-lived JWTs if you later need stronger controls for automation.
References
- Security settings: Security guidance
- Settings reference (API/UI envs): Settings reference
- Docker Compose self-hosting: Self-hosted via Docker Compose
- oauth2-proxy: oauth2-proxy docs
If you share your IdP (Okta/Azure AD/Google/etc.), I can fill in the exact oauth2-proxy environment variables for that provider and add any required scopes/claims.Mohamed Zaki
08/29/2025, 12:43 PMMarvin
08/29/2025, 12:43 PMMarvin
08/29/2025, 12:47 PMauth_request
.
3) Have NGINX pass the Authorization or X-API-Key header to your auth app’s validation endpoint.
Example NGINX snippet (proxying everything to your Prefect Server):
# Upstreams
upstream prefect_api {
server 127.0.0.1:4200; # wherever `prefect server start` exposes the API/UI
}
upstream auth_backend {
server 127.0.0.1:9000; # your auth service that validates tokens/API keys
}
server {
listen 443 ssl;
server_name <http://prefect.example.com|prefect.example.com>;
# TLS config omitted for brevity
# Subrequest that validates auth via your app
location = /_auth {
internal;
proxy_pass <http://auth_backend/validate;> # your app's validation endpoint
proxy_set_header Authorization $http_authorization;
proxy_set_header X-API-Key $http_x_api_key;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
# Protect everything (UI + API)
location / {
auth_request /_auth;
proxy_pass <http://prefect_api>;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Helpful for long-running/streaming endpoints
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_http_version 1.1;
proxy_set_header Connection "";
# If you see issues with event streams, consider:
# proxy_buffering off;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
}
}
Notes:
- Prefect Server itself does not read your auth headers — the proxy enforces auth.
- Keep the UI and API on the same origin/host to avoid CORS headaches; the UI will call the API via the same host by default.
Prefect clients (workers/flows/CLI) with your proxy
- For non-interactive clients, you want them to include an Authorization header that your proxy enforces. Prefect provides a setting for basic auth:
- `PREFECT_API_URL`: point clients to your proxy’s API URL
- `PREFECT_API_AUTH_STRING`: a username:password
string; the client will send Authorization: Basic ...
- Example:
export PREFECT_API_URL="<https://prefect.example.com/api>"
export PREFECT_API_AUTH_STRING="svc:YOUR_API_KEY_OR_PASSWORD"
or persist it to a profile
prefect config set PREFECT_API_URL=<https://prefect.example.com/api>
prefect config set PREFECT_API_AUTH_STRING="svc:YOUR_API_KEY"
Marvin
08/29/2025, 12:47 PMPREFECT_API_URL
, PREFECT_API_AUTH_STRING
, CSRF protection, etc.
- Community reference (reverse proxy + Prefect): example repo (community-maintained, but useful for patterns).
A few clarifying questions to tailor a concrete setup:
- Are you using Prefect Cloud or self-hosting Prefect Server?
- For self-hosting: Do you want the UI accessible to end users (with your sign-in) or is it primarily machine-to-machine?
- Can your auth app expose a synchronous “validate token/key” HTTP endpoint that NGINX or oauth2-proxy can call?
- Do you want clients to use Basic auth to the proxy, or must they use your Bearer/JWT format directly?
If you can share those, I can provide an exact NGINX (or oauth2-proxy) config and the precise Prefect settings to use.Mohamed Zaki
08/29/2025, 5:03 PMMarvin
08/29/2025, 5:03 PMMarvin
08/29/2025, 5:04 PMauth_request
to oauth2-proxy, and proxying to Prefect Server
Step 1) Create a Keycloak client for the UI
- Realm: your choice (e.g., myrealm)
- Client type: OpenID Connect (confidential)
- Redirect URI: https://prefect.example.com/oauth2/callback
- Standard Flow: Enabled
- Save client ID and secret
Step 2) Deploy oauth2-proxy (pointed at Keycloak)
Example (Docker-like config; adapt to your platform):
# env: .env.oauth2-proxy
OAUTH2_PROXY_CLIENT_SECRET=... # from Keycloak client
OAUTH2_PROXY_COOKIE_SECRET=... # 32-byte base64 (e.g., `openssl rand -base64 32`)
# docker compose-style service (or run_args)
oauth2-proxy:
image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7.6.0|quay.io/oauth2-proxy/oauth2-proxy:v7.6.0>
env_file: .env.oauth2-proxy
ports:
- "4180:4180"
command:
- --provider=oidc
- --oidc-issuer-url=<https://keycloak.example.com/realms/myrealm>
- --client-id=prefect-ui
- --client-secret=${OAUTH2_PROXY_CLIENT_SECRET}
- --redirect-url=<https://prefect.example.com/oauth2/callback>
- --cookie-secret=${OAUTH2_PROXY_COOKIE_SECRET}
- --scope=openid email profile
- --http-address=0.0.0.0:4180
- --reverse-proxy=true
- --cookie-secure=true
- --cookie-samesite=lax
- --email-domain=*
- --upstream=<static://200>
Step 3) Configure NGINX to enforce Keycloak and proxy to Prefect
This keeps the actual Prefect API private; only NGINX can reach it, and only after login.
```
upstream prefect_backend {
server 10.0.0.20:4200; # Prefect Server (private)
}
upstream oauth2_proxy {
server 127.0.0.1:4180; # oauth2-proxy
}
server {
listen 443 ssl;
server_name prefect.example.com;
# TLS config omitted
# oauth2-proxy endpoints
location /oauth2/ {
proxy_pass http://oauth2_proxy;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $request_uri;
}
# Subrequest endpoint for auth checks
location = /oauth2/auth {
proxy_pass http://oauth2_proxy;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
}
# Protect everything (UI + API) with Keycloak
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/start?rd=$request_uri;
# Optional: forward user info headers to Prefect (not required)
auth_request_set $user $upstream_http_x_auth_request_user;
proxy_set_header X-Auth-Request-User $user;
proxy_set_header X-Forwarded-User $user;
proxy_pass http://prefect_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Prefect benefits from long timeouts and streaming
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # helpful for log/event streams
}
# If you want unauthenticated health checks, you can exempt specific paths
# location /api/health {
# proxy_pass http://prefect_backend;Marvin
08/29/2025, 5:04 PMprefect server start
behind your firewall or in a private subnet).
- You generally do not need special settings for the UI if everything is served on the same host and proxied together.
- Optionally set these so links and redirects use the public URL:
- PREFECT_UI_URL=<https://prefect.example.com>
- PREFECT_API_URL=<https://prefect.example.com/api>
(only if you want generated links to use the public URL; clients on the private network can still use the private API URL)
Key points and caveats
- The Prefect UI must be able to talk to the API. By proxying both via NGINX on the same host, the browser uses a relative /api
path and stays on the same origin—no CORS issues.
- The API is not directly exposed to the internet; only the proxy can reach it, and the proxy enforces Keycloak auth.
- If you’re on Kubernetes, you can do the same with Ingress-NGINX and oauth2-proxy (via auth-url
annotations). The pattern is identical.
- If you see issues with streams or logs in the UI, ensure proxy_buffering off
and long read timeouts for the API paths.
References
- Prefect security settings: Security settings
- Prefect Server Helm (useful if you’re on K8s): Server Helm
- oauth2-proxy docs: oauth2-proxy
If you share:
- Your deployment target (VMs vs Kubernetes)
- Desired Keycloak group restrictions (who should get access)
- Your current Prefect Server endpoint(s)
…I can tailor the exact manifests/config you need (including K8s Ingress annotations or a full docker-compose stack).Mohamed Zaki
08/30/2025, 7:25 PMname: prefect-wizard
services:
keycloak:
image: <http://quay.io/keycloak/keycloak:24.0|quay.io/keycloak/keycloak:24.0>
command: start-dev
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KEYCLOAK_HOSTNAME: "<http://localhost:8080/realms/wizard>"
ports:
- "8080:8080"
volumes:
- keycloak_data:/opt/keycloak/data
oauth2-proxy:
image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7.5.1|quay.io/oauth2-proxy/oauth2-proxy:v7.5.1>
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "4180:4180"
environment:
OAUTH2_PROXY_PROVIDER: oidc
OAUTH2_PROXY_OIDC_ISSUER_URL: <http://host.docker.internal:8080/realms/wizard>
OAUTH2_PROXY_CLIENT_ID: wizard-oauth
OAUTH2_PROXY_CLIENT_SECRET: "olcWi1lGcyrU82MtdvNgK8Nm7x4C8ieA"
OAUTH2_PROXY_COOKIE_SECRET: "bHEyY2E0dEZHSTlnY2hJa0FRWnRERURxaDg4Qm5qY3k="
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"
OAUTH2_PROXY_UPSTREAMS: "<http://server:4200>"
OAUTH2_PROXY_REDIRECT_URL: "<http://host.docker.internal:4180/oauth2/callback>"
depends_on:
- keycloak
- server
api:
build:
context: .
dockerfile: docker/api/nonprod/Dockerfile
ports:
- "9832:9832"
environment:
PREFECT_API_URL: <http://server:4200/api>
volumes:
- ./api:/app
- ./autoupdate:/app/autoupdate
- ./core:/app/core
extra_hosts:
- "host.docker.internal:host-gateway"
env_file:
- .env
restart: always
healthcheck:
test: [ "CMD-SHELL", "curl -f <http://localhost:9832/health> || exit 1" ]
interval: 5s
timeout: 5s
retries: 5
db:
image: postgres:14
environment:
POSTGRES_USER: prefect
POSTGRES_PASSWORD: prefect
POSTGRES_DB: prefect
ports:
- "5466:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U prefect" ]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7
volumes:
- redis_data:/data
healthcheck:
test: [ "CMD-SHELL", "redis-cli ping" ]
interval: 5s
timeout: 5s
retries: 5
server:
image: prefecthq/prefect:3.4.13-python3.11
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
environment:
PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@db:5432/prefect>
PREFECT_SERVER_API_HOST: 0.0.0.0
PREFECT_UI_API_URL: "<http://localhost:4200/api>"
PREFECT_MESSAGING_BROKER: prefect_redis.messaging
PREFECT_MESSAGING_CACHE: prefect_redis.messaging
PREFECT_REDIS_MESSAGING_HOST: redis
PREFECT_REDIS_MESSAGING_PORT: 6379
PREFECT_REDIS_MESSAGING_DB: 0
PREFECT_LOGGING_LEVEL: DEBUG
PREFECT_LOGGING_LOG_PRINTS: True
PREFECT_SERVER_LOGGING_LEVEL: DEBUG
PREFECT_SERVER_LOGGING_LOG_PRINTS: True
#PREFECT_SERVER_API_AUTH_STRING: "admin:admin"
command: prefect server start --no-services
ports:
- "4200:4200"
healthcheck:
test: [ "CMD-SHELL", "python -c 'import json,urllib.request,sys; sys.exit(0 if json.loads(urllib.request.urlopen(\"<http://localhost:4200/api/health>\", timeout=1).read()) is True else 1)' " ]
interval: 5s
timeout: 3s
retries: 30
services:
image: prefecthq/prefect:3.4.13-python3.11
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
environment:
PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@db:5432/prefect>
PREFECT_API_URL: <http://server:4200/api>
PREFECT_MESSAGING_BROKER: prefect_redis.messaging
PREFECT_MESSAGING_CACHE: prefect_redis.messaging
PREFECT_REDIS_MESSAGING_HOST: redis
PREFECT_REDIS_MESSAGING_PORT: 6379
PREFECT_REDIS_MESSAGING_DB: 0
command: prefect server services start
create-deployment:
image: prefect-workpool
build:
context: .
dockerfile: docker/autoupdate/Dockerfile
depends_on:
- server
environment:
PREFECT_API_URL: <http://server:4200/api>
# command: prefect deployment build autoupdate/pipeline/deployment.py:autoupdate_pipeline --name autoupdate-pipeline --cron "0 0 * * *" --timezone "America/New_York" --infra prefect-wizard-infra --storage-block prefect-s3-block
command: bash -c "cd /opt/prefect/autoupdate && prefect --no-prompt deploy --name autoupdate-deployment"
volumes:
- ./autoupdate:/opt/prefect/autoupdate
- ./core:/opt/prefect/autoupdate/core
env_file:
- .env
autoupdate-worker:
image: prefect-workpool
depends_on:
server:
condition: service_healthy
services:
condition: service_started
create-deployment:
condition: service_completed_successfully
environment:
PREFECT_API_URL: <http://server:4200/api>
DOCKER_HOST: <tcp://host.docker.internal:2375>
PREFECT_LOCAL_STORAGE_PATH: /logs/storage
# DOCKER_TLS_VERIFY: ${DOCKER_TLS_VERIFY:-}
# DOCKER_CERT_PATH: ${DOCKER_CERT_PATH:-}
restart: always
volumes:
- ./autoupdate:/opt/prefect/autoupdate
- ./core:/opt/prefect/autoupdate/core
- ${HOME}/.wizard:/logs
extra_hosts:
- "host.docker.internal:host-gateway"
env_file:
- .env
volumes:
postgres_data:
redis_data:
keycloak_data:
Now there's an issue with networking, i am having DNS split brains problem, docker network sees oauth and keycloak differently than how browser view them from local hostMarvin
08/30/2025, 7:25 PMMarvin
08/30/2025, 7:29 PMKEYCLOAK_HOSTNAME
to a full realm URL with http + path. For Keycloak 24, you should use KC_HOSTNAME
and it should be just the host (and optional port), not include /realms/...
.
- Issuer mismatch: OAUTH2_PROXY_OIDC_ISSUER_URL
uses <http://host.docker.internal:8080/realms/wizard>
but Keycloak may be advertising a different issuer (due to the hostname setting above). OAuth2 Proxy requires the discovered issuer
to exactly match what you configured.
- Browser vs container names: The browser cannot resolve Docker service names like `server`; it needs a browser-visible URL. Meanwhile containers should use Docker DNS (service names).
- UI calling the API: Your PREFECT_UI_API_URL
is currently "<http://localhost:4200/api>"
, which bypasses your proxy. If you want the UI (in the browser) to go through oauth2-proxy, point this to the proxy’s external URL (and make oauth2-proxy forward /api
to the Prefect server).
A solid way to fix this
- Pick one canonical external URL for Keycloak that both the browser and oauth2-proxy will use (e.g., auth.localtest.me:8080 — localtest.me resolves to 127.0.0.1, no hosts file needed).
- Configure Keycloak to advertise that URL.
- Configure oauth2-proxy to use that exact issuer and a redirect URL that the browser will reach.
- Configure Prefect UI to call the API via the proxy.
Concrete changes
1) Keycloak (use KC_* vars in v24 and set a single hostname)
keycloak:
image: <http://quay.io/keycloak/keycloak:24.0|quay.io/keycloak/keycloak:24.0>
command: ["start-dev", "--http-port=8080"]
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_HOSTNAME: "<http://auth.localtest.me|auth.localtest.me>" # no path, just host (and optional port)
KC_HOSTNAME_STRICT: "false" # dev convenience
KC_HOSTNAME_STRICT_HTTPS: "false" # dev convenience
KC_PROXY: "edge" # recommended when behind a proxy
ports:
- "8080:8080"
volumes:
- keycloak_data:/opt/keycloak/data
- In your Keycloak realm “wizard,” the well-known config should then show issuer as <http://auth.localtest.me:8080/realms/wizard>
.
2) OAuth2 Proxy (match issuer, use browser-visible callback)
oauth2-proxy:
image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7.5.1|quay.io/oauth2-proxy/oauth2-proxy:v7.5.1>
ports:
- "4180:4180"
environment:
OAUTH2_PROXY_PROVIDER: oidc
OAUTH2_PROXY_OIDC_ISSUER_URL: "<http://auth.localtest.me:8080/realms/wizard>"
OAUTH2_PROXY_CLIENT_ID: wizard-oauth
OAUTH2_PROXY_CLIENT_SECRET: "olcWi1lGcyrU82MtdvNgK8Nm7x4C8ieA"
OAUTH2_PROXY_COOKIE_SECRET: "bHEyY2E0dEZHSTlnY2hJa0FRWnRERURxaDg4Qm5qY3k=" # 32B base64
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"
OAUTH2_PROXY_REDIRECT_URL: "<http://localhost:4180/oauth2/callback>" # must match Keycloak client redirect
OAUTH2_PROXY_UPSTREAMS: "<http://server:4200>"
- Update your Keycloak client “wizard-oauth” to include <http://localhost:4180/oauth2/callback>
as a valid redirect URI.
- If you want a nicer hostname than localhost:4180 for the browser, you can use something like <http://prefect.localtest.me>
and set cookie/domain options accordingly.
3) Prefect server behind the proxy (make the UI call the API through oauth2-proxy)
- Proxy both “/” (the UI) and “/api” through oauth2-proxy to server:4200
.
- Then set:
```
server:
image: prefecthq/prefect:3.4.13-python3.11
environment:Marvin
08/30/2025, 7:29 PM<http://prefect.localtest.me>
, use PREFECT_UI_API_URL: "<http://prefect.localtest.me/api>"
. The key is: this must be the browser-visible address, not the Docker service name or port 4200 directly.
4) Avoid host.docker.internal unless you must
- On Mac/Windows it works, but on Linux it often causes confusion. Prefer a single canonical hostname that your browser resolves (like localtest.me) and let containers talk to each other by service name on the Docker network (e.g., server
, keycloak
).
- If you must use host.docker.internal
, ensure every container that references it has the extra_hosts
mapping and that the browser can resolve that name too.
Quick checklist to validate
- Visit http://auth.localtest.me:8080/realms/wizard/.well-known/openid-configuration in your browser. The issuer
value must exactly match OAUTH2_PROXY_OIDC_ISSUER_URL
.
- In oauth2-proxy logs, there should be no “issuer mismatch” warnings.
- The Keycloak client redirect URI must exactly match OAUTH2_PROXY_REDIRECT_URL
.
- The browser DevTools network tab should show the UI calling <http://localhost:4180/api/>...
(or your chosen host), not <http://localhost:4200>
.
If you’d like, I can help you wire oauth2-proxy to route /api
to <http://server:4200/api>
and everything else to <http://server:4200/>
explicitly. Also, let me know:
- Are you on Linux, macOS, or Windows?
- Do you want the API protected by oauth2-proxy as well, or just the UI?
References
- Prefect 3.x security settings
- Example: Prefect Server behind a reverse proxyMohamed Zaki
08/30/2025, 7:42 PM[2025/08/30 19:40:53] [main.go:60] ERROR: Failed to initialise OAuth2 Proxy: error initialising provider: could not create provider data: error building OIDC ProviderVerifier: could not get verifier builder: error while discovery OIDC configuration: failed to discover OIDC configuration: error performing request: Get "<http://auth.localtest.me:8080/realms/wizard/.well-known/openid-configuration>": dial tcp 127.0.0.1:8080: connect: connection refused
Marvin
08/30/2025, 7:42 PMMarvin
08/30/2025, 7:43 PM<http://auth.localtest.me|auth.localtest.me>
resolves to 127.0.0.1 inside the oauth2-proxy container, so it’s trying to hit itself on 8080. That’s why you see “connect: connection refused.”
You need that hostname to resolve differently depending on who is asking:
- In the browser: to 127.0.0.1 (so it reaches the host’s published port 8080 → Keycloak)
- In containers: to the Keycloak container IP (so they can reach Keycloak directly)
Recommended fix: give Keycloak a Docker network alias that matches the public hostname, and set Keycloak to advertise that same hostname.
Changes
1) Keycloak: switch to KC_* vars and add a network alias
keycloak:
image: <http://quay.io/keycloak/keycloak:24.0|quay.io/keycloak/keycloak:24.0>
command: ["start-dev", "--http-port=8080"]
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_HOSTNAME: "<http://auth.localtest.me|auth.localtest.me>" # no path
KC_HOSTNAME_STRICT: "false"
KC_HOSTNAME_STRICT_HTTPS: "false"
KC_PROXY: "edge"
ports:
- "8080:8080"
networks:
default:
aliases:
- <http://auth.localtest.me|auth.localtest.me>
2) oauth2-proxy: point issuer to that hostname (no extra_hosts needed)
oauth2-proxy:
image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7.5.1|quay.io/oauth2-proxy/oauth2-proxy:v7.5.1>
ports:
- "4180:4180"
environment:
OAUTH2_PROXY_PROVIDER: oidc
OAUTH2_PROXY_OIDC_ISSUER_URL: "<http://auth.localtest.me:8080/realms/wizard>"
OAUTH2_PROXY_CLIENT_ID: wizard-oauth
OAUTH2_PROXY_CLIENT_SECRET: "olcWi1lGcyrU82MtdvNgK8Nm7x4C8ieA"
OAUTH2_PROXY_COOKIE_SECRET: "bHEyY2E0dEZHSTlnY2hJa0FRWnRERURxaDg4Qm5qY3k="
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"
OAUTH2_PROXY_REDIRECT_URL: "<http://localhost:4180/oauth2/callback>"
OAUTH2_PROXY_UPSTREAMS: "<http://server:4200>"
Why this works
- Containers on the compose network will resolve <http://auth.localtest.me|auth.localtest.me>
via Docker’s embedded DNS to the Keycloak container IP.
- Your browser will resolve <http://auth.localtest.me|auth.localtest.me>
to 127.0.0.1 (thanks to localtest.me), which maps to the host’s published 8080 and then to the Keycloak container.
- Keycloak advertises <http://auth.localtest.me|auth.localtest.me>
in the discovery document, so oauth2-proxy’s configured issuer matches exactly.
After changing network aliases, bring the stack down and back up so Docker recreates the network:
docker compose down
docker compose up -d --build
Validate from within the oauth2-proxy container:
docker compose exec oauth2-proxy getent hosts <http://auth.localtest.me|auth.localtest.me>
docker compose exec oauth2-proxy curl -s <http://auth.localtest.me:8080/realms/wizard/.well-known/openid-configuration>
Alternative if you don’t want a network alias
- Map the hostname to the host gateway for oauth2-proxy:
oauth2-proxy:
extra_hosts:
- "<http://auth.localtest.me:host-gateway|auth.localtest.me:host-gateway>"
This makes oauth2-proxy reach Keycloak via the host’s published port. Still set KC_HOSTNAME=<http://auth.localtest.me|auth.localtest.me>
so the issuer matches.
Reminder for Prefect UI
- If you’re protecting the API via oauth2-proxy, set PREFECT_UI_API_URL
to the browser-visible proxy URL, e.g.:
PREFECT_UI_API_URL: "<http://localhost:4180/api>"
If you share your OS (Linux/macOS/Windows), I can tailor which of the two approaches (alias vs host-gateway) is the smoother fit for your setup.