deploy: target jwilder-proxy production stack
Production deployment now matches the host setup that already runs neuronetz.ai / neuro-landing: the gateway sits behind the jwilder nginx-proxy + acme-companion already on the host, instead of bundling its own Caddy sidecar. - docker-compose.yml: drop the Caddy service entirely. The gateway joins an external `proxy` Docker network (the same one neuronetz-web / neuronetz-www use) and advertises itself with VIRTUAL_HOST / VIRTUAL_PORT / LETSENCRYPT_HOST / LETSENCRYPT_EMAIL. nginx-proxy routes TLS-terminated traffic to it on the shared network; acme-companion handles Let's Encrypt issuance + renewal for api.neuronetz.ai automatically. NO host ports are published in this compose file anywhere — gateway, postgres, redis, ollama all stay unreachable from the host. Pinned container_names (neuronetz-gateway / -postgres / -redis / -ollama) for stable identification by nginx-proxy and ops scripts. - .env.example: add GATEWAY_VIRTUAL_HOST + LETSENCRYPT_EMAIL; flip the default GATEWAY_TRUSTED_PROXIES to `127.0.0.1,nginx-proxy`. - docs/DEPLOYMENT.md: the canonical path is now jwilder-proxy. Reorganized prerequisites + steps around it; documented adding HSTS and the other security headers via the nginx-proxy custom-config mechanism (/etc/nginx/vhost.d/<host>). The Caddy sidecar lives on as a documented alternative for hosts without jwilder-proxy (ops/caddy/Caddyfile.example is kept). The Ollama-never-exposed non-negotiable is unchanged.
This commit is contained in:
@@ -9,7 +9,14 @@ GATEWAY_BIND_PORT=8080
|
|||||||
GATEWAY_LOG_LEVEL=INFO
|
GATEWAY_LOG_LEVEL=INFO
|
||||||
GATEWAY_LOG_FORMAT=json # json|console
|
GATEWAY_LOG_FORMAT=json # json|console
|
||||||
GATEWAY_REQUEST_ID_HEADER=X-Request-ID
|
GATEWAY_REQUEST_ID_HEADER=X-Request-ID
|
||||||
GATEWAY_TRUSTED_PROXIES=127.0.0.1,caddy # for X-Forwarded-For
|
GATEWAY_TRUSTED_PROXIES=127.0.0.1,nginx-proxy # for X-Forwarded-For
|
||||||
|
|
||||||
|
# ──────────── Public hostname (jwilder-proxy / acme-companion) ───────
|
||||||
|
# These are consumed by docker-compose.yml's gateway service so that the
|
||||||
|
# host's nginx-proxy stack routes TLS-terminated traffic for your domain.
|
||||||
|
# Mirrors the pattern used by neuro-landing.
|
||||||
|
GATEWAY_VIRTUAL_HOST=api.neuronetz.ai
|
||||||
|
LETSENCRYPT_EMAIL=admin@neuronetz.ai
|
||||||
|
|
||||||
# ──────────────────────────── Upstream ───────────────────────────
|
# ──────────────────────────── Upstream ───────────────────────────
|
||||||
OLLAMA_BASE_URL=http://ollama:11434
|
OLLAMA_BASE_URL=http://ollama:11434
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
# neuronetz-gateway — FULL production stack (SPEC §4.1).
|
# neuronetz-gateway — FULL production stack, hosted behind jwilder-proxy
|
||||||
|
# (the same setup used by neuronetz.ai / neuro-landing).
|
||||||
#
|
#
|
||||||
# Internet ──TLS──▶ caddy ──HTTP/1.1 internal──▶ gateway ──▶ postgres / redis / ollama
|
# Internet ──TLS──▶ nginx-proxy (jwilder) ──HTTP/1.1──▶ gateway
|
||||||
|
# │
|
||||||
|
# └─▶ postgres / redis / ollama
|
||||||
|
# (private network only)
|
||||||
#
|
#
|
||||||
# Only Caddy publishes ports to the host. The gateway is reachable solely through
|
# Public traffic is terminated by the jwilder/nginx-proxy that already runs on
|
||||||
# Caddy on the internal network. Postgres, Redis and (critically) Ollama are NOT
|
# this host. The gateway joins its external `proxy` network and advertises
|
||||||
# published to the host at all.
|
# itself with VIRTUAL_HOST / VIRTUAL_PORT; letsencrypt-nginx-proxy-companion
|
||||||
|
# obtains and renews the cert for api.neuronetz.ai automatically.
|
||||||
#
|
#
|
||||||
# ┌─────────────────────────────────────────────────────────────────────────┐
|
# ┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
# │ SECURITY NON-NEGOTIABLE: │
|
# │ SECURITY NON-NEGOTIABLE: │
|
||||||
@@ -14,45 +19,44 @@
|
|||||||
# │ unauthenticated exposure this whole project exists to close. │
|
# │ unauthenticated exposure this whole project exists to close. │
|
||||||
# └─────────────────────────────────────────────────────────────────────────┘
|
# └─────────────────────────────────────────────────────────────────────────┘
|
||||||
#
|
#
|
||||||
# Copy `.env.example` to `.env` and adjust before running:
|
# Prerequisites on the host:
|
||||||
|
# - A jwilder-proxy stack (nginx-proxy + acme-companion) already running and
|
||||||
|
# attached to an external Docker network named `proxy`.
|
||||||
|
# - DNS A/AAAA record for api.neuronetz.ai pointing at this host.
|
||||||
|
#
|
||||||
|
# Bring it up:
|
||||||
|
# cp .env.example .env # set POSTGRES_PASSWORD and any overrides
|
||||||
# docker compose up -d --build
|
# docker compose up -d --build
|
||||||
|
#
|
||||||
|
# Users without jwilder-proxy can use the Caddy sidecar example in
|
||||||
|
# ops/caddy/Caddyfile.example instead — see docs/DEPLOYMENT.md.
|
||||||
|
|
||||||
services:
|
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:
|
gateway:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
container_name: neuronetz-gateway
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# NOTE: deliberately NO `ports:` — the gateway is internal-only and is
|
# NOTE: deliberately NO `ports:` — the gateway is reached only via the
|
||||||
# reached exclusively through Caddy.
|
# jwilder nginx-proxy on the shared external `proxy` network.
|
||||||
expose:
|
expose:
|
||||||
- "8080"
|
- "8080"
|
||||||
environment:
|
environment:
|
||||||
|
# jwilder/nginx-proxy + acme-companion routing (matches neuro-landing).
|
||||||
|
VIRTUAL_HOST: ${GATEWAY_VIRTUAL_HOST:-api.neuronetz.ai}
|
||||||
|
VIRTUAL_PORT: "8080"
|
||||||
|
LETSENCRYPT_HOST: ${GATEWAY_VIRTUAL_HOST:-api.neuronetz.ai}
|
||||||
|
LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL:-admin@neuronetz.ai}
|
||||||
|
# ─── Gateway runtime ──────────────────────────────────────────────
|
||||||
GATEWAY_BIND_HOST: 0.0.0.0
|
GATEWAY_BIND_HOST: 0.0.0.0
|
||||||
GATEWAY_BIND_PORT: "8080"
|
GATEWAY_BIND_PORT: "8080"
|
||||||
GATEWAY_LOG_LEVEL: ${GATEWAY_LOG_LEVEL:-INFO}
|
GATEWAY_LOG_LEVEL: ${GATEWAY_LOG_LEVEL:-INFO}
|
||||||
GATEWAY_LOG_FORMAT: ${GATEWAY_LOG_FORMAT:-json}
|
GATEWAY_LOG_FORMAT: ${GATEWAY_LOG_FORMAT:-json}
|
||||||
GATEWAY_REQUEST_ID_HEADER: ${GATEWAY_REQUEST_ID_HEADER:-X-Request-ID}
|
GATEWAY_REQUEST_ID_HEADER: ${GATEWAY_REQUEST_ID_HEADER:-X-Request-ID}
|
||||||
GATEWAY_TRUSTED_PROXIES: ${GATEWAY_TRUSTED_PROXIES:-127.0.0.1,caddy}
|
# nginx-proxy forwards from the `proxy` network — trust its IP space.
|
||||||
# Service-name addressing on the internal network.
|
GATEWAY_TRUSTED_PROXIES: ${GATEWAY_TRUSTED_PROXIES:-127.0.0.1,nginx-proxy}
|
||||||
|
# ─── Internal service addressing ──────────────────────────────────
|
||||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-gateway}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/${POSTGRES_DB:-neuronetz}
|
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-gateway}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/${POSTGRES_DB:-neuronetz}
|
||||||
DATABASE_POOL_SIZE: ${DATABASE_POOL_SIZE:-10}
|
DATABASE_POOL_SIZE: ${DATABASE_POOL_SIZE:-10}
|
||||||
DATABASE_POOL_OVERFLOW: ${DATABASE_POOL_OVERFLOW:-20}
|
DATABASE_POOL_OVERFLOW: ${DATABASE_POOL_OVERFLOW:-20}
|
||||||
@@ -62,6 +66,8 @@ services:
|
|||||||
OLLAMA_CONNECT_TIMEOUT_S: ${OLLAMA_CONNECT_TIMEOUT_S:-5}
|
OLLAMA_CONNECT_TIMEOUT_S: ${OLLAMA_CONNECT_TIMEOUT_S:-5}
|
||||||
OLLAMA_READ_TIMEOUT_S: ${OLLAMA_READ_TIMEOUT_S:-600}
|
OLLAMA_READ_TIMEOUT_S: ${OLLAMA_READ_TIMEOUT_S:-600}
|
||||||
OLLAMA_MAX_CONNECTIONS: ${OLLAMA_MAX_CONNECTIONS:-64}
|
OLLAMA_MAX_CONNECTIONS: ${OLLAMA_MAX_CONNECTIONS:-64}
|
||||||
|
MODEL_DISCOVERY_REFRESH_S: ${MODEL_DISCOVERY_REFRESH_S:-60}
|
||||||
|
MODEL_DISCOVERY_CACHE_TTL_S: ${MODEL_DISCOVERY_CACHE_TTL_S:-120}
|
||||||
DEFAULT_RPM: ${DEFAULT_RPM:-60}
|
DEFAULT_RPM: ${DEFAULT_RPM:-60}
|
||||||
DEFAULT_TPM: ${DEFAULT_TPM:-100000}
|
DEFAULT_TPM: ${DEFAULT_TPM:-100000}
|
||||||
DEFAULT_CONCURRENT: ${DEFAULT_CONCURRENT:-8}
|
DEFAULT_CONCURRENT: ${DEFAULT_CONCURRENT:-8}
|
||||||
@@ -74,6 +80,9 @@ services:
|
|||||||
AUDIT_BUFFER_SIZE: ${AUDIT_BUFFER_SIZE:-1000}
|
AUDIT_BUFFER_SIZE: ${AUDIT_BUFFER_SIZE:-1000}
|
||||||
PROMPT_LOG_DEFAULT_RETENTION_DAYS: ${PROMPT_LOG_DEFAULT_RETENTION_DAYS:-30}
|
PROMPT_LOG_DEFAULT_RETENTION_DAYS: ${PROMPT_LOG_DEFAULT_RETENTION_DAYS:-30}
|
||||||
AUDIT_LOG_DEFAULT_RETENTION_DAYS: ${AUDIT_LOG_DEFAULT_RETENTION_DAYS:-365}
|
AUDIT_LOG_DEFAULT_RETENTION_DAYS: ${AUDIT_LOG_DEFAULT_RETENTION_DAYS:-365}
|
||||||
|
# Playground + auto-docs OFF by default in prod.
|
||||||
|
PLAYGROUND_ENABLED: ${PLAYGROUND_ENABLED:-false}
|
||||||
|
DOCS_ENABLED: ${DOCS_ENABLED:-false}
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -90,10 +99,12 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- proxy # for nginx-proxy / acme-companion (TLS-fronted public traffic)
|
||||||
|
- internal # for postgres / redis / ollama (private)
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
container_name: neuronetz-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-gateway}
|
POSTGRES_USER: ${POSTGRES_USER:-gateway}
|
||||||
@@ -112,6 +123,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
|
container_name: neuronetz-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
||||||
# No `ports:` — Redis is internal-only.
|
# No `ports:` — Redis is internal-only.
|
||||||
@@ -129,6 +141,7 @@ services:
|
|||||||
# ───────────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────────
|
||||||
ollama:
|
ollama:
|
||||||
image: ollama/ollama:latest
|
image: ollama/ollama:latest
|
||||||
|
container_name: neuronetz-ollama
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# !!! NO `ports:` — never publish Ollama to the host or the internet. !!!
|
# !!! NO `ports:` — never publish Ollama to the host or the internet. !!!
|
||||||
volumes:
|
volumes:
|
||||||
@@ -137,16 +150,14 @@ services:
|
|||||||
- internal
|
- internal
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
# Public-facing network: only Caddy is attached alongside `internal`.
|
# External network managed by the host's jwilder-proxy stack
|
||||||
edge:
|
# (the same network neuronetz-web / neuronetz-www are attached to).
|
||||||
driver: bridge
|
proxy:
|
||||||
|
external: true
|
||||||
# Private network for inter-service traffic; not reachable from the host.
|
# Private network for inter-service traffic; not reachable from the host.
|
||||||
internal:
|
internal:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
internal: false
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
ollama_data:
|
ollama_data:
|
||||||
caddy_data:
|
|
||||||
caddy_config:
|
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
# neuronetz-gateway — Deployment
|
# neuronetz-gateway — Deployment
|
||||||
|
|
||||||
Production deployment is a single Docker Compose stack: **Caddy + gateway + Postgres + Redis
|
Production deployment is a Docker Compose stack — **gateway + Postgres + Redis + Ollama** —
|
||||||
+ Ollama**. Caddy is the only public-facing component; it terminates TLS via Let's Encrypt
|
that sits behind the host's existing **jwilder/nginx-proxy** stack (the same one already
|
||||||
for `api.neuronetz.ai` and reverse-proxies to the internal-only gateway.
|
serving `neuronetz.ai` / `neuro-landing`). Public traffic enters via `nginx-proxy` and
|
||||||
|
`acme-companion`, which terminate TLS and obtain/renew the Let's Encrypt certificate for
|
||||||
|
`api.neuronetz.ai`. The gateway joins the host's external `proxy` Docker network alongside
|
||||||
|
the other public-facing containers and advertises itself with `VIRTUAL_HOST` /
|
||||||
|
`VIRTUAL_PORT`. Postgres, Redis, and Ollama stay on a private internal network with no
|
||||||
|
published ports.
|
||||||
|
|
||||||
|
> ▶ Don't have jwilder-proxy on the host? See
|
||||||
|
> [§ "Alternative: TLS via Caddy sidecar"](#alternative-tls-via-caddy-sidecar) — the
|
||||||
|
> `ops/caddy/Caddyfile.example` is shipped for that case.
|
||||||
|
|
||||||
> For the local, no-GPU demo (mock Ollama + playground), see [`PLAYGROUND.md`](PLAYGROUND.md)
|
> For the local, no-GPU demo (mock Ollama + playground), see [`PLAYGROUND.md`](PLAYGROUND.md)
|
||||||
> and run `./demo.sh`. This document is the **production** path.
|
> and run `./demo.sh`. This document is the **production** path.
|
||||||
@@ -18,53 +27,61 @@ for `api.neuronetz.ai` and reverse-proxies to the internal-only gateway.
|
|||||||
> Publishing it would re-open the exact unauthenticated exposure this whole project exists
|
> Publishing it would re-open the exact unauthenticated exposure this whole project exists
|
||||||
> to close (SPEC §1, §3; AGENT_PROMPT non-negotiable #2).
|
> to close (SPEC §1, §3; AGENT_PROMPT non-negotiable #2).
|
||||||
|
|
||||||
The same posture applies to **Postgres** and **Redis** in the production compose file — no
|
The same posture applies to **Postgres**, **Redis**, and the gateway itself in the
|
||||||
published ports. Only **Caddy** binds host ports (80/443, 443/udp for HTTP/3).
|
production compose file — **no published ports anywhere in this compose file**. Only
|
||||||
|
the host's jwilder `nginx-proxy` container binds 80/443; the gateway is reached via the
|
||||||
|
shared external `proxy` Docker network.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- A host with Docker + Docker Compose.
|
- A host with Docker + Docker Compose.
|
||||||
- DNS: `api.neuronetz.ai` → the host's public IP (for Let's Encrypt).
|
- A jwilder-proxy stack already running on the host, attached to an external Docker
|
||||||
- Ports 80 and 443 reachable from the internet (ACME HTTP/TLS challenge + serving).
|
network named `proxy`. Typically `jwilder/nginx-proxy` + `nginxproxy/acme-companion`,
|
||||||
|
the same setup serving `neuronetz.ai` / `neuro-landing`.
|
||||||
|
- DNS: `api.neuronetz.ai` → the host's public IP.
|
||||||
|
- Ports 80 and 443 already published by the jwilder-proxy container on that host (for
|
||||||
|
ACME HTTP-01 + serving). This compose file does **not** publish them itself.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Steps
|
## Steps (production — jwilder-proxy)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repo> neuronetz-gateway && cd neuronetz-gateway
|
git clone ssh://git@gitea.neuronetz.ai:222/m17hr1l/neuronetz-gateway.git
|
||||||
|
cd neuronetz-gateway
|
||||||
|
|
||||||
# 1. Configure. Copy the example env and change EVERY secret.
|
# 1. Configure. Copy the example env and change EVERY secret.
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# - POSTGRES_PASSWORD: a strong, unique value
|
# - POSTGRES_PASSWORD : a strong, unique value
|
||||||
# - DATABASE_URL: must match the POSTGRES_* values
|
# - GATEWAY_VIRTUAL_HOST : api.neuronetz.ai (read by nginx-proxy)
|
||||||
# - GATEWAY_LOG_FORMAT=json for production
|
# - LETSENCRYPT_EMAIL : admin@neuronetz.ai (read by acme-companion)
|
||||||
|
# - GATEWAY_LOG_FORMAT=json : for production
|
||||||
|
# - GATEWAY_TRUSTED_PROXIES : 127.0.0.1,nginx-proxy
|
||||||
|
|
||||||
# 2. Configure Caddy for your domain + ACME email.
|
# 2. Bring up the stack. The gateway joins the external `proxy` network and
|
||||||
cp ops/caddy/Caddyfile.example ops/caddy/Caddyfile # then edit the site + email
|
# runs `alembic upgrade head` before serving.
|
||||||
# (docker-compose.yml mounts Caddyfile.example by default; point it at your edited file
|
|
||||||
# or edit in place.)
|
|
||||||
|
|
||||||
# 3. Bring up the full stack. The gateway runs `alembic upgrade head`, then serves.
|
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
|
# nginx-proxy observes the new container, generates an nginx vhost for
|
||||||
|
# api.neuronetz.ai, and acme-companion issues the cert via Let's Encrypt.
|
||||||
|
# Cert renewals are automatic.
|
||||||
|
|
||||||
# 4. Bootstrap a tenant + key (CLI runs inside the gateway container).
|
# 3. Bootstrap a tenant + key (CLI runs inside the gateway container).
|
||||||
docker compose exec gateway neuronetz-gateway create-tenant --name acme --rpm 120 --tpm 200000
|
docker compose exec gateway neuronetz-gateway create-tenant --name acme --rpm 120 --tpm 200000
|
||||||
docker compose exec gateway neuronetz-gateway create-key --tenant acme --name prod-server-1
|
docker compose exec gateway neuronetz-gateway create-key --tenant acme --name prod-server-1
|
||||||
# ^ prints the full key ONCE — store it in your secret manager now.
|
# ^ prints the full key ONCE — store it in your secret manager now.
|
||||||
|
|
||||||
# 5. Smoke test (through Caddy / TLS).
|
# 4. Smoke test through public TLS.
|
||||||
curl https://api.neuronetz.ai/healthz
|
curl https://api.neuronetz.ai/healthz
|
||||||
curl -N https://api.neuronetz.ai/v1/chat/completions \
|
curl -N https://api.neuronetz.ai/v1/chat/completions \
|
||||||
-H "Authorization: Bearer nz_…" -H "Content-Type: application/json" \
|
-H "Authorization: Bearer nz_…" -H "Content-Type: application/json" \
|
||||||
-d '{"model":"llama3.1:8b","stream":true,"messages":[{"role":"user","content":"hi"}]}'
|
-d '{"model":"llama3.1:8b","stream":true,"messages":[{"role":"user","content":"hi"}]}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Caddy obtains and renews the certificate automatically. For local testing without a public
|
The compose file pins `container_name: neuronetz-gateway` (and `neuronetz-postgres` /
|
||||||
domain, use the `localhost { tls internal … }` block documented in `Caddyfile.example`
|
`neuronetz-redis` / `neuronetz-ollama`) for stable identification by nginx-proxy and
|
||||||
(trust Caddy's local CA or pass `-k` to curl).
|
for ops scripts.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -102,7 +119,9 @@ All configuration is via environment variables, validated by Pydantic Settings o
|
|||||||
| `GATEWAY_LOG_LEVEL` | `INFO` | |
|
| `GATEWAY_LOG_LEVEL` | `INFO` | |
|
||||||
| `GATEWAY_LOG_FORMAT` | `json` | `json` in prod, `console` for local dev. |
|
| `GATEWAY_LOG_FORMAT` | `json` | `json` in prod, `console` for local dev. |
|
||||||
| `GATEWAY_REQUEST_ID_HEADER` | `X-Request-ID` | |
|
| `GATEWAY_REQUEST_ID_HEADER` | `X-Request-ID` | |
|
||||||
| `GATEWAY_TRUSTED_PROXIES` | `127.0.0.1,caddy` | Sources trusted for `X-Forwarded-For`. |
|
| `GATEWAY_TRUSTED_PROXIES` | `127.0.0.1,nginx-proxy` | Sources trusted for `X-Forwarded-For`. Set to your front-proxy's container name / IP. |
|
||||||
|
| `GATEWAY_VIRTUAL_HOST` | `api.neuronetz.ai` | Read by jwilder `nginx-proxy` and `acme-companion`. |
|
||||||
|
| `LETSENCRYPT_EMAIL` | `admin@neuronetz.ai` | Read by `acme-companion`. |
|
||||||
|
|
||||||
### Upstream (Ollama)
|
### Upstream (Ollama)
|
||||||
| Var | Default | Notes |
|
| Var | Default | Notes |
|
||||||
@@ -157,17 +176,31 @@ All configuration is via environment variables, validated by Pydantic Settings o
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TLS & security headers (Caddy)
|
## TLS & security headers
|
||||||
|
|
||||||
`ops/caddy/Caddyfile.example` already sets:
|
In the canonical (jwilder-proxy) setup, TLS termination and security headers belong on
|
||||||
|
the host's `nginx-proxy` container, not in this repo. Use the standard nginx-proxy
|
||||||
|
custom-config mechanism (`/etc/nginx/vhost.d/api.neuronetz.ai`) to add HSTS and the rest:
|
||||||
|
|
||||||
- **HSTS** `max-age=63072000; includeSubDomains; preload`
|
```
|
||||||
- `X-Content-Type-Options: nosniff`
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||||
- `X-Frame-Options: DENY`
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
- `Referrer-Policy: no-referrer`
|
add_header X-Frame-Options "DENY" always;
|
||||||
- strips `Server` and `X-Powered-By`
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
```
|
||||||
|
|
||||||
Edit the site address and ACME `email` before deploying.
|
If you prefer to terminate TLS in this repo (no jwilder-proxy on the host), see the
|
||||||
|
section below.
|
||||||
|
|
||||||
|
<a id="alternative-tls-via-caddy-sidecar"></a>
|
||||||
|
## Alternative: TLS via Caddy sidecar
|
||||||
|
|
||||||
|
`ops/caddy/Caddyfile.example` is provided for hosts without jwilder-proxy. It sets HSTS,
|
||||||
|
the security headers above, strips the `Server` header, and obtains a Let's Encrypt
|
||||||
|
cert. To use it, add a `caddy` service to your local copy of `docker-compose.yml`
|
||||||
|
(binding host 80/443), drop the gateway's `VIRTUAL_HOST` / `LETSENCRYPT_HOST` env vars,
|
||||||
|
and remove the `proxy` external-network requirement. The Caddyfile itself is self-
|
||||||
|
documenting; edit the site address and ACME `email` before deploying.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user