-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
271 lines (261 loc) · 10.5 KB
/
Copy pathdocker-compose.yml
File metadata and controls
271 lines (261 loc) · 10.5 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
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
# Default `docker compose up -d` runs the infra (db, redis, grafana, loki, alloy);
# run the app on the host with `go run ./cmd/gateway`.
# `docker compose --profile app up -d --build` also builds+runs the gateway and
# migrations as containers, so their JSON logs flow through Alloy into Loki.
#
# Only the gateway publishes a host port. Everything else talks over the compose
# network by service name, which needs no `ports`/`expose` at all. To reach
# db/redis/nats/grafana from the host (host-run gateway, psql, Grafana UI):
# docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# There is no environment block. Every value comes from `internal/config/config.dev.yml` — one
# grouped, fully-required document — and the only thing the environment still decides is where that
# file is. `gateway-dev` bind-mounts the source, so it reads the file at its default path; the images
# built from the `runtime` stage carry no source, so they get it mounted at /config.yml.
#
# That is also why nothing here interpolates `${...}` with a fallback any more: a default in this file
# was a second place a value could come from, and the one that wins was whichever was less obvious.
services:
db:
image: timescale/timescaledb-ha:pg18
environment:
POSTGRES_DB: shopnexus
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- db-data:/home/postgres/pgdata/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d shopnexus"]
interval: 5s
timeout: 3s
retries: 10
ports:
- 5002:5432
redis:
image: redis:7
command: ["redis-server", "--requirepass", "app"]
nats:
# JetStream buffers observability telemetry between the Sink (publisher) and
# the writer (batching consumer), so a slow/down database never blocks a
# request and queued samples survive a restart. -m serves monitoring on 8222.
image: nats:2.10-alpine
command: ["-js", "-sd", "/data", "-m", "8222"]
volumes:
- nats-data:/data
# --- Observability: metrics (Postgres/Timescale) + logs (Loki), one Grafana ---
grafana:
# Datasources (TimescaleDB + Loki) and dashboards are provisioned from files
# (dev/grafana), so no manual setup.
image: grafana/grafana:11.3.0
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer
volumes:
- ./dev/grafana/provisioning:/etc/grafana/provisioning
- ./dev/grafana/dashboards:/var/lib/grafana/dashboards
- grafana-data:/var/lib/grafana
depends_on:
- db
- loki
loki:
# Single-binary Loki (filesystem storage) using the image's default config.
image: grafana/loki:3.3.0
command: ["-config.file=/etc/loki/local-config.yaml"]
volumes:
- loki-data:/loki
alloy:
# Tails every container's stdout (Docker API) and ships JSON logs to Loki,
# labeling each line with its compose `service`.
image: grafana/alloy:v1.5.1
command:
- "run"
- "--server.http.listen-addr=0.0.0.0:12345"
- "--storage.path=/var/lib/alloy/data"
- "/etc/alloy/config.alloy"
volumes:
- ./dev/alloy/config.alloy:/etc/alloy/config.alloy:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- alloy-data:/var/lib/alloy/data
depends_on:
- loki
# --- App (opt-in: `--profile app`), so its logs reach Loki via Alloy ---
# Also runs under `--profile dev` and `--profile embed`, since both need a migrated database.
migrate:
profiles: ["app", "dev", "embed"]
build: .
# entrypoint, not command: the image's ENTRYPOINT is /gateway, so a command
# would be passed to the gateway as arguments instead of replacing it.
entrypoint: ["/migrate"]
environment:
CONFIG_FILE: /config.yml
volumes:
- ./internal/config/config.dev.yml:/config.yml:ro
depends_on:
db:
condition: service_healthy
restart: "no"
gateway:
profiles: ["app"]
build: .
environment:
CONFIG_FILE: /config.yml
volumes:
- ./internal/config/config.dev.yml:/config.yml:ro
# `storage.provider: local` keeps objects here, so they outlive the container. With a real
# store this volume goes away and the bytes never reach the process at all.
- object-data:/data/objects
ports:
- "5000:8080"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
nats:
condition: service_started
migrate:
condition: service_completed_successfully
# --- Demo data (opt-in: `--profile seed`) ---
# Run it, do not `up` it:
#
# docker compose --profile seed run --rm seed # load
# docker compose --profile seed run --rm seed -wipe -yes-i-mean-it # remove again
#
# It has to run here rather than on the host for two reasons. The DSNs in config.dev.yml name
# `db`, which only resolves inside this network; and the product photographs it draws have to
# land in `object-data`, the volume the gateway serves `local` objects from — a host-run seeder
# would write them onto the host's disk where nothing can read them back.
#
# It writes only its own demo data and never the bootstrap rows: no option registry, no
# category is ever deleted, and the three accounts the report signs in as are never removed.
seed:
profiles: ["seed"]
build: .
# Root for the same reason `mockspec` is: the named volume starts root-owned and the image
# is nonroot, so a nonroot seeder cannot create the `seed/` directory it draws into. The
# files it writes are world-readable, which is all the gateway needs to serve them.
user: "0:0"
entrypoint: ["/seed"]
environment:
CONFIG_FILE: /config.yml
volumes:
- ./internal/config/config.dev.yml:/config.yml:ro
- object-data:/data/objects
depends_on:
db:
condition: service_healthy
restart: "no"
# --- Embedding worker (opt-in: `--profile embed`) ---
# Drains catalog's three stale queues into their vector tables. Its own service and not a
# goroutine in the gateway: a pass is a batch of model inferences, and the process serving
# requests should not be holding that. Opt-in to run, but not optional to have: retrieval is
# bge-m3 dense+sparse only, so a listing nothing here embedded is a listing no search finds.
#
# With `embedding.provider: mock` it needs nothing else running. Point it at the real BGE-M3
# service and set the provider to get vectors that mean something.
embedder:
profiles: ["dev", "embed"]
build: .
entrypoint: ["/embedder"]
environment:
CONFIG_FILE: /config.yml
volumes:
- ./internal/config/config.dev.yml:/config.yml:ro
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully
restart: unless-stopped
# --- Hot reload (opt-in: `--profile dev`) ---
# air rebuilds on save inside the container, so logs still reach Loki via Alloy —
# the one thing running `go run ./cmd/gateway` on the host does not give you.
# Shares host port 5000 with `gateway`: the two profiles are alternatives, not
# meant to run together.
gateway-dev:
profiles: ["dev"]
build:
context: .
target: dev
# No CONFIG_FILE: the source is bind-mounted at /src, which is the working directory, so the
# default `internal/config/config.dev.yml` resolves to the file you are editing.
ports:
- "5000:8080"
volumes:
- .:/src
# Caches outside the bind mount, so a rebuild does not start from scratch.
- go-mod-cache:/go/pkg/mod
- go-build-cache:/root/.cache/go-build
- object-data:/data/objects
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
nats:
condition: service_started
migrate:
condition: service_completed_successfully
# --- Durable execution (opt-in: `--profile restate`) ---
# The runtime that holds the timers. It invokes the handlers the gateway serves on
# :9080, so it has to be told where they are once:
#
# docker compose --profile restate exec restate restate deployments register http://gateway:9080
#
# Only useful with WORKFLOW_RUNTIME=restate in the env above; with `off` the gateway
# serves no handlers and this container would have nothing to invoke.
restate:
profiles: ["restate"]
# Pinned at 1.7: sdk-go 1.0 refuses to talk to a server older than 1.3, so a "latest
# stable-looking" tag below that fails at discovery rather than at startup.
image: restatedev/restate:1.7
volumes:
- restate-data:/restate-data
# --- Mock API (opt-in: `docker compose --profile mock up -d --build mock`) ---
# Naming the service keeps it to these two containers; a bare `--profile mock up`
# would also start the infra, which the mock has no use for.
# Prism answers the whole contract from api/openapi.gen.yaml on :4010, so a client
# can be built against a route before the handler behind it exists. It validates
# the request and the security requirement, so a bad body still gets the real
# error envelope. Needs no database, no Redis and no migrations.
mockspec:
profiles: ["mock"]
build: .
# Prism mounts `paths` as written and ignores the relative servers[0].url, so
# the spec is rewritten with the /api/v1 prefix before it is served. Runs as
# root because the named volume starts root-owned and the image is nonroot.
user: "0:0"
entrypoint: ["/mockspec"]
command: ["/spec/openapi.mock.yaml"]
volumes:
- mock-spec:/spec
restart: "no"
mock:
profiles: ["mock"]
image: stoplight/prism:5
# Serves the spec's `example`s, so a body is realistic and the same every time —
# what a client being written against it wants. For variety instead, add
# `-d --json-schema-faker-fillProperties false`: json-schema-faker then builds each
# body from the schema, which respects bounds and patterns but invents lorem text
# and 1904 timestamps. `-m false` is not optional: the image crashes on startup
# with multiprocess left on.
command: ["mock", "-h", "0.0.0.0", "-p", "4010", "-m", "false", "/spec/openapi.mock.yaml"]
ports:
- "5003:4010"
volumes:
- mock-spec:/spec
depends_on:
mockspec:
condition: service_completed_successfully
volumes:
object-data:
restate-data:
db-data:
nats-data:
mock-spec:
grafana-data:
loki-data:
alloy-data:
go-mod-cache:
go-build-cache: