Pav Staz
02/20/2026, 12:44 PMMarvin
02/20/2026, 12:44 PMMarvin
02/20/2026, 12:46 PMexport PREFECT_SERVER_UI_API_URL="<https://prefect.example.com/api>"
prefect server start --host 0.0.0.0 --port 4200
- Minimal Nginx config for a subdomain at the root path:
upstream prefect_upstream {
server 127.0.0.1:4200;
}
server {
listen 80;
server_name <http://prefect.example.com|prefect.example.com>;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name <http://prefect.example.com|prefect.example.com>;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# Forward original request info
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket/SSE support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Reasonable timeouts for streaming/logs/events
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
location / {
proxy_pass <http://prefect_upstream>;
}
}
If you’re hosting the UI under a subpath (e.g., https://example.com/prefect), add:
- export PREFECT_UI_SERVE_BASE="/prefect/"
- export PREFECT_SERVER_UI_API_URL="<https://example.com/prefect/api>"
- In Nginx: location /prefect/ { proxy_pass <http://prefect_upstream/;> }
Quick diagnostics
- From anywhere that reaches your domain:
curl -v <https://prefect.example.com/api/health>
curl -s <https://prefect.example.com/ui-settings>
Both should return JSON. In particular, /ui-settings should show an api_url that matches <https://prefect.example.com/api> (not 127.0.0.1).
- In your browser DevTools (Network tab), reload the page and look for failed calls to /ui-settings or /api/... and any CORS/upgrade errors.
- Check logs:
# Nginx
tail -f /var/log/nginx/error.log /var/log/nginx/access.log
# Prefect (wherever you run it)
prefect server logs (or your process logs)
If that doesn’t resolve it, can you share:
- Your current Nginx server block for prefect.example.com
- The output of curl -s <https://prefect.example.com/ui-settings>
- Any errors you see in the browser console or Prefect/Nginx logs
I’ll pinpoint the exact issue from there.Pav Staz
02/20/2026, 12:56 PMx-project-image: &prefect-img
image: <http://myregistry.com/prefect:prod_latest|myregistry.com/prefect:prod_latest>
x-prefect-common: &prefect-common
<<: *prefect-img
tty: true
stdin_open: true
working_dir: /opt/project
volumes: # required for email to send
- ./utils/gmail_credentials.json:/opt/project/utils/gmail_credentials.json:ro
- ./utils/gmail_token.json:/opt/project/utils/gmail_token.json
#env_file: # uncomment this for portainer to work (production)
# - stack.env
x-prefect-deploy-common: &prefect-deploy-common
<<: *prefect-common
depends_on:
server: {condition: service_healthy}
secret_loader: {condition: service_completed_successfully}
environment:
PYTHONPATH: /opt/project # this is required so that imports into files the deployment folder can be prefect-served from a directory
PREFECT_API_URL: <http://server:4200/api>
PREFECT_API_AUTH_STRING: ${PREFECT_SERVER_API_AUTH_STRING} # needed to authorise deployment
services:
server:
<<: *prefect-img
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
# env_file: # uncomment this for portainer to work (production)
# - stack.env
environment:
PREFECT_PROFILES_ACTIVE: server
PREFECT_SERVER_API_AUTH_STRING: ${PREFECT_SERVER_API_AUTH_STRING}
PREFECT_API_AUTH_STRING: ${PREFECT_SERVER_API_AUTH_STRING}
volumes:
- prefect_data:/root/.prefect
healthcheck:
test: [
"CMD-SHELL",
"python - <<'PY'\nimport sys, urllib.request, json, time\nurl='<http://localhost:4200/api/health>'\ntry:\n with urllib.request.urlopen(url, timeout=3) as r:\n sys.exit(0 if r.status==200 else 1)\nexcept Exception:\n sys.exit(1)\nPY"
]
interval: 5s
timeout: 3s
retries: 40
secret_loader:
<<: *prefect-common
depends_on:
server: {condition: service_healthy}
command: >
bash -c "
python utils/save_secrets.py
"
volumes:
- ./utils/snowflake.pem:/opt/project/utils/snowflake.pem:ro
environment:
PREFECT_API_URL: <http://server:4200/api>
PREFECT_API_AUTH_STRING: ${PREFECT_SERVER_API_AUTH_STRING}
AIRBYTE_URL: ${AIRBYTE_URL}
AIRBYTE_CLIENT_ID: ${AIRBYTE_CLIENT_ID}
AIRBYTE_CLIENT_SECRET: ${AIRBYTE_CLIENT_SECRET}
SNOWFLAKE_ACCOUNT: ${SNOWFLAKE_ACCOUNT}
SNOWFLAKE_USER: ${SNOWFLAKE_USER}
SNOWFLAKE_PRIVATE_KEY: ${SNOWFLAKE_PRIVATE_KEY}
POSTGRES_HOST : ${POSTGRES_HOST}
POSTGRES_USER : ${POSTGRES_USER}
POSTGRES_PASSWORD : ${POSTGRES_PASSWORD}
CC_HOST_NAME : ${CC_HOST_NAME}
CC_DB_USER : ${CC_DB_USER}
CC_DB_PASS : ${CC_DB_PASS}
PIPEDRIVE_API_KEY : ${PIPEDRIVE_API_KEY}
SLACK_DATA_ORCH_WEBHOOK_URL: ${SLACK_DATA_ORCH_WEBHOOK_URL}
pre_dbt_sync_deployment:
<<: *prefect-deploy-common
depends_on:
server: {condition: service_healthy}
secret_loader: {condition: service_completed_successfully}
command: >
bash -c "
python deployments/pre_dbt_syncs.py
"
post_dbt_jobs_deployment:
<<: *prefect-deploy-common
command: >
bash -c "
python deployments/post_dbt_jobs.py
"
log_yesterday_activity:
<<: *prefect-deploy-common
command: >
bash -c "
python deployments/log_yesterday_activity.py
"
test_messaging:
<<: *prefect-deploy-common
command: >
bash -c "
python deployments/test_messaging.py
"
volumes:
prefect_data:
What would I have to change?Marvin
02/20/2026, 12:56 PMMarvin
02/20/2026, 1:00 PM/ui-settings, which you control with PREFECT_UI_API_URL.
Minimal changes to your compose
- Keep your internal PREFECT_API_URL=<http://server:4200/api> for services talking to the API inside Docker — that’s correct.
- Add PREFECT_UI_API_URL to the server service so the browser points to your public domain through Nginx.
- Ensure Prefect listens on all interfaces (you already do with --host 0.0.0.0).
Here’s the smallest change to your server service:
services:
server:
<<: *prefect-img
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
environment:
PREFECT_PROFILES_ACTIVE: server
PREFECT_SERVER_API_AUTH_STRING: ${PREFECT_SERVER_API_AUTH_STRING}
PREFECT_API_AUTH_STRING: ${PREFECT_SERVER_API_AUTH_STRING}
# IMPORTANT: Set this to your public URL (match your scheme: http vs https)
PREFECT_UI_API_URL: "<http://prefect.example.com/api>"
volumes:
- prefect_data:/root/.prefect
healthcheck:
test: [
"CMD-SHELL",
"python - <<'PY'\nimport sys, urllib.request, json, time\nurl='<http://localhost:4200/api/health>'\ntry:\n with urllib.request.urlopen(url, timeout=3) as r:\n sys.exit(0 if r.status==200 else 1)\nexcept Exception:\n sys.exit(1)\nPY"
]
interval: 5s
timeout: 3s
retries: 40
Nginx essentials (for completeness)
Make sure your Nginx block includes WebSocket upgrade headers and forwards standard proxy headers. Example for a subdomain at the root path:
upstream prefect_upstream { server 127.0.0.1:4200; }
server {
listen 80;
server_name <http://prefect.example.com|prefect.example.com>;
# If you’re using HTTPS, redirect here; if not, keep as-is
# return 301 https://$host$request_uri;
}
# If using HTTPS, add a 443 server block; otherwise keep just the port 80 block above
# server {
# listen 443 ssl http2;
# server_name <http://prefect.example.com|prefect.example.com>;
# ssl_certificate /path/to/cert.pem;
# ssl_certificate_key /path/to/key.pem;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# If you use BASIC auth with Prefect, ensure this header is preserved:
proxy_set_header Authorization $http_authorization;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
location / {
proxy_pass <http://prefect_upstream>;
}
# }
Verify after changes
- Restart the server container, then check:
curl -s <http://prefect.example.com/ui-settings>
curl -v <http://prefect.example.com/api/health>
- /ui-settings should return JSON with "api_url": "<http://prefect.example.com/api>".
- If you’re using HTTPS, use https:// in both PREFECT_UI_API_URL and your curl tests.
If it’s still blank, please share:
- The JSON from curl -s http(s)://prefect.example.com/ui-settings
- Any errors from your browser console (Network tab, failed /ui-settings or /api/... calls)
- Your Nginx server block for <http://prefect.example.com|prefect.example.com>
I’ll help you zero in from there.Pav Staz
02/20/2026, 1:03 PMMarvin
02/20/2026, 1:03 PMMarvin
02/20/2026, 1:04 PMPREFECT_UI_API_URL must be the full base for API calls, which includes “/api” (e.g., https://prefect.example.com/api). The UI will append paths like /health, /flow-runs, etc., to that base.
Two common setups
1) Dedicated subdomain, UI at root (recommended)
- Navigate to https://prefect.example.com or https://prefect.example.com/dashboard — both work because they’re SPA routes.
- API lives at https://prefect.example.com/api
- Settings:
PREFECT_UI_API_URL="<https://prefect.example.com/api>"
# PREFECT_UI_SERVE_BASE stays at default "/"
- Nginx proxies both / and /api to the Prefect upstream. Visiting /dashboard works because the SPA handles that route.
2) Mount everything under a subpath (e.g., /dashboard)
- UI is served under /dashboard (so index.html is at /dashboard), and API is under /dashboard/api
- Settings:
PREFECT_UI_SERVE_BASE="/dashboard/"
PREFECT_UI_API_URL="<https://prefect.example.com/dashboard/api>"
- Nginx:
location /dashboard/ {
proxy_pass <http://127.0.0.1:4200/;>
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
- Then you visit https://prefect.example.com/dashboard and the UI calls https://prefect.example.com/dashboard/api.
Quick checks
- After setting env vars and restarting:
curl -s <https://prefect.example.com/ui-settings>
Look for "api_url" — it should include “/api” and match your intended public URL.
- Also:
curl -v <https://prefect.example.com/api/health>
Should return 200.
Clarifying question to tailor the config
- Do you want Prefect at the root of the subdomain (prefect.example.com) and use routes like /dashboard, or do you want to mount all of Prefect under a subpath like /dashboard on an existing domain? I can provide the exact Nginx + compose env for your chosen layout.