scaffold: project skeleton, schema, healthz/readyz, CI
Initial project structure for neuronetz-gateway per scope-docs/SPEC.md: - Python 3.12 / FastAPI / SQLAlchemy 2.0 (async) / Redis / Postgres stack managed by uv. Multi-stage non-root Dockerfile, prod + dev compose files (ollama service is NEVER published in either), Caddyfile + systemd unit, justfile, GitHub Actions CI (ruff, mypy --strict, pytest, bandit, pip-audit). - Pydantic-Settings config covering every env var from SPEC §7, including the MODEL_DISCOVERY_* keys for the dynamic-discovery feature (§4.6). - Alembic 0001_initial creates the full gateway schema (8 tables, 3 enums, notify_key_revoked() trigger), incl. allow_all_models on tenant_limits and key_limits for the per-tenant auto-grant toggle. - Working /healthz, /readyz (fail-closed when deps unreachable), and a Prometheus /metrics stub. Sanitizing error handlers that attach X-Request-ID to every response and never leak upstream internals. - SPEC + AGENT_PROMPT included under scope-docs/ (source of truth).
This commit is contained in:
152
docker-compose.yml
Normal file
152
docker-compose.yml
Normal file
@@ -0,0 +1,152 @@
|
||||
# neuronetz-gateway — FULL production stack (SPEC §4.1).
|
||||
#
|
||||
# Internet ──TLS──▶ caddy ──HTTP/1.1 internal──▶ gateway ──▶ postgres / redis / ollama
|
||||
#
|
||||
# Only Caddy publishes ports to the host. The gateway is reachable solely through
|
||||
# Caddy on the internal network. Postgres, Redis and (critically) Ollama are NOT
|
||||
# published to the host at all.
|
||||
#
|
||||
# ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
# │ SECURITY NON-NEGOTIABLE: │
|
||||
# │ The `ollama` service has NO `ports:` mapping and MUST NEVER get one. │
|
||||
# │ Ollama is reachable only on the internal Docker network via the │
|
||||
# │ service name `ollama:11434`. Publishing it would re-open the exact │
|
||||
# │ unauthenticated exposure this whole project exists to close. │
|
||||
# └─────────────────────────────────────────────────────────────────────────┘
|
||||
#
|
||||
# Copy `.env.example` to `.env` and adjust before running:
|
||||
# docker compose up -d --build
|
||||
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
gateway:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp" # HTTP/3
|
||||
volumes:
|
||||
- ./ops/caddy/Caddyfile.example:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
networks:
|
||||
- edge
|
||||
- internal
|
||||
|
||||
gateway:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
# NOTE: deliberately NO `ports:` — the gateway is internal-only and is
|
||||
# reached exclusively through Caddy.
|
||||
expose:
|
||||
- "8080"
|
||||
environment:
|
||||
GATEWAY_BIND_HOST: 0.0.0.0
|
||||
GATEWAY_BIND_PORT: "8080"
|
||||
GATEWAY_LOG_LEVEL: ${GATEWAY_LOG_LEVEL:-INFO}
|
||||
GATEWAY_LOG_FORMAT: ${GATEWAY_LOG_FORMAT:-json}
|
||||
GATEWAY_REQUEST_ID_HEADER: ${GATEWAY_REQUEST_ID_HEADER:-X-Request-ID}
|
||||
GATEWAY_TRUSTED_PROXIES: ${GATEWAY_TRUSTED_PROXIES:-127.0.0.1,caddy}
|
||||
# Service-name addressing on the internal network.
|
||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-gateway}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/${POSTGRES_DB:-neuronetz}
|
||||
DATABASE_POOL_SIZE: ${DATABASE_POOL_SIZE:-10}
|
||||
DATABASE_POOL_OVERFLOW: ${DATABASE_POOL_OVERFLOW:-20}
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
REDIS_KEY_CACHE_TTL_S: ${REDIS_KEY_CACHE_TTL_S:-60}
|
||||
OLLAMA_BASE_URL: http://ollama:11434
|
||||
OLLAMA_CONNECT_TIMEOUT_S: ${OLLAMA_CONNECT_TIMEOUT_S:-5}
|
||||
OLLAMA_READ_TIMEOUT_S: ${OLLAMA_READ_TIMEOUT_S:-600}
|
||||
OLLAMA_MAX_CONNECTIONS: ${OLLAMA_MAX_CONNECTIONS:-64}
|
||||
DEFAULT_RPM: ${DEFAULT_RPM:-60}
|
||||
DEFAULT_TPM: ${DEFAULT_TPM:-100000}
|
||||
DEFAULT_CONCURRENT: ${DEFAULT_CONCURRENT:-8}
|
||||
MAX_REQUEST_BODY_BYTES: ${MAX_REQUEST_BODY_BYTES:-262144}
|
||||
MAX_NUM_PREDICT: ${MAX_NUM_PREDICT:-4096}
|
||||
ARGON2_TIME_COST: ${ARGON2_TIME_COST:-3}
|
||||
ARGON2_MEMORY_COST_KIB: ${ARGON2_MEMORY_COST_KIB:-65536}
|
||||
ARGON2_PARALLELISM: ${ARGON2_PARALLELISM:-4}
|
||||
AUTH_FAILURE_RATE_LIMIT_PER_IP_PER_MIN: ${AUTH_FAILURE_RATE_LIMIT_PER_IP_PER_MIN:-20}
|
||||
AUDIT_BUFFER_SIZE: ${AUDIT_BUFFER_SIZE:-1000}
|
||||
PROMPT_LOG_DEFAULT_RETENTION_DAYS: ${PROMPT_LOG_DEFAULT_RETENTION_DAYS:-30}
|
||||
AUDIT_LOG_DEFAULT_RETENTION_DAYS: ${AUDIT_LOG_DEFAULT_RETENTION_DAYS:-365}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ollama:
|
||||
condition: service_started
|
||||
# Apply migrations, then start the server.
|
||||
command: ["sh", "-c", "alembic upgrade head && exec python -m neuronetz_gateway"]
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/healthz"]
|
||||
interval: 15s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- internal
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-gateway}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-neuronetz}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
# No `ports:` — Postgres is internal-only.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-gateway} -d ${POSTGRES_DB:-neuronetz}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- internal
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
||||
# No `ports:` — Redis is internal-only.
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- internal
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────
|
||||
# Ollama — INTERNAL NETWORK ONLY. DO NOT ADD A `ports:` MAPPING.
|
||||
# Reachable only as `http://ollama:11434` from the gateway container.
|
||||
# ───────────────────────────────────────────────────────────────────────────
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
restart: unless-stopped
|
||||
# !!! NO `ports:` — never publish Ollama to the host or the internet. !!!
|
||||
volumes:
|
||||
- ollama_data:/root/.ollama
|
||||
networks:
|
||||
- internal
|
||||
|
||||
networks:
|
||||
# Public-facing network: only Caddy is attached alongside `internal`.
|
||||
edge:
|
||||
driver: bridge
|
||||
# Private network for inter-service traffic; not reachable from the host.
|
||||
internal:
|
||||
driver: bridge
|
||||
internal: false
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
ollama_data:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
Reference in New Issue
Block a user