Docker System
The only way to make local == dev == later environments is:
- Same Dockerfiles per service
- One canonical
docker-compose.ymlfor the stack - Only configuration changes via
.envfiles (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):
- Dev (live dev server):
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_datavolume - 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:
frontend/.env.frontend.dev:
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.ymleverywhere. - Same processes inside containers (same
uvicorn/next startcommands). -
Only env values change via
--env-fileandenv_fileentries: -
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.ymlthat: -
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.