Local dev gets an unauthenticated Adminer instance bound to loopback only. In production, any account with is_admin=true can open it - the app mints a short-lived token from a live admin session, which Nginx's new db.ciagent.org block exchanges for a session cookie that re-checks admin status on every request, instead of a shared static password that wouldn't scale to multiple admins or revoke live. Co-Authored-By: Claude Sonnet 5 <[email protected]>
188 lines
6.6 KiB
YAML
188 lines
6.6 KiB
YAML
# Production stack for ciagent.org - a standalone file, not merged with
|
|
# docker-compose.yml (the dev file). The two differ enough (nginx + gitea
|
|
# added, Postgres/Redis no longer host-published, prod Dockerfile targets,
|
|
# ${POSTGRES_PASSWORD} instead of a hardcoded literal) that an
|
|
# override-merge risked someone running plain `docker compose up` on the
|
|
# box and silently getting the dev config instead. See DEPLOYMENT.md for
|
|
# the full first-boot runbook.
|
|
#
|
|
# Run `scripts/bootstrap-env.sh` first so POSTGRES_PASSWORD/JWT_SECRET/
|
|
# API_KEY_ENCRYPTION_SECRET are real values, not .env.example's dev
|
|
# placeholders, before bringing this up.
|
|
|
|
name: ci-agent-prod
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-ciagent}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set - run scripts/bootstrap-env.sh}
|
|
POSTGRES_DB: ${POSTGRES_DB:-ciagent}
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-ciagent} -d ${POSTGRES_DB:-ciagent}"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
logging: &default-logging
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
logging: *default-logging
|
|
|
|
# No ports published to the host for api/worker/beat/web/gitea below -
|
|
# nginx is the only internet-facing service; everything else is reached
|
|
# over the internal Compose network by service name.
|
|
|
|
api:
|
|
build:
|
|
context: .
|
|
dockerfile: infrastructure/docker/api.Dockerfile.prod
|
|
restart: unless-stopped
|
|
env_file: .env
|
|
environment:
|
|
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-ciagent}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-ciagent}
|
|
REDIS_URL: redis://redis:6379/0
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
logging: *default-logging
|
|
|
|
worker:
|
|
build:
|
|
context: .
|
|
dockerfile: infrastructure/docker/api.Dockerfile.prod
|
|
restart: unless-stopped
|
|
env_file: .env
|
|
environment:
|
|
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-ciagent}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-ciagent}
|
|
REDIS_URL: redis://redis:6379/0
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
command: celery -A app.tasks.celery_app worker --loglevel=INFO -Q default,collection,analysis,notifications,maintenance,enrichment
|
|
logging: *default-logging
|
|
|
|
beat:
|
|
# Must stay exactly one instance, always - duplicate scheduled runs
|
|
# otherwise. Don't `docker compose up --scale beat=N`.
|
|
build:
|
|
context: .
|
|
dockerfile: infrastructure/docker/api.Dockerfile.prod
|
|
restart: unless-stopped
|
|
env_file: .env
|
|
environment:
|
|
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-ciagent}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-ciagent}
|
|
REDIS_URL: redis://redis:6379/0
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
command: celery -A app.tasks.celery_app beat --loglevel=INFO
|
|
logging: *default-logging
|
|
|
|
web:
|
|
build:
|
|
context: .
|
|
dockerfile: infrastructure/docker/web.Dockerfile.prod
|
|
args:
|
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
|
NEXT_PUBLIC_GIT_REPO_URL: ${NEXT_PUBLIC_GIT_REPO_URL:-}
|
|
NEXT_PUBLIC_DB_VIEWER_URL: ${NEXT_PUBLIC_DB_VIEWER_URL:-}
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- api
|
|
logging: *default-logging
|
|
|
|
gitea:
|
|
image: gitea/gitea:1
|
|
restart: unless-stopped
|
|
environment:
|
|
GITEA__database__DB_TYPE: sqlite3
|
|
GITEA__database__PATH: /data/gitea/gitea.db
|
|
GITEA__server__DOMAIN: git.ciagent.org
|
|
GITEA__server__ROOT_URL: https://git.ciagent.org/
|
|
GITEA__server__SSH_DOMAIN: git.ciagent.org
|
|
GITEA__server__SSH_PORT: 2222
|
|
# Deliberately NOT setting GITEA__server__START_SSH_SERVER=true - that
|
|
# flag is for the rootless image variant only. This standard image
|
|
# already bundles its own system sshd for SSH clone/push, configured
|
|
# via the plain (unprefixed) SSH_PORT below - setting START_SSH_SERVER
|
|
# here makes Gitea's own embedded SSH server also try to bind the same
|
|
# port the system sshd already holds, crash-looping the container
|
|
# (confirmed live: "listen tcp :22: bind: address already in use").
|
|
# The system sshd binds container-internal port 22 by default, which
|
|
# is exactly what the "2222:22" host port mapping below expects.
|
|
SSH_PORT: 2222
|
|
# No public sign-ups - only the CLI-created admin account exists.
|
|
GITEA__service__DISABLE_REGISTRATION: "true"
|
|
# Allows anonymous browsing - actual visibility still comes from
|
|
# each repo's own public/private flag, so this alone exposes
|
|
# nothing by itself. See DEPLOYMENT.md.
|
|
GITEA__service__REQUIRE_SIGNIN_VIEW: "false"
|
|
# Skips the web install wizard from the very first boot - never
|
|
# leaves a window where an unauthenticated visitor could complete
|
|
# first-time setup themselves.
|
|
GITEA__security__INSTALL_LOCK: "true"
|
|
volumes:
|
|
- gitea-data:/data
|
|
ports:
|
|
# SSH push (optional, secondary to HTTPS+token push) - Cloudflare's
|
|
# proxy only speaks HTTP(S), so this rides straight to the origin.
|
|
# Firewall this port to your own IP only (see DEPLOYMENT.md).
|
|
- "2222:22"
|
|
logging: *default-logging
|
|
|
|
# Settings -> Database viewer: internal-only, reached through nginx's
|
|
# db.ciagent.org block (app/api/v1/db_viewer.py issues the credential -
|
|
# see that module's docstring). No host port published.
|
|
adminer:
|
|
image: adminer:4.8.1-standalone
|
|
restart: unless-stopped
|
|
environment:
|
|
ADMINER_DEFAULT_SERVER: postgres
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
logging: *default-logging
|
|
|
|
nginx:
|
|
image: nginx:1.27-alpine
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- web
|
|
- api
|
|
- gitea
|
|
- adminer
|
|
volumes:
|
|
- ./infrastructure/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
# Cloudflare Origin CA cert/key, generated once via the Cloudflare
|
|
# dashboard - kept outside the repo entirely, never committed.
|
|
- /etc/ci-agent/certs:/etc/nginx/certs:ro
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
logging: *default-logging
|
|
|
|
volumes:
|
|
postgres-data:
|
|
gitea-data:
|