Deploying an AI-Generated Monorepo With a Frontend and a Backend

·5 min read·Updated
monorepovibe-codingaidockerdeploymentcors

Ask an AI tool for a full-stack app and you'll get a repository with frontend/ and backend/ folders, each with its own Dockerfile. Then you try to deploy it and hit a wall: the build reads a Dockerfile at the root of what you push, and only that one. There's no way to point it at backend/Dockerfile. You have three real options — serve both from a single app, push each folder as its own root, or build the images in CI — and for most AI-generated projects the first one is simpler than the internet will tell you.

Why the layout fights the platform

The build looks for exactly Dockerfile at the top level of the pushed repository. Not Dockerfile.prod, not docker/Dockerfile, and not backend/Dockerfile. A monorepo with two subfolder Dockerfiles and nothing at the root has no entry point.

That's the whole problem. Everything below is a way of giving the build a root to work with.

Option 1: one app, backend serves the frontend

The one I'd try first for a small project. Build the frontend during the image build, then have your backend serve the resulting static files alongside its API.

FROM node:22-slim AS frontend
WORKDIR /fe
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build

FROM python:3.12-slim
WORKDIR /app
COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ .
COPY --from=frontend /fe/dist ./static
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

That root Dockerfile reaches into both folders, so the layout stays as the AI generated it. FastAPI, Express, and Flask all serve a static directory in a line or two.

What you get:

  • One app, one port, one URL. Nothing to configure between the halves.
  • No CORS at all, because the browser talks to a single origin — see below for why that matters.
  • One deploy. The frontend and backend can't drift out of sync, because they ship together.

What you give up: you can't scale or restart them independently, and a frontend-only change rebuilds everything. For an app with one developer and modest traffic, that's rarely a real cost.

If your frontend is a single-page app, remember the fallback rule — unknown paths need to serve index.html so refreshing a route doesn't 404. In a backend-serving-static setup that's a catch-all route registered after your API routes.

Option 2: two apps, pushing each subfolder as a root

If you genuinely want them separate — different scaling, different restart cadence, different languages you'd rather not build together — you can push a subdirectory so that its Dockerfile becomes the root.

Each application has its own deploy remote, so you add both and push the relevant subtree to each:

git remote add backend  <backend-app-url>
git remote add frontend <frontend-app-url>

git subtree push --prefix=backend  backend  deploy
git subtree push --prefix=frontend frontend deploy

git subtree push synthesises a history containing only that folder, so backend/Dockerfile arrives as Dockerfile at the root. It works, and it's fiddly enough that I'd only reach for it if option 1 doesn't fit — subtree pushes get slow on a long history, and you have two deploys to remember.

Option 3: build in CI, push images

The most flexible, and the answer once the project is real. In a workflow you're free to build each folder with -f frontend/Dockerfile, then push each image to its own app.

This also solves two problems the platform's own build has:

  • Build memory. Builds here run in roughly 0.75 GB, and a frontend bundle plus a backend install can exceed that. CI runners don't have that constraint — more on the build limit.
  • Build-time configuration. The platform's build receives no build arguments, so any framework that inlines config at build time — NEXT_PUBLIC_*, VITE_* — gets undefined. In CI you can set those properly.

If your frontend needs a build-time API URL and you're splitting into two apps, this is effectively the required option, because that URL isn't known until the backend app exists.

If you split, you own CORS

This is the consequence people don't anticipate, and it follows from a structural fact: there's no private networking here. Every port binding gets a public DNS name, so your frontend doesn't reach your backend over an internal address — it makes an ordinary public HTTPS request from the user's browser to a different origin.

Different origin means CORS. Nothing in the platform's routing adds CORS headers for you; it terminates TLS and forwards to your container. So your backend has to send them:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://your-frontend-app.example"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Two things worth getting right while you're there. List your actual origin rather than * — a wildcard is incompatible with credentialed requests anyway, so it tends to break the moment you add auth. And your backend is publicly reachable regardless of CORS, because CORS is a browser policy, not a firewall. Anyone can call your API directly with curl. If it needs protecting, it needs authentication.

None of this exists in option 1, which is a substantial part of why I'd start there.

Choosing

Situation Option
Small app, one developer, modest traffic One app — backend serves the frontend
Frontend and backend need to scale or restart independently Two apps
Frontend needs a build-time API URL Two apps, built in CI
Build doesn't fit in the platform's build container Build in CI, push images
You want the simplest thing that works today One app

Remember that app count isn't a billing unit — you pay for allocated CPU, RAM, and disk, not per application. So splitting doesn't cost extra by itself, but two apps do consume two slices of your account's resource limits, and on a small allocation that's the real constraint.

FAQ

Can the build use a Dockerfile in a subfolder?

No. It reads Dockerfile at the root of what you push. Give it a root Dockerfile, push a subtree, or build the image yourself.

How do I deploy a monorepo with a frontend and a backend?

Either a root Dockerfile that builds both and serves the frontend from the backend, or two apps — pushing each folder as a subtree, or building both images in CI.

Should the frontend and backend be separate apps?

Not for a small project. One app avoids CORS entirely, keeps the halves in sync, and is one deploy instead of two.

Do I need CORS?

Only if they're separate apps. There's no private networking, so a split frontend calls the backend across origins and your backend must send the headers.

Does two apps cost twice as much?

No. Billing is on the resources you allocate, not the number of apps. But both draw from the same account limits.

Why does my frontend call undefined/api?

A build-time variable that wasn't set during the build. The platform's build receives no build arguments — set it in CI, or fetch the value at runtime.

Wrapping up

The monorepo shape AI tools produce is fine; it just assumes a build system that can be pointed at a subfolder. Giving the build a root Dockerfile that reaches into both folders keeps the generated layout intact and collapses the whole problem — no CORS, no second deploy, no build-time URL to thread through. Split them when you have an actual reason to, and when you do, expect to write the CORS middleware yourself and to move the build into CI.