Add dockerized app and infra scaffolding

This commit is contained in:
2025-11-26 06:49:58 +01:00
parent 575c02431e
commit 01ebc23e3f
17 changed files with 2426 additions and 111 deletions

126
app/.dockerignore Normal file
View File

@@ -0,0 +1,126 @@
# ==============================================================================
# .dockerignore Python + Next.js (Docker Compose)
# ==============================================================================
# ----------------------------------------------------------------------------
# Git & Version Control
# ----------------------------------------------------------------------------
.git
.gitignore
.gitattributes
.github
.gitpod.yml
# ----------------------------------------------------------------------------
# Python-specific (already in .gitignore, but repeat for safety)
# ----------------------------------------------------------------------------
__pycache__/
*.py[cod]
*$py.class
*.so
*.egg-info/
.installed.cfg
*.egg
*.manifest
*.spec
# Virtual environments & caches
.venv
venv/
env/
ENV/
.pixi/
__pypackages__/
.tox/
.nox/
.pdm-python
.pdm-build/
# Testing & coverage
htmlcov/
.coverage
.coverage.*
.pytest_cache/
.coverage/
.ruff_cache/
.mypy_cache/
.pyre/
.pytype/
# Jupyter / notebooks
.ipynb_checkpoints
# IDEs & editors
.idea/
.vscode/
*.swp
*.swo
*~
# ----------------------------------------------------------------------------
# Next.js / Node.js
# ----------------------------------------------------------------------------
node_modules/
.next/
out/
build/
dist/
.npm
.pnp.*
.yarn/
.yarn-cache/
.yarn-unplugged/
# TypeScript build info
*.tsbuildinfo
next-env.d.ts
# Logs & debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# ----------------------------------------------------------------------------
# Environment & Secrets (never send to Docker daemon)
# ----------------------------------------------------------------------------
.env
.env.local
.env*.local
.env.production
.env.development
.envrc
*.pem
*.key
*.crt
*.secrets
.streamlit/secrets.toml
# ----------------------------------------------------------------------------
# Docker & Compose (avoid recursive inclusion)
# ----------------------------------------------------------------------------
Dockerfile*
docker-compose*.yml
docker-compose*.yaml
.dockerignore
# ----------------------------------------------------------------------------
# Misc / OS
# ----------------------------------------------------------------------------
.DS_Store
Thumbs.db
desktop.ini
# Local documentation builds
/site
docs/_build/
# Temporary files
tmp/
temp/
*.tmp
*.log
# ==============================================================================
# End of file
# ==============================================================================

11
app/.env.example Normal file
View File

@@ -0,0 +1,11 @@
APP_ENV=development
COMPOSE_PROFILES=development
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=app
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/app
LIVEKIT_URL=http://livekit:7880
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=devsecret
OPENAI_API_KEY=change_me
GOOGLE_API_KEY=change_me

63
app/backend/Dockerfile Normal file
View File

@@ -0,0 +1,63 @@
# Backend image for FastAPI + LiveKit Agent runtime
# Supports dev (uvicorn reload) and production (gunicorn) via APP_ENV.
# Builder: install deps with uv into an isolated venv
FROM python:3.12-slim AS builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_PROJECT_ENV=/opt/venv
# System packages needed for common Python builds + uv installer
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential curl && \
rm -rf /var/lib/apt/lists/*
# Install uv (fast Python package manager) as the dependency tool
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:${PATH}"
# Create virtualenv and prepare workspace
RUN python -m venv ${UV_PROJECT_ENV}
ENV PATH="${UV_PROJECT_ENV}/bin:${PATH}"
WORKDIR /app
# Copy the full source; dependency install is guarded to avoid failures while the app is scaffolded
COPY . /app
RUN if [ -f uv.lock ] && [ -f pyproject.toml ]; then \
uv sync --frozen --no-dev; \
elif ls requirements*.txt >/dev/null 2>&1; then \
for req in requirements*.txt; do \
uv pip install --no-cache -r "$req"; \
done; \
elif [ -f pyproject.toml ]; then \
uv pip install --no-cache .; \
else \
echo "No dependency manifest found; skipping install"; \
fi && \
if [ -f pyproject.toml ]; then \
uv pip install --no-cache -e .; \
fi
# Runtime: slim image with non-root user and prebuilt venv
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:${PATH}"
# Create unprivileged user
RUN addgroup --system app && adduser --system --ingroup app app
WORKDIR /app
# Copy runtime artifacts from builder
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /app /app
RUN chown -R app:app /app /opt/venv
USER app
EXPOSE 8000
# Default command switches between uvicorn (dev) and gunicorn (prod) based on APP_ENV
CMD ["sh", "-c", "if [ \"$APP_ENV\" = \"production\" ]; then exec gunicorn app.main:app -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --workers 4 --timeout 120; else exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload; fi"]

202
app/docker-compose.yml Normal file
View File

@@ -0,0 +1,202 @@
version: "3.9"
# A single compose file that supports development and production.
# Switch modes by setting APP_ENV and COMPOSE_PROFILES to either
# "development" (default) or "production" before running docker compose up.
x-backend-common: &backend-common
build:
context: ./backend
dockerfile: Dockerfile
env_file:
- .env
environment:
APP_ENV: ${APP_ENV:-development}
DATABASE_URL: ${DATABASE_URL}
LIVEKIT_URL: ${LIVEKIT_URL}
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY}
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET}
restart: unless-stopped
x-frontend-common: &frontend-common
build:
context: ./frontend
dockerfile: Dockerfile
env_file:
- .env
environment:
APP_ENV: ${APP_ENV:-development}
# Server-side calls from Next.js hit the backend by container name
BACKEND_URL: http://backend:8000
restart: unless-stopped
x-postgres-common: &postgres-common
image: pgvector/pgvector:pg16
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-app}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-app}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
x-livekit-common: &livekit-common
image: livekit/livekit-server:latest
env_file:
- .env
environment:
# Keys are passed in via env; LiveKit will refuse to start without them.
LIVEKIT_KEYS: "${LIVEKIT_API_KEY:-devkey}:${LIVEKIT_API_SECRET:-devsecret}"
LIVEKIT_PORT: 7880
LIVEKIT_RTC_PORT_RANGE_START: 50000
LIVEKIT_RTC_PORT_RANGE_END: 60000
restart: unless-stopped
services:
backend-dev:
<<: *backend-common
profiles: ["development"]
container_name: backend
command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
ports:
- "8000:8000"
volumes:
# Mount source for hot reload; keep venv inside image
- ./backend:/app
depends_on:
postgres:
condition: service_healthy
livekit:
condition: service_started
networks:
app_internal:
aliases: ["backend"]
backend-prod:
<<: *backend-common
profiles: ["production"]
container_name: backend
command:
- gunicorn
- app.main:app
- -k
- uvicorn.workers.UvicornWorker
- --bind
- 0.0.0.0:8000
- --workers
- "4"
- --timeout
- "120"
expose:
- "8000"
depends_on:
postgres-prod:
condition: service_healthy
livekit-prod:
condition: service_started
networks:
app_internal:
aliases: ["backend"]
proxy:
aliases: ["backend"]
frontend-dev:
<<: *frontend-common
build:
context: ./frontend
dockerfile: Dockerfile
target: dev
profiles: ["development"]
container_name: frontend
command: ["npm", "run", "dev"]
ports:
- "3000:3000"
volumes:
- ./frontend:/app
- frontend_node_modules:/app/node_modules
depends_on:
backend:
condition: service_started
networks:
app_internal:
aliases: ["frontend"]
frontend-prod:
<<: *frontend-common
build:
context: ./frontend
dockerfile: Dockerfile
target: runner
profiles: ["production"]
container_name: frontend
# Uses the standalone Next.js output from the Dockerfile
command: ["node", "server.js"]
expose:
- "3000"
depends_on:
backend-prod:
condition: service_started
networks:
app_internal:
aliases: ["frontend"]
proxy:
aliases: ["frontend"]
postgres-dev:
<<: *postgres-common
profiles: ["development"]
container_name: postgres
ports:
- "5432:5432"
networks:
app_internal:
aliases: ["postgres"]
postgres-prod:
<<: *postgres-common
profiles: ["production"]
container_name: postgres
networks:
app_internal:
aliases: ["postgres"]
livekit-dev:
<<: *livekit-common
profiles: ["development"]
container_name: livekit
ports:
- "7880:7880"
- "50000-60000:50000-60000/udp"
networks:
app_internal:
aliases: ["livekit"]
livekit-prod:
<<: *livekit-common
profiles: ["production"]
container_name: livekit
# UDP media must be published even in production; signaling stays internal.
ports:
- "50000-60000:50000-60000/udp"
networks:
app_internal:
aliases: ["livekit"]
proxy:
aliases: ["livekit"]
volumes:
postgres_data:
frontend_node_modules:
networks:
app_internal:
# Private app network for service-to-service traffic
driver: bridge
proxy:
# External network provided by the infra stack (Caddy attaches here)
external: true

44
app/frontend/Dockerfile Normal file
View File

@@ -0,0 +1,44 @@
# Frontend image for Next.js (dev server + standalone production runner)
# Dependency + build stage for production
FROM node:22-bookworm-slim AS builder
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1
# Install dependencies first for better Docker layer caching
COPY package*.json ./
RUN npm ci --ignore-scripts
# Copy full source and build standalone output
COPY . .
RUN npm run build
# Dev image keeps the toolchain for next dev
FROM node:22-bookworm-slim AS dev
WORKDIR /app
ENV NODE_ENV=development \
NEXT_TELEMETRY_DISABLED=1
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# Production runtime: minimal Node image serving the standalone build
FROM node:22-slim AS runner
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
PORT=3000
# Copy only the files required to serve the built app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/static ./.next/static
# Drop privileges to the bundled node user for safety
USER node
EXPOSE 3000
# Next.js standalone exposes server.js at the root of the standalone output
CMD ["node", "server.js"]