docker-compose.images.yaml
Rama main · réplica de lectura del repositorio original.
# 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: