Ctrl K
ComunidadGuías…

docker-compose.yml

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

Archivo original ↗Preguntar a KiBot
# Managoat, self-hosted.
#
#   cp .env.compose.example .env    # then fill in the generated keys
#   docker compose up -d
#   open http://localhost:4000
#
# For local development you only want the database, and `docker compose up -d
# postgres` still does exactly that — `mix setup` creates fountain_dev itself.
# See SETUP.md.
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
      POSTGRES_DB: fountain
    ports:
      # Host-side convenience only (psql, and the database-only dev path) —
      # the app reaches Postgres over the compose network. Parameterised
      # because a machine evaluating Managoat usually already runs its own
      # Postgres on 5432 (#549).
      - "${POSTGRES_HOST_PORT:-5432}:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      # The app runs migrations at boot, so it must not start against a
      # database that is still initialising.
      test: ["CMD-SHELL", "pg_isready -U postgres -d fountain"]
      interval: 5s
      timeout: 5s
      retries: 10

  app:
    # The fallback tag is a pinned release, deliberately not `latest`:
    # `latest` moves on every merge to main, across breaking minors, with
    # migrations applied at boot and downgrade unsupported — the exact
    # combination the versioning guide tells you never to run. Upgrade by
    # setting MANAGOAT_IMAGE_TAG in .env (see .env.compose.example). An .env
    # from before ADR 0082 that sets FOUNTAIN_IMAGE_TAG still works.
    image: ghcr.io/managoat/managoat:${MANAGOAT_IMAGE_TAG:-${FOUNTAIN_IMAGE_TAG:-v0.39.2}}
    # To run your own build instead of the published image, comment the line
    # above and uncomment this:
    #   build: .
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - "${PORT:-4000}:4000"
      # The egress broker. Sandboxes dial it at BROKER_PROXY_URL, so this
      # port has to be reachable from the sandbox provider's network.
      - "${BROKER_PORT:-14322}:14322"
    environment:
      PHX_SERVER: "true"
      PORT: "4000"

      DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:-postgres}@postgres:5432/fountain
      # A stock postgres image does not serve TLS, and this was hardcoded on —
      # which is why a compose setup could not have worked before.
      DATABASE_SSL: "false"
      # Only meaningful against an external Postgres that serves TLS (which
      # also means editing DATABASE_URL and DATABASE_SSL above): verify its
      # certificate instead of the default encrypted-but-unauthenticated mode.
      DATABASE_SSL_VERIFY: ${DATABASE_SSL_VERIFY:-}
      DATABASE_SSL_CA_FILE: ${DATABASE_SSL_CA_FILE:-}

      # Absolute and scheme-ful: it builds the links in verification emails and
      # is handed to every sprite as MANAGOAT_BASE_URL. Change it if you reach
      # this instance by anything other than localhost.
      PUBLIC_URL: ${PUBLIC_URL:-http://localhost:4000}

      # Behind a reverse proxy, set this to the proxy's address range before
      # exposing the instance — otherwise per-IP rate limits collapse into a
      # single bucket keyed on the proxy's address.
      TRUSTED_PROXIES: ${TRUSTED_PROXIES:-}

      # Required, but enforced by the app at boot (config/runtime.exs raises
      # a descriptive error for either one missing or empty) rather than by a
      # compose `:?` marker: compose interpolates this whole file at load
      # time regardless of which service is targeted, so a hard requirement
      # here made even `docker compose up -d postgres` — the documented
      # database-only dev path — abort on a fresh clone (#392).
      SECRET_KEY_BASE: ${SECRET_KEY_BASE:-}
      MASTER_SECRETS_KEY: ${MASTER_SECRETS_KEY:-}

      # Conversations need a Sprites token. The app boots without one; every
      # attempt to start a conversation then fails.
      SPRITES_TOKEN: ${SPRITES_TOKEN:-}
      SPRITES_BASE_URL: ${SPRITES_BASE_URL:-}
      SANDBOX_PROVIDER: ${SANDBOX_PROVIDER:-}
      E2B_API_KEY: ${E2B_API_KEY:-}
      E2B_BASE_URL: ${E2B_BASE_URL:-}
      E2B_TEMPLATE: ${E2B_TEMPLATE:-}
      E2B_USER: ${E2B_USER:-}

      # The browser apps on top of this server's /api. The console is not an
      # interactive client: watching a conversation lives in a separate
      # single-page app, and CONVERSATIONS_APP_URL already defaults to the
      # hosted build. That default links
      # somewhere real, so a deployment that does not admit its origin in
      # API_CORS_ORIGINS shows working links that fail CORS in the reader's
      # browser with nothing in the server log to explain it (#1215).
      #
      # These three are written with no value on purpose. That form passes the
      # variable through only when .env actually sets it, which is the only
      # way to keep all three states the app distinguishes: unset keeps the
      # app's own default, `CONVERSATIONS_APP_URL=` passes "" and means this
      # deployment has no such app, and a URL points at your own build.
      # `${VAR:-}` cannot express the first state — it would send "" for an
      # absent key and silently retire the hosted app.
      API_CORS_ORIGINS:
      OAUTH_CLIENTS:
      CONVERSATIONS_APP_URL:

      # Empty means the app's defaults apply. Advertised in
      # .env.compose.example; a key listed there but not passed through here
      # is silently ignored, which is what the drift test now refuses.
      CHECK_ORIGIN_EXTRA: ${CHECK_ORIGIN_EXTRA:-}
      SANDBOX_IDLE_TIMEOUT_MINUTES: ${SANDBOX_IDLE_TIMEOUT_MINUTES:-}
      SANDBOX_MAX_LIFETIME_HOURS: ${SANDBOX_MAX_LIFETIME_HOURS:-}

      # Mail is off until SMTP_HOST is set, so a first run needs nothing
      # external. Accounts self-verify at registration while it is off
      # (ADR 0011); set SMTP_HOST before you invite anyone.
      SMTP_HOST: ${SMTP_HOST:-}
      SMTP_PORT: ${SMTP_PORT:-587}
      SMTP_USERNAME: ${SMTP_USERNAME:-}
      SMTP_PASSWORD: ${SMTP_PASSWORD:-}
      # Deliberately blank, not defaulted: with SMTP_HOST set the app
      # refuses to boot until EMAIL_FROM names an address on a domain your
      # provider is verified for. With mail off it is unused.
      EMAIL_FROM: ${EMAIL_FROM:-}

      # Blank means auto (ADR 0011): open so the first account can be
      # created, then closed to self-registration once an admin exists.
      # "true" keeps it open for everyone, for good; "false" closes it now.
      REGISTRATION_ENABLED: ${REGISTRATION_ENABLED:-}

      # Error tracking. Off unless set — nothing leaves the instance.
      SENTRY_DSN: ${SENTRY_DSN:-}

      # The first account to verify becomes the admin (ADR 0011), so
      # register before exposing the instance. Images after v0.39.2 do this
      # unconditionally and ignore FIRST_USER_ADMIN; up to v0.39.2 it is the
      # switch. Drop it once the pin is past v0.39.2.
      FIRST_USER_ADMIN: ${FIRST_USER_ADMIN:-true}

      # ── The credential broker ───────────────────────────────────────────
      #
      # Every conversation is brokered (ADR 0019): the real token never
      # enters the sandbox. The proxy listens on 14322 inside the container,
      # published above. BROKER_PROXY_URL is required, and enforced by the
      # app at boot for the same reason as SECRET_KEY_BASE: it is the
      # address a sandbox dials, through an ingress the sandbox provider can
      # reach (a public IP, Tailscale Funnel, an ngrok TCP tunnel). See
      # docs/configuration.md.
      BROKER_LISTEN_PORT: "14322"
      BROKER_PROXY_URL: ${BROKER_PROXY_URL:-}
      BROKER_LOG_RETENTION_HOURS: ${BROKER_LOG_RETENTION_HOURS:-}

      # There is no OTLP collector in a default compose setup, and without this
      # the exporter retries per span and floods the logs.
      OTEL_TRACES_EXPORTER: "none"
    healthcheck:
      # Probes the documented /health endpoint, so `docker compose ps` flags a
      # hung-but-running app instead of reporting it healthy forever. The
      # runtime image (debian slim) ships neither curl nor wget, so this does
      # the HTTP round-trip with bash's /dev/tcp — bash, printf and grep are
      # all in the base image.
      test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/4000 && printf 'GET /health HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n' >&3 && head -n1 <&3 | grep -q ' 200 '"]
      interval: 30s
      timeout: 5s
      retries: 3
      # Boot runs migrations before the endpoint listens; don't count failures
      # against the container while that is still happening.
      start_period: 40s
    restart: unless-stopped

  # Nightly pg_dump into the backup_data volume, pruned after
  # BACKUP_RETENTION_DAYS. Off unless you opt in:
  #
  #   docker compose --profile backup up -d
  #
  # The volume lives on the same host as the database, so this protects
  # against bad migrations and fat fingers, not a dead machine — copy dumps
  # off-host too, and back up MASTER_SECRETS_KEY separately: a database
  # backup alone cannot decrypt itself. Restore drill:
  # https://managoat.com/docs/guides/operate/back-up-and-restore
  backup:
    image: postgres:16
    profiles: ["backup"]
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      PGHOST: postgres
      PGUSER: postgres
      PGPASSWORD: ${POSTGRES_PASSWORD:-postgres}
      PGDATABASE: fountain
      BACKUP_INTERVAL_SECONDS: ${BACKUP_INTERVAL_SECONDS:-86400}
      BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-14}
    entrypoint: ["/bin/bash", "-c"]
    command:
      # A failed dump logs, deletes its partial file, and leaves every old
      # backup alone — pruning happens only after a successful dump, so a
      # broken database can never age out the backups you would restore from.
      - |
        while true; do
          TS="$$(date -u +%Y-%m-%dT%H-%M-%SZ)"
          OUT="/backups/fountain-$${TS}.dump"
          echo "Dumping to $${OUT}..."
          if pg_dump -Fc -Z6 --no-owner --no-privileges -f "$$OUT" \
             && [ "$$(stat -c %s "$$OUT")" -ge 10240 ]; then
            echo "Backup complete: $${OUT} ($$(stat -c %s "$$OUT") bytes)"
            find /backups -name 'fountain-*.dump' -mtime "+$${BACKUP_RETENTION_DAYS}" -delete
          else
            echo "FAIL: dump did not complete; keeping old backups"
            rm -f "$$OUT"
          fi
          sleep "$$BACKUP_INTERVAL_SECONDS"
        done
    volumes:
      - backup_data:/backups
    restart: unless-stopped

volumes:
  postgres_data:
  backup_data: