Ctrl K
ComunidadGuías…

docker-compose.images.yaml

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

Archivo original ↗Preguntar a KiBot
# ClassroomIO — pull-based stack (pre-built images, no local build).
#
# This is the same topology as docker-compose.yaml but the app services pull
# published images from Docker Hub instead of building from source. You only need
# THIS file + a .env — no repository checkout, no 8 GB dashboard build.
#
#   CIO_VERSION=1.4.2 docker compose -f docker-compose.images.yaml --env-file .env pull
#   CIO_VERSION=1.4.2 docker compose -f docker-compose.images.yaml --env-file .env up -d
#
# CIO_VERSION pins the release. Use a specific version (e.g. 1.4.2) in production;
# "latest" = freshest main build (moves on every merge — rolling, may include unreleased code).
#
# KEEP IN SYNC with docker-compose.yaml: the api/dashboard/jobs `environment`,
# healthchecks, depends_on, and volumes must match the build-based file. The only
# intended difference is build: -> image:.
services:
  postgres:
    image: postgres:16-alpine
    container_name: cio-postgres
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-classroomio}
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      # Require a successful query so we only pass once Postgres is out of recovery
      # (pg_isready can pass while DB is still in recovery, causing 57P03 on sign-in)
      test:
        [
          'CMD-SHELL',
          "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-classroomio} && psql -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-classroomio} -tAc 'SELECT 1'"
        ]
      interval: 5s
      timeout: 5s
      retries: 15
      start_period: 10s

  redis:
    image: redis:7-alpine
    container_name: cio-redis
    command: ['redis-server', '--appendonly', 'yes']
    volumes:
      - redis-data:/data
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 5s

  # Bundled S3-compatible storage (SeaweedFS). Only 9000 is published: its other ports have no auth.
  storage:
    image: chrislusf/seaweedfs:4.48
    container_name: cio-storage
    entrypoint:
      - /bin/sh
      - -c
      - |
        esc() { printf '%s' "$$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
        mkdir -p /etc/seaweedfs
        cat > /etc/seaweedfs/s3.json <<EOF
        {"identities":[{"name":"admin","credentials":[{"accessKey":"$$(esc "$$OBJECT_STORAGE_ACCESS_KEY_ID")","secretKey":"$$(esc "$$OBJECT_STORAGE_SECRET_ACCESS_KEY")"}],"actions":["Admin","Read","Write","List","Tagging"]},{"name":"anonymous","actions":["Read:$${OBJECT_STORAGE_BUCKET_MEDIA}"]}]}
        EOF
        exec /entrypoint.sh server -s3 -s3.port=9000 -s3.config=/etc/seaweedfs/s3.json -s3.allowedOrigins=*
    ports:
      - '9000:9000'
    environment:
      OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID:-classroomio}
      OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY:-classroomio-local-dev-secret}
      OBJECT_STORAGE_BUCKET_MEDIA: ${OBJECT_STORAGE_BUCKET_MEDIA:-media}
    volumes:
      - storage-data:/data
    healthcheck:
      # 127.0.0.1: localhost resolves to IPv6 in the image, SeaweedFS binds IPv4.
      test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1:9000/healthz']
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 10s
    profiles:
      - storage

  storage-init:
    image: chrislusf/seaweedfs:4.48
    container_name: cio-storage-init
    depends_on:
      storage:
        condition: service_healthy
    entrypoint:
      - /bin/sh
      - -c
      - |
        set -- "$$OBJECT_STORAGE_BUCKET_VIDEOS" "$$OBJECT_STORAGE_BUCKET_DOCUMENTS" "$$OBJECT_STORAGE_BUCKET_MEDIA"
        for bucket in "$$@"; do
          echo "s3.bucket.create -name $$bucket" | weed shell -master=storage:9333
        done
        # weed shell exits 0 even when a create fails, so confirm each bucket exists.
        echo s3.bucket.list | weed shell -master=storage:9333 > /tmp/buckets 2>&1
        for bucket in "$$@"; do
          grep -q "^[[:space:]]*$$bucket[[:space:]]" /tmp/buckets || { echo "storage-init: bucket $$bucket is missing"; exit 1; }
        done
    environment:
      OBJECT_STORAGE_BUCKET_VIDEOS: ${OBJECT_STORAGE_BUCKET_VIDEOS:-videos}
      OBJECT_STORAGE_BUCKET_DOCUMENTS: ${OBJECT_STORAGE_BUCKET_DOCUMENTS:-documents}
      OBJECT_STORAGE_BUCKET_MEDIA: ${OBJECT_STORAGE_BUCKET_MEDIA:-media}
    profiles:
      - storage

  api:
    image: classroomio/api:${CIO_VERSION:-latest}
    container_name: cio-api
    ports:
      - '3081:3081'
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      # node:slim has no curl/wget; use Node's global fetch (Node 20+).
      test:
        [
          'CMD',
          'node',
          '-e',
          "fetch('http://localhost:3081/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
        ]
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 30s
    environment:
      NODE_ENV: production
      PORT: 3081
      PUBLIC_SERVER_URL: ${PUBLIC_SERVER_URL:-http://localhost:3081}
      TRUSTED_ORIGINS: ${TRUSTED_ORIGINS:-}
      DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-classroomio}
      REDIS_URL: redis://redis:6379
      PUBLIC_IS_SELFHOSTED: ${PUBLIC_IS_SELFHOSTED:-true}
      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-docker-local-client-id}
      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-docker-local-client-secret}
      OPENAI_API_KEY: ${OPENAI_API_KEY:-}
      SMTP_HOST: ${SMTP_HOST:-}
      SMTP_USER: ${SMTP_USER:-}
      SMTP_PASSWORD: ${SMTP_PASSWORD:-}
      SMTP_PORT: ${SMTP_PORT:-}
      SMTP_SENDER: ${SMTP_SENDER:-}
      # Object storage (S3-compatible). Works with the bundled storage service, AWS S3, or Cloudflare R2
      # — for R2, point OBJECT_STORAGE_ENDPOINT at your R2 endpoint.
      OBJECT_STORAGE_ENDPOINT: ${OBJECT_STORAGE_ENDPOINT:-}
      OBJECT_STORAGE_PUBLIC_ENDPOINT: ${OBJECT_STORAGE_PUBLIC_ENDPOINT:-}
      OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID:-}
      OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY:-}
      OBJECT_STORAGE_FORCE_PATH_STYLE: ${OBJECT_STORAGE_FORCE_PATH_STYLE:-true}
      OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL: ${OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL:-}
      # Auth secrets — required. ./classroomio.sh generates them into .env; if you run
      # compose directly, set strong values yourself — the stack refuses to start otherwise.
      PRIVATE_SERVER_KEY: ${PRIVATE_SERVER_KEY:?set a strong PRIVATE_SERVER_KEY in .env}
      WEBHOOK_ALLOW_PRIVATE_NETWORKS: ${WEBHOOK_ALLOW_PRIVATE_NETWORKS:-false}
      DASHBOARD_ORIGIN: ${DASHBOARD_ORIGIN:-}
      LICENSE_KEY: ${LICENSE_KEY:-}
      UNSPLASH_API_KEY: ${UNSPLASH_API_KEY:-}
      # Upload size limits (megabytes). Unset = built-in defaults — see .env.example.
      UPLOAD_MAX_DOCUMENT_MB: ${UPLOAD_MAX_DOCUMENT_MB:-}
      UPLOAD_MAX_IMAGE_MB: ${UPLOAD_MAX_IMAGE_MB:-}
      UPLOAD_MAX_VIDEO_MB: ${UPLOAD_MAX_VIDEO_MB:-}
      UPLOAD_MAX_EXERCISE_FILE_MB: ${UPLOAD_MAX_EXERCISE_FILE_MB:-}
      UPLOAD_MAX_AGENT_DOCUMENT_MB: ${UPLOAD_MAX_AGENT_DOCUMENT_MB:-}
      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?set a strong BETTER_AUTH_SECRET in .env}
      SKIP_DB_SETUP: ${SKIP_DB_SETUP:-}
    restart: unless-stopped

  dashboard:
    image: classroomio/dashboard:${CIO_VERSION:-latest}
    container_name: cio-dashboard
    ports:
      - '3082:3082'
    depends_on:
      api:
        condition: service_healthy
    environment:
      NODE_ENV: production
      PORT: 3082
      ORIGIN: ${DASHBOARD_ORIGIN:-http://localhost:3082}
      PUBLIC_IS_SELFHOSTED: ${PUBLIC_IS_SELFHOSTED:-true}
      PRIVATE_SERVER_URL: ${PRIVATE_SERVER_URL:-http://api:3081}
      PRIVATE_SERVER_KEY: ${PRIVATE_SERVER_KEY:?set a strong PRIVATE_SERVER_KEY in .env}
      PRIVATE_APP_HOST: ${PRIVATE_APP_HOST:-localhost}
      PRIVATE_APP_SUBDOMAINS: ${PRIVATE_APP_SUBDOMAINS:-app}
      ALLOWED_EXTERNAL_DOMAINS: ${ALLOWED_EXTERNAL_DOMAINS:-}
      CSP_SCRIPT_SRC_DOMAINS: ${CSP_SCRIPT_SRC_DOMAINS:-}
      CSP_STYLE_SRC_DOMAINS: ${CSP_STYLE_SRC_DOMAINS:-}
      CSP_CONNECT_SRC_DOMAINS: ${CSP_CONNECT_SRC_DOMAINS:-}
      CSP_FRAME_SRC_DOMAINS: ${CSP_FRAME_SRC_DOMAINS:-}
      CSP_FONT_SRC_DOMAINS: ${CSP_FONT_SRC_DOMAINS:-}
      CSP_MEDIA_SRC_DOMAINS: ${CSP_MEDIA_SRC_DOMAINS:-}
      # Upload size limits (megabytes). Unset = built-in defaults — see .env.example.
      UPLOAD_MAX_DOCUMENT_MB: ${UPLOAD_MAX_DOCUMENT_MB:-}
      UPLOAD_MAX_IMAGE_MB: ${UPLOAD_MAX_IMAGE_MB:-}
      UPLOAD_MAX_VIDEO_MB: ${UPLOAD_MAX_VIDEO_MB:-}
      UPLOAD_MAX_EXERCISE_FILE_MB: ${UPLOAD_MAX_EXERCISE_FILE_MB:-}
      UPLOAD_MAX_AGENT_DOCUMENT_MB: ${UPLOAD_MAX_AGENT_DOCUMENT_MB:-}
      UPLOAD_MAX_LANDING_IMAGE_MB: ${UPLOAD_MAX_LANDING_IMAGE_MB:-}
      UPLOAD_MAX_THUMBNAIL_MB: ${UPLOAD_MAX_THUMBNAIL_MB:-}
    restart: unless-stopped

  # Background worker (BullMQ). Drains the Redis queue: video processing, transcription,
  # AI course generation, and emails.
  jobs:
    image: classroomio/jobs:${CIO_VERSION:-latest}
    container_name: cio-jobs
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      NODE_ENV: production
      REDIS_URL: redis://redis:6379
      PRIVATE_SERVER_KEY: ${PRIVATE_SERVER_KEY:?set a strong PRIVATE_SERVER_KEY in .env}
      PUBLIC_IS_SELFHOSTED: ${PUBLIC_IS_SELFHOSTED:-true}
      WEBHOOK_ALLOW_PRIVATE_NETWORKS: ${WEBHOOK_ALLOW_PRIVATE_NETWORKS:-false}
      DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-classroomio}
      # Object storage (workers stream object bytes). Mirrors the api service.
      OBJECT_STORAGE_ENDPOINT: ${OBJECT_STORAGE_ENDPOINT:-}
      OBJECT_STORAGE_PUBLIC_ENDPOINT: ${OBJECT_STORAGE_PUBLIC_ENDPOINT:-}
      OBJECT_STORAGE_REGION: ${OBJECT_STORAGE_REGION:-}
      OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID:-}
      OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY:-}
      OBJECT_STORAGE_FORCE_PATH_STYLE: ${OBJECT_STORAGE_FORCE_PATH_STYLE:-true}
      OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL: ${OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL:-}
      # Bucket names match those created by storage-init above.
      OBJECT_STORAGE_BUCKET_VIDEOS: ${OBJECT_STORAGE_BUCKET_VIDEOS:-videos}
      OBJECT_STORAGE_BUCKET_DOCUMENTS: ${OBJECT_STORAGE_BUCKET_DOCUMENTS:-documents}
      OBJECT_STORAGE_BUCKET_MEDIA: ${OBJECT_STORAGE_BUCKET_MEDIA:-media}
      # Email (worker sends invites, notifications, etc.).
      SMTP_HOST: ${SMTP_HOST:-}
      SMTP_PORT: ${SMTP_PORT:-}
      SMTP_USER: ${SMTP_USER:-}
      SMTP_PASSWORD: ${SMTP_PASSWORD:-}
      SMTP_SENDER: ${SMTP_SENDER:-}
      # Transcription + OpenAI-backed agent runs.
      OPENAI_API_KEY: ${OPENAI_API_KEY:-}
      CIO_DEPLOYMENT_METHOD: ${CIO_DEPLOYMENT_METHOD:-}
      CIO_TELEMETRY_DISABLED: ${CIO_TELEMETRY_DISABLED:-}
      LICENSE_KEY: ${LICENSE_KEY:-}
    restart: unless-stopped

volumes:
  postgres-data:
  redis-data:
  storage-data: