-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile
More file actions
97 lines (83 loc) · 4.38 KB
/
Copy pathDockerfile
File metadata and controls
97 lines (83 loc) · 4.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# ============================================================
# Stage 1: Build Frontend (React/Vite)
# ============================================================
FROM node:20-alpine AS frontend-builder
WORKDIR /frontend
# Copy package files first for better caching
COPY frontend/package*.json ./
# Install dependencies
RUN npm ci
# Copy frontend source and build
COPY frontend ./
# Build React app (outputs to /frontend/dist/)
RUN npm run build
# ============================================================
# Stage 2: Backend Runtime (FastAPI)
# ============================================================
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
WORKDIR /app
# Install system dependencies.
# curl — health checks / debugging.
# postgresql-client — REQUIRED by scripts/backup_db.sh + restore_db.sh
# (pg_dump/pg_restore/psql) and by the ON_CALL
# runbook's manual recovery commands. Without it
# the scheduled backup workflow and every
# documented recovery path fail on the live
# machine.
#
# Version 18 specifically, from PGDG rather than Debian: pg_dump REFUSES
# to dump a server whose major version is newer than its own ("aborting
# because of server version mismatch"), and bookworm ships client 15
# against our 18.x server. Verified directly — client 15 fails on this
# exact server. Bump this pin whenever the cluster's major version moves.
#
# The sqlite3 CLI was here until 2026-09 for the SQLite-era backup
# scripts. The hosted database is Postgres now and nothing in this image
# reads a SQLite file; the Python sqlite3 module (stdlib, no apt package)
# is untouched, so a self-hosted SQLite run of this codebase still works.
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates gnupg \
&& install -d /usr/share/postgresql-common/pgdg \
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
-o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
&& echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list \
&& apt-get update && apt-get install -y --no-install-recommends \
postgresql-client-18 \
&& apt-get purge -y gnupg && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
# Copy dependency files and install Python packages
# Note: pyproject.toml goes to /app/pyproject.toml (not /app/backend/)
# This ensures uv creates the venv at /app/.venv
COPY backend/pyproject.toml backend/uv.lock* ./
RUN uv sync --frozen --no-dev
# Copy backend application code to /app (so app module is at /app/app/)
COPY backend ./
# Copy frontend build output to /app/static (where main.py expects it)
# main.py: static_dir = Path(__file__).parent.parent / "static"
# __file__ = /app/app/main.py, parent = /app/app, parent.parent = /app
# So static_dir = /app/static
COPY --from=frontend-builder /frontend/dist ./static
# Set environment variables
ENV PYTHONUNBUFFERED=1
# Expose FastAPI port
EXPOSE 8000
# Run FastAPI directly using the venv created during build
# Working directory is /app, so app.main:app resolves to /app/app/main.py
# Note: uv sync creates .venv at /app/.venv
#
# --forwarded-allow-ips="*" tells uvicorn to trust the X-Forwarded-Proto
# (and friends) header from any source. Required because we're behind
# Fly's edge proxy: without this, uvicorn defaults to trusting only
# 127.0.0.1, ignores the "https" forwarded scheme, and any FastAPI
# redirect (e.g. /mcp -> /mcp/ for the mounted MCP app) is emitted as
# http:// instead of https://. Strict HTTPS clients like mcp-remote
# refuse the HTTPS->HTTP downgrade and the request fails with
# "Unexpected content type: text/html". "*" is safe here because Fly's
# private network ensures only their edge can reach this container.
# --no-access-log: at 20 segment-pushes/s/node plus ~2 req/s per live
# viewer, uvicorn's per-request access line is a measurable slice of the
# single shared CPU and drowns the app's structured logs in Fly's
# ingest. Request-id app logging (request_context.py) already covers
# the forensic need.
CMD ["/app/.venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--timeout-keep-alive", "65", "--forwarded-allow-ips=*", "--no-access-log"]