forked from innotelinc/olympus
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
289 lines (281 loc) · 15.7 KB
/
Copy pathdocker-compose.yml
File metadata and controls
289 lines (281 loc) · 15.7 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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# Olympus — containerized factory (FactoryOps)
# Usage: cp .env.example .env && $EDITOR .env # TELEGRAM_BOT_TOKEN, OMNIROUTE_* etc.
# docker compose up --build -d # or: make docker-up
# Then: make docker-logs | make docker-app SPEC=build-requests/my-todo.md
#
# Builds/ is a named volume (factory output, gitignored); build-requests/ is
# bind-mounted so dropping a spec triggers the next `make docker-app` without rebuild.
# OmniRoute: the gateway exposes /healthz for liveness (200, unauthenticated).
# /health is NOT a route (404) despite appearing in older files here — do not
# probe it. Containers labelled `autoheal=true` are restarted by the autoheal
# service below when their healthcheck fails.
#
# THE GATEWAY IS A SHARED PLATFORM SERVICE — this file starts none. The single
# OmniRoute lives in Group 2 (`2-voice/`, mesh `10.10.2.1`; Consul service
# `omniroute`), because one gateway means one provider pool and one place to
# rotate. Olympus used to carry a second copy behind `profiles: ['gateway']`;
# it was removed once the shared one held the provider connections (see
# ips/docs/convergence-onyx-olympus-distro-atlas.md §4.4).
#
# `OMNIROUTE_BASE_URL` still has to survive the topology it is used from. READ THE
# CAVEAT: `host.docker.internal:20128` only works when the gateway is bound to a
# routable address, and a gateway published as 127.0.0.1:20128 (loopback-only) is
# NOT reachable from a bridge network — verified. Pick the row that matches:
# shared gateway over the mesh OMNIROUTE_BASE_URL=http://10.10.2.1:20128/v1 (default below)
# (OmniRoute serves its API and dashboard on that one port;
# the :20129 door is this stack's own SSO proxy, below)
# loopback-published on host run the services with network_mode: host, use http://127.0.0.1:20128/v1
# published on 0.0.0.0 OMNIROUTE_BASE_URL=http://host.docker.internal:20128/v1
#
# Nothing here picks the URL for a service: `OMNIROUTE_BASE_URL` is read from the
# same `.env` the host-side build runner uses. Whenever the gateway is published
# on THIS host's loopback, every service that talks to it runs with
# `compose.host-gateway.yml`. `make docker-studio-up` is how Studio gets there;
# starting Studio without it is silent — Studio resolves `127.0.0.1:20128` to
# itself and every generation fails ECONNREFUSED.
# Project name is pinned so container/volume names never depend on where this
# repo is checked out (the mesh puts it in 5-dev/olympus).
name: olympus
services:
olympus:
build:
context: .
dockerfile: Dockerfile
image: ghcr.io/innotelinc/olympus:local
container_name: olympus
restart: unless-stopped
# Autoheal restarts any container carrying this label when its healthcheck
# reports unhealthy. Requires a healthcheck to exist (see below).
labels:
- "autoheal=true"
env_file:
# Optional, because a render has to work without one: `make check-compose`
# validates these files against `.env.example` in a checkout that has no
# `.env`, and a missing env_file is an error rather than a warning. A
# deployment has one, and it is loaded exactly as before.
- path: .env
required: false
environment:
OMNIROUTE_BASE_URL: ${OMNIROUTE_BASE_URL:-http://${MESH_GATEWAY_HOST:-10.10.2.1}:20128/v1}
OMNIROUTE_API_KEY: ${OMNIROUTE_API_KEY:-}
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
SPEC: ${SPEC:-}
OPENAI_API_KEY: ${OMNIROUTE_API_KEY:-${OPENAI_API_KEY:-}}
volumes:
- olympus-builds:/app/builds
- ./build-requests:/app/build-requests:ro
# Persist the factory token across restarts when using env_file .env
- ./.factory-env:/app/.factory-env
# NO PUBLISHED PORT, deliberately, and this is a fix rather than an omission.
# This service used to publish ${OLYMPUS_PORT:-20128}:20128 — the same host port
# the gateway service below owns — because the image can start a *bundled*
# gateway. It only ever worked because this container runs host-networked here
# (its ports are discarded); on a bridge run the two services fight for 20128,
# and if this one wins, the build runner and the SSO proxy reach an empty
# gateway with no provider connections. Nothing outside talks to this container:
# `make docker-app` uses `docker compose exec`, not the network. One OmniRoute.
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
# The gateway has no /health route (it answers 404), so doctor.py is the signal.
# Match the status line exactly — a bare `grep -i ok` would match the per-check
# '[ ok ]' rows and could never fail.
test: ["CMD-SHELL", "python3 factory/doctor.py 2>/dev/null | grep -q 'Status: READY' || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40s
# Studio — the vibe-coding web UI (web/studio). Talks to the gateway
# server-side; the key never reaches the browser. Build context is the app
# directory so the image carries no host node_modules.
studio:
build:
context: ./web/studio
dockerfile: Dockerfile
image: ghcr.io/innotelinc/olympus-studio:local
container_name: olympus-studio
restart: unless-stopped
labels:
- "autoheal=true"
env_file:
# Optional, because a render has to work without one: `make check-compose`
# validates these files against `.env.example` in a checkout that has no
# `.env`, and a missing env_file is an error rather than a warning. A
# deployment has one, and it is loaded exactly as before.
- path: .env
required: false
# See the OmniRoute caveat at the top of this file before changing the base URL.
environment:
OMNIROUTE_BASE_URL: ${OMNIROUTE_BASE_URL:-http://${MESH_GATEWAY_HOST:-10.10.2.1}:20128/v1}
OMNIROUTE_API_KEY: ${OMNIROUTE_API_KEY:-}
OMNIROUTE_MODEL: ${OMNIROUTE_MODEL:-auto/coding}
STUDIO_ACCESS_TOKEN: ${STUDIO_ACCESS_TOKEN:-}
OIDC_ISSUER_URL: ${OIDC_ISSUER_URL:-}
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
OIDC_REDIRECT_URI: ${OIDC_REDIRECT_URI:-}
OIDC_SCOPES: ${OIDC_SCOPES:-openid email profile}
# Group allow-list. Empty = any authenticated user; set = members of a
# listed group only, everyone else gets 403.
OIDC_ALLOWED_GROUPS: ${OIDC_ALLOWED_GROUPS:-}
STUDIO_SESSION_SECRET: ${STUDIO_SESSION_SECRET:-}
# Tenancy (Distro's control plane — convergence §5.2). With these set, a
# turn is spent on the signed-in user's own gateway key, quota is checked
# before it and usage recorded after it; with them empty Studio falls back
# to the single shared OMNIROUTE_API_KEY above, exactly as before.
CONTROL_PLANE_INTERNAL_URL: ${CONTROL_PLANE_INTERNAL_URL:-}
CONTROL_INTERNAL_TOKEN: ${CONTROL_INTERNAL_TOKEN:-}
# Saved-app library. Without this volume the store lives in the container
# and every app disappears on the next image build.
STUDIO_DATA_DIR: ${STUDIO_DATA_DIR:-/app/data/studio}
# Where "Build it" writes a request for the host-side runner. Studio cannot
# run `make app` itself — this image carries no Archon CLI, no Codex CLI and
# no checkout — so it drops a small request file here and the runner
# (scripts/build-runner.py, olympus-build-runner.service) picks it up where
# the toolchain actually is. The runner re-validates every field.
STUDIO_BUILD_QUEUE_DIR: ${STUDIO_BUILD_QUEUE_DIR:-/app/build-queue}
# Where packaged website builds land (`builds/<slug>/site.zip`, written by
# scripts/package-website.py). Read-only and optional: with it mounted, the
# download button serves the exact archive the publish step staged; without
# it, Studio falls back to zipping the saved source itself.
STUDIO_BUILDS_DIR: ${STUDIO_BUILDS_DIR:-/app/builds}
volumes:
- studio-data:/app/data
# The packaged builds, read-only. Only the archive route reads this.
- ./builds:/app/builds:ro
# The factory handoff. "Export to factory" writes a spec into this
# directory, which is the repo's build-requests/ — the same place
# `make app SPEC=…` reads and the CI builder watches on push. Studio runs
# as uid 1001 (the image drops privileges), so the host directory has to
# be writable by that uid: `chown 1001:1001 build-requests` once. If it is
# not, the export fails with a message naming the fix rather than a 500.
- ./build-requests:/app/build-requests
# The build queue the runner watches. Root-owned here means Studio can
# neither queue nor see a heartbeat; `make studio-build-queue-dir` (or
# scripts/studio-export-dir.sh) settles it for uid 1001.
- ./.factory/build-queue:/app/build-queue
# Not published on every interface. Studio was on 0.0.0.0, so anyone on the
# LAN could reach the app directly and skip the edge — and with it the
# sign-in the edge performs (docs/gateway-sso.md). The edge is a container on
# its own bridge, so it cannot reach a loopback-only port; it dials the
# docker0 gateway instead, which is the same pattern the DNS console uses.
# STUDIO_EDGE_HOST is the address the NPM proxy hosts must point at.
ports:
- "127.0.0.1:${STUDIO_PORT:-3001}:3001"
- "${STUDIO_EDGE_HOST:-172.17.0.1}:${STUDIO_PORT:-3001}:3001"
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
# Liveness only: assert Studio answers its own root and never follow the
# redirect. Following it walks straight into the OIDC login chain, so the
# check would then depend on Authentik being up AND on the loopback
# callback URI being registered in the provider — neither of which should
# decide whether this container is healthy. It did report unhealthy for
# exactly that reason once Studio began deriving the callback per host.
# 2xx = running with auth disabled; 3xx = running, redirecting to login.
test:
- CMD-SHELL
- node -e "fetch('http://127.0.0.1:3001/',{redirect:'manual'}).then(r=>process.exit(r.status>=200&&r.status<400?0:1)).catch(()=>process.exit(1))"
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
# Static sites — what Studio's "Build & publish" serves (opt-in, profile `sites`).
#
# `scripts/package-website.py --publish` stages a built site under
# `OLYMPUS_SITES_ROOT` (default /var/lib/olympus/sites); this serves that tree so
# the staged copy is reachable without anyone installing a web server by hand.
#
# Host networking for the same reason the SSO proxy uses it: the NPM edge lives
# on another host (192.168.1.46) and has to reach back to a LAN address, and
# `network_mode: host` is what makes that address exist. It binds ONE port —
# ${SITE_PORT:-20130} — on every interface, so a site staged here is readable by
# anything that can route to this host. That is the intent: a published site is
# meant to be public. Nothing else about this container is exposed; it has no
# shell access to the repo and the sites tree is mounted read-only.
#
# The site is chosen by HOSTNAME (`<slug>.<SITE_HOST_SUFFIX>`) and, for direct
# address access, by path (`/<slug>/`). See deploy/nginx-sites.conf.template.
#
# This is a plain static file server with no auth. Sites published here are open
# to the LAN on purpose, and the name they answer on is what makes them public.
# If a site needs to be private, do not publish it — Studio's zip download is the
# delivery path for that.
sites:
image: nginx:1.27-alpine
container_name: olympus-sites
profiles: ["sites"]
restart: unless-stopped
network_mode: host
environment:
# One substitution only — see the header of the template. Without the filter,
# envsubst would blank nginx's own `$uri` in try_files.
SITE_PORT: ${SITE_PORT:-20130}
# The leftmost label of a published name is the site's directory, so the
# template needs this too. Both are in the filter and nothing else is —
# an unfiltered envsubst would blank nginx's own `$uri` and `$site`.
SITE_HOST_SUFFIX: ${SITE_HOST_SUFFIX:-studio.olympus.innotel.us}
NGINX_ENVSUBST_FILTER: SITE_PORT|SITE_HOST_SUFFIX
volumes:
- ${OLYMPUS_SITES_ROOT:-/var/lib/olympus/sites}:/srv/sites:ro
# One generated `server` block per running application, written here by
# scripts/app-runtime.py and included from the template below.
#
# This is how an app gets a name without the edge config growing: the block's
# `server_name` is exact, and nginx prefers an exact name over the template's
# regex, so `<slug>.<suffix>` reaches the app's container while everything
# else still falls through to the static tree. `nginx -s reload` picks up a new
# file; nothing else has to change.
- ${OLYMPUS_APPS_ROOT:-/var/lib/olympus/apps}/nginx:/etc/nginx/app-conf.d:ro
# Named `default.conf.template` on purpose: the image's entrypoint renders
# templates to /etc/nginx/conf.d/<same name>, so this replaces the stock
# default.conf instead of sitting beside it. Laid down under any other name
# nginx would keep serving :80 as well.
- ./deploy/nginx-sites.conf.template:/etc/nginx/templates/default.conf.template:ro
# The OmniRoute gateway is NOT here. It is the shared platform service in
# Group 2 (`2-voice/`), one provider pool for every platform; the `omniroute`
# service and the `omniroute-data` volume that used to sit at this point were
# removed in ips/docs/convergence-onyx-olympus-distro-atlas.md §4.4.
#
# The gateway's STATE (provider connections, settings, the dashboard password
# hash, and `server.env`, whose STORAGE_ENCRYPTION_KEY decrypts them) is one
# volume on the gateway's host. `make gateway-vault-backup` / `-check` /
# `-restore` copy it to Cerulean Vault and must be run THERE, not here —
# scripts/omniroute-vault-backup.py asks Docker for the container's own mount,
# so it needs a Docker that can see the gateway (`--container g2-omniroute`).
# `make gateway-sso-up` / `gateway-edge` / `gateway-auth-mode` belong on that
# host too: the SSO proxy forwards to the gateway's loopback binding, so the
# dashboard gate has to run beside the dashboard. See docs/gateway-sso.md.
# Autoheal — restarts containers whose healthcheck reports "unhealthy".
#
# Only containers labelled `autoheal=true` are touched. A container in another
# compose project is out of reach here — this stack cannot label containers it
# does not own — so the label and an autoheal service must be added to THAT
# project instead. (Done for the external OmniRoute gateway this stack
# defaults to: it now carries the label and its own autoheal service.)
#
# Mounting the Docker socket grants this container control over the daemon.
# That is inherent to autoheal; scope it by keeping the socket read-mostly and
# the label explicit rather than relying on defaults.
# Pinned by digest because it holds the Docker socket — a silent upstream
# retag would swap code that can restart anything on this host.
# Tag at time of pinning: willfarrell/autoheal:latest
autoheal:
image: willfarrell/autoheal@sha256:a75fcda3176f6b3b70b75cc72fb1979ed86183fc45daacf1a2647902dc44916a
container_name: olympus-autoheal
restart: unless-stopped
environment:
AUTOHEAL_CONTAINER_LABEL: autoheal
AUTOHEAL_INTERVAL: "15"
AUTOHEAL_START_PERIOD: "60"
AUTOHEAL_DEFAULT_STOP_TIMEOUT: "20"
DOCKER_SOCK: /var/run/docker.sock
volumes:
- /var/run/docker.sock:/var/run/docker.sock
volumes:
olympus-builds:
# Studio's saved apps — per Authentik identity, one JSON file each.
studio-data:
networks:
default:
name: olympus-net