Ctrl K
ComunidadGuías…

docker-compose.byo-postgres.yml

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

Archivo original ↗Preguntar a KiBot
# ─────────────────────────────────────────────────────────────────────────────
# Bring-your-own PostgreSQL — drop the bundled metadata database.
#
#   docker compose -f docker-compose.quickstart.yml -f docker-compose.byo-postgres.yml up -d
#
# Requires the connection details in .env, e.g.:
#   POSTGRES_HOST=my-instance.abc123.us-east-1.rds.amazonaws.com
#   POSTGRES_PORT=5432
#   POSTGRES_USER=rsync
#   POSTGRES_PASSWORD=...
#   POSTGRES_DB=pipeline_db
#   POSTGRES_SSLMODE=require
#   POSTGRES_TLS_ENABLED=true        # Temporal's own TLS switch, see below
#
# This is the metadata database: pipeline definitions, run history, and the
# encrypted connection credentials. Pipeline *rows* never land here. Combine
# freely with docker-compose.byo-kafka.yml — the two overlays are independent.
#
# ── What you must create first ───────────────────────────────────────────────
# One thing: the ROLE named by POSTGRES_USER, with a password matching
# POSTGRES_PASSWORD and the right to create databases.
#
#   CREATE ROLE rsync LOGIN PASSWORD '…' CREATEDB;
#
# Nothing in this file can create that role, because everything in it
# authenticates AS it. The databases are no longer yours to create: `db-init`
# below makes POSTGRES_DB and Temporal's two, plus the two extensions the
# migrations need, before any service that reads them starts. It is idempotent,
# so bringing the stack up again is a no-op.
#
# If your instance's application role cannot hold CREATEDB, create those three
# databases as an admin and db-init will find them and move on. What it will
# NOT do is treat "permission denied" and "already exists" as the same outcome:
# it fails loudly and prints the grant to run. Per-provider caveats (Azure's
# azure.extensions allow-list, Cloud SQL's cloudsqlsuperuser):
# docs/deployment/self-hosting.md § Bring Your Own PostgreSQL.
#
# ── TLS is not one switch ────────────────────────────────────────────────────
# POSTGRES_SSLMODE covers `db-init` below and the three Go services
# (api-gateway's DSN, orchestrator's DB_SSLMODE, temporal-adapter's
# POSTGRES_SSLMODE). Temporal ignores it, and Temporal is really two programs:
# auto-setup runs temporal-sql-tool to create the schemas, then starts the
# server, and they read DIFFERENT env names for the same fact. The base file
# feeds both from one POSTGRES_TLS_* key each, so you set these once:
#
#   POSTGRES_TLS_ENABLED      -> SQL_TLS_ENABLED (server) + SQL_TLS (schema tool)
#   POSTGRES_TLS_CA_FILE      -> SQL_CA          + SQL_TLS_CA_FILE
#   POSTGRES_TLS_SERVER_NAME  -> SQL_HOST_NAME   + SQL_TLS_SERVER_NAME
#   POSTGRES_TLS_VERIFY_HOST  -> SQL_HOST_VERIFICATION
#   POSTGRES_TLS_SKIP_HOST_VERIFY -> SQL_TLS_DISABLE_HOST_VERIFICATION (inverse!)
#
# Set sslmode alone against a database that mandates TLS and the schema step
# fails before the server ever binds — no workflow engine, every pipeline hangs,
# and no TLS error in any log to point at the cause.
#
# ── How the removal works, and why it is spelled this way ────────────────────
# Compose overlays merge; they cannot delete a service. Assigning a profile that
# is never activated is the supported way to exclude one, because a service in a
# non-active profile is absent from the rendered project entirely.
#
# That half alone is a trap. A service excluded by profile is still named by
# `depends_on` in the base file, and Compose rejects the whole project with
# "depends on undefined service postgres" — nothing starts, which is strictly
# worse than the waste this overlay removes. The pairing fix lives in the base
# file: every `depends_on: postgres` there carries `required: false`. Both
# halves are needed; either one alone takes the stack down.
#
# Do not copy that flag onto the three `depends_on: db-init` entries below. It
# reads like the house style here, and it would quietly undo this whole file:
# measured on Compose 2.x, a FAILED `service_completed_successfully` dependency
# marked `required: false` degrades to the warning `optional dependency "…"
# didn't complete successfully: exit 7`, and the dependent starts anyway with
# `up` still exiting 0. That is the silent start on an empty schema db-init
# exists to prevent. The flag is for a service that may be absent from the
# project; db-init is defined right here and never is.
#
# ── What still runs ──────────────────────────────────────────────────────────
# `demo-warehouse` is a different service and is deliberately NOT excluded. It
# is the sample destination behind "Start with sample data" — a throwaway
# Postgres holding demo rows, nothing to do with the metadata database. Drop it
# by removing the demo profile, not with this overlay.
# ─────────────────────────────────────────────────────────────────────────────
services:
  postgres:
    # Never activated => the bundled database is excluded from the project.
    profiles: ["bundled-db-disabled"]

  # ─── DATABASE BOOTSTRAP ──────────────────────────────────────────────────────
  #
  # The bundled path creates three databases without anyone noticing, through two
  # upstream images: the postgres image's initdb makes POSTGRES_DB, and Temporal's
  # auto-setup makes its own two. Excluding `postgres` above removes the first and
  # SKIP_DB_CREATE below removes the second, so on this path NOTHING creates them
  # — and until this service existed, that gap was fifty lines of instructions in
  # this header, enforced by nothing.
  #
  # It is worth being exact about why the gap had to be closed with code rather
  # than prose: two of the three failures are silent. Temporal's is loud (its
  # entrypoint is `auto-setup.sh && start-temporal.sh` under `set -e`, so the
  # container crash-loops). POSTGRES_DB's is not. A failed `db.Init` is a warning
  # and not an exit (main.go:306-308), and the migration runner sits inside that
  # call's success branch — so no schema is created and the gateway serves
  # regardless. Missing extensions fail the same way one step later: the runner
  # returns on the first failing file (migrate.go:126-129) and 001_init_schema.sql
  # is the first, needing uuid-ossp.
  #
  # Nothing in this project observes either one. api-gateway and orchestrator
  # declare no healthcheck at all — of the services here only kafka, postgres,
  # redis, minio, temporal, llm-service, kafka-connect and demo-warehouse do — so
  # `docker compose ps` reports both as plain `running` over an empty database.
  # `/ready` is the endpoint that knows (ready.go:15-25 returns `db_not_connected`
  # then `schema_not_migrated`), and install.sh polls it for exactly this reason
  # (install.sh:744-754). A hand-run `docker compose up` polls nothing.
  #
  # Mirrors the chart's templates/jobs/db-init.yaml, which does the same work as
  # a pre-install hook, on the two paths where the chart renders it: external
  # Postgres with dbInit left on and IAM auth off.
  # The two are hand-kept in step for the same reason kafka-init's script is
  # inline: install.sh downloads compose files alone, with no repo checkout, so
  # there is no shared script on the host to call.
  db-init:
    image: postgres:16-alpine
    container_name: rsync-ai-db-init
    logging:
      driver: json-file
      options:
        max-size: ${RSYNC_LOG_MAX_SIZE:-10m}
        max-file: "${RSYNC_LOG_MAX_FILE:-3}"
    # Run-once job: one psql client, a handful of statements.
    mem_limit: 256m
    environment:
      PGHOST: ${POSTGRES_HOST:-postgres}
      PGPORT: ${POSTGRES_PORT:-5432}
      PGUSER: ${POSTGRES_USER:-rsync}
      PGPASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
      # The database this connects TO in order to run CREATE DATABASE — never one
      # of the databases it creates. `postgres` exists on RDS, Cloud SQL and Azure
      # Database alike; override it only for an instance that dropped it.
      PGDATABASE: ${POSTGRES_MAINTENANCE_DB:-postgres}
      PGSSLMODE: ${POSTGRES_SSLMODE:-disable}
      # Not PGSSLROOTCERT directly: libpq reads an EMPTY PGSSLROOTCERT as a
      # filename and fails to open it, so under verify-ca an unset CA would break
      # the connection rather than fall back to the system store. The script
      # exports it only when non-empty. Note that nothing here mounts the file —
      # if you set this, give this service the same bind mount you gave `temporal`.
      POSTGRES_TLS_CA_FILE: ${POSTGRES_TLS_CA_FILE:-}
      # Fail fast per attempt; the retry loop below supplies the patience.
      PGCONNECT_TIMEOUT: "10"
      APP_DATABASE: ${POSTGRES_DB:-pipeline_db}
      # auto-setup's own defaults, and the SAME two names `temporal` is given
      # explicitly below. A name that the creator and the consumer each hardcode
      # separately is a name that can drift.
      TEMPORAL_DATABASE: ${TEMPORAL_DB:-temporal}
      TEMPORAL_VISIBILITY_DATABASE: ${TEMPORAL_VISIBILITY_DB:-temporal_visibility}
    # /bin/sh, not /bin/bash: the image is Alpine and has no bash.
    entrypoint: ["/bin/sh", "-c"]
    # Single-element list, not a bare block scalar: Compose splits a string
    # command into argv, which would leave `sh -c` running only the first word.
    # `docker compose config` must show ONE item.
    command:
      - |
        set -u;
        if [ -n "$${POSTGRES_TLS_CA_FILE:-}" ]; then
          export PGSSLROOTCERT="$$POSTGRES_TLS_CA_FILE";
        fi;
        # A managed instance that is still provisioning answers the port before
        # it accepts connections. The error is captured rather than discarded
        # because the failures that land here read identically from outside — a
        # wrong password and an unreachable host both just end this loop — and
        # the operator needs the server's own words to tell them apart.
        ERR="";
        n=0;
        until ERR="$$(psql -w -Atc 'SELECT 1' 2>&1)"; do
          n=$$((n + 1));
          if [ "$$n" -ge 30 ]; then
            echo "FATAL: cannot reach PostgreSQL at $$PGHOST:$$PGPORT as \"$$PGUSER\" after 60s." >&2;
            echo "  Last error from the server: $$ERR" >&2;
            echo "  Checked in this order:" >&2;
            echo "    * POSTGRES_HOST / POSTGRES_PORT reach the instance" >&2;
            echo "    * the role \"$$PGUSER\" exists and its password matches" >&2;
            echo "      POSTGRES_PASSWORD (nothing here creates the role it logs in as)" >&2;
            echo "    * POSTGRES_MAINTENANCE_DB=\"$$PGDATABASE\" exists and \"$$PGUSER\"" >&2;
            echo "      may connect to it" >&2;
            echo "    * POSTGRES_SSLMODE=\"$$PGSSLMODE\" matches what the server requires" >&2;
            exit 1;
          fi;
          echo "waiting for postgres at $$PGHOST:$$PGPORT ($$n/30): $$ERR";
          sleep 2;
        done;
        echo "connected to $$PGHOST:$$PGPORT as \"$$PGUSER\" (maintenance database \"$$PGDATABASE\")";
        # CREATE DATABASE has no IF NOT EXISTS and cannot run inside a
        # transaction, so idempotency is an existence check against pg_database
        # rather than a swallowed error. Swallowing would be worse than it looks:
        # "permission denied to create database" and "already exists" would then
        # be the same outcome, and the first has to stay fatal.
        #
        # No OWNER clause. The creating role owns what it creates, and naming an
        # owner is how this would break on a managed instance whose admin role is
        # not POSTGRES_USER.
        mkdb() {
          if [ "$$(psql -w -Atc "SELECT 1 FROM pg_database WHERE datname = '$$1'")" = "1" ]; then
            echo "  database \"$$1\" already exists";
            return 0;
          fi;
          if ! OUT="$$(psql -w -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"$$1\"" 2>&1)"; then
            if [ "$$(psql -w -Atc "SELECT 1 FROM pg_database WHERE datname = '$$1'")" = "1" ]; then
              echo "  database \"$$1\" was created concurrently";
              return 0;
            fi;
            echo "FATAL: could not create database \"$$1\"." >&2;
            echo "  $$OUT" >&2;
            echo "  The role \"$$PGUSER\" needs CREATEDB:" >&2;
            echo "    ALTER ROLE \"$$PGUSER\" CREATEDB;" >&2;
            echo "  On a managed instance grant its admin role instead (RDS:" >&2;
            echo "  rds_superuser; Azure: azure_pg_admin; Cloud SQL:" >&2;
            echo "  cloudsqlsuperuser). Or create the three databases yourself —" >&2;
            echo "  this finds them and moves on." >&2;
            exit 1;
          fi;
          echo "  database \"$$1\" created";
        };
        mkdb "$$APP_DATABASE";
        mkdb "$$TEMPORAL_DATABASE";
        mkdb "$$TEMPORAL_VISIBILITY_DATABASE";
        # In the APPLICATION database only. Temporal's two need no extensions,
        # and creating one inside a database whose schema another product owns is
        # not ours to do.
        #
        # Fatal, not best-effort. Both are trusted extensions on PostgreSQL 13+,
        # so a database owner can create them without superuser — but a managed
        # instance can still refuse (Azure Flexible Server permits only what
        # `azure.extensions` lists). Continuing past that refusal hands it to the
        # migration runner, which stops at the first failing file and logs one
        # line while the process keeps serving: the whole schema is then missing
        # and nothing says why.
        mkext() {
          if ! OUT="$$(psql -w -v ON_ERROR_STOP=1 -d "$$APP_DATABASE" -c "CREATE EXTENSION IF NOT EXISTS \"$$1\"" 2>&1)"; then
            echo "FATAL: could not create extension \"$$1\" in \"$$APP_DATABASE\"." >&2;
            echo "  $$OUT" >&2;
            echo "  $$2" >&2;
            echo "  Both are trusted extensions on PostgreSQL 13+, so the database" >&2;
            echo "  owner may create them — but a managed instance can still refuse." >&2;
            echo "  Azure Database for PostgreSQL needs the extension allowlisted in" >&2;
            echo "  the azure.extensions server parameter before this succeeds." >&2;
            exit 1;
          fi;
          echo "  extension \"$$1\" ready";
        };
        mkext "uuid-ossp" "001_init_schema.sql calls uuid_generate_v4() and is the FIRST migration, so without it NO table is created.";
        mkext "pg_trgm" "020b_workspace_connection_refs.sql builds the gin_trgm_ops indexes.";
        echo "✅ Databases and extensions ready"
    restart: "no"

  temporal:
    depends_on:
      db-init:
        condition: service_completed_successfully
    environment:
      # db-init above creates the two databases, so auto-setup must not try.
      # Its create is unconditional: the only guard is the name test
      # `[[ ${DBNAME} != ${POSTGRES_USER} && ${SKIP_DB_CREATE} != true ]]`, so
      # pre-existence does not stop it, and it runs as POSTGRES_USER, which on a
      # managed instance normally lacks CREATEDB. The bundled path does not need
      # this (there POSTGRES_USER is the superuser), which is why it lives in
      # this overlay: it must not change the bundled-database behaviour.
      SKIP_DB_CREATE: "true"
      # auto-setup's own defaults, set explicitly so that the creator above and
      # the consumer here read ONE pair of names instead of two hardcoded copies.
      DBNAME: ${TEMPORAL_DB:-temporal}
      VISIBILITY_DBNAME: ${TEMPORAL_VISIBILITY_DB:-temporal_visibility}

  orchestrator:
    depends_on:
      db-init:
        condition: service_completed_successfully

  api-gateway:
    depends_on:
      db-init:
        condition: service_completed_successfully