video-accessibility/backend/Dockerfile
Vadym Samoilenko 77a9d3b255 fix(docker): add ffmpeg to base image — fixes pydub AudioSegment in worker
ffmpeg was missing from the base image, causing all pydub operations
(AudioSegment.from_file, export) to fail in worker and tts-worker containers.
Moved ffmpeg install from whisper-worker stage to the shared base stage so
all container variants (api, worker, tts-worker, whisper-worker) have it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 19:12:57 +01:00

156 lines
6 KiB
Docker

# =============================================================================
# Multi-stage Dockerfile for Accessible Video Processing Platform
# =============================================================================
# Stage 1: Builder - Install dependencies
# Stage 2: Base - Common runtime for API and Worker
# Stage 3: API - FastAPI + Gunicorn (no ffmpeg — heavy tasks run on Cloud Run Jobs)
# Stage 4: Worker - Celery worker, lightweight queues only (notify, embed)
# =============================================================================
# -----------------------------------------------------------------------------
# Stage 1: Builder - Install Python dependencies using Poetry
# -----------------------------------------------------------------------------
FROM python:3.11-slim AS builder
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install Poetry
RUN pip install --no-cache-dir poetry==2.1.4
# Configure Poetry to not create virtual environment (we're in a container)
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false \
POETRY_CACHE_DIR=/tmp/poetry_cache
WORKDIR /app
# Copy dependency files
COPY pyproject.toml poetry.lock ./
# Install dependencies using Poetry directly (simpler and more reliable)
RUN poetry config virtualenvs.create false \
&& poetry install --only main --no-root --no-interaction --no-ansi \
&& rm -rf $POETRY_CACHE_DIR
# -----------------------------------------------------------------------------
# Stage 2: Base - Common runtime environment
# -----------------------------------------------------------------------------
FROM python:3.11-slim AS base
# Install common runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libmagic1 \
curl \
tini \
ffmpeg \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Create non-root user for security
RUN groupadd --gid 1000 app \
&& useradd --uid 1000 --gid app --shell /bin/bash --create-home app
# Copy Python packages from builder (installed globally, not in user dir)
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
# Set environment variables
ENV PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
# Copy application code
COPY --chown=app:app . .
# Switch to non-root user
USER app
# -----------------------------------------------------------------------------
# Stage 3: API - FastAPI + Gunicorn (Production API Server)
# Heavy pipeline tasks (ingest/translate/render) run on Cloud Run Jobs
# -----------------------------------------------------------------------------
FROM base AS api
# Set production environment variables
ENV APP_ENV=prod
# Health check for API
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Expose API port
EXPOSE 8000
# Use tini as init system for proper signal handling
ENTRYPOINT ["tini", "--"]
# Start Gunicorn with Uvicorn workers
CMD ["gunicorn", "-c", "gunicorn_conf.py", "app.main:app"]
# -----------------------------------------------------------------------------
# Stage 4: Worker - Celery Worker (lightweight queues: notify, embed)
# -----------------------------------------------------------------------------
FROM base AS worker
# Set production environment variables
# WORKER_CONCURRENCY can be overridden at runtime (default: 8)
ENV APP_ENV=prod \
C_FORCE_ROOT=0 \
WORKER_CONCURRENCY=8
# Health check for worker (check if Celery is responding)
HEALTHCHECK --interval=60s --timeout=15s --start-period=10s --retries=3 \
CMD python -c "from celery import Celery; app=Celery('accessible-video-tasks', broker='redis://redis:6379/0'); app.control.inspect().ping() or exit(1)" || exit 1
# Use tini as init system for proper signal handling
ENTRYPOINT ["tini", "--"]
# Start Celery worker listening to all queues EXCEPT whisper
# Whisper has a dedicated worker (see Stage 5) to prevent memory overload
# Concurrency is set via WORKER_CONCURRENCY env var (default: 8)
# When using Cloud Run for FFmpeg, can increase to handle more parallel HTTP calls
CMD celery -A celery_worker worker \
-Q default,ingest,notify,render,ffmpeg \
--loglevel=info \
--concurrency=${WORKER_CONCURRENCY} \
--max-tasks-per-child=100
# -----------------------------------------------------------------------------
# Stage 5: Whisper Worker - Dedicated worker for Whisper transcription
# -----------------------------------------------------------------------------
FROM base AS whisper-worker
# Pre-download Whisper medium model during build to avoid cold start delays
# Model is cached in ~/.cache/huggingface/hub (~1.5GB)
RUN python -c "from faster_whisper import WhisperModel; WhisperModel('medium', device='cpu', compute_type='int8')"
# Set production environment variables
# WHISPER_WORKER_CONCURRENCY can be overridden at runtime
# Default: 1 for local mode (RAM constrained), set to 10 for Cloud Run mode
ENV APP_ENV=prod \
C_FORCE_ROOT=0 \
WHISPER_WORKER_CONCURRENCY=1
# Health check for worker (check if Celery is responding)
HEALTHCHECK --interval=60s --timeout=15s --start-period=10s --retries=3 \
CMD python -c "from celery import Celery; app=Celery('accessible-video-tasks', broker='redis://redis:6379/0'); app.control.inspect().ping() or exit(1)" || exit 1
# Use tini as init system for proper signal handling
ENTRYPOINT ["tini", "--"]
# Start Celery worker listening ONLY to whisper queue
# Concurrency is set via WHISPER_WORKER_CONCURRENCY env var
# Local mode: 1 (RAM constrained - Whisper model uses ~4-6GB)
# Cloud Run mode: 10 (just HTTP calls, match Cloud Run max instances)
# --max-tasks-per-child=50 to periodically recycle workers and free memory
CMD celery -A celery_worker worker \
-Q whisper \
--loglevel=info \
--concurrency=${WHISPER_WORKER_CONCURRENCY} \
--max-tasks-per-child=50