Muhammad Ridwan
05/19/2026, 3:42 AMMarvin
05/19/2026, 3:42 AMMarvin
05/19/2026, 3:45 AMtext
React container ---> app backend container ---> Prefect server/container
(browser) (Node/Python/FastAPI) (Prefect API/UI)
If you’re self-hosting Prefect:
- your backend should call Prefect over the Docker/Kubernetes internal network
- your React app should call only your backend
Docker example
Use the container/service name, not localhost, for container-to-container calls.
yaml
services:
prefect:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0
ports:
- "4200:4200"
backend:
build: ./backend
environment:
PREFECT_API_URL: <http://prefect:4200/api>
depends_on:
- prefect
frontend:
build: ./frontend
environment:
VITE_API_URL: <http://localhost:8000>
depends_on:
- backend
Key point:
- inside backend, use <http://prefect:4200/api>
- from the browser, use whatever public URL exposes your backend, e.g. <http://localhost:8000>
Do not do this from React directly:
text
<http://prefect:4200/api>
That hostname only resolves inside the Docker network, not in the user’s browser.
When direct browser → Prefect is acceptable
Only for:
- local prototypes
- internal tools with very controlled access
- cases where you fully understand the CORS/auth tradeoffs
If you do it, you’ll need CORS configured on Prefect Server, but I would still avoid exposing Prefect credentials in frontend code.
Security guidance
- never put PREFECT_API_KEY or server auth credentials in React env vars
- keep Prefect API internal if possible
- expose only your backend publicly
- have your backend enforce which flows/deployments users can trigger or view
For Kubernetes
Same idea:
- React service talks to your backend service
- backend service talks to Prefect service via cluster DNS, e.g. <http://prefect-server:4200/api>
- only ingress your frontend/backend, not necessarily Prefect itself
Typical backend responsibilities
Your backend can:
- list flows/deployments
- create flow runs
- read run state
- stream filtered logs
- map your app’s users/roles to allowed Prefect actions
Good pattern for API endpoints
Example backend endpoints:
- GET /api/prefect/deployments
- POST /api/prefect/flow-runs
- GET /api/prefect/flow-runs/:id
Then your backend translates those into Prefect API calls.
If you’re using Prefect Cloud
Same recommendation applies even more strongly:
- React should not hold a long-lived Prefect Cloud API key
- your backend should use the key server-side
Useful docs:
- Prefect docs: docs.prefect.io
- Prefect server concepts: Prefect Server
- Prefect API/client concepts: API reference
If you want, I can give you a complete working example for either:
1. Docker Compose with React + FastAPI + Prefect, or
2. Kubernetes ingress/service setupMuhammad Ridwan
05/19/2026, 4:17 AMMarvin
05/19/2026, 4:18 AMMarvin
05/19/2026, 4:19 AMtext
your-project/
docker-compose.yml
.env
backend/
Dockerfile
requirements.txt
app/
main.py
frontend/
Dockerfile
package.json
src/
App.jsx
---
## 1) docker-compose.yml
yaml
services:
prefect:
image: prefecthq/prefect:3-latest
command: prefect server start --host 0.0.0.0 --port 4200
ports:
- "4200:4200"
environment:
PREFECT_SERVER_API_HOST: 0.0.0.0
PREFECT_SERVER_API_PORT: 4200
healthcheck:
test: ["CMD", "curl", "-f", "<http://localhost:4200/api/health>"]
interval: 10s
timeout: 5s
retries: 12
backend:
build: ./backend
ports:
- "8000:8000"
environment:
PREFECT_API_URL: <http://prefect:4200/api>
FRONTEND_ORIGIN: <http://localhost:3000>
depends_on:
prefect:
condition: service_healthy
frontend:
build: ./frontend
ports:
- "3000:3000"
environment:
VITE_API_BASE_URL: <http://localhost:8000>
depends_on:
- backend
Why this works
- backend reaches Prefect at <http://prefect:4200/api> using the service name prefect
- browser-based React reaches FastAPI at <http://localhost:8000>
- browser never talks directly to Prefect
---
## 2) FastAPI backend
### backend/requirements.txt
text
fastapi
uvicorn[standard]
httpx
### backend/Dockerfile
dockerfile
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app /app/app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
### backend/app/main.py
```python
import os
import httpx
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
PREFECT_API_URL = os.getenv("PREFECT_API_URL", "http://prefect:4200/api")
FRONTEND_ORIGIN = os.getenv("FRONTEND_ORIGIN", "http://localhost:3000")
app = FastAPI(title="FastAPI Prefect Proxy")
app.add_middleware(
CORSMiddleware,
allow_origins=[FRONTEND_ORIGIN],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
client = httpx.AsyncClient(base_url=PREFECT_API_URL, timeout=30.0)
@app.get("/health")
async def health():
try:
response = await client.get("/health")
response.raise_for_status()
return {
"backend": "ok",
"prefect": response.json(),
"prefect_api_url": PREFECT_API_URL,
}
except Exception as exc:
raise HTTPException(status_code=503, detail=str(exc))
@app.get("/api/flows")
async def list_flows():
try:
response = await client.post("/flows/filter", json={})
response.raise_for_status()
return response.json()
except httpx.HTTPError as exc:
raise HTTPException(status_code=500, detail=str(exc))
@app.get("/api/deployments")
async def list_deployments():
try:
response = await client.post("/deployments/filter", json={})
response.raise_for_status()
return response.json()
except httpx.HTTPError as exc:
raise HTTPException(status_code=500, detail=str(exc))
@app.post("/api/deployments/{deployment_id}/run")
async def run_deployment(deployment_id: str, payload: dict | None = None):
try:
response = await client.post(
f"/deployments/{deployment_id}/create_flow_run",
json=payload or {},
)
response.raise_for_status()
return response.json()
except httpx.HTTPError as exc:
raise HTTPException(status_code=500, detail=str(exc))
@app.get("/api/flow-runs")
async def list_flow_runs():
try:Marvin
05/19/2026, 4:19 AMA few notes:
- I’m using the HTTP API directly from FastAPI because it’s simple for a proxy layer
- This keeps your React app decoupled from Prefect internals
- You *can* also use the Python client via `prefect.client.orchestration.get_client`, but for a thin web proxy, plain `httpx` is often easier
---
## 3) React frontend
Below is a minimal Vite-style example.
### `frontend/Dockerfile`
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "3000"]
### frontend/src/App.jsx
jsx
import { useEffect, useState } from "react";
const API_BASE = import.meta.env.VITE_API_BASE_URL || "<http://localhost:8000>";
export default function App() {
const [deployments, setDeployments] = useState([]);
const [flowRuns, setFlowRuns] = useState([]);
const [error, setError] = useState("");
async function loadDeployments() {
try {
const res = await fetch(`${API_BASE}/api/deployments`);
const data = await res.json();
setDeployments(data);
} catch (err) {
setError(String(err));
}
}
async function loadFlowRuns() {
try {
const res = await fetch(`${API_BASE}/api/flow-runs`);
const data = await res.json();
setFlowRuns(data);
} catch (err) {
setError(String(err));
}
}
async function runDeployment(id) {
try {
const res = await fetch(`${API_BASE}/api/deployments/${id}/run`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
parameters: {},
}),
});
const data = await res.json();
alert(`Created flow run: ${data.id}`);
loadFlowRuns();
} catch (err) {
setError(String(err));
}
}
useEffect(() => {
loadDeployments();
loadFlowRuns();
}, []);
return (
<div style={{ padding: 24, fontFamily: "sans-serif" }}>
<h1>Prefect Dashboard</h1>
{error && <p style={{ color: "red" }}>{error}</p>}
<h2>Deployments</h2>
<ul>
{deployments.map((d) => (
<li key={d.id}>
<strong>{d.name}</strong>{" "}
<button onClick={() => runDeployment(d.id)}>Run</button>
</li>
))}
</ul>
<h2>Flow Runs</h2>
<ul>
{flowRuns.map((fr) => (
<li key={fr.id}>
{fr.name} - {fr.state?.type}
</li>
))}
</ul>
</div>
);
}
---
## 4) Example frontend package.json
json
{
"name": "prefect-react-ui",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"vite": "^5.4.0"
}
}
---
## 5) Run it
bash
docker compose up --build
Then open:
- React app: http://localhost:3000
- FastAPI health: http://localhost:8000/health
- Prefect UI/API: http://localhost:4200
---
## 6) Important best practices
### A. Don’t use localhost between containers
Inside Docker:
- backend → prefect must use <http://prefect:4200/api>
- not <http://localhost:4200/api>
localhost from inside backend means “the backend container itself”.
### B. Keep Prefect behind your backend
Your React app should call:
- <http://localhost:8000/api/>...
not:
- <http://localhost:4200/api/>...
### C. Add auth in FastAPI if this is real production
This example is intentionally minimal. In production you should add:
- user auth
- authorization rules
- rate limiting
- request validation
- filtered/approved Prefect operations onlyMarvin
05/19/2026, 4:19 AMprefect server start is fine.
For more durable setups, use Postgres with Prefect Server.
---
## 7) Optional: use the Prefect Python client instead of raw HTTP
If you want your FastAPI backend to use the official client, the entry point is:
prefect.client.orchestration:get_client
I verified that signature exists.
That said, for a browser-facing proxy, httpx is often simpler unless you need richer Prefect client behavior.
Docs:
- Prefect docs
- API reference
Also, I verified the server command:
bash
prefect server start --host 0.0.0.0 --port 4200
---
## 8) One likely gotcha
Your React UI will only show deployments/runs if you actually have flows and deployments registered in Prefect.
If you want, I can next give you a complete example flow + deployment that you can add to this stack so the React UI has something real to trigger.