<@ULVA73B9P> I want the prefect UI to be available...
# ask-marvin
p
@Marvin I want the prefect UI to be available to by access to my custom domain prefect.example.com I have routed this domain via nginx to the port 4200 where prefect is but I get a blank screen with 'something went wrong' error
m
thought for 146.4 seconds
Thanks for the details — in Prefect 3.x a blank UI with “Something went wrong” almost always means the browser UI can’t reach the API through your reverse proxy. Since you’re proxying a dedicated subdomain (prefect.example.com) to port 4200, the two most common fixes are: 1) Tell the UI the public API URL 2) Ensure Nginx is set up to forward WebSockets and the usual X-Forwarded headers Quick fix checklist - Start Prefect so it listens on all interfaces and expose the correct public API URL:
Copy code
export 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:
Copy code
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:
Copy code
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:
Copy code
# 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.
p
@Marvin This is my current docker compose:
Copy code
x-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?
m
thought for 252.9 seconds
Great — thanks for sharing your compose. The blank UI is almost certainly because the UI can’t find the public API URL. In Prefect 3.x the UI reads this from
/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:
Copy code
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:
Copy code
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:
Copy code
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.
p
@Marvin why do I have to specify the subpath "/api" in that, how will the browser pick up the UI when it is at /dashboard?
m
thought for 42.1 seconds
Great question — the short answer is: you include “/api” because Prefect’s API is mounted at that base path. The UI is a single‑page app (SPA) and “/dashboard” is just a front‑end route, not where the API lives. How it works - The browser loads the UI at whatever path you visit (/, /dashboard, /flows, etc.). These are SPA routes handled client‑side and all serve the same index.html. - The UI then calls GET /ui-settings to discover the API base URL. - The value you set in
PREFECT_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:
Copy code
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:
Copy code
PREFECT_UI_SERVE_BASE="/dashboard/"
PREFECT_UI_API_URL="<https://prefect.example.com/dashboard/api>"
- Nginx:
Copy code
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:
Copy code
curl -s <https://prefect.example.com/ui-settings>
Look for "api_url" — it should include “/api” and match your intended public URL. - Also:
Copy code
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.