Ctrl K
ComunidadGuías…

docker-compose.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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, and the
# topology it has to survive CHANGED: the gateway no longer answers on the LAN at
# all. `make gateway-auth-mode` turned its own login off, so the reachability of
# 20128 is the whole control and it is published on loopback + docker0 only. The
# routable door is the SSO proxy in front of it, `:20128`, which exempts `/v1` for
# API clients. Pick the row that matches where the gateway runs:
#   on the gateway's host        OMNIROUTE_BASE_URL=http://127.0.0.1:20128/v1
#   on any OTHER host            OMNIROUTE_BASE_URL=http://<gateway host>:20128/v1
#                                (that host's LAN address — this is what `.50` uses:
#                                 http://192.168.1.71:20128/v1)
#   host-networked caller        OMNIROUTE_BASE_URL=http://127.0.0.1:20128/v1
#
# The old `10.10.2.1:20128` mesh form and `host.docker.internal:20128` are both
# dead ends now: the first is the ceremony of a binding that was deliberately
# removed, the second resolves to the caller's *own* docker0, where nothing
# listens (it was already silently wrong after the split, for n8n and dashboard-api
# on `.30` alike). `docs/gateway-sso.md` has the evidence.
#
# Nothing here picks the URL for a service: `OMNIROUTE_BASE_URL` is read from the
# same `.env` the host-side build runner uses. A caller on the same host as the
# gateway still needs `compose.host-gateway.yml` for the loopback *proxy* only if
# it dials `127.0.0.1:20128`; through the docker0 alias the bridge is enough.
# `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://192.168.1.71: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://192.168.1.71: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}
      # The port Studio listens on, and the ONLY place it is decided. The image
      # carries PORT=3001 for a bare `docker run`, so without this the knob below
      # would set the *published* port while the app stayed on 3001 — invisible on
      # the bridge network, where the mapping bridges the two, and a silent 502
      # under `compose.host-gateway.yml`, which discards published ports and so
      # leaves nothing listening on STUDIO_PORT at all (measured on .50: Studio
      # answered 3050 nowhere, while the edge forwarded there and every page
      # 502'd). `make studio-dev` already reads STUDIO_PORT this way, so both
      # modes now agree on what the number means.
      PORT: ${STUDIO_PORT:-3001}
    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}:${STUDIO_PORT:-3001}"
      - "${STUDIO_EDGE_HOST:-172.17.0.1}:${STUDIO_PORT:-3001}:${STUDIO_PORT:-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:'+(process.env.PORT||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.71) 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 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