Ctrl K
ComunidadGuías…

docker-compose.yml

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

Archivo original ↗Preguntar a KiBot
# ─────────────────────────────────────────────────────────────
# Self-hosting stack: postgres + rustfs + api + worker + bot + web.
# Coolify uses docker-compose.coolify.yml; local dev uses docker-compose.dev.yml
# (backing services only).
#
#   cp .env.example .env      # fill in the required values below
#   docker compose up -d
#
# Required in .env (the stack refuses to start without them):
#   POSTGRES_PASSWORD, BETTER_AUTH_SECRET, APP_ENCRYPTION_KEY,
#   S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, API_URL, APP_URL
# Generate each secret with: openssl rand -base64 32
#
# The four services run from images published on each release; update with
#   docker compose pull && docker compose up -d
# VERSION in .env pins a release (defaults to the latest one). To run this
# checkout instead of the published images, build them:
#   docker compose up -d --build
# ─────────────────────────────────────────────────────────────
name: itsaplan

services:
  postgres:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-itsaplan}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      POSTGRES_DB: ${POSTGRES_DB:-itsaplan}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-itsaplan} -d ${POSTGRES_DB:-itsaplan}']
      interval: 5s
      timeout: 5s
      retries: 10

  # S3-compatible object store for issue attachments (RustFS).
  # Both the S3 API (:9000) and the console (:9001) stay inside the compose
  # network: the api streams attachment bytes through itself, so no port is
  # published. Administer through an S3 client from inside the network or an SSH tunnel.
  #
  # The service and volume keep the names of the MinIO stack this replaced: a renamed
  # service would leave the old MinIO container running on the same volume, and a
  # renamed volume would start empty. RustFS reads the files MinIO wrote in place;
  # MinIO wrote them as root, so the container runs as root to keep them writable.
  minio:
    image: rustfs/rustfs:1.0.0
    restart: unless-stopped
    user: '0:0'
    environment:
      RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY_ID:?set S3_ACCESS_KEY_ID in .env}
      RUSTFS_SECRET_KEY: ${S3_SECRET_ACCESS_KEY:?set S3_SECRET_ACCESS_KEY in .env}
    volumes:
      - minio-data:/data
    healthcheck:
      test: ['CMD', 'curl', '-fsS', '-o', '/dev/null', 'http://127.0.0.1:9000/health']
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 20s

  # One-shot: creates the attachments bucket, then exits. A PUT on a bucket that
  # already exists succeeds, so it runs on every start.
  minio-init:
    image: rustfs/rustfs:1.0.0
    restart: 'no'
    depends_on:
      minio:
        condition: service_healthy
    environment:
      S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
      S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
      S3_BUCKET: ${S3_BUCKET:-planner-attachments}
    entrypoint: >
      /bin/sh -c "
      curl -fsS -X PUT --aws-sigv4 aws:amz:us-east-1:s3 --user \"$$S3_ACCESS_KEY_ID:$$S3_SECRET_ACCESS_KEY\" http://minio:9000/\"$$S3_BUCKET\" &&
      echo 'bucket ready: '\"$$S3_BUCKET\"
      "

  # One-shot: dumps the database and applies the migrations, then exits. api, worker
  # and bot start only after it succeeds, so none of them runs against an old schema.
  # restart 'no' keeps a finished run from being started again.
  migrate:
    image: ghcr.io/croffasia/itsaplan-api:${VERSION:-latest}
    build:
      context: .
      dockerfile: apps/api/Dockerfile
    restart: 'no'
    command: ['bun', 'run', 'packages/db/src/migrate.ts']
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER:-itsaplan}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-itsaplan}
      # How long the dumps are kept, and the switch that upgrades without one.
      # BACKUP_DIR is not among them: the volume below pins it.
      BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-}
      SKIP_PRE_MIGRATION_BACKUP: ${SKIP_PRE_MIGRATION_BACKUP:-}
    # Holds the dump taken before a release's migrations, kept for
    # BACKUP_RETENTION_DAYS (30) so a downgrade has something to restore.
    volumes:
      - db-backups:/backups

  api:
    image: ghcr.io/croffasia/itsaplan-api:${VERSION:-latest}
    build:
      context: .
      dockerfile: apps/api/Dockerfile
    restart: unless-stopped
    # The image's own CMD migrates first; here the migrate service has done that.
    command: ['bun', 'run', 'apps/api/src/index.ts']
    depends_on:
      postgres:
        condition: service_healthy
      migrate:
        condition: service_completed_successfully
      minio-init:
        condition: service_completed_successfully
    environment:
      NODE_ENV: production
      API_PORT: 3000
      DATABASE_URL: postgres://${POSTGRES_USER:-itsaplan}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-itsaplan}
      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?set BETTER_AUTH_SECRET in .env}
      # Encrypts stored provider credentials at rest (AES-256-GCM). Changing it
      # makes existing stored credentials undecryptable.
      APP_ENCRYPTION_KEY: ${APP_ENCRYPTION_KEY:?set APP_ENCRYPTION_KEY in .env}
      # Public origins, each entered once. API_URL is the api origin (auth handler,
      # email links); APP_URL is the web origin (CORS, passkey, email links).
      API_URL: ${API_URL:?public origin of the api, e.g. https://api.example.com}
      APP_URL: ${APP_URL:?public origin of the web app, e.g. https://app.example.com}
      # Set COOKIE_DOMAIN only to override the parent domain derived from
      # APP_URL (multi-label TLDs, deep subdomains).
      COOKIE_DOMAIN: ${COOKIE_DOMAIN:-}
      # Where the client IP for the rate limit is read; empty reads X-Forwarded-For.
      CLIENT_IP_HEADER: ${CLIENT_IP_HEADER:-}
      TRUSTED_PROXIES: ${TRUSTED_PROXIES:-}
      # Public origin for incoming Git webhooks when API_URL is private; empty uses API_URL.
      GIT_WEBHOOK_BASE_URL: ${GIT_WEBHOOK_BASE_URL:-}
      S3_ENDPOINT: http://minio:9000
      S3_BUCKET: ${S3_BUCKET:-planner-attachments}
      S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
      S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
      S3_REGION: ${S3_REGION:-us-east-1}
      S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
      # Hosts trusted even though they resolve privately. Also governs webhook URL
      # validation and attachment import.
      SSRF_ALLOWED_HOSTS: ${SSRF_ALLOWED_HOSTS:-}
    healthcheck:
      test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:3000/']
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 30s
    ports:
      - '${API_PORT:-3000}:3000'

  # Webhook delivery, notification delivery, agent schedules, and agent runs.
  # Shares the database with the api. No public ports.
  worker:
    image: ghcr.io/croffasia/itsaplan-worker:${VERSION:-latest}
    build:
      context: .
      dockerfile: apps/worker/Dockerfile
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
      migrate:
        condition: service_completed_successfully
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://${POSTGRES_USER:-itsaplan}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-itsaplan}
      # Notification delivery sends email and Telegram messages itself, with the
      # provider credentials stored in the database — encrypted with this key, the
      # same value the api uses.
      APP_ENCRYPTION_KEY: ${APP_ENCRYPTION_KEY:?set APP_ENCRYPTION_KEY in .env}
      # Anonymous telemetry (TELEMETRY.md). Either variable, set to anything but 0,
      # turns it off.
      TELEMETRY_DISABLED: ${TELEMETRY_DISABLED:-}
      DO_NOT_TRACK: ${DO_NOT_TRACK:-}
      TELEMETRY_DEBUG: ${TELEMETRY_DEBUG:-}
      # The same list the api reads. This process delivers webhooks to those hosts.
      SSRF_ALLOWED_HOSTS: ${SSRF_ALLOWED_HOSTS:-}
      # A downloaded Plane import attachment is stored here, the same bucket the
      # api's own uploads use.
      S3_ENDPOINT: http://minio:9000
      S3_BUCKET: ${S3_BUCKET:-planner-attachments}
      S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
      S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
      S3_REGION: ${S3_REGION:-us-east-1}
      S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
      # Optional tuning lives in apps/worker/src/config.ts.

  # Telegram bot: long polling for the instance bot, completing the account links
  # users start from their profile. The bot token is stored in the database and
  # edited in god mode, so it is not an environment variable here.
  #
  # Must stay at one replica: Telegram hands each getUpdates call to a single
  # caller, so a second instance would take updates away from the first.
  bot:
    image: ghcr.io/croffasia/itsaplan-bot:${VERSION:-latest}
    build:
      context: .
      dockerfile: apps/bot/Dockerfile
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
      migrate:
        condition: service_completed_successfully
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://${POSTGRES_USER:-itsaplan}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-itsaplan}
      APP_ENCRYPTION_KEY: ${APP_ENCRYPTION_KEY:?set APP_ENCRYPTION_KEY in .env}

  # The origins are read at startup and handed to the browser by the server render,
  # so the image carries none of them and a changed API_URL only needs a restart.
  web:
    image: ghcr.io/croffasia/itsaplan-web:${VERSION:-latest}
    build:
      context: .
      dockerfile: apps/web/Dockerfile
    restart: unless-stopped
    depends_on:
      - api
    environment:
      NODE_ENV: production
      PORT: 3001
      HOSTNAME: 0.0.0.0
      # The api origin the browser uses.
      API_URL: ${API_URL:?public origin of the api, e.g. https://api.example.com}
      # Where web itself reaches the api: it streams avatars and attachments from
      # there (/media) so they are served from the web origin.
      SERVICE_URL_API: http://api:3000
      # Legal document URLs shown on the logged-out screens; empty hides the notice.
      PRIVACY_URL: ${PRIVACY_URL:-}
      TERMS_URL: ${TERMS_URL:-}
    # /login renders without a session, and without reaching the api.
    healthcheck:
      test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:3001/login']
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 20s
    ports:
      - '${WEB_PORT:-3001}:3001'

volumes:
  postgres-data:
  minio-data:
  db-backups: