mirror of
https://github.com/Heretek-AI/AetherTable.git
synced 2026-10-12 02:02:28 +00:00
coturn 4.17.2-alpine with static-auth-secret mode (secret via env, never in client bundles), STUN+TURN on 3478, relay range 49160-49200. Client buildIceServers(): public STUN default, TURN appended only when all VITE_TURN_* vars set; typed env + compose build args. Honest limits documented: NAT traversal itself unverifiable locally; TLS/DTLS deferred. 9 new tests; vitest 168/14. Co-Authored-By: Claude <[email protected]>
325 lines
11 KiB
YAML
325 lines
11 KiB
YAML
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
container_name: vtt-postgres
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_USER: vtt_admin
|
|
POSTGRES_PASSWORD: vtt_secure_password_123
|
|
POSTGRES_DB: vtt_campaign_db
|
|
ports:
|
|
- "5432:5432"
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
- ./database/postgres:/docker-entrypoint-initdb.d:ro,z
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U vtt_admin -d vtt_campaign_db"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
networks:
|
|
- vtt-net
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
container_name: vtt-redis
|
|
restart: unless-stopped
|
|
ports:
|
|
- "6379:6379"
|
|
volumes:
|
|
- redisdata:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
networks:
|
|
- vtt-net
|
|
|
|
neo4j:
|
|
image: neo4j:5-community
|
|
container_name: vtt-neo4j
|
|
restart: unless-stopped
|
|
environment:
|
|
NEO4J_AUTH: neo4j/vtt-lore-graph
|
|
ports:
|
|
- "7474:7474"
|
|
- "7687:7687"
|
|
volumes:
|
|
- neo4jdata:/data
|
|
# Schema + canon seed constraints (database/neo4j/01_lore_graph_schema.cypher).
|
|
- ./database/neo4j:/var/lib/neo4j/import/cypher:ro,z
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -q --spider http://localhost:7474 || exit 1"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
networks:
|
|
- vtt-net
|
|
|
|
qdrant:
|
|
image: qdrant/qdrant:latest
|
|
container_name: vtt-qdrant
|
|
restart: unless-stopped
|
|
ports:
|
|
- "6333:6333"
|
|
- "6334:6334"
|
|
volumes:
|
|
- qdrantdata:/qdrant/storage
|
|
# Collection configs (rules + episodic memory namespaces).
|
|
- ./database/qdrant:/qdrant/config:ro,z
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "bash -c ':<(/dev/tcp/127.0.0.1/6333)' 2>/dev/null || exit 1"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
networks:
|
|
- vtt-net
|
|
|
|
vtt-engine:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.rust
|
|
container_name: vtt-engine
|
|
restart: unless-stopped
|
|
environment:
|
|
RUST_LOG: info
|
|
PORT: 8088
|
|
DATABASE_URL: postgres://vtt_admin:vtt_secure_password_123@postgres:5432/vtt_campaign_db
|
|
REDIS_URL: redis://redis:6379
|
|
ports:
|
|
- "8088:8088"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -f http://localhost:8088/health || exit 1"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
networks:
|
|
- vtt-net
|
|
|
|
vtt-orchestrator:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.python
|
|
container_name: vtt-orchestrator
|
|
restart: unless-stopped
|
|
environment:
|
|
ENGINE_API_URL: http://vtt-engine:8088
|
|
DATABASE_URL: postgres://vtt_admin:vtt_secure_password_123@postgres:5432/vtt_campaign_db
|
|
REDIS_URL: redis://redis:6379
|
|
AUTH_SECRET: ${AUTH_SECRET:?set AUTH_SECRET (openssl rand -hex 32) — services abort without it}
|
|
VTT_ENGINE_SECRET: ${AUTH_SECRET:?set AUTH_SECRET (openssl rand -hex 32)} # must match gateway
|
|
NEO4J_URI: bolt://neo4j:7687
|
|
NEO4J_USER: neo4j
|
|
NEO4J_PASSWORD: vtt-lore-graph
|
|
QDRANT_URL: http://qdrant:6333
|
|
ports:
|
|
- "8000:8000"
|
|
depends_on:
|
|
vtt-engine:
|
|
condition: service_healthy
|
|
postgres:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -f http://localhost:8000/api/v1/dynasty/factions || exit 1"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
networks:
|
|
- vtt-net
|
|
|
|
ysync-relay:
|
|
image: node:20-alpine
|
|
container_name: vtt-ysync
|
|
restart: unless-stopped
|
|
working_dir: /app/client
|
|
command: sh -c "node ../scripts/ysync-server.mjs"
|
|
environment:
|
|
PORT: 6380
|
|
YSYNC_DATA_DIR: /data
|
|
AUTH_SECRET: ${AUTH_SECRET:?set AUTH_SECRET (openssl rand -hex 32) — services abort without it}
|
|
VTT_ENGINE_SECRET: ${AUTH_SECRET:?set AUTH_SECRET (openssl rand -hex 32)} # must match gateway
|
|
ports:
|
|
- "6380:6380"
|
|
volumes:
|
|
- ./client/node_modules:/app/client/node_modules:ro,z
|
|
- ./scripts:/app/scripts:ro,z
|
|
- ysyncdata:/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -q --spider http://localhost:6380/ || exit 1"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
networks:
|
|
- vtt-net
|
|
|
|
vtt-peerjs:
|
|
# PeerJS signaling server for the REAL WebRTC video mesh (Pillar 9).
|
|
# Dependency-light on purpose: no image build, no runtime npm download —
|
|
# it requires the `peer` package already installed in ./client/node_modules
|
|
# (same hoisted mount trick as ysync-relay). allow_discovery is REQUIRED:
|
|
# the client roster is built by filtering this server's /peers listing by
|
|
# lobby peer-id prefix (see client/src/render/webrtc_mesh.ts).
|
|
image: node:20-alpine
|
|
container_name: vtt-peerjs
|
|
restart: unless-stopped
|
|
working_dir: /app/client
|
|
command: |-
|
|
node -e "
|
|
const { PeerServer } = require('peer');
|
|
PeerServer(
|
|
{
|
|
port: Number(process.env.PEERJS_PORT || 9000),
|
|
key: process.env.PEERJS_KEY || 'aethertable',
|
|
path: process.env.PEERJS_PATH || '/peerjs',
|
|
allow_discovery: true,
|
|
},
|
|
() => console.log('[peerjs] signaling ready on :' + (process.env.PEERJS_PORT || 9000) +
|
|
' key=' + (process.env.PEERJS_KEY || 'aethertable') +
|
|
' path=' + (process.env.PEERJS_PATH || '/peerjs') + ' discovery=on')
|
|
);
|
|
"
|
|
environment:
|
|
PEERJS_PORT: "9000"
|
|
PEERJS_KEY: aethertable
|
|
PEERJS_PATH: /peerjs
|
|
ports:
|
|
- "9000:9000"
|
|
volumes:
|
|
- ./client/node_modules:/app/client/node_modules:ro,z
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD-SHELL",
|
|
"node -e \"require('http').get('http://127.0.0.1:9000/peerjs/aethertable/peers', r => process.exit(r.statusCode && r.statusCode < 500 ? 0 : 1)).on('error', () => process.exit(1))\"",
|
|
]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
networks:
|
|
- vtt-net
|
|
|
|
vtt-coturn:
|
|
# STUN + TURN relay for the WebRTC video mesh (Pillar 9). Without a TURN
|
|
# hop, peers behind symmetric NAT can never complete ICE and the mesh
|
|
# reports honest 'signaling-down'/'connection failed' states instead of
|
|
# media — this service is the fix path, not a fake-success shim.
|
|
#
|
|
# Auth: static-auth-secret (TURN REST API / time-limited HMAC creds) —
|
|
# see the full rationale in database/coturn/turnserver.conf. The secret is
|
|
# AUTH_SECRET, reused from the rest of this self-hosted stack so there is
|
|
# exactly ONE secret to rotate. Override with COTURN_SECRET if you must
|
|
# split it.
|
|
image: coturn/coturn:4.17.2-alpine
|
|
container_name: vtt-coturn
|
|
restart: unless-stopped
|
|
command:
|
|
- -c=/etc/coturn/turnserver.conf
|
|
- --log-file=stdout
|
|
# $$ = a literal $ after Compose interpolation; the coturn/coturn
|
|
# ENTRYPOINT bash-evals each argument at runtime, so $COTURN_SECRET is
|
|
# expanded INSIDE the container from `environment:` below — the secret
|
|
# never appears in the checked-in config file nor in the compose spec.
|
|
- --static-auth-secret=$${COTURN_SECRET}
|
|
# Auto-detect the host's public IP (needed when this host sits behind
|
|
# NAT so relay allocations advertise a reachable address). Honors
|
|
# REAL_EXTERNAL_IP if you need to pin it; fails the container fast if
|
|
# no DNS provider answers rather than advertising a broken address.
|
|
- --external-ip=$$(detect-external-ip)
|
|
environment:
|
|
COTURN_SECRET: ${COTURN_TURN_SECRET:-${AUTH_SECRET:?set AUTH_SECRET (openssl rand -hex 32)}}
|
|
# Optional override consumed by the image's detect-external-ip helper
|
|
# (pin the advertised public IP instead of DNS auto-detection).
|
|
REAL_EXTERNAL_IP: ${REAL_EXTERNAL_IP:-}
|
|
ports:
|
|
- "3478:3478/udp"
|
|
- "3478:3478/tcp"
|
|
# Relay window — must match min-port/max-port in turnserver.conf.
|
|
- "49160-49200:49160-49200/udp"
|
|
volumes:
|
|
- ./database/coturn:/etc/coturn:ro,z
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD-SHELL",
|
|
"turnutils_stunclient -p 3478 127.0.0.1 >/dev/null 2>&1 || exit 1",
|
|
]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
networks:
|
|
- vtt-net
|
|
|
|
vtt-client:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.client
|
|
args:
|
|
# Vite bakes VITE_* vars at BUILD time — set these before
|
|
# `docker compose build vtt-client`. ICE servers for the WebRTC mesh
|
|
# (see client/src/render/webrtc_mesh.ts): unset/empty VITE_TURN_*
|
|
# ships public STUN only and symmetric-NAT pairs keep failing
|
|
# honestly (no fake success). When set, point at the `vtt-coturn`
|
|
# service above; credentials must be REST-API-shaped HMAC values
|
|
# minted from AUTH_SECRET / COTURN_TURN_SECRET. No hardcoded
|
|
# credentials anywhere.
|
|
VITE_PEERJS_URL: ${VITE_PEERJS_URL:-http://localhost:9000}
|
|
VITE_PEERJS_ROOM: ${VITE_PEERJS_ROOM:-aethertable-live}
|
|
VITE_YSYNC_WS_URL: ${VITE_YSYNC_WS_URL:-ws://localhost:6380}
|
|
VITE_TURN_URL: ${VITE_TURN_URL:-}
|
|
VITE_TURN_USERNAME: ${VITE_TURN_USERNAME:-}
|
|
VITE_TURN_CREDENTIAL: ${VITE_TURN_CREDENTIAL:-}
|
|
container_name: vtt-client
|
|
restart: unless-stopped
|
|
environment:
|
|
# Vite bakes VITE_* vars at BUILD time; export/set these before
|
|
# `docker compose build vtt-client`. Unset, the client falls back to
|
|
# http://localhost:9000 + room `aethertable-live` (webrtc_mesh.ts).
|
|
PEERJS_URL: ${PEERJS_URL:-http://vtt-peerjs:9000}
|
|
VITE_PEERJS_URL: ${VITE_PEERJS_URL:-http://localhost:9000}
|
|
VITE_PEERJS_ROOM: ${VITE_PEERJS_ROOM:-aethertable-live}
|
|
# Yjs CRDT relay (service `ysync-relay`, container_name `vtt-ysync`).
|
|
# Inside the compose network the browser client is NOT the dialer here:
|
|
# this default keeps a containerized build from silently pointing at
|
|
# ws://localhost:6380 (the host loopback, where no relay listens) and
|
|
# falling back to the legacy LWW transport on every containerized
|
|
# session. Set VITE_YSYNC_WS_URL to a host-reachable ws:// URL when the
|
|
# browser runs outside the compose network.
|
|
VITE_YSYNC_WS_URL: ${VITE_YSYNC_WS_URL:-ws://localhost:6380}
|
|
ports:
|
|
- "3000:3000"
|
|
depends_on:
|
|
vtt-engine:
|
|
condition: service_healthy
|
|
vtt-orchestrator:
|
|
condition: service_healthy
|
|
vtt-peerjs:
|
|
condition: service_healthy
|
|
ysync-relay:
|
|
condition: service_healthy
|
|
vtt-coturn:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/ || exit 1"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
networks:
|
|
- vtt-net
|
|
|
|
volumes:
|
|
pgdata:
|
|
redisdata:
|
|
neo4jdata:
|
|
qdrantdata:
|
|
ysyncdata:
|
|
|
|
networks:
|
|
vtt-net:
|
|
driver: bridge
|