Agentic AI with Java: Live Cohort
Claude CodeDeploying the Application on Google Cloud

Preparing the App for Deployment

Replace hardcoded localhost URLs with relative paths, serve the frontend as static files from FastAPI in the correct order, and test the single-origin app locally.

Two changes make the app deployable as a single container:

  1. The frontend must stop calling http://localhost:8000 and use relative URLs
  2. FastAPI must serve the frontend as static files

Commit your current work first, so these changes are easy to review and roll back.

Change 1: Relative URLs in the frontend

The backend URL is hardcoded in frontend/app.js. That works on your laptop, but on Cloud Run there is no localhost:8000. A relative URL like /api/tasks simply means "same host as this page", which works locally and in the cloud.

Prompt: relative URLs
We're preparing the frontend for production deployment to Cloud Run.
In frontend/app.js, the backend URL is hardcoded as http://localhost:8000
in two places. Both need to become relative URLs so the same code works
locally AND on Cloud Run.

1. Inside apiFetch: change
   fetch(`http://localhost:8000${url}`, ...)
   to
   fetch(url, ...)

2. Inside handleLoginSubmit (which does NOT use apiFetch, the login
   form calls fetch directly): change
   fetch('http://localhost:8000/api/auth/login', ...)
   to
   fetch('/api/auth/login', ...)

Show me the diff before applying. Also scan the file and tell me if
there are any OTHER localhost references (comments, debug logs,
anywhere) that I might have missed.

Notice two good habits in this prompt:

  • "Show me the diff before applying" keeps you in control
  • "Tell me if there are any OTHER references" asks Claude to check what you might have missed

Claude found exactly two occurrences and no others.

Claude pushes back, and that is good

Claude also pointed out that CLAUDE.md contains a hard rule: "NEVER modify files in /frontend". It asked whether to proceed anyway. Since you wrote that rule and you are now deliberately overriding it, confirm and let it continue. This is exactly the behaviour you want: Claude respects project rules and asks before breaking them.

The result:

frontend/app.js
-    const response = await fetch('http://localhost:8000/api/auth/login', {
+    const response = await fetch('/api/auth/login', {
frontend/app.js
-  const response = await fetch(`http://localhost:8000${url}`, { ...options, headers });
+  const response = await fetch(url, { ...options, headers });

Verify with /diff.

Change 2: Serve the frontend from FastAPI

Prompt: static files
We need FastAPI to serve our frontend as static files so the entire
app runs from a single origin in production.

In backend/app/main.py (NOT backend/main.py, that's just a stub):

1. Add an import at the top:
   from fastapi.staticfiles import StaticFiles

2. AFTER both router includes (auth_router and tasks_router), add a
   catch-all static files mount. The directory must be "../frontend"
   because when we deploy via Docker the WORKDIR will be /app/backend
   and the frontend folder gets copied to /app/frontend (one level up).

   app.mount("/", StaticFiles(directory="../frontend", html=True), name="static")

Critical: the mount MUST come AFTER both routers. FastAPI checks routes
in registration order. If the catch-all static mount comes first, it
will intercept every request including /api/* and break the API.

Show me the diff. Confirm the mount is positioned after both routers.
Do not touch any existing code. Only add the import and the mount.

The prompt names the exact file and warns about the stub. Remember the leftover backend/main.py we spotted while reviewing in Section 8? This is why cleaning up early matters: otherwise you keep having to warn Claude about it.

backend/app/main.py (relevant part)
from fastapi.staticfiles import StaticFiles

# ... app creation and CORS ...

app.include_router(auth_router)
app.include_router(tasks_router)

# Must come AFTER the API routers, or it would swallow /api/* requests
app.mount("/", StaticFiles(directory="../frontend", html=True), name="static")

Why order matters

OrderRequest to /api/tasks
Routers first, then static mountMatched by the tasks router. Works
Static mount firstThe / mount catches it first and looks for a file called api/tasks. The API breaks
Route order: API routers before the static mount

html=True makes the static mount serve index.html when someone opens /.

Test locally

From the backend folder, start the app on port 8080:

cd backend
uv run uvicorn app.main:app --port 8080

Open http://localhost:8080. The login page loads from FastAPI, and after logging in as admin / admin123, the board works. One server, one port, one origin.

Commit the changes before moving on.

What a relative URL resolves to

A relative URL takes its host from the page that is currently open. That is why the same app.js works everywhere without changes:

Page opened fromfetch('/api/tasks') goes to
http://localhost:8080 (local test)http://localhost:8080/api/tasks
http://localhost:8080 (Docker)http://localhost:8080/api/tasks
https://telusko-app-xxxx.run.app (Cloud Run)https://telusko-app-xxxx.run.app/api/tasks

With the old hardcoded http://localhost:8000, the deployed site would try to call port 8000 on the visitor's own computer, and that request fails.

Troubleshooting

SymptomLikely causeFix
/ returns {"detail":"Not Found"}The static mount is missing or points to the wrong folderCheck directory="../frontend" and that you start uvicorn from backend/
API calls return HTML instead of JSONThe static mount is registered before the routersMove app.mount(...) below both include_router lines
RuntimeError: Directory '../frontend' does not existuvicorn was started from the project root, not from backend/cd backend first, exactly like the Docker WORKDIR
Login works but the board is emptyAn old app.js is cached in the browserHard refresh with Ctrl+Shift+R (Cmd+Shift+R on macOS)

Checklist before you write the Dockerfile

  • No localhost left in frontend/app.js (search for it)
  • app.mount("/", StaticFiles(...)) is the last route registration in app/main.py
  • The app works on a single port with uv run uvicorn app.main:app --port 8080 from backend/
  • Login, moving a card and logout all work in the browser
  • Changes are committed

Official references


Written By: Shramik Masti

How is this guide?

Last updated on