Skip to content

Step 8 · Deploy

Step 8 of 8Deploy

Get a public URL, with your secrets in the platform rather than the repo.

It’s the point of the whole workshop. Everything before this was so that this step works.

You’re shipping two separate things, and they are not the same job:

What Where Hard because
1 · The agent app/ — a running FastAPI process Render or FastAPI Cloud it must keep working after the response is sent
2 · The client client/ — a folder of static files Vercel or GitHub Pages it isn’t. Only CORS is

Do them in that order — the client needs a backend URL to point at.

Both hosts in each row are free, need no credit card, and deploy from GitHub. Pick one from each row to finish the step; do the second one afterwards if you want to see how little of this is platform-specific.

  1. Push your repo to GitHub.

  2. Render → New +Web Service → connect your repo.

  3. Settings:

    Field Value
    Root Directory app — the uv project, not the repo root
    Build Command pip install uv && uv sync --frozen
    Start Command uv run fastapi run main.py --port $PORT
    Instance Type Free
  4. Environment → add LLM_API_KEY, SUPABASE_URL, SUPABASE_SECRET_KEY. Add TAVILY_API_KEY too if you want the deployed agent to be able to search the live web.

    This is where secrets live in production: in the platform, not the repo. Same code, different source — app/config.py reads os.environ either way.

  5. Deploy.

Bind to 0.0.0.0, not 127.0.0.1. Binding to localhost inside a container means nothing outside can reach you — it shows up as a maddening “port scan timeout” in the deploy logs.

Full Render guide →

Other hosts, and how to judge one →

Terminal window
curl https://<your-service>.onrender.com/health
{"ok": true, "database": true, "provider": "google",
"model": "gemini-flash-latest", "web_search": false}

Two seconds, and it tells you the deploy worked and the database is reachable. This is why that endpoint exists.

Before deploying anything else, prove the backend works from a browser. From client/, run npm run dev, paste your deployed URL into Backend URL, and ask a question. The field is saved in your browser, so you can flip between your local backend and the deployed one without touching any config.

If you get “Failed to fetch”, that’s CORS — and now it’s real, because your page and your API are genuinely different origins. Set ALLOWED_ORIGINS on the backend to your frontend’s origin (no trailing slash) and redeploy:

ALLOWED_ORIGINS=http://localhost:5173

Strictly speaking you’re already done — the agent is live and that’s the workshop. But a backend nobody can click on is a hard thing to show anyone, and this part takes two minutes.

The client builds to a folder of static files, so any static host will do.

client/vercel.json is already written, so there are exactly two fields to fill in. Import the repo at vercel.com/new, then:

  1. Root Directoryclient — the Vite project, not the repo root
  2. Environment VariablesVITE_API_URL = your deployed backend URL
  3. Deploy

Every push to your default branch redeploys, and pull requests get preview URLs.

Then add your new frontend origin to ALLOWED_ORIGINS on the backend, because you’ve just created another one:

ALLOWED_ORIGINS=https://your-client.vercel.app,http://localhost:5173

Full client deployment guide →

Your tools are now on the internet. Add them to Claude Desktop or Claude Code:

claude_desktop_config.json
{
"mcpServers": {
"research-tools": {
"url": "https://<your-service>.onrender.com/mcp"
}
}
}

Restart Claude, and wiki_search and wiki_read appear as tools you can use in any conversation. Same functions your agent uses — no second deployment, because the MCP server is mounted inside the app you just shipped.

  • The agent: https://<your-backend>/health returns ok: true, and /docs gives you interactive API docs
  • The client: a public page that runs the agent against that backend
  • Steps stream in live, from the internet, on someone else’s laptop
  • Your MCP tools work inside Claude

You’re done. That URL is yours to send to anyone.

First request after a while takes ~a minute

Free services spin down after ~15 minutes idle. Not a bug. Hit /health ten minutes before any demo to wake it.

Build fails: uv: command not found

The build command needs pip install uv && in front of uv sync --frozen. Render’s Python image doesn’t ship uv.

Build succeeds, service won’t start

Check the start command binds 0.0.0.0 and uses $PORT. Also check the Root Directory is set to app — that’s where pyproject.toml and main.py live, and without it uv has no project to sync.

RuntimeError: Missing environment variable

The variables are set in the dashboard but the service hasn’t redeployed. Environment changes need a redeploy to take effect.

Everything worked, now database: false

Supabase paused your project after a week of inactivity. Open the dashboard and resume — and ping it weekly if this project matters.

More in Troubleshooting →

  • Keep it alive. The free-tier survival guide
  • Ship the agent to the other host too. Render and FastAPI Cloud, from the same repo, with no code change. You get a fallback URL for demo day, and comparing two build logs teaches you what’s actually your code’s fault.
  • Handle lost runs. BackgroundTasks lives inside your server process, so a restart mid-run leaves the row stuck at running forever. The cheap fix is to mark runs stale after 10 minutes so the UI stops spinning; the real fix is a task queue with a separate worker — the upgrade path.
  • Look at other hosts. Other hosts — how to tell whether a platform can run this architecture at all.
  • Change the model. Model providers — Cerebras, OpenRouter, Groq, all free, all three environment variables away.
  • Add RAG. The memories table and pgvector are already there — Learn · Memory and RAG.
  • Real search. Swap Wikipedia for Tavily or Brave in app/tools/wikipedia.py. The agent loop doesn’t change.
  • Rate limiting. Cap runs per IP before someone burns your daily quota.