Single-Container Pattern¶
The most common Marketplace deployment pattern is a single Docker container that serves both the React frontend and the FastAPI backend. This page explains the pattern in detail, why it works, and how to structure your code to support both local development and Marketplace deployment from the same codebase.
Why Single-Container?¶
In traditional deployments, you'd run a separate web server (Nginx) to serve static files and a separate process for the backend API. In the Marketplace, simplicity wins:
- One image to build and manage — fewer moving parts, easier updates.
- No Nginx configuration — FastAPI's
StaticFilesserves the React build directly. - Same origin — React and the API share a port; no CORS headers needed in production.
- Simpler AppSpec — One container, one Service, one ReplicaSet.
Architecture¶
Docker Container (port 8080)
┌────────────────────────────────────────────────────┐
│ │
│ uvicorn │
│ └── FastAPI app │
│ ├── /api/* → API routes │
│ └── /* → StaticFiles("static/") │
│ │ │
│ React build │
│ (compiled at │
│ image build time) │
└────────────────────────────────────────────────────┘
The Multi-Stage Dockerfile¶
# ── Stage 1: Build the React frontend ─────────────────────────────
FROM node:20-slim AS frontend-builder
WORKDIR /frontend
# Install dependencies (cached layer if package.json unchanged)
COPY frontend/package*.json ./
RUN npm ci
# Copy source and build
COPY frontend/ ./
RUN npm run build
# Output: /frontend/dist/
# ── Stage 2: Python runtime ────────────────────────────────────────
FROM python:3.11-slim
WORKDIR /app
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY main.py settings.py wrapper.sh ./
COPY api/ ./api/
RUN chmod +x wrapper.sh
# Copy compiled React from Stage 1
COPY --from=frontend-builder /frontend/dist ./static
EXPOSE 8080
CMD ["./wrapper.sh"]
The COPY --from=frontend-builder /frontend/dist ./static line is the key: the compiled React app becomes part of the Python image as a directory named static/.
FastAPI StaticFiles Mount¶
In main.py, mount the static files directory at the root path, but only when the directory exists:
import os
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
# Mount API routes first
app.include_router(router, prefix="/api")
# Serve React build if static/ directory exists (Docker/Marketplace)
# In local development, the directory won't exist — Vite handles the frontend
static_dir = os.path.join(os.path.dirname(__file__), "static")
if os.path.isdir(static_dir):
app.mount("/", StaticFiles(directory=static_dir, html=True))
The html=True parameter tells StaticFiles to serve index.html for any path that doesn't match a file — this is required for React Router to work correctly (client-side routing).
Vite Configuration for Dev Mode¶
During local development, you run the React dev server (Vite) on a different port. The Vite dev server needs to proxy /api requests to the FastAPI backend:
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
build: {
outDir: 'dist', // Output dir — Docker copies this to ./static
},
})
This proxy means your React code can call /api/datasets and the dev server forwards the request to FastAPI running on :8080, without any CORS headers.
CORS: Dev vs Production¶
In Docker/Marketplace, React and the API are on the same origin — no CORS is needed. In local development, Vite's proxy eliminates CORS issues too. However, if you need to support a standalone frontend (without Vite proxy), you can enable CORS conditionally:
# settings.py
class Settings(BaseSettings):
allow_cors_origin: str = "" # Empty = CORS disabled
# main.py
from fastapi.middleware.cors import CORSMiddleware
s = get_settings()
if s.allow_cors_origin:
app.add_middleware(
CORSMiddleware,
allow_origins=[s.allow_cors_origin],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Set ALLOW_CORS_ORIGIN=http://localhost:5173 in your local .env only if you need to run the frontend without the Vite proxy.
Development vs Production Comparison¶
| Local Dev | Docker / Marketplace | |
|---|---|---|
| Frontend | Vite dev server (:5173) | FastAPI StaticFiles (:8080) |
| API | FastAPI (:8080) | FastAPI (:8080) |
| Proxy | Vite proxies /api | Same origin, no proxy |
| CORS | Not needed (Vite proxy) | Not needed (same origin) |
| HMR | Yes (Vite) | No |
| Build step | Not needed | npm run build in Dockerfile |
Local Development Workflow¶
# Terminal 1: Start the backend
cd examples/05-marketplace-chat
pip install -r requirements.txt
cp .env.example .env && vim .env # set GAIA_API_KEY
uvicorn main:app --reload --port 8080
# Terminal 2: Start the frontend dev server
cd examples/05-marketplace-chat/frontend
npm install
npm run dev # opens http://localhost:5173
Building and Testing the Docker Image¶
cd examples/05-marketplace-chat
# Build
docker build -t gaia-chat:latest .
# Run locally
docker run --rm -p 8080:8080 \
-e GAIA_API_KEY="your-key" \
-e GAIA_BASE_URL="https://helios.cohesity.com/v2/mcm/gaia" \
gaia-chat:latest
# Open http://localhost:8080
Multi-Container Alternative¶
If your app needs separate scaling for frontend and backend, or you want to use Nginx for static file serving, you can split into two containers. The AppSpec would include two container definitions in the same pod, or separate ReplicaSets with internal ClusterIP services.
# Two containers in one pod (sidecar pattern)
spec:
containers:
- name: backend
image: gaia-chat-backend:latest
ports:
- containerPort: 8080
- name: frontend
image: gaia-chat-frontend:latest # Nginx serving React build
ports:
- containerPort: 80
For most Gaia applications, the single-container pattern is simpler and sufficient. Multi-container becomes worthwhile when: - The backend and frontend have significantly different resource requirements. - You want to update the frontend independently of the backend. - You need Nginx features like gzip compression, caching headers, or advanced routing.
Next Steps¶
- MCP Server on the Marketplace — Example 06 (no frontend at all)
- Auth in the Marketplace — Token gate and API key patterns
- Testing & Submission — Pre-submission checklist