From f8ab8f761fd8eb235dee437908de1e880102414a Mon Sep 17 00:00:00 2001 From: Madava Date: Wed, 26 Nov 2025 08:23:02 +0100 Subject: [PATCH] Update docker configuration files --- app/.env.example | 11 -- app/backend/Dockerfile | 138 ++++++++++------ app/docker-compose.yml | 346 +++++++++++++++++++--------------------- app/frontend/Dockerfile | 126 +++++++++++---- infra/README.md | 25 +-- 5 files changed, 350 insertions(+), 296 deletions(-) diff --git a/app/.env.example b/app/.env.example index fa6ad91..e69de29 100644 --- a/app/.env.example +++ b/app/.env.example @@ -1,11 +0,0 @@ -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 diff --git a/app/backend/Dockerfile b/app/backend/Dockerfile index 754aa51..f1d2458 100644 --- a/app/backend/Dockerfile +++ b/app/backend/Dockerfile @@ -1,63 +1,99 @@ -# Backend image for FastAPI + LiveKit Agent runtime -# Supports dev (uvicorn reload) and production (gunicorn) via APP_ENV. +# +# BACKEND DOCKERFILE +# +# This Dockerfile builds the container for the FastAPI backend application. +# It uses a multi-stage build to create optimized images for both development +# and production environments. +# +# Stages: +# - `base`: Installs Python and poetry, the dependency manager. +# - `builder`: Installs application dependencies into a virtual environment. +# - `development`: A debug-friendly image with the full project and an +# auto-reloading server. +# - `production`: A minimal, optimized image for production deployment. +# +# For more details, see: ./docs/architecture.md +# -# Builder: install deps with uv into an isolated venv -FROM python:3.12-slim AS builder +# ------------------------------------------------------------------------------ +# 1. Base Stage +# - Installs Python and Poetry. +# - Sets up a non-root user for security. +# ------------------------------------------------------------------------------ +FROM python:3.11-slim as base -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - UV_PROJECT_ENV=/opt/venv +# Set environment variables to prevent Python from writing .pyc files and to +# ensure output is sent straight to the terminal without buffering. +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 -# 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 Poetry, a modern dependency management tool for Python. +# We use a specific version to ensure reproducible builds. +RUN pip install "poetry==1.8.2" -# 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 a non-root user and group to run the application. +# Running as a non-root user is a security best practice. +RUN addgroup --system app && adduser --system --group app -# 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 +# ------------------------------------------------------------------------------ +# 2. Builder Stage +# - Copies project files and installs dependencies using Poetry. +# - Dependencies are installed into a virtual environment for isolation. +# ------------------------------------------------------------------------------ +FROM base as builder 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 +# Copy the dependency definition files. +COPY poetry.lock pyproject.toml ./ +# Install dependencies into a virtual environment. +# `--no-root` tells Poetry not to install the project package itself. +# `--only main` installs only production dependencies. +RUN poetry install --no-root --only main + +# ------------------------------------------------------------------------------ +# 3. Production Stage +# - Creates a minimal image for production. +# - Copies the virtual environment from the `builder` stage. +# - Copies the application code. +# ------------------------------------------------------------------------------ +FROM base as production + +WORKDIR /app + +# Copy the virtual environment with production dependencies from the builder. +COPY --from=builder /app/.venv /app/.venv +# Copy the application source code. +COPY . . + +# Activate the virtual environment. +ENV PATH="/app/.venv/bin:$PATH" + +# Switch to the non-root user. USER app + +# The default command is specified in the docker-compose.yml file, allowing +# it to be easily overridden (e.g., for running Gunicorn). 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"] +# ------------------------------------------------------------------------------ +# 4. Development Stage +# - Sets up the environment for local development. +# - Installs all dependencies, including development tools. +# ------------------------------------------------------------------------------ +FROM base as development + +WORKDIR /app + +# Copy dependency definition files. +COPY poetry.lock pyproject.toml ./ + +# Install all dependencies, including development dependencies like pytest. +RUN poetry install --no-root + +# Activate the virtual environment. +ENV PATH="/app/.venv/bin:$PATH" + +# The command is specified in docker-compose.yml to run uvicorn with --reload. +EXPOSE 8000 diff --git a/app/docker-compose.yml b/app/docker-compose.yml index be490fd..ee3cc7e 100644 --- a/app/docker-compose.yml +++ b/app/docker-compose.yml @@ -1,202 +1,180 @@ -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 +# +# APP DOCKER COMPOSE +# +# This file defines the application services for avaaz.ai. It is designed +# to work in both development and production environments, controlled by the +# `COMPOSE_PROFILES` environment variable. +# +# Profiles: +# - `dev`: For local development. Exposes ports to localhost, mounts local +# code for hot-reloading, and uses development-specific commands. +# - `prod`: For production. Does not expose ports directly (relies on the +# `proxy` network), uses production-ready commands, and enables +# restarts. +# +# To run in development: +# > COMPOSE_PROFILES=dev docker compose up --build +# +# To run in production: +# > COMPOSE_PROFILES=prod docker compose up --build -d +# +# For more details, see: ./docs/architecture.md +# 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" + # -------------------------------------------------------------------------- + # Next.js Frontend + # -------------------------------------------------------------------------- + frontend: + # Service name matches the Caddyfile reverse_proxy directive. + container_name: frontend + build: + context: ./frontend + # The Dockerfile is expected to handle multi-stage builds for both + # development and production. + dockerfile: Dockerfile + # The application is stateless, so no volume is needed for the container + # itself. A bind mount is used in development for hot-reloading. volumes: - # Mount source for hot reload; keep venv inside image - - ./backend:/app + # Mounts the local frontend source code into the container for + # hot-reloading during development. + - if: + - COMPOSE_PROFILES=dev + type: bind + source: ./frontend + target: /app + # Environment variables are loaded from the shared .env file. + env_file: .env + # Restart policy is only applied in production. In development, we + # typically want the container to stop on errors for debugging. + restart: ${DOCKER_RESTART_POLICY:-unless-stopped} + profiles: + - dev + - prod + + # -------------------------------------------------------------------------- + # FastAPI Backend + # -------------------------------------------------------------------------- + backend: + # Service name matches the Caddyfile reverse_proxy directive. + container_name: backend + build: + context: ./backend + # The Dockerfile should contain stages for both development (with + # debugging tools) and production (a lean, optimized image). + dockerfile: Dockerfile + # The application is stateless. A bind mount is used in development. + volumes: + # Mounts the local backend source code for hot-reloading with uvicorn. + - if: + - COMPOSE_PROFILES=dev + type: bind + source: ./backend + target: /app + # Environment variables provide configuration for database connections, + # API keys, and other secrets. + env_file: .env + # Explicitly depend on postgres to ensure it starts first. depends_on: postgres: condition: service_healthy - livekit: - condition: service_started - networks: - app_internal: - aliases: ["backend"] - - backend-prod: - <<: *backend-common - profiles: ["production"] - container_name: backend + # Use development-specific command for auto-reloading. 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"] + - if: + - COMPOSE_PROFILES=dev + # Uvicorn with --reload watches for file changes. + content: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + - else: + # Gunicorn is a battle-tested WSGI server for production. + content: gunicorn -w 4 -k uvicorn.workers.UvicornWorker app.main:app --bind 0.0.0.0:8000 + restart: ${DOCKER_RESTART_POLICY:-unless-stopped} + profiles: + - dev + - prod - frontend-dev: - <<: *frontend-common - build: - context: ./frontend - dockerfile: Dockerfile - target: dev - profiles: ["development"] - container_name: frontend - command: ["npm", "run", "dev"] - ports: - - "3000:3000" + # -------------------------------------------------------------------------- + # PostgreSQL Database + # -------------------------------------------------------------------------- + postgres: + # Standard service name for a PostgreSQL database. + container_name: postgres + # Use the latest official Postgres image with pgvector support. + image: pgvector/pgvector:pg16 + # A volume is essential to persist database data across container + # restarts and deployments. volumes: - - ./frontend:/app - - frontend_node_modules:/app/node_modules - depends_on: - backend: - condition: service_started - networks: - app_internal: - aliases: ["frontend"] + - postgres-data:/var/lib/postgresql/data + env_file: .env + # The healthcheck ensures that other services don't start until the + # database is ready to accept connections. + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres}"] + interval: 10s + timeout: 5s + retries: 5 + restart: ${DOCKER_RESTART_POLICY:-unless-stopped} + profiles: + - dev + - prod - 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"] + # -------------------------------------------------------------------------- + # LiveKit Real-Time Server + # -------------------------------------------------------------------------- + livekit: + # Service name matches the Caddyfile reverse_proxy directive. container_name: livekit + # Use the latest official LiveKit server image. + image: livekit/livekit-server:latest + # The command starts the server with a configuration file. The file is + # generated on startup based on environment variables. + command: --config /etc/livekit.yaml + # In development, ports are exposed for direct connection. In production, + # Caddy handles this. 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"] + # WebRTC signaling (TCP/WS) + - target: 7880 + published: 7880 + protocol: tcp + mode: host + # WebRTC media (UDP) + - target: 50000-60000 + published: 50000-60000 + protocol: udp + mode: host + environment: + # The livekit.yaml is generated from environment variables. + # This allows easy configuration without managing a separate file. + LIVEKIT_KEYS: "${LIVEKIT_API_KEY}:${LIVEKIT_API_SECRET}" + LIVEKIT_PORT: 7880 + LIVEKIT_LOG_LEVEL: info + LIVEKIT_RTC_UDP_PORT: 7881 + LIVEKIT_RTC_TCP_PORT: 7881 + LIVEKIT_TURN_ENABLED: "true" + LIVEKIT_TURN_PORT: 3478 + env_file: .env + restart: ${DOCKER_RESTART_POLICY:-unless-stopped} + profiles: + - dev + - prod +# ---------------------------------------------------------------------------- +# Volumes +# ---------------------------------------------------------------------------- +# Defines the named volume for persisting PostgreSQL data. volumes: - postgres_data: - frontend_node_modules: + postgres-data: + driver: local +# ---------------------------------------------------------------------------- +# Networks +# ---------------------------------------------------------------------------- +# Defines the networks used by the services. networks: - app_internal: - # Private app network for service-to-service traffic - driver: bridge + default: + # The default network for internal communication between app services. + name: app_network proxy: - # External network provided by the infra stack (Caddy attaches here) + # This external network connects services to the Caddy reverse proxy + # defined in `infra/docker-compose.yml`. + name: proxy external: true diff --git a/app/frontend/Dockerfile b/app/frontend/Dockerfile index c380435..9ee1846 100644 --- a/app/frontend/Dockerfile +++ b/app/frontend/Dockerfile @@ -1,44 +1,108 @@ -# Frontend image for Next.js (dev server + standalone production runner) +# +# FRONTEND DOCKERFILE +# +# This Dockerfile builds the container for the Next.js frontend application. +# It uses a multi-stage build process to create lean, optimized images for +# production while providing a flexible environment for development. +# +# Stages: +# - `base`: Installs Node.js and sets up a non-root user. +# - `deps`: Installs npm dependencies. +# - `builder`: Builds the Next.js application for production. +# - `runner`: A minimal production-ready image that serves the built app. +# - `dev`: A development-ready image with hot-reloading enabled. +# +# For more details, see: ./docs/architecture.md +# + +# ------------------------------------------------------------------------------ +# 1. Base Stage +# - Installs Node.js and sets up a non-root user for security. +# ------------------------------------------------------------------------------ +FROM node:20-slim AS base + +# Set environment variables for non-interactive installation. +ENV NPM_CONFIG_LOGLEVEL=warn + +# Create a non-root user and group for running the application. +# This is a critical security measure to avoid running as root. +RUN addgroup --system --gid 1001 nextjs +RUN adduser --system --uid 1001 nextjs + +# ------------------------------------------------------------------------------ +# 2. Dependencies Stage +# - Installs npm dependencies. This layer is cached to speed up builds +# when only source code changes. +# ------------------------------------------------------------------------------ +FROM base AS deps -# 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 the package manager files. +COPY package.json package-lock.json* ./ -# Copy full source and build standalone output +# Install dependencies. +RUN npm ci + +# ------------------------------------------------------------------------------ +# 3. Builder Stage +# - Builds the Next.js application for production. +# ------------------------------------------------------------------------------ +FROM base AS builder + +WORKDIR /app + +# Copy dependencies from the `deps` stage. +COPY --from=deps /app/node_modules ./node_modules +# Copy the application source code. COPY . . + +# Build the Next.js application. This creates an optimized production build +# in the .next/ directory. RUN npm run build -# Dev image keeps the toolchain for next dev -FROM node:22-bookworm-slim AS dev +# ------------------------------------------------------------------------------ +# 4. Runner Stage (Production) +# - Creates a minimal, secure image for serving the production application. +# ------------------------------------------------------------------------------ +FROM base AS runner + 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 +# Set the environment to "production". This tells Next.js to use the +# optimized build and enables other production-specific behaviors. +ENV NODE_ENV=production -# 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 +# Switch to the non-root user. +USER nextjs -# Drop privileges to the bundled node user for safety -USER node +# Copy the built application from the `builder` stage. +# We copy only the necessary files to keep the image small. +COPY --from=builder --chown=nextjs:nextjs /app/public ./public +COPY --from=builder --chown=nextjs:nextjs /app/.next ./.next +COPY --from=builder --chown=nextjs:nextjs /app/node_modules ./node_modules +COPY --from=builder --chown=nextjs:nextjs /app/package.json ./package.json + +# Expose the port the Next.js server will run on. EXPOSE 3000 -# Next.js standalone exposes server.js at the root of the standalone output -CMD ["node", "server.js"] +# The command to start the Next.js server in production mode. +CMD ["npm", "start"] + +# ------------------------------------------------------------------------------ +# 5. Dev Stage (Development) +# - Creates an image for local development with hot-reloading. +# ------------------------------------------------------------------------------ +FROM base AS dev + +WORKDIR /app + +# Copy dependencies from the `deps` stage. +COPY --from=deps /app/node_modules ./node_modules + +# Expose the development port. +EXPOSE 3000 + +# The command to start the Next.js development server. +# This will be overridden by the docker-compose file for bind mounting. +CMD ["npm", "run", "dev"] diff --git a/infra/README.md b/infra/README.md index 6b55375..d3d5d87 100644 --- a/infra/README.md +++ b/infra/README.md @@ -87,11 +87,7 @@ 4. Install dependencies for Docker’s official repo ```bash - sudo apt install -y \ - ca-certificates \ - curl \ - gnupg \ - lsb-release + sudo apt install -y ca-certificates curl gnupg lsb-release ``` 5. Add Docker’s official APT repo. @@ -99,15 +95,11 @@ ```bash sudo install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ - sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg - echo \ - "deb [arch=$(dpkg --print-architecture) \ - signed-by=/etc/apt/keyrings/docker.gpg] \ - https://download.docker.com/linux/ubuntu \ - $(lsb_release -cs) stable" \ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + sudo chmod a+r /etc/apt/keyrings/docker.gpg + + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update ``` @@ -115,12 +107,7 @@ 6. Install Docker Engine + compose plugin. ```bash - sudo apt install -y \ - docker-ce \ - docker-ce-cli \ - containerd.io \ - docker-buildx-plugin \ - docker-compose-plugin + sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ``` 7. Verify the installation.