Skip to content

Docker System

The only way to make local == dev == later environments is:

  • Same Dockerfiles per service
  • One canonical docker-compose.yml for the stack
  • Only configuration changes via .env files (no “special dev-only” containers, no different commands)

If you start adding dev-only volumes/commands into Compose, the environments stop being identical.

Below is a concrete, “real” layout that follows Docker & FastAPI/Next.js docs, and is already used in public Next.js+FastAPI templates. (travisluong.com)


1. Repo layout for Docker (monorepo)

Add this at the root of mbpanel:

mbpanel/
  docker-compose.yml       # canonical stack, used everywhere
  .env.local               # local dev config (all devs)
  .env.dev                 # live dev server config

  backend/
    Dockerfile
    .dockerignore
    ... your app code ...

  frontend/
    Dockerfile
    .dockerignore
    ... your Next.js app ...
  • One compose file for all envs, as recommended by Docker docs + multi-service examples. (DataCamp)
  • Different .env.* files for local vs dev, as Docker Compose best-practices suggest. (Docker Documentation)

Command usage:

  • Local (all developers):
docker compose --env-file .env.local up --build
  • Dev (live dev server):
docker compose --env-file .env.dev up -d --build

Same images, same compose, only the env file changes.


2. Backend Dockerfile (FastAPI) – production-grade, but env-agnostic

Based directly on the official FastAPI Docker docs (Python base image, requirements.txt, uvicorn). (FastAPI)

backend/Dockerfile:

# backend/Dockerfile

FROM python:3.12-slim AS base

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

# Install system deps if you need (psycopg, etc.)
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Copy dependency definitions
COPY requirements.txt /app/requirements.txt

RUN pip install --upgrade pip && \
    pip install --no-cache-dir -r /app/requirements.txt

# Copy application code
COPY app /app/app

# Environment decides behaviour; image is the same everywhere
ENV APP_ENV=${APP_ENV:-local}

EXPOSE 8000

# Single canonical command; if you want Gunicorn later, you swap this once
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

This follows the FastAPI docs pattern: Python base image, copy requirements.txt, install, copy app, uvicorn entrypoint. (FastAPI)

You can later change CMD to a Gunicorn+Uvicorn worker combo (as recommended for production). (FastAPI) But you don’t need separate Dockerfiles per environment.


3. Frontend Dockerfile (Next.js) – multi-stage

Based on recent Next.js Docker docs + multi-stage examples. (Next.js)

frontend/Dockerfile:

# frontend/Dockerfile
# syntax=docker/dockerfile:1

FROM node:22-alpine AS deps
WORKDIR /app

COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
RUN npm ci || pnpm install || yarn install

FROM node:22-alpine AS builder
WORKDIR /app

ENV NEXT_TELEMETRY_DISABLED=1

COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Build for production; works in local + dev + prod
RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production \
    NEXT_TELEMETRY_DISABLED=1

# Only what we need at runtime
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/package.json ./package.json

EXPOSE 3000

CMD ["npm", "run", "start"]

This is exactly the pattern Next.js recommends for Docker: multi-stage, small runtime image, next start for all environments. (Next.js)


4. Single canonical docker-compose.yml

This is the only compose file used (local + dev). Pattern matches public Next.js + FastAPI templates that combine backend, frontend, and Postgres via Docker Compose. (travisluong.com)

docker-compose.yml at repo root:

version: "3.9"

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - mbpanel

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    networks:
      - mbpanel

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    restart: unless-stopped
    env_file:
      - ${BACKEND_ENV_FILE:-.env.backend}   # resolved from .env.local/.env.dev
    depends_on:
      - db
      - redis
    ports:
      - "${BACKEND_PORT:-8000}:8000"
    networks:
      - mbpanel

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    restart: unless-stopped
    env_file:
      - ${FRONTEND_ENV_FILE:-.env.frontend}
    depends_on:
      - backend
    ports:
      - "${FRONTEND_PORT:-3000}:3000"
    networks:
      - mbpanel

  qdrant:
    image: qdrant/qdrant:v1.7.4
    restart: unless-stopped
    ports:
      - "56333:6333"  # External API port
      - "56334:6334"  # Web UI port
    volumes:
      - qdrant_data:/qdrant/storage
    networks:
      - mbpanel
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:6333/"]
      interval: 10s
      timeout: 5s
      retries: 3

networks:
  mbpanel:

volumes:
  db_data:
  qdrant_data:

Key points:

  • No bind-mounts of app code. That's the trade-off: if you want "identical environment", you don't do dev-only code mounts; you always run from built image (same as dev server).
  • Service hostnames (db, backend, redis, qdrant) are stable and work the same in all envs (Docker Compose networking). (travisluong.com)
  • Each service can still load more env from env_file (backend/frontend specific).
  • Qdrant is the vector database for semantic search:
  • External API: http://localhost:56333
  • Web UI: http://localhost:56334/dashboard
  • Data persists in qdrant_data volume
  • Required for semantic documentation search
  • See Semantic Search Guide

If later you really want hot-reload locally, you can add a separate docker-compose.local.yml with volumes and uvicorn --reload, but then you must accept that local behaviour deviates from dev. Docker docs explicitly recommend base compose + environment-specific override files for that pattern. (DataCamp)


5. Environment files

Root env files (decide which env we’re in)

.env.local (used by all devs locally):

APP_ENV=local

BACKEND_PORT=8000
FRONTEND_PORT=3000

DB_NAME=mbpanel_local
DB_USER=mbpanel_local
DB_PASSWORD=local_password

BACKEND_ENV_FILE=.env.backend.local
FRONTEND_ENV_FILE=.env.frontend.local

.env.dev (live dev server):

APP_ENV=dev

BACKEND_PORT=8000
FRONTEND_PORT=3000

DB_NAME=mbpanel_dev
DB_USER=mbpanel_dev
DB_PASSWORD=dev_stronger_password

BACKEND_ENV_FILE=.env.backend.dev
FRONTEND_ENV_FILE=.env.frontend.dev

Docker’s own docs describe using separate .env files per environment and how precedence works. (Docker Documentation)

Backend env files

backend/.env.backend.local:

APP_ENV=local
DB_HOST=db
DB_PORT=5432
DB_NAME=mbpanel_local
DB_USER=mbpanel_local
DB_PASSWORD=local_password

REDIS_HOST=redis
REDIS_PORT=6379

BACKEND_CORS_ORIGINS=http://localhost:3000

backend/.env.backend.dev:

APP_ENV=dev
DB_HOST=db
DB_PORT=5432
DB_NAME=mbpanel_dev
DB_USER=mbpanel_dev
DB_PASSWORD=dev_stronger_password

REDIS_HOST=redis
REDIS_PORT=6379

BACKEND_CORS_ORIGINS=http://dev-frontend.internal:3000

Frontend env files

frontend/.env.frontend.local:

NEXT_PUBLIC_API_URL=http://localhost:8000
NODE_ENV=production

frontend/.env.frontend.dev:

NEXT_PUBLIC_API_URL=http://backend:8000
NODE_ENV=production

Next.js docs explicitly support .env files and NEXT_PUBLIC_* for client-exposed endpoints; this pattern is standard. (Next.js)


6. How this meets your requirement (no hand-waving)

You asked for:

local docker that can be and will be the same for ALL environments. Local (All Developers) > Dev (Live Dev Server)

What this setup guarantees:

  • Same Dockerfiles (backend + frontend) everywhere.
  • Same docker-compose.yml everywhere.
  • Same processes inside containers (same uvicorn/next start commands).
  • Only env values change via --env-file and env_file entries:

  • DB name/user/password

  • CORS origins
  • API URLs

This is exactly how multi-env setups are described in modern Docker Compose guides: one base compose file, multiple env configs. (DataCamp)

Blunt trade-off:

  • If you want perfect environment parity, you do not bind-mount code or run dev-only commands (uvicorn --reload, next dev) in local Docker.
  • If you later decide DX > purity, you can bolt on docker-compose.local.yml that:

  • adds volumes ./backend:/app/backend, ./frontend:/app

  • overrides commands to use uvicorn --reload / next dev

…but that will be a conscious deviation from “same across all envs”.

If you want, next step I can blueprint the exact create_app() wiring in backend/app/core/app_factory.py so that your FastAPI app reads these env vars cleanly and you don’t accidentally hard-code environment-specific values.