Files
AetherTable/docker-compose.yml
T
John SmithandClaude c8cf376feb feat(infra): coturn TURN service + PeerJS ICE plumbing (iteration 26)
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]>
2026-08-25 01:32:30 -04:00

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