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:
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,82 @@
|
||||
# Reverse proxy for the three ciagent.org subdomains, sitting between
|
||||
# Cloudflare (which terminates public-facing TLS and hides this origin's
|
||||
# real IP) and the app's own containers. TLS here is a Cloudflare Origin CA
|
||||
# certificate (Cloudflare dashboard -> SSL/TLS -> Origin Server -> Create
|
||||
# Certificate; covers ciagent.org + *.ciagent.org, up to 15yr validity, only
|
||||
# trusted by Cloudflare - no ACME/renewal machinery needed). Cloudflare SSL
|
||||
# mode must be "Full (strict)" for this to be meaningful. See DEPLOYMENT.md.
|
||||
#
|
||||
# CF-Connecting-IP (the header app.core.security.get_client_ip reads once
|
||||
# TRUSTED_PROXY_IP_HEADER=CF-Connecting-IP is set) needs no special handling
|
||||
# here - Nginx forwards any header it doesn't explicitly touch straight
|
||||
# through to the upstream unmodified.
|
||||
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
server_tokens off;
|
||||
|
||||
# Bare :80 hits get redirected to :443 - Cloudflare already enforces
|
||||
# HTTPS at the edge, but the origin shouldn't 400 a direct :80 probe.
|
||||
server {
|
||||
listen 80;
|
||||
server_name ciagent.org api.ciagent.org git.ciagent.org;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name ciagent.org;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/cloudflare-origin.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/cloudflare-origin.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name api.ciagent.org;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/cloudflare-origin.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/cloudflare-origin.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://api:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name git.ciagent.org;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/cloudflare-origin.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/cloudflare-origin.key;
|
||||
|
||||
# Large git pushes (Gitea's HTTPS push path).
|
||||
client_max_body_size 512m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://gitea:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user