Files
ai-memory/docker/compose.tls.caddy.yml
AkitaOnRails 1c8bb1aa18 docs: HTTPS-via-proxy guide + compose templates + sharpened startup warning
Per Phase 2 decision to NOT terminate TLS in-binary: ai-memory
stays HTTP-on-loopback by default (zero change for existing users
on upgrade) and operators front it with a battle-tested reverse
proxy when they cross the deployment shapes that need TLS.

Three new files:

- docs/https-via-proxy.md — the deployment guide. Covers:
  - When you DON'T need TLS (loopback + stdio cases honestly
    don't, called out up front so we don't add ceremony where
    it doesn't earn its keep).
  - When you DO need TLS (multi-user, non-loopback bind, /web
    from another machine, public exposure).
  - Five deployment paths with copy-paste configs:
    - Caddy + public domain + Let's Encrypt
    - Caddy + internal CA (LAN-only)
    - Cloudflare Tunnel (no open ports, TLS at the edge)
    - External cert files (Caddy or nginx)
    - Native (non-Docker) Caddy
  - Each path documents what can go wrong + the trust-install
    step for the internal-CA case (the load-bearing manual
    step — skipping it produces security theatre).
  - Final section "Don't paper over the security gap" calls out
    three specific anti-patterns operators reach for when
    things "don't work" (disable allowed-hosts, --insecure on
    clients, --no-tls-verify on cloudflared).

- docker/compose.tls.caddy.yml — copy-paste compose template
  with Caddy front. Three variants documented inline (LE,
  internal CA, external cert files). Same service definition
  across all three; only the Caddyfile differs.

- docker/compose.tls.cloudflared.yml — Cloudflare Tunnel sidecar
  compose template. Walks through the one-time CF dashboard
  setup, the .env.production shape (incl. CLOUDFLARE_TUNNEL_TOKEN),
  and the client-config commands. No ports exposed; the tunnel is
  outbound-only.

Code change (small):

- crates/ai-memory-cli/src/commands/serve.rs: extend the
  existing non-loopback startup warning. Previously fired only
  when bind wasn't loopback AND no auth token was configured;
  now ALSO fires (with different message) when bind isn't
  loopback AND auth IS configured — to remind the operator
  that bearer tokens still travel cleartext on plain HTTP and
  point them at docs/https-via-proxy.md. One-shot at startup,
  not a refusal to serve (operators may already be behind a
  proxy we can't reliably detect; refusing to bind would break
  their flow). Single line of log output, links to the doc.

Cross-linking:

- README "Security" section gets a clear "Want HTTPS?"
  paragraph pointing at docs/https-via-proxy.md + the two
  compose templates. Spells out the loopback exception so the
  single-user happy-path user doesn't think they're missing
  something.
- README docs table updated: docs/deploy.md row reframed as
  "pointers to the TLS guide"; new row added for
  docs/https-via-proxy.md with a full description of what it
  covers.
- docs/deploy.md's brief "encrypted transport" section
  trimmed from inline two-paragraph table to pointers at the
  new dedicated doc + the compose templates. Avoids drift
  between two places documenting the same thing.

Why this shape (instead of in-binary TLS):

- Reverse proxies are the production-grade answer anyway.
- Caddy does Let's Encrypt better than we ever would; we don't
  have to chase rcgen / instant-acme / rustls CVEs.
- Scope discipline — TLS termination is infrastructure, not
  ai-memory's job. Bundling them would repeat cognee's
  LiteLLM mistake at a smaller scale.
- The four "thinking you're secure when you're not" failure
  modes all become the proxy's problem, not ours: silent ACME
  expiry, trust-store dance, cert reload races, fail-open
  fallback.

No behavior change for existing single-user installs.
720/720 tests pass.
2026-05-30 12:36:21 -03:00

128 lines
5.0 KiB
YAML

# docker compose template — ai-memory + Caddy TLS-terminating reverse proxy.
#
# Three usable variants, all in this one file. Pick one and either:
# - Copy this file, prune the variants you don't want, then
# `docker compose -f compose.tls.caddy.yml up -d`, or
# - Use it verbatim and switch via the `Caddyfile` you put next to
# it. The compose service definitions are the same across variants.
#
# Adjacent files this compose expects:
# ./Caddyfile — your reverse-proxy config; see variants below
# ./.env.production — AI_MEMORY_AUTH_TOKEN + AI_MEMORY_ALLOWED_HOSTS +
# your LLM provider creds. NOT committed.
#
# Walkthrough + variant explanations + client-side install:
# docs/https-via-proxy.md
name: ai-memory-tls
services:
ai-memory:
image: akitaonrails/ai-memory:latest
container_name: ai-memory
restart: unless-stopped
# No host port. Caddy reaches ai-memory over the internal docker
# network; the only inbound surface is Caddy's 80/443 below.
expose:
- "49374"
volumes:
- ai-memory-data:/data
env_file:
- .env.production
environment:
- RUST_LOG=ai_memory=info,ai_memory_store=info,ai_memory_wiki=info,ai_memory_mcp=info,tracing_appender=warn
healthcheck:
test: ["CMD", "/usr/local/bin/ai-memory", "status"]
interval: 30s
timeout: 5s
retries: 3
start_period: 5s
caddy:
image: caddy:2-alpine
container_name: ai-memory-caddy
restart: unless-stopped
depends_on:
- ai-memory
ports:
# 80 is needed for Let's Encrypt's HTTP-01 challenge during issuance
# and renewal. Drop it when using internal-CA mode (Variant B) since
# there's no ACME to solve.
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
# Persist Caddy state across recreates. caddy-data holds the
# account key and issued certs — back this up; losing it forces a
# re-issuance and (in LE's case) brief unavailability.
- caddy-data:/data
- caddy-config:/config
# Only mount this for Variant C (external certs):
# - /your/cert/path:/etc/caddy/certs:ro
volumes:
ai-memory-data:
name: ai-memory-data
caddy-data:
caddy-config:
# ──────────────────────────────────────────────────────────────────────
# Variant A — Public domain + Let's Encrypt
# ──────────────────────────────────────────────────────────────────────
#
# Put this in ./Caddyfile next to this compose file:
#
# memory.example.com {
# reverse_proxy ai-memory:49374
# }
#
# Requires:
# - A DNS A or AAAA record for memory.example.com → your public IP
# - Port 80 + 443 reachable from the internet (for ACME challenges + serving)
#
# Caddy issues + auto-renews from Let's Encrypt with no operator
# action after first start. Browser-trusted by every client out of
# the box.
#
# ──────────────────────────────────────────────────────────────────────
# Variant B — LAN-only + Caddy's internal CA
# ──────────────────────────────────────────────────────────────────────
#
# Put this in ./Caddyfile:
#
# {
# local_certs
# }
#
# homelab.local, 192.168.1.50 {
# reverse_proxy ai-memory:49374
# }
#
# List every hostname + IP a client will use in the site address.
# Caddy puts them all in the cert's SAN.
#
# Requires:
# - One-time install of Caddy's root cert into each client OS trust
# store. Pull it with:
# docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt > caddy-root.crt
# Then follow the per-OS install in docs/https-via-proxy.md.
#
# Skipping that step gives you security theatre, not security. See
# the doc for why.
#
# ──────────────────────────────────────────────────────────────────────
# Variant C — External cert files
# ──────────────────────────────────────────────────────────────────────
#
# Put this in ./Caddyfile:
#
# memory.example.com {
# tls /etc/caddy/certs/memory.crt /etc/caddy/certs/memory.key
# reverse_proxy ai-memory:49374
# }
#
# Uncomment the cert volume mount in the caddy service above.
# Caddy hot-reloads certs on file change — no compose restart needed
# at renewal time.