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:
@@ -1,8 +1,17 @@
|
||||
# neuronetz-gateway — Deployment
|
||||
|
||||
Production deployment is a single Docker Compose stack: **Caddy + gateway + Postgres + Redis
|
||||
+ Ollama**. Caddy is the only public-facing component; it terminates TLS via Let's Encrypt
|
||||
for `api.neuronetz.ai` and reverse-proxies to the internal-only gateway.
|
||||
Production deployment is a Docker Compose stack — **gateway + Postgres + Redis + Ollama** —
|
||||
that sits behind the host's existing **jwilder/nginx-proxy** stack (the same one already
|
||||
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)
|
||||
> 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
|
||||
> to close (SPEC §1, §3; AGENT_PROMPT non-negotiable #2).
|
||||
|
||||
The same posture applies to **Postgres** and **Redis** in the production compose file — no
|
||||
published ports. Only **Caddy** binds host ports (80/443, 443/udp for HTTP/3).
|
||||
The same posture applies to **Postgres**, **Redis**, and the gateway itself in the
|
||||
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
|
||||
|
||||
- A host with Docker + Docker Compose.
|
||||
- DNS: `api.neuronetz.ai` → the host's public IP (for Let's Encrypt).
|
||||
- Ports 80 and 443 reachable from the internet (ACME HTTP/TLS challenge + serving).
|
||||
- A jwilder-proxy stack already running on the host, attached to an external Docker
|
||||
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
|
||||
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.
|
||||
cp .env.example .env
|
||||
# - POSTGRES_PASSWORD: a strong, unique value
|
||||
# - DATABASE_URL: must match the POSTGRES_* values
|
||||
# - GATEWAY_LOG_FORMAT=json for production
|
||||
# - POSTGRES_PASSWORD : a strong, unique value
|
||||
# - GATEWAY_VIRTUAL_HOST : api.neuronetz.ai (read by nginx-proxy)
|
||||
# - 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.
|
||||
cp ops/caddy/Caddyfile.example ops/caddy/Caddyfile # then edit the site + email
|
||||
# (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.
|
||||
# 2. Bring up the stack. The gateway joins the external `proxy` network and
|
||||
# runs `alembic upgrade head` before serving.
|
||||
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-key --tenant acme --name prod-server-1
|
||||
# ^ 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 -N https://api.neuronetz.ai/v1/chat/completions \
|
||||
-H "Authorization: Bearer nz_…" -H "Content-Type: application/json" \
|
||||
-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
|
||||
domain, use the `localhost { tls internal … }` block documented in `Caddyfile.example`
|
||||
(trust Caddy's local CA or pass `-k` to curl).
|
||||
The compose file pins `container_name: neuronetz-gateway` (and `neuronetz-postgres` /
|
||||
`neuronetz-redis` / `neuronetz-ollama`) for stable identification by nginx-proxy and
|
||||
for ops scripts.
|
||||
|
||||
---
|
||||
|
||||
@@ -102,7 +119,9 @@ All configuration is via environment variables, validated by Pydantic Settings o
|
||||
| `GATEWAY_LOG_LEVEL` | `INFO` | |
|
||||
| `GATEWAY_LOG_FORMAT` | `json` | `json` in prod, `console` for local dev. |
|
||||
| `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)
|
||||
| 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`
|
||||
- `X-Frame-Options: DENY`
|
||||
- `Referrer-Policy: no-referrer`
|
||||
- strips `Server` and `X-Powered-By`
|
||||
```
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
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