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
+18 -1
View File
@@ -16,6 +16,10 @@ BACKEND_URL=http://localhost:8000
# so for LAN access this must be the host's LAN IP, not localhost (e.g.
# http://192.168.1.190:8000). Leave unset for localhost-only access.
NEXT_PUBLIC_API_URL=http://localhost:8000
# "Git Repository" link on the landing page header, next to Sign in - only
# rendered when this is set (e.g. https://git.ciagent.org/you/ci-agent).
# Leave blank if you don't run a public git server for this deployment.
NEXT_PUBLIC_GIT_REPO_URL=
# --- Reverse proxy (only relevant once deployed behind Cloudflare/Nginx) -----
# Empty = trust the direct connection for client-IP resolution (correct for
@@ -40,9 +44,22 @@ AUTH_MODE=local
JWT_SECRET=dev-only-change-me-32-characters-minimum
JWT_ACCESS_TOKEN_MINUTES=15
JWT_REFRESH_TOKEN_DAYS=7
# Encrypts per-user API keys / server secrets at rest (Fernet - must stay
# exactly 32 raw bytes, urlsafe-base64-encoded). The value below is a real,
# working dev-only key so local Docker Compose functions out of the box;
# scripts/bootstrap-env.sh regenerates a fresh one for any real deployment.
# Rotating this in a deployment that already has stored keys makes them
# permanently undecryptable - treat it like any other production secret.
API_KEY_ENCRYPTION_SECRET=_wYtsm3nJ070987snBFp2eWVI5pyC0H9gGFUb6Cy4cQ=
# --- Database ----------------------------------------------------------------
# Postgres (Docker Compose default):
# Postgres (Docker Compose default) - POSTGRES_USER/PASSWORD/DB feed both the
# `postgres` container's own credentials and DATABASE_URL below (see
# docker-compose.yml) - change the password here, not in DATABASE_URL
# directly, or they'll disagree.
POSTGRES_USER=ciagent
POSTGRES_PASSWORD=ciagent
POSTGRES_DB=ciagent
DATABASE_URL=postgresql+psycopg://ciagent:ciagent@postgres:5432/ciagent
# SQLite fallback for running the API without Docker:
# DATABASE_URL=sqlite+aiosqlite:///./ciagent_dev.db