# syntax=docker/dockerfile:1 # Dockerfile for Piper Morgan Main Application # PM-055 Step 2: Docker configuration for Python 3.11 consistency # Created: 2025-07-22 # Use Python 3.11-slim base image for PM-055 compliance. # MUST be slim-bookworm (Debian 12 → sqlite 3.40.1): chromadb requires sqlite >= 3.35. # slim-bullseye (Debian 11) ships sqlite 3.34.1 and crash-loops the app on startup (#1299). # Do NOT downgrade to bullseye. The live 0.8.7 droplet image was bookworm; the repo had # drifted to bullseye, which broke the 0.8.8 deploy (rolled back 2026-06-19). FROM python:3.11-slim-bookworm # Set Python version environment variable for verification ENV PYTHON_VERSION=3.11 # Set working directory WORKDIR /app # Install system dependencies RUN apt-get update && \ apt-get install -y --no-install-recommends \ build-essential \ && rm -rf /var/lib/apt/lists/* # Copy requirements first for better Docker layer caching COPY requirements.txt . # Install Python dependencies. # BuildKit cache mount (#1408): persists pip's download/wheel cache across builds # in BuildKit storage — NOT in the image layer — so a code-only rebuild whose # docker layer-cache misses re-installs from local wheels instead of re-downloading # the whole dep set. torch alone is 821MB; at the droplet's ~290kB/s that download # was ~45min of every dot-deploy. The mount keeps the image slim (cache isn't a # layer) while killing the re-download. Requires BuildKit (the `# syntax` directive # above enables it; the droplet + Fly builders both use buildx). RUN --mount=type=cache,target=/root/.cache/pip \ pip install --upgrade pip && \ # #1409: pre-seed the CPU-only torch wheel BEFORE the requirements install. # `pip install torch` on Linux defaults to the CUDA build and drags ~4GB of # nvidia_*_cu12 wheels no CPU-only host (droplet, Fly) ever uses. With # torch already satisfied at the same version, the requirements resolve # skips it entirely. Keep the version in lockstep with requirements.txt. pip install torch==2.7.1 --index-url https://download.pytorch.org/whl/cpu && \ pip install -r requirements.txt # Add Python version verification during build RUN python --version && \ python -c "import sys; assert sys.version_info >= (3, 11), f'Python {sys.version} < 3.11 (PM-055 requirement)'" # Copy application code COPY . . # #1839: deploy identity baked at build time, so a running container can answer # "what commit am I?" without SSH. Inert when unset — the health surface reports # the literal string "unknown" rather than inventing a plausible value, which is # the failure mode it replaces (a hardcoded "PM-038-staging" that was always # populated, always confident, and carried no information). # Do NOT read the host's .git at runtime to fill this: that reports the *host's* # checkout, not the *image's* provenance — the exact build-vs-release confusion # this field exists to end. ARG PIPER_GIT_SHA="" ENV PIPER_GIT_SHA=${PIPER_GIT_SHA} # Create version verification script inline (avoids CRLF issues from Windows hosts) RUN set -e && cat > /usr/local/bin/verify-python-version.sh << 'VERIFY_EOF' #!/bin/bash set -e echo "Verifying Python version for PM-055 compliance..." PYTHON_FULL_VERSION=$(python --version 2>&1) PYTHON_VERSION=$(echo "$PYTHON_FULL_VERSION" | awk '{print $2}' | cut -d. -f1-2) PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1) PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2) echo "Detected Python version: $PYTHON_VERSION" if [[ "$PYTHON_MAJOR" -lt 3 ]] || [[ "$PYTHON_MAJOR" -eq 3 && "$PYTHON_MINOR" -lt 11 ]]; then echo "ERROR: Python $PYTHON_VERSION does not meet PM-055 requirements (>=3.11)" exit 1 fi echo "Python version $PYTHON_VERSION meets PM-055 requirements." python -c " import sys try: import asyncio, fastapi, sqlalchemy, uvicorn print('Core dependencies compatible with Python {}.{}'.format(sys.version_info.major, sys.version_info.minor)) except ImportError as e: print('Dependency compatibility issue:', e) sys.exit(1) " || exit 1 python -c " import asyncio async def test_async(): return 'async_ok' result = asyncio.run(test_async()) assert result == 'async_ok', 'Async test failed' print('Async patterns working correctly') " || exit 1 echo "Docker container ready with Python 3.11 (PM-055 compliant)" VERIFY_EOF RUN chmod +x /usr/local/bin/verify-python-version.sh # Set PYTHONPATH for proper module imports ENV PYTHONPATH=/app:$PYTHONPATH # #1662: boot-phase print() lines are block-buffered under Fly (no tty) — they land in chunks, # out of order against uvicorn's stderr, and the earliest chunk falls outside the log window # (the UploadStorageProbePhase line was "missing" from the v60 boot log for exactly this # reason). Unbuffered stdout makes every phase line appear where and when it printed. ENV PYTHONUNBUFFERED=1 # Create non-root user for security RUN groupadd -r piper && useradd -r -g piper piper RUN chown -R piper:piper /app # #1656: NO `USER piper` here — the entrypoint starts as root so it can prepare # the Fly volume mount (root-owned /data) for the app user, then drops to piper # via setpriv before exec'ing the app. With the old `USER piper` directive the # app user could never mkdir under /data and EVERY upload 500'd. RUN cp /app/deploy/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh && \ chmod 755 /usr/local/bin/docker-entrypoint.sh ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] # Expose port for main application EXPOSE 8001 # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD python -c "import requests; requests.get('http://localhost:8001/health', timeout=5)" || exit 1 # Run version verification on startup, then start application. # `exec` so python replaces the bash shim (signals reach the app; the # entrypoint's setpriv-exec chain stays unbroken). CMD ["/bin/bash", "-c", "/usr/local/bin/verify-python-version.sh && exec python main.py"]