Add production deployment tooling: Nginx, Gitea, prod Dockerfiles

Sets up everything needed to deploy behind Cloudflare with a self-hosted
git server: multi-stage prod Dockerfiles (non-root), docker-compose.prod.yml
(Postgres/Redis with no host ports, Nginx reverse proxy, Gitea with
public-read/admin-write access control), scripts/bootstrap-env.sh to
auto-generate required secrets on first clone, and DEPLOYMENT.md covering
the full runbook. Provider API keys (Anthropic/Brave/NinjaPear/USPTO/
Turnstile) are deliberately kept out of .env in favor of the existing
DB-backed Settings UI, so the public repo stays safe to expose.

Also fixes two bugs only surfaced by live-testing the prod stack: Celery
beat couldn't write its schedule file as a non-root user, and Gitea's
embedded SSH server conflicted with the base image's own sshd on port 22.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-05 12:59:41 -04:00
co-authored by Claude Sonnet 5
parent 1a4c80958f
commit 086ca1f13f
14 changed files with 644 additions and 1 deletions
+44
View File
@@ -0,0 +1,44 @@
# Production image for api/worker/beat - no bind mounts, no --reload, no
# dev-only dependencies. See infrastructure/docker/api.Dockerfile for the
# dev image (kept separate and untouched).
FROM python:3.12-slim AS builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
RUN python -m venv /venv
ENV PATH="/venv/bin:$PATH"
COPY apps/api /app
RUN pip install --upgrade pip && pip install .
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/venv/bin:$PATH"
RUN useradd --create-home --uid 1000 appuser
WORKDIR /app
COPY --from=builder /venv /venv
# --chown so appuser can actually write here - celery beat needs to write
# its schedule state file (celerybeat-schedule) into the working directory,
# and a plain COPY leaves everything root-owned even after USER switches
# the running process to appuser.
COPY --chown=appuser:appuser apps/api /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
+39
View File
@@ -0,0 +1,39 @@
# Production image for the Next.js frontend - built with `next build`
# (output: "standalone" in next.config.js), served with `node server.js`,
# not `next dev`/`npm start`. See infrastructure/docker/web.Dockerfile for
# the dev image (kept separate and untouched).
FROM node:20-alpine AS deps
WORKDIR /app
COPY apps/web/package.json apps/web/package-lock.json* ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY apps/web ./
# Baked into the browser bundle at build time - must be the public URL the
# *browser* will use, not a Docker-internal service name. See .env.example.
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_GIT_REPO_URL
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} \
NEXT_PUBLIC_GIT_REPO_URL=${NEXT_PUBLIC_GIT_REPO_URL}
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
# output:"standalone" already traces only the files actually needed at
# runtime - no full node_modules copy required.
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]