Skip to content

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 StaticFiles serves 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

Text Only
Docker Container (port 8080)
┌────────────────────────────────────────────────────┐
│                                                    │
│  uvicorn                                           │
│   └── FastAPI app                                  │
│        ├── /api/*         → API routes             │
│        └── /*             → StaticFiles("static/") │
│                                  │                 │
│                              React build           │
│                              (compiled at          │
│                               image build time)    │
└────────────────────────────────────────────────────┘

The Multi-Stage Dockerfile

Docker
# ── 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:

Python
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:

TypeScript
// 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:

Python
# 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

Bash
# 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

Bash
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.

YAML
# 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