File size: 12,773 Bytes
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656439d
 
 
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656439d
 
 
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656439d
 
 
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# Auralynq stack β€” Podman-compatible, rootless, no sudo. `make stack-up`.
#
# Security posture (ADR-0012/0013/0014):
#   * Public:   the Caddy TLS proxy ONLY (one HTTPS port on 0.0.0.0). All other
#               services additionally bind their dev ports to
#               ${AURALYNQ_BIND_INTERNAL:-127.0.0.1} (host loopback, off the
#               external NIC) for host tooling/debugging.
#   * Routing:  all services share podman-compose's default network and address
#               peers by container_name (auralynq-qdrant/-api/-web).
#               scripts/stack_up.sh pins that network's CNI conflist to 0.4.0 and
#               adds the dnsname plugin so container DNS works without sudo
#               (the firewall plugin only validates <=0.4.0) β€” see ADR-0014.
#   * API auth: AURALYNQ_SERVE__API_KEY (empty == open). The browser never holds
#               the key β€” the web container proxies /api/* with the bearer header.
#   * Least privilege: api/worker run as the image's non-root user +
#               no-new-privileges; all services restart unless-stopped + healthcheck.
name: auralynq

# No explicit networks block: podman-compose places all services on its default
# network, where the dnsname plugin resolves peers by their container_name
# (auralynq-qdrant / auralynq-api / auralynq-web). scripts/stack_up.sh ensures
# that default network's CNI conflist is 0.4.0 + has dnsname (see ADR-0014).
services:
  caddy:
    build:
      context: .
      dockerfile: containers/caddy.Dockerfile
      args:
        # Bakes a self-signed cert with this host as a SAN (IP or domain).
        AURALYNQ_CERT_HOST: ${AURALYNQ_CERT_HOST:-localhost}
    image: ${AURALYNQ_IMAGE_PREFIX:-auralynq-}caddy:${AURALYNQ_IMAGE_TAG:-0.2.0}
    container_name: auralynq-caddy
    restart: unless-stopped
    security_opt: ["no-new-privileges"]
    # The ONLY service published on the external NIC; reaches `web` by name.
    ports:
      - "${AURALYNQ_HTTPS_PORT:-8443}:8443"
    environment:
      - AURALYNQ_SITE_ADDRESS=${AURALYNQ_SITE_ADDRESS:-:8443}
      - AURALYNQ_TLS=${AURALYNQ_TLS:-/certs/site.crt /certs/site.key}
      - AURALYNQ_WEB_UPSTREAM=${AURALYNQ_WEB_UPSTREAM:-http://auralynq-web:3000}
    volumes:
      - ./containers/Caddyfile:/etc/caddy/Caddyfile:ro,Z
      - auralynq-caddy-data:/data
      - auralynq-caddy-config:/config
    depends_on:
      - web
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- --no-check-certificate https://localhost:8443 >/dev/null 2>&1 || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 20s

  qdrant:
    image: docker.io/qdrant/qdrant:v1.12.1
    container_name: auralynq-qdrant
    restart: unless-stopped
    security_opt: ["no-new-privileges"]
    # Internal: loopback publish for host tooling; reachable in-cluster as qdrant:6333.
    ports:
      - "${AURALYNQ_BIND_INTERNAL:-127.0.0.1}:${AURALYNQ_QDRANT_HTTP_PORT:-6333}:6333"
      - "${AURALYNQ_BIND_INTERNAL:-127.0.0.1}:${AURALYNQ_QDRANT_GRPC_PORT:-6334}:6334"
    volumes:
      - auralynq-qdrant:/qdrant/storage
    healthcheck:
      test: ["CMD-SHELL", "bash -c '</dev/tcp/127.0.0.1/6333' 2>/dev/null || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 20s

  phoenix:
    image: docker.io/arizephoenix/phoenix:latest
    container_name: auralynq-phoenix
    restart: unless-stopped
    security_opt: ["no-new-privileges"]
    ports:
      - "${AURALYNQ_BIND_INTERNAL:-127.0.0.1}:${AURALYNQ_PHOENIX_PORT:-6006}:6006"
      - "${AURALYNQ_BIND_INTERNAL:-127.0.0.1}:${AURALYNQ_PHOENIX_OTLP_PORT:-4317}:4317"
    environment:
      - PHOENIX_WORKING_DIR=/data
    volumes:
      - auralynq-phoenix:/data
    healthcheck:
      # The Phoenix image is distroless (no /bin/sh), so a CMD-SHELL test can
      # never execute and the service shows perpetually "unhealthy" even while
      # serving fine. Use the exec form with the image's own python interpreter.
      test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:6006').status == 200 else 1)"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s

  api:
    build:
      context: .
      dockerfile: containers/api.Dockerfile
    image: ${AURALYNQ_IMAGE_PREFIX:-auralynq-}api:${AURALYNQ_IMAGE_TAG:-0.2.0}
    container_name: auralynq-api
    restart: unless-stopped
    security_opt: ["no-new-privileges"]
    command: ["uvicorn", "auralynq.serving.app:app", "--host", "0.0.0.0", "--port", "8000"]
    # Host services (e.g. Ollama on :11434) are reachable at
    # host.containers.internal: rootless Podman injects this into every
    # container's /etc/hosts automatically (slirp4netns/pasta/netavark β†’ the
    # host gateway, 10.0.2.2). We deliberately do NOT declare it via
    # `extra_hosts: "host.containers.internal:host-gateway"` β€” that magic value
    # requires podman >= 4.1 and HARD-ERRORS on 3.x ("invalid IP address in
    # add-host: host-gateway"), which is what broke `make start`.
    # Internal: loopback publish for host tooling; reachable in-cluster as api:8000.
    ports:
      - "${AURALYNQ_BIND_INTERNAL:-127.0.0.1}:${AURALYNQ_API_PORT:-8000}:8000"
    environment:
      - HUGGINGFACE_TOKEN=${HUGGINGFACE_TOKEN:-}
      - OPENAI_API_KEY=${OPENAI_API_KEY:-}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
      - COHERE_API_KEY=${COHERE_API_KEY:-}
      - LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY:-}
      - LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY:-}
      # In-cluster service-name addressing (container DNS via the pinned network).
      - AURALYNQ_VECTOR__URL=${AURALYNQ_VECTOR_URL:-http://auralynq-qdrant:6333}
      - AURALYNQ_VECTOR__BACKEND=${AURALYNQ_VECTOR_BACKEND:-auto}
      - AURALYNQ_LLM__PROVIDER=${AURALYNQ_LLM__PROVIDER:-auto}
      - AURALYNQ_LLM__MODEL=${AURALYNQ_LLM__MODEL:-llama3.1:8b}
      - AURALYNQ_LLM__BASE_URL=${AURALYNQ_LLM__BASE_URL:-http://host.containers.internal:11434}
      # vLLM serving backend (optional). Host-side servers are reached via the
      # LAN IP, not localhost β€” scripts/stack_up.sh derives it.
      - AURALYNQ_LLM__VLLM_BASE_URL=${AURALYNQ_LLM__VLLM_BASE_URL:-http://host.containers.internal:8001/v1}
      - AURALYNQ_TELEMETRY__OTLP_ENDPOINT=${AURALYNQ_OTLP_ENDPOINT:-http://auralynq-phoenix:4317}
      - AURALYNQ_DATA_DIR=/app/data
      - AURALYNQ_SERVE__API_KEY=${AURALYNQ_SERVE__API_KEY:-}
      - AURALYNQ_SERVE__CORS_ORIGINS=${AURALYNQ_SERVE__CORS_ORIGINS:-["http://localhost:3000"]}
      # Compounding Wiki (off by default): synthesize durable entity pages at ingest.
      - AURALYNQ_WIKI__ENABLED=${AURALYNQ_WIKI__ENABLED:-false}
      # Watch Folder (off by default): auto-reindex files dropped in the bind below.
      - AURALYNQ_WATCH__ENABLED=${AURALYNQ_WATCH__ENABLED:-false}
    volumes:
      # Podman-managed named volumes initialize with the image's (non-root) owner,
      # so the non-root runtime user can write without host-ownership clashes.
      - auralynq-data:/app/data
      - auralynq-reports:/app/reports
      # Host folder auto-reindexed when AURALYNQ_WATCH__ENABLED=true.
      - ${AURALYNQ_WATCH_HOST:-./data/watch}:/app/data/watch:z
    depends_on:
      - qdrant
      - phoenix
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8000/health || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 20s

  # MCP server exposing the 7 Auralynq tools over the streamable-HTTP transport,
  # so remote MCP clients (Claude Desktop, IDEs, agents) can call them β€” not just
  # local stdio. Internal-only (loopback publish); front with Caddy for public TLS.
  mcp:
    image: ${AURALYNQ_IMAGE_PREFIX:-auralynq-}api:${AURALYNQ_IMAGE_TAG:-0.2.0}
    container_name: auralynq-mcp
    restart: unless-stopped
    security_opt: ["no-new-privileges"]
    command: ["python", "-m", "auralynq.mcp_server.server", "--transport", "streamable-http"]
    ports:
      - "${AURALYNQ_BIND_INTERNAL:-127.0.0.1}:${AURALYNQ_MCP_PORT:-8765}:8765"
    environment:
      - HUGGINGFACE_TOKEN=${HUGGINGFACE_TOKEN:-}
      - OPENAI_API_KEY=${OPENAI_API_KEY:-}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
      - COHERE_API_KEY=${COHERE_API_KEY:-}
      - AURALYNQ_MCP_HOST=0.0.0.0
      - AURALYNQ_MCP_PORT=8765
      # Bearer auth on the HTTP MCP surface: dedicated key wins, else the API key.
      # Empty == open (local/demo). See ADR-0016.
      - AURALYNQ_MCP_API_KEY=${AURALYNQ_MCP_API_KEY:-}
      - AURALYNQ_SERVE__API_KEY=${AURALYNQ_SERVE__API_KEY:-}
      - AURALYNQ_VECTOR__URL=${AURALYNQ_VECTOR_URL:-http://auralynq-qdrant:6333}
      - AURALYNQ_VECTOR__BACKEND=${AURALYNQ_VECTOR_BACKEND:-auto}
      - AURALYNQ_LLM__PROVIDER=${AURALYNQ_LLM__PROVIDER:-auto}
      - AURALYNQ_LLM__MODEL=${AURALYNQ_LLM__MODEL:-llama3.1:8b}
      - AURALYNQ_LLM__BASE_URL=${AURALYNQ_LLM__BASE_URL:-http://host.containers.internal:11434}
      # vLLM serving backend (optional). Host-side servers are reached via the
      # LAN IP, not localhost β€” scripts/stack_up.sh derives it.
      - AURALYNQ_LLM__VLLM_BASE_URL=${AURALYNQ_LLM__VLLM_BASE_URL:-http://host.containers.internal:8001/v1}
      - AURALYNQ_DATA_DIR=/app/data
    volumes:
      - auralynq-data:/app/data
    depends_on:
      - qdrant
    healthcheck:
      test: ["CMD-SHELL", "bash -c '</dev/tcp/127.0.0.1/8765' 2>/dev/null || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 20s

  worker:
    build:
      context: .
      dockerfile: containers/api.Dockerfile
    image: ${AURALYNQ_IMAGE_PREFIX:-auralynq-}api:${AURALYNQ_IMAGE_TAG:-0.2.0}
    container_name: auralynq-worker
    restart: unless-stopped
    security_opt: ["no-new-privileges"]
    command: ["python", "-m", "auralynq.serving.worker"]
    environment:
      - HUGGINGFACE_TOKEN=${HUGGINGFACE_TOKEN:-}
      - OPENAI_API_KEY=${OPENAI_API_KEY:-}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
      - COHERE_API_KEY=${COHERE_API_KEY:-}
      - AURALYNQ_VECTOR__URL=${AURALYNQ_VECTOR_URL:-http://auralynq-qdrant:6333}
      - AURALYNQ_VECTOR__BACKEND=${AURALYNQ_VECTOR_BACKEND:-auto}
      - AURALYNQ_LLM__PROVIDER=${AURALYNQ_LLM__PROVIDER:-auto}
      - AURALYNQ_LLM__MODEL=${AURALYNQ_LLM__MODEL:-llama3.1:8b}
      - AURALYNQ_LLM__BASE_URL=${AURALYNQ_LLM__BASE_URL:-http://host.containers.internal:11434}
      # vLLM serving backend (optional). Host-side servers are reached via the
      # LAN IP, not localhost β€” scripts/stack_up.sh derives it.
      - AURALYNQ_LLM__VLLM_BASE_URL=${AURALYNQ_LLM__VLLM_BASE_URL:-http://host.containers.internal:8001/v1}
      - AURALYNQ_DATA_DIR=/app/data
      - AURALYNQ_WIKI__ENABLED=${AURALYNQ_WIKI__ENABLED:-false}
      - AURALYNQ_WATCH__ENABLED=${AURALYNQ_WATCH__ENABLED:-false}
    volumes:
      - auralynq-data:/app/data
      - ${AURALYNQ_WATCH_HOST:-./data/watch}:/app/data/watch:z
    depends_on:
      - qdrant
    healthcheck:
      test: ["CMD-SHELL", "pgrep -f auralynq.serving.worker || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 15s

  web:
    build:
      context: ./web
      dockerfile: ../containers/web.Dockerfile
      args:
        # Same-origin proxy path baked into the bundle (no IP, no secret).
        NEXT_PUBLIC_API_BASE: ${NEXT_PUBLIC_API_BASE:-/api}
    image: ${AURALYNQ_IMAGE_PREFIX:-auralynq-}web:${AURALYNQ_IMAGE_TAG:-0.2.0}
    container_name: auralynq-web
    restart: unless-stopped
    security_opt: ["no-new-privileges"]
    # Internal: loopback publish for host tooling; Caddy reaches it as web:3000.
    ports:
      - "${AURALYNQ_BIND_INTERNAL:-127.0.0.1}:${AURALYNQ_WEB_PORT:-3000}:3000"
    environment:
      # Server-side only (never sent to the browser): the /api proxy forwards here
      # (by service name) and injects the bearer token.
      - AURALYNQ_API_INTERNAL=${AURALYNQ_API_INTERNAL:-http://auralynq-api:8000}
      - AURALYNQ_SERVE__API_KEY=${AURALYNQ_SERVE__API_KEY:-}
      # Optional failover (ADR-0020): if the local API is unreachable, the proxy
      # replays the request against this remote backup (e.g. https://<server>:8443/api).
      # Empty = single upstream, no failover (default).
      - AURALYNQ_API_FALLBACK=${AURALYNQ_API_FALLBACK:-}
      - AURALYNQ_API_FALLBACK_INSECURE_TLS=${AURALYNQ_API_FALLBACK_INSECURE_TLS:-0}
      - AURALYNQ_API_PRIMARY_TIMEOUT_MS=${AURALYNQ_API_PRIMARY_TIMEOUT_MS:-12000}
    depends_on:
      - api
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000 >/dev/null 2>&1 || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 20s

volumes:
  auralynq-qdrant:
  auralynq-phoenix:
  auralynq-data:
  auralynq-reports:
  auralynq-caddy-data:
  auralynq-caddy-config: