OpenMAIC Docker & Self-Hosted Deployment

GitHub

Open Multi-Agent Interactive Classroom โ€” Get an immersive, multi-agent learning experience in just one click

41,284 stars TypeScript TypeScript 44 files ยท ~224,966 tokens
RAW Compose
1. Pull Image (DOCKERHUB) docker pull library/postgres
2. Docker Run (1-Click Launch) docker run -d --name openmaic -p 3000:3000 --restart always postgres:16
3. Podman Run podman run -d --name openmaic -p 3000:3000 --restart always postgres:16
4. Skopeo Inspect (Zero-Pull Manifest) skopeo inspect docker://docker.io/library/postgres:16
5. ORAS Pull (OCI Artifact) oras pull postgres:16
6. Compose Deployment docker compose up -d
docker-compose.yml
Download .yml
services:
  openmaic:
    build:
      context: .
      args:
        - ALPINE_MIRROR=${ALPINE_MIRROR:-}
        - NPM_REGISTRY=${NPM_REGISTRY:-}
        # NEXT_PUBLIC_* values are compiled into the browser bundle. Empty values
        # follow each feature's documented default; explicit build-time overrides
        # can be supplied on the command line.
        # env_file below does not feed build args.
        - ALLOWED_FRAME_ANCESTORS=${ALLOWED_FRAME_ANCESTORS:-}
        - NEXT_PUBLIC_MAIC_EDITOR_ENABLED=${NEXT_PUBLIC_MAIC_EDITOR_ENABLED:-}
        - NEXT_PUBLIC_MAIC_EDITOR_RENDERER_ENABLED=${NEXT_PUBLIC_MAIC_EDITOR_RENDERER_ENABLED:-}
        - NEXT_PUBLIC_MAIC_PLAYBACK_RENDERER_ENABLED=${NEXT_PUBLIC_MAIC_PLAYBACK_RENDERER_ENABLED:-}
        - NEXT_PUBLIC_PI_CHAT_ENABLED=${NEXT_PUBLIC_PI_CHAT_ENABLED:-}
        - NEXT_PUBLIC_COURSEWARE_REFERENCE_ENABLED=${NEXT_PUBLIC_COURSEWARE_REFERENCE_ENABLED:-}
        - NEXT_PUBLIC_SHOW_VOCATIONAL_TEST_UI=${NEXT_PUBLIC_SHOW_VOCATIONAL_TEST_UI:-}
        - NEXT_PUBLIC_ENABLE_VIDEO_EXPORT=${NEXT_PUBLIC_ENABLE_VIDEO_EXPORT:-}
        - NEXT_PUBLIC_VIDEO_EXPORT_CTA_DESTINATION=${NEXT_PUBLIC_VIDEO_EXPORT_CTA_DESTINATION:-}
        - NEXT_PUBLIC_ENABLE_PPTX_IMPORT=${NEXT_PUBLIC_ENABLE_PPTX_IMPORT:-}
        - NEXT_PUBLIC_PRO_WORKBENCH_ENABLED=${NEXT_PUBLIC_PRO_WORKBENCH_ENABLED:-}
    # Published on loopback by default: the single-user owner (see
    # docker-compose.defaults.env) serves one course library to every request,
    # so only this machine should reach it. To publish on the network, set
    # ACCESS_CODE in .env.local (strongly recommended: without it anyone who
    # reaches the port shares, edits and can delete the library) and start with
    # OPENMAIC_PUBLISH_ADDRESS=0.0.0.0 (or a specific interface address).
    ports:
      - '${OPENMAIC_PUBLISH_ADDRESS:-127.0.0.1}:${OPENMAIC_PORT:-3000}:3000'
    env_file:
      # Compose's defaults for this deployment (the bundled DATABASE_URL, the
      # single-user owner); .env.local is read after it, so a key set there
      # (for example an external DATABASE_URL) overrides the default.
      - docker-compose.defaults.env
      - .env.local
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      # The address the port above is published on, passed in so the app can
      # warn when it is published beyond loopback with the default PostgreSQL
      # password below.
      - OPENMAIC_PUBLISH_ADDRESS=${OPENMAIC_PUBLISH_ADDRESS:-127.0.0.1}
      # Point the app at the render service when it's running. This wins over any
      # RENDER_SERVICE_URL in .env.local; the service only starts under the
      # "video-export" profile. The app's capability check probes the service's
      # /health, so if the profile is off (service absent) the app reports the
      # MP4 export disabled and degrades to the ZIP-download path โ€” this URL
      # being set does not, by itself, advertise a working render.
      - RENDER_SERVICE_URL=http://render-service:9000
    networks:
      - default # outbound internet (LLM/media provider APIs, etc.)
      - render # reach the render service over the isolated network
    volumes:
      # Optional: the model configuration (copy openmaic.example.yml to
      # openmaic.yml first; a missing file would be mounted as a directory).
      # - ./openmaic.yml:/app/openmaic.yml:ro
      # Legacy, read only when there is no openmaic.yml:
      # - ./server-providers.yml:/app/server-providers.yml:ro
      - openmaic-data:/app/data
    restart: unless-stopped

  # PostgreSQL for the app-embedded persistence API. Started by default; the
  # app waits for its health check. Its port is not published to the host here;
  # `pnpm db:up` runs this service definition as a separate development
  # database (its own project and volume, see docker-compose.db.yml).
  postgres:
    image: postgres:16
    environment:
      - POSTGRES_DB=openmaic
      - POSTGRES_USER=openmaic
      # Development default. It initializes the role only when the volume is
      # empty; the default DATABASE_URL in docker-compose.defaults.env is built
      # from the same variable, so use letters and digits only (or set an
      # encoded DATABASE_URL in .env.local). Set PERSISTENCE_POSTGRES_PASSWORD before
      # publishing the app beyond loopback; the app warns at startup if it is
      # published elsewhere with this default.
      - POSTGRES_PASSWORD=${PERSISTENCE_POSTGRES_PASSWORD:-openmaic-dev}
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U openmaic -d openmaic']
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 5s
    volumes:
      - openmaic-postgres:/var/lib/postgresql/data
    restart: unless-stopped

  # Isolated MP4 render service (Node 22 + Chromium + FFmpeg) for classroom video
  # export (issue #866). Opt-in: only starts with `--profile video-export`, since
  # the image is heavy (bundles Chromium + FFmpeg) and the capability is optional.
  #
  #   docker compose --profile video-export up --build
  #
  # Sits on the `render` network (`internal: true`, no host/internet gateway).
  # It shares that network with `openmaic` only so the app can reach it; to stop
  # the untrusted Chromium from initiating connections *back* to the app over
  # that shared network, the container's entrypoint installs an iptables egress
  # lockdown (needs CAP_NET_ADMIN) that drops all outbound except loopback and
  # replies on app-initiated connections. The export ZIP is self-contained, so
  # the render needs no outbound at all.
  render-service:
    build: ./render-service
    profiles:
      - video-export
    expose:
      - '9000'
    # Required for the entrypoint's iptables egress lockdown. Without it the
    # service still boots, but logs a warning and does NOT block Chromium egress.
    cap_add:
      - NET_ADMIN
    environment:
      - PORT=9000
      # The standard CPU profile prefers BeginFrame but permits producer's
      # compatibility fallback (for example iframe GenUI). It still fixes one
      # producer worker, one render, and one extraction. Select low-memory to
      # force screenshot instead of relying on producer auto-selection.
      - RENDER_RESOURCE_PROFILE=${RENDER_RESOURCE_PROFILE:-standard}
      - RENDER_CHUNK_EXECUTION=${RENDER_CHUNK_EXECUTION:-false}
      - RENDER_CHUNK_COUNT=${RENDER_CHUNK_COUNT:-1}
      - RENDER_CHUNK_WORKERS=${RENDER_CHUNK_WORKERS:-1}
      - RENDER_MAX_PARALLEL_CHUNKS=${RENDER_MAX_PARALLEL_CHUNKS:-1}
      - RENDER_CHUNK_SIZE_FRAMES=${RENDER_CHUNK_SIZE_FRAMES:-0}
      - RENDER_TARGET_CHUNK_FRAMES=${RENDER_TARGET_CHUNK_FRAMES:-0}
      - PRODUCER_HEADLESS_SHELL_PATH=/usr/bin/chromium-headless-shell
      - RENDER_MAX_CONCURRENCY=1
      - RENDER_MAX_CONCURRENT_EXTRACTIONS=1
      # Long compositions still need CDP headroom even with one explicit worker.
      - PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS=900000
      # OpenMAIC's long slide exports currently exhaust producer's 15s static
      # verification budget and disable dedup anyway. Skip that guaranteed-lost
      # startup cost until the verifier can share/scale its plan across workers.
      - HF_STATIC_DEDUP=false
      # In this default topology the app is exposed directly and does NOT set
      # TRUST_PROXY_HEADERS, so every caller collapses to one identity. A
      # per-identity limit would then throttle the whole deployment to a single
      # render, so disable it here and rely on RENDER_MAX_CONCURRENCY + the global
      # RENDER_MAX_QUEUE cap. Set this >0 only with a trusted proxy supplying
      # per-user identity (see render-service/README.md).
      - RENDER_MAX_JOBS_PER_USER=0
      - RENDER_PREVIEW_TIMEOUT_MS=${RENDER_PREVIEW_TIMEOUT_MS:-20000}
      # Separate Chromium capacity: previews can run alongside a video export.
      - RENDER_PREVIEW_MAX_CONCURRENCY=${RENDER_PREVIEW_MAX_CONCURRENCY:-1}
      - RENDER_PREVIEW_MAX_IN_FLIGHT=${RENDER_PREVIEW_MAX_IN_FLIGHT:-8}
      # Preview callers send a durable owner identity in x-openmaic-client, so
      # keep this per-owner guard enabled. Set it to 0 only in deployments that
      # call /preview without an owner identity.
      - RENDER_PREVIEW_MAX_PER_USER=${RENDER_PREVIEW_MAX_PER_USER:-2}
    # Bound RAM for the export (Chromium + FFmpeg + archive expansion) and the
    # independent preview browsers. Provision more before raising preview concurrency.
    # The standard profile requires 8 GiB. For the explicit low-memory profile,
    # set RENDER_RESOURCE_PROFILE=low-memory and RENDER_SERVICE_MEMORY_LIMIT=4g.
    mem_limit: ${RENDER_SERVICE_MEMORY_LIMIT:-8g}
    # Chromium media/frame work can exceed Docker's 64 MiB default shared-memory
    # mount. This is a ceiling inside the selected profile cgroup, not eager allocation.
    shm_size: 2gb
    networks:
      - render
    restart: unless-stopped

networks:
  default: {}
  # Isolated network for app <-> render-service traffic. `internal: true` means
  # no route to the host or internet. Both services share it so the app can
  # reach the render service; the render container additionally blocks its own
  # egress (iptables, see the service's entrypoint) so the untrusted Chromium
  # can't use this shared network to reach the app.
  render:
    internal: true

volumes:
  openmaic-data:
  openmaic-postgres: