# CI/CD build — used by .github/workflows/docker-publish.yml # Janua API - Multi-stage Dockerfile for FastAPI # Build stage FROM python:3.11-slim AS builder WORKDIR /app # Install build dependencies (including xmlsec/lxml requirements) RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ curl \ pkg-config \ libxml2-dev \ libxmlsec1-dev \ libxmlsec1-openssl \ && rm -rf /var/lib/apt/lists/* # Copy the dependency lock (compiled from apps/api/requirements.in; every # package pinned with hashes, so two builds of one commit get the same set) COPY apps/api/requirements.txt ./ # Install dependencies exactly as locked RUN pip install --no-cache-dir --require-hashes -r requirements.txt # Production stage FROM python:3.11-slim AS runner WORKDIR /app # Install runtime dependencies for xmlsec RUN apt-get update && apt-get install -y --no-install-recommends \ libxml2 \ libxmlsec1 \ libxmlsec1-openssl \ curl \ && rm -rf /var/lib/apt/lists/* # Create non-root user RUN groupadd --system --gid 1001 python && \ useradd --system --uid 1001 --gid python janua # Copy installed packages from builder COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --from=builder /usr/local/bin /usr/local/bin # Copy application code COPY apps/api/app ./app # Email templates. NEVER SHIPPED until 2026-08-16: this file copied only # app/ + alembic/, so every registered template resolved to a missing file in # production and send-template silently degraded to the bare-key-dump # fallback — repo tests pass because they check the repo, not the image. The # registry resolves Path(__file__).parents[3]/templates/emails == /app/ # templates/emails, which this line is what makes true. COPY apps/api/templates ./templates # Copy alembic for database migrations COPY apps/api/alembic ./alembic COPY apps/api/alembic.ini ./alembic.ini # Copy entrypoint script COPY apps/api/docker-entrypoint.sh ./docker-entrypoint.sh # Set ownership RUN chown -R janua:python /app USER janua # Default to MADFAM ecosystem standard port 4100 ENV PORT=4100 ENV PYTHONUNBUFFERED=1 ENV ENVIRONMENT=production EXPOSE 4100 # Health check - uses PORT environment variable HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ CMD curl -f http://localhost:${PORT:-4100}/health || exit 1 # Use entrypoint script that runs migrations before starting ENTRYPOINT ["./docker-entrypoint.sh"] # Use shell form to read PORT environment variable (set via K8s or docker-compose) CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-4100}"]