-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
192 lines (186 loc) · 7.81 KB
/
Copy pathdocker-compose.yml
File metadata and controls
192 lines (186 loc) · 7.81 KB
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
# Self-contained agent stack: Mastra (server + Studio) + Postgres/pgvector + Dolt.
# One `docker compose up` brings up a complete, portable voice agent. Deploys as
# a single Coolify "Docker Compose" resource (only `mastra` gets a public domain;
# postgres + dolt stay private on the internal network).
#
# Secrets come from .env / Coolify env (never commit them):
# APP_SECRET, POSTGRES_PASSWORD, DOLT_PASSWORD, and an LLM key
# (ANTHROPIC_API_KEY / OPENAI_API_KEY). See .env.example.
# Shared build CONTEXT for the two app services. One Dockerfile, but two images:
# each service builds the stage that carries only the runtime it executes, so
# neither ships the other's ~600-840 MB. They share the base layers and the baked
# models, so the second build is nearly free.
#
# `target` is therefore per-service and MANDATORY — it is no longer a safety net
# against stage reordering but the thing that decides what is in the image. A
# service pointed at the wrong stage will not start: the server stage has no
# `src/`, and the agent stage has no `.mastra/output`.
x-app-build: &app-build
context: .
dockerfile: Dockerfile
services:
mastra:
build:
<<: *app-build
target: runtime
image: mastra-voice:latest
restart: unless-stopped
# Only expose on the internal network — Coolify/Traefik routes the public
# domain to this service. Do NOT bind a host port here: binding 4111:4111
# collides ("address already in use") whenever another agent stack is also
# deployed on the same host, and it bypasses Traefik so FQDN routing and
# auto-SSL never apply. Local host access is via docker-compose.override.yml.
#
# `container_name: mastra` removed for the same reason: container names are
# global to the Docker host, not namespaced per compose project, so a second
# stack claiming `mastra` collides even once the ports are correct. Services
# still reach each other by SERVICE name (mastra, worker, postgres, dolt).
expose:
- "4111"
env_file:
- .env
environment:
# The agent's instructions bake in the current date and time from the
# container clock, and container clocks are UTC — so an agent answering a US
# phone line will confidently say the wrong DAY for most of the evening.
# Set TZ to the timezone the business actually operates in.
- TZ=${TZ:-UTC}
- NODE_ENV=production
# Mastra memory + RAG → the postgres service (private, internal network)
- SUPABASE_DB_URL=postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
# Versioned business data → the dolt service (private, internal network)
- DOLT_HOST=dolt
- DOLT_PORT=3306
- DOLT_USER=root
- DOLT_PASSWORD=${DOLT_PASSWORD}
- DOLT_DATABASE=${DOLT_DATABASE:-appdb}
depends_on:
postgres:
condition: service_healthy
dolt:
condition: service_healthy
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
reservations:
memory: 256M
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:4111/health"]
interval: 30s
timeout: 5s
start_period: 20s
retries: 3
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# LiveKit worker — the realtime audio loop. Its OWN image (the `agent` stage),
# carrying the source tree and node_modules the server image no longer holds. It
# connects OUTBOUND to LiveKit (no inbound ports, no public domain) and runs from
# source via tsx. Keep >=1 always-on: with zero workers a call connects to
# silence, and scale-to-zero deregisters it from LiveKit. Only ONE worker flavor
# can run at a time (they all register under the same agentName).
worker:
build:
<<: *app-build
target: agent
image: mastra-voice-worker:latest
# No container_name — same host-global collision reason as the mastra service.
restart: unless-stopped
# No `command:` override — the agent stage's CMD is already the worker. That is
# the point of the stage: LiveKit Cloud Agents cannot override a command, so the
# image has to be correct on its own, and compose gets to use the same one.
env_file:
- .env
environment:
# The agent's instructions bake in the current date and time from the
# container clock, and container clocks are UTC — so an agent answering a US
# phone line will confidently say the wrong DAY for most of the evening.
# Set TZ to the timezone the business actually operates in.
- TZ=${TZ:-UTC}
- NODE_ENV=production
# Same stores as the server — both processes write concurrently (Postgres
# handles that; a single-writer store would not).
- SUPABASE_DB_URL=postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
- DOLT_HOST=dolt
- DOLT_PORT=3306
- DOLT_USER=root
- DOLT_PASSWORD=${DOLT_PASSWORD}
- DOLT_DATABASE=${DOLT_DATABASE:-appdb}
depends_on:
postgres:
condition: service_healthy
dolt:
condition: service_healthy
# The worker does have an HTTP endpoint, just not the server's: @livekit/agents
# serves a health check on 8081 (`/` → 200 healthy, 503 unhealthy). Probe that
# instead of the image's :4111/health, which this container never serves.
#
# It reports 503 for `inference process not running` (voice-a97), and that is
# the failure worth catching: the shared inference process hosts the turn
# detector, and when it is killed nothing restarts it — so end-of-turn stops
# being detected and EVERY later call sits in silence while the worker happily
# keeps registering and accepting jobs. Nothing else surfaces that state.
#
# NOTE: plain `docker compose` marks the container unhealthy but will NOT
# restart it — `restart: unless-stopped` only reacts to the process exiting.
# Acting on health needs an orchestrator that does (Swarm, Kubernetes, ECS,
# Coolify) or a watchdog. Probing is still the prerequisite for all of them.
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8081/"]
interval: 30s
timeout: 5s
start_period: 60s
retries: 3
deploy:
resources:
limits:
# Heavier than the server: the main process (~2GB, fastembed/onnx) plus
# one prewarmed idle runner (numIdleProcesses=1, another ~2GB) plus active
# job runners during calls. Give it real headroom on a box that can spare it.
memory: 4G
reservations:
memory: 2G
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
postgres:
image: pgvector/pgvector:pg16
restart: unless-stopped
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=postgres
volumes:
- pgdata:/var/lib/postgresql/data
# enables the `vector` extension on first init
- ./docker/postgres-init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 10
# no published port → reachable only as `postgres` on the internal network
dolt:
image: dolthub/dolt-sql-server:latest
restart: unless-stopped
environment:
# allow root from any host on the internal network (default is localhost-only,
# which denies the mastra container). Applied on first init only.
- DOLT_ROOT_HOST=%
- DOLT_ROOT_PASSWORD=${DOLT_PASSWORD}
volumes:
- doltdata:/var/lib/dolt # versioned DB + full commit history (persist!)
healthcheck:
test: ["CMD-SHELL", "dolt sql -q 'SELECT 1' >/dev/null 2>&1 || exit 1"]
interval: 10s
timeout: 5s
retries: 10
# no published port → reachable only as `dolt` on the internal network
volumes:
pgdata: {}
doltdata: {}