# 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: