Sign inSign up

harlenalvarez/huddle-stack:latest

Manifest digest

sha256:34b459fc425563595039a43388c73b9496d8c472511ffb45092e19f54773e0e7

Last pushed

about 6 hours by harlenalvarez

Type

Compose

Manifest digest

sha256:34b459fc425563595039a43388c73b9496d8c472511ffb45092e19f54773e0e7

Compose file content

name: huddle

services:
  web-renderer:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-web-renderer:${HUDDLE_IMAGE_TAG:-dev}
    init: true
    user: pwuser
    security_opt:
      - seccomp=./web-renderer/seccomp_profile.json
      - no-new-privileges:true
    cap_drop: [ALL]
    read_only: true
    tmpfs:
      - /tmp:size=256m,mode=1777
      - /home/pwuser/.cache:size=64m,uid=1001,gid=1001
    shm_size: 512m
    mem_limit: 2g
    memswap_limit: 2g
    cpus: 2
    pids_limit: 512
    networks: [renderer]
    healthcheck:
      test:
        [
          CMD,
          node,
          -e,
          "const s=require('net').connect(3000,'127.0.0.1');s.setTimeout(3000);s.on('connect',()=>{s.end();process.exit(0)});s.on('error',()=>process.exit(1));s.on('timeout',()=>process.exit(1))",
        ]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped

  postgres:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-postgres:${HUDDLE_IMAGE_TAG:-dev}
    environment:
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DATABASE_ALLOWED_HOSTS: postgres
    volumes:
      - postgres_data:/var/lib/postgresql
    networks:
      - backend
    healthcheck:
      # Check the private listener without assuming a role from DATABASE_URL;
      # the custom entrypoint may initialize any configured username.
      test:
        [
          'CMD',
          'python3',
          '-c',
          "import socket; connection = socket.create_connection(('127.0.0.1', 5432), 3); connection.close()",
        ]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 15s
    restart: unless-stopped

  zitadel-db-init:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-postgres:${HUDDLE_IMAGE_TAG:-dev}
    entrypoint: ['python3', '/usr/local/lib/huddle/zitadel_db_init.py']
    environment:
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DATABASE_ALLOWED_HOSTS: postgres
      ZITADEL_DB_NAME: ${ZITADEL_DB_NAME:-zitadel}
      ZITADEL_DB_USER: ${ZITADEL_DB_USER:-zitadel}
      ZITADEL_DB_PASSWORD: ${ZITADEL_DB_PASSWORD:?ZITADEL_DB_PASSWORD is required}
      ZITADEL_MASTERKEY: ${ZITADEL_MASTERKEY:?ZITADEL_MASTERKEY is required}
      ZITADEL_SESSION_COOKIE_SECRET: ${ZITADEL_SESSION_COOKIE_SECRET:?ZITADEL_SESSION_COOKIE_SECRET is required}
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - zitadel_bootstrap:/zitadel/bootstrap
    networks:
      - backend
    restart: 'no'

  zitadel-api:
    image: ghcr.io/zitadel/zitadel:${ZITADEL_VERSION:-v4.19.3}
    user: '0'
    command:
      ['start-from-init', '--masterkey', '${ZITADEL_MASTERKEY:?ZITADEL_MASTERKEY is required}']
    environment:
      ZITADEL_PORT: '8080'
      ZITADEL_TLS_ENABLED: 'false'
      ZITADEL_EXTERNALDOMAIN: ${ZITADEL_DOMAIN:-localhost}
      ZITADEL_EXTERNALPORT: ${ZITADEL_EXTERNALPORT:-${ZITADEL_HTTP_PORT:-8080}}
      ZITADEL_EXTERNALSECURE: ${ZITADEL_EXTERNALSECURE:-false}
      ZITADEL_DATABASE_POSTGRES_DSN: postgresql://${ZITADEL_DB_USER:-zitadel}:${ZITADEL_DB_PASSWORD:?ZITADEL_DB_PASSWORD is required}@postgres:5432/${ZITADEL_DB_NAME:-zitadel}?sslmode=disable
      ZITADEL_FIRSTINSTANCE_ORG_NAME: ${ZITADEL_ADMIN_ORG:-Huddle Admin}
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: ${ZITADEL_ADMIN_EMAIL:?ZITADEL_ADMIN_EMAIL is required}
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_ADDRESS: ${ZITADEL_ADMIN_EMAIL:?ZITADEL_ADMIN_EMAIL is required}
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_VERIFIED: 'true'
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_FIRSTNAME: Huddle
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_LASTNAME: Admin
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_ADMIN_PASSWORD:?ZITADEL_ADMIN_PASSWORD is required}
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: 'false'
      ZITADEL_FIRSTINSTANCE_PATPATH: /zitadel/bootstrap/admin.pat
      ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: huddle-bootstrap
      ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Huddle bootstrap administrator
      ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE: ${ZITADEL_ADMIN_PAT_EXPIRATION:-2027-10-01T00:00:00Z}
      ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /zitadel/bootstrap/login-client.pat
      ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client
      ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Huddle Login v2
      ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: ${ZITADEL_LOGIN_PAT_EXPIRATION:-2027-10-01T00:00:00Z}
      ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: 'true'
      ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: ${ZITADEL_PUBLIC_SCHEME:-http}://${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-${ZITADEL_HTTP_PORT:-8080}}/ui/v2/login/
      ZITADEL_OIDC_DEFAULTLOGINURLV2: ${ZITADEL_PUBLIC_SCHEME:-http}://${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-${ZITADEL_HTTP_PORT:-8080}}/ui/v2/login/login?authRequest=
      ZITADEL_OIDC_DEFAULTLOGOUTURLV2: ${ZITADEL_PUBLIC_SCHEME:-http}://${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-${ZITADEL_HTTP_PORT:-8080}}/ui/v2/login/logout?post_logout_redirect=
      ZITADEL_SAML_DEFAULTLOGINURLV2: ${ZITADEL_PUBLIC_SCHEME:-http}://${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-${ZITADEL_HTTP_PORT:-8080}}/ui/v2/login/login?samlRequest=
      # Invite-only from first start, before the operator runs bootstrap.
      ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWREGISTER: 'false'
      ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWUSERNAMEPASSWORD: 'true'
      ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_FORCEMFA: 'false'
      ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_HIDEPASSWORDRESET: 'true'
      ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWEXTERNALIDP: 'false'
      ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWDOMAINDISCOVERY: 'false'
      ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDLESSTYPE: '0'
    depends_on:
      zitadel-db-init:
        condition: service_completed_successfully
    volumes:
      - zitadel_bootstrap:/zitadel/bootstrap
    networks:
      - backend
    healthcheck:
      test: ['CMD', '/app/zitadel', 'ready']
      interval: 10s
      timeout: 30s
      retries: 12
      start_period: 30s
    restart: unless-stopped

  zitadel-login:
    image: ghcr.io/zitadel/zitadel-login:${ZITADEL_VERSION:-v4.19.3}
    user: '0'
    environment:
      ZITADEL_API_URL: http://zitadel-api:8080
      NEXT_PUBLIC_BASE_PATH: /ui/v2/login
      ZITADEL_SERVICE_USER_TOKEN_FILE: /zitadel/bootstrap/login-client.pat
      ZITADEL_SESSION_COOKIE_SECRET: ${ZITADEL_SESSION_COOKIE_SECRET:?ZITADEL_SESSION_COOKIE_SECRET is required}
      CUSTOM_REQUEST_HEADERS: Host:${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-${ZITADEL_HTTP_PORT:-8080}},X-Forwarded-Proto:${ZITADEL_PUBLIC_SCHEME:-http}
    depends_on:
      zitadel-api:
        condition: service_healthy
    volumes:
      - zitadel_bootstrap:/zitadel/bootstrap:ro
    networks:
      - backend
    healthcheck:
      test: ['CMD', 'node', '/app/healthcheck.mjs', 'http://localhost:3000/ui/v2/login/healthy']
      interval: 10s
      timeout: 30s
      retries: 12
      start_period: 30s
    restart: unless-stopped

  zitadel:
    image: caddy:2-alpine
    environment:
      ZITADEL_SITE_ADDRESS: ${ZITADEL_SITE_ADDRESS:-http://:8080}
    configs:
      - source: zitadel_caddyfile
        target: /etc/caddy/Caddyfile
    depends_on:
      zitadel-login:
        condition: service_healthy
      zitadel-api:
        condition: service_healthy
    networks:
      - frontend
      - backend
    healthcheck:
      test:
        ['CMD', 'wget', '--quiet', '--output-document=/dev/null', 'http://127.0.0.1:2019/config/']
      interval: 10s
      timeout: 5s
      retries: 12
    restart: unless-stopped

  redis:
    image: redis:8-alpine
    command: ['redis-server', '--appendonly', 'yes']
    volumes:
      - redis_data:/data
    networks:
      - backend
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 5s
      timeout: 3s
      retries: 20
    restart: unless-stopped

  nats:
    image: nats:2-alpine
    command: ['-js', '-sd', '/data', '-m', '8222']
    volumes:
      - nats_data:/data
    networks:
      - backend
    healthcheck:
      test: ['CMD', 'wget', '--quiet', '--spider', 'http://127.0.0.1:8222/healthz']
      interval: 5s
      timeout: 3s
      retries: 20
    restart: unless-stopped

  nats-tools:
    image: natsio/nats-box:latest
    profiles: ['tools']
    entrypoint: ['sleep', 'infinity']
    depends_on:
      nats:
        condition: service_healthy
    networks:
      - backend

  artifact-storage-init:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-api:${HUDDLE_IMAGE_TAG:-dev}
    user: '0:0'
    entrypoint: ['sh', '-c']
    command:
      - mkdir -p /var/lib/huddle/artifacts /var/lib/huddle/artifact-staging && chown -R 10001:10001 /var/lib/huddle/artifacts /var/lib/huddle/artifact-staging && chmod 700 /var/lib/huddle/artifact-staging
    volumes:
      - artifact_data:/var/lib/huddle/artifacts
      - artifact_staging:/var/lib/huddle/artifact-staging
    restart: 'no'

  api:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-api:${HUDDLE_IMAGE_TAG:-dev}
    environment:
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DATABASE_STARTUP_TIMEOUT: ${DATABASE_STARTUP_TIMEOUT:-120}
      # Allow Zitadel bootstrap to render the stack before the org exists.
      # The ownership migration rejects a missing ID before changing the schema.
      ZITADEL_BOOTSTRAP_OWNER_ID: ${ZITADEL_BOOTSTRAP_OWNER_ID:-}
      FERNET_KEY: ${FERNET_KEY:?FERNET_KEY is required}
      ARTIFACT_BLOB_ROOT: /var/lib/huddle/artifacts
      ARTIFACT_STAGING_ROOT: /var/lib/huddle/artifact-staging
      ARTIFACT_MAX_UPLOAD_BYTES: ${ARTIFACT_MAX_UPLOAD_BYTES:-26214400}
      ARTIFACT_MAX_FILES_PER_UPLOAD: ${ARTIFACT_MAX_FILES_PER_UPLOAD:-20}
      ARTIFACT_AGENT_WRITE_MAX_CHARS: ${ARTIFACT_AGENT_WRITE_MAX_CHARS:-100000}
      ARTIFACT_TEXT_MAX_BYTES: ${ARTIFACT_TEXT_MAX_BYTES:-2097152}
      ARTIFACT_VISION_MAX_EDGE_PX: ${ARTIFACT_VISION_MAX_EDGE_PX:-2048}
      ARTIFACT_SVG_MAX_BYTES: ${ARTIFACT_SVG_MAX_BYTES:-5242880}
      ARTIFACT_HTML_MAX_BYTES: ${ARTIFACT_HTML_MAX_BYTES:-5242880}
      ARTIFACT_HTML_MAX_IMAGES: ${ARTIFACT_HTML_MAX_IMAGES:-10}
      ARTIFACT_HTML_MIN_IMAGE_DIMENSION_PX: ${ARTIFACT_HTML_MIN_IMAGE_DIMENSION_PX:-32}
      # Public HTTPS fetches (get_web_page). The byte limit follows the upload limit unless set.
      WEB_PAGE_MAX_BYTES: ${WEB_PAGE_MAX_BYTES:-${ARTIFACT_MAX_UPLOAD_BYTES:-26214400}}
      WEB_PAGE_CONNECT_TIMEOUT_SECONDS: ${WEB_PAGE_CONNECT_TIMEOUT_SECONDS:-5}
      WEB_PAGE_TIMEOUT_SECONDS: ${WEB_PAGE_TIMEOUT_SECONDS:-60}
      WEB_PAGE_MAX_REDIRECTS: ${WEB_PAGE_MAX_REDIRECTS:-5}
      WEB_PAGE_USER_AGENT: ${WEB_PAGE_USER_AGENT:-Huddle/0.1.0 (+https://huddlehub.ai)}
      WEB_PAGE_MAX_TEXT_BYTES: ${WEB_PAGE_MAX_TEXT_BYTES:-${ARTIFACT_TEXT_MAX_BYTES:-2097152}}
      WEB_PAGE_IMAGE_MAX_BYTES: ${WEB_PAGE_IMAGE_MAX_BYTES:-8388608}
      WEB_PAGE_RENDER_ENABLED: ${WEB_PAGE_RENDER_ENABLED:-true}
      WEB_PAGE_RENDER_WS_URL: ${WEB_PAGE_RENDER_WS_URL-ws://web-renderer:3000/}
      WEB_PAGE_RENDER_TIMEOUT_SECONDS: ${WEB_PAGE_RENDER_TIMEOUT_SECONDS:-30}
      WEB_PAGE_RENDER_SETTLE_SECONDS: ${WEB_PAGE_RENDER_SETTLE_SECONDS:-5}
      WEB_PAGE_RENDER_MAX_CONCURRENCY: ${WEB_PAGE_RENDER_MAX_CONCURRENCY:-2}
      WEB_PAGE_RENDER_MAX_REQUESTS: ${WEB_PAGE_RENDER_MAX_REQUESTS:-150}
      WEB_PAGE_RENDER_MAX_RESOURCE_BYTES: ${WEB_PAGE_RENDER_MAX_RESOURCE_BYTES:-5242880}
      WEB_PAGE_RENDER_MAX_TOTAL_BYTES: ${WEB_PAGE_RENDER_MAX_TOTAL_BYTES:-26214400}
      WEB_PAGE_VIEW_MAX_SEGMENTS: ${WEB_PAGE_VIEW_MAX_SEGMENTS:-3}
      WEB_PAGE_VIEW_CACHE_TTL_SECONDS: ${WEB_PAGE_VIEW_CACHE_TTL_SECONDS:-3600}
      WEB_PAGE_VIEW_MAX_OUTPUT_TOKENS: ${WEB_PAGE_VIEW_MAX_OUTPUT_TOKENS:-2000}
      HTTP_TIMEOUT: ${HTTP_TIMEOUT:-300}
      HTTPX_MAX_CONNECTIONS: ${HTTPX_MAX_CONNECTIONS:-100}
      HTTP_MAX_KEEPALIVE: ${HTTP_MAX_KEEPALIVE:-20}
      ASR_WS_URL: ${ASR_WS_URL:-ws://spark-427e.local:8005/v1/audio/transcriptions/realtime}
      VLLM_API_KEY: ${VLLM_API_KEY:-EMPTY}
      CHAT_HEIC_MODE: ${CHAT_HEIC_MODE:-passthrough}
      CHAT_CONTEXT_USAGE_TIMEOUT_SECONDS: ${CHAT_CONTEXT_USAGE_TIMEOUT_SECONDS:-2.0}
      EXA_API_KEY: ${EXA_API_KEY:-}
      GOOGLE_PLACES_API_KEY: ${GOOGLE_PLACES_API_KEY:-}
      REDIS_HOST: redis:6379
      REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX:-huddle:}
      REDIS_MAX_CONNECTIONS: ${REDIS_MAX_CONNECTIONS:-10}
      REDIS_SOCKET_CONNECT_TIMEOUT: ${REDIS_SOCKET_CONNECT_TIMEOUT:-10.0}
      REDIS_SOCKET_TIMEOUT: ${REDIS_SOCKET_TIMEOUT:-30.0}
      REDIS_SOCKET_KEEPALIVE: ${REDIS_SOCKET_KEEPALIVE:-true}
      REDIS_SSL_ENABLED: 'false'
      REDIS_IS_CLUSTER: 'false'
      NATS_URL: nats://nats:4222
      LOG_LEVEL: ${LOG_LEVEL:-INFO}
      WEB_PUSH_ENABLED: ${WEB_PUSH_ENABLED:-false}
      WEB_PUSH_VAPID_PUBLIC_KEY: ${WEB_PUSH_VAPID_PUBLIC_KEY:-}
      OAUTH_CLIENT_ID: ${OAUTH_CLIENT_ID:-}
      OAUTH_ISSUER: ${OAUTH_ISSUER:-${ZITADEL_PUBLIC_SCHEME:-http}://${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-${ZITADEL_HTTP_PORT:-8080}}}
      OAUTH_AUDIENCE: ${OAUTH_AUDIENCE:-}
      OAUTH_JWKS_URL: ${OAUTH_JWKS_URL:-http://zitadel:8080/oauth/v2/keys}
      OAUTH_JWKS_HOST_HEADER: ${OAUTH_JWKS_HOST_HEADER:-${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-${ZITADEL_HTTP_PORT:-8080}}}
      OAUTH_OWNER_CLAIM: ${OAUTH_OWNER_CLAIM:-urn:zitadel:iam:user:resourceowner:id}
      OAUTH_END_SESSION_ENDPOINT: ${OAUTH_END_SESSION_ENDPOINT:-}
      OAUTH_SCOPES: ${OAUTH_SCOPES:-}
      HUDDLE_PUBLIC_ORIGIN: ${HUDDLE_PUBLIC_ORIGIN:-https://localhost}
      HUDDLE_LANDING_ORIGIN: ${HUDDLE_LANDING_ORIGIN:-}
      INTEREST_TOKEN_SECRET: ${INTEREST_TOKEN_SECRET:-}
      INTEREST_HOURLY_SIGNUP_LIMIT: ${INTEREST_HOURLY_SIGNUP_LIMIT:-60}
      INTEREST_DAILY_EMAIL_LIMIT: ${INTEREST_DAILY_EMAIL_LIMIT:-100}
      SMTP_HOST: ${SMTP_HOST:-}
      SMTP_PORT: ${SMTP_PORT:-587}
      SMTP_USERNAME: ${SMTP_USERNAME:-}
      SMTP_PASSWORD: ${SMTP_PASSWORD:-}
      SMTP_FROM: ${SMTP_FROM:-}
      SMTP_STARTTLS: ${SMTP_STARTTLS:-true}
      EBAY_ENVIRONMENT: ${EBAY_ENVIRONMENT:-sandbox}
      EBAY_CLIENT_ID: ${EBAY_CLIENT_ID:-}
      EBAY_CLIENT_SECRET: ${EBAY_CLIENT_SECRET:-}
      EBAY_REDIRECT_URI: ${EBAY_REDIRECT_URI:-}
      EBAY_ALLOW_PRODUCTION: ${EBAY_ALLOW_PRODUCTION:-false}
      EBAY_ACCOUNT_DELETION_VERIFICATION_TOKEN: ${EBAY_ACCOUNT_DELETION_VERIFICATION_TOKEN:-}
      EBAY_ACCOUNT_DELETION_PUBLIC_ORIGIN: ${EBAY_ACCOUNT_DELETION_PUBLIC_ORIGIN:-}
      EBAY_SANDBOX_API_URL: ${EBAY_SANDBOX_API_URL:-https://api.sandbox.ebay.com}
      EBAY_PRODUCTION_API_URL: ${EBAY_PRODUCTION_API_URL:-https://api.ebay.com}
      EBAY_SANDBOX_INVENTORY_API_URL: ${EBAY_SANDBOX_INVENTORY_API_URL:-https://api.sandbox.ebay.com/sell/inventory/v1}
      EBAY_PRODUCTION_INVENTORY_API_URL: ${EBAY_PRODUCTION_INVENTORY_API_URL:-https://api.ebay.com/sell/inventory/v1}
    depends_on:
      artifact-storage-init:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      nats:
        condition: service_healthy
    volumes:
      - artifact_data:/var/lib/huddle/artifacts
      - artifact_staging:/var/lib/huddle/artifact-staging
      - hf_cache:/var/lib/huddle/hf-cache
    networks:
      - renderer
      - backend
      - frontend
      - webhook_backend
    healthcheck:
      test:
        [
          'CMD',
          'python',
          '-c',
          "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9000/api/health', timeout=3)",
        ]
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 30s
    restart: unless-stopped

  schedule-publisher:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-api:${HUDDLE_IMAGE_TAG:-dev}
    entrypoint: ['python', '-m', 'huddle_api.scheduled_worker', 'scheduler']
    environment:
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DATABASE_HOST_OVERRIDE: postgres
      DATABASE_PORT_OVERRIDE: '5432'
      FERNET_KEY: ${FERNET_KEY:?FERNET_KEY is required}
      NATS_URL: nats://nats:4222
      LOG_LEVEL: ${LOG_LEVEL:-INFO}
      HUDDLE_PUBLIC_ORIGIN: ${HUDDLE_PUBLIC_ORIGIN:-https://localhost}
      REDIS_HOST: redis:6379
      REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX:-huddle:}
      VLLM_API_KEY: ${VLLM_API_KEY:-EMPTY}
      EXA_API_KEY: ${EXA_API_KEY:-}
      GOOGLE_PLACES_API_KEY: ${GOOGLE_PLACES_API_KEY:-}
    depends_on:
      postgres:
        condition: service_healthy
      nats:
        condition: service_healthy
      api:
        condition: service_healthy
    networks:
      - backend
    restart: unless-stopped

  notification-live-worker:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-api:${HUDDLE_IMAGE_TAG:-dev}
    entrypoint: ['python', '-m', 'huddle_api.live_outbox_worker', 'notifications']
    environment:
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DATABASE_HOST_OVERRIDE: postgres
      DATABASE_PORT_OVERRIDE: '5432'
      FERNET_KEY: ${FERNET_KEY:?FERNET_KEY is required}
      NATS_URL: nats://nats:4222
      LOG_LEVEL: ${LOG_LEVEL:-INFO}
      WEB_PUSH_VAPID_PUBLIC_KEY: ${WEB_PUSH_VAPID_PUBLIC_KEY:-}
      WEB_PUSH_VAPID_PRIVATE_KEY: ${WEB_PUSH_VAPID_PRIVATE_KEY:-}
      WEB_PUSH_VAPID_SUBJECT: ${WEB_PUSH_VAPID_SUBJECT:-}
      WEB_PUSH_ENABLED: ${WEB_PUSH_ENABLED:-false}
      HUDDLE_PUBLIC_ORIGIN: ${HUDDLE_PUBLIC_ORIGIN:-https://localhost}
    depends_on:
      postgres:
        condition: service_healthy
      nats:
        condition: service_healthy
      api:
        condition: service_healthy
    networks:
      - backend
      # Web Push must reach external browser push providers.
      - frontend
    restart: unless-stopped

  schedule-live-worker:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-api:${HUDDLE_IMAGE_TAG:-dev}
    entrypoint: ['python', '-m', 'huddle_api.live_outbox_worker', 'schedules']
    environment:
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DATABASE_HOST_OVERRIDE: postgres
      DATABASE_PORT_OVERRIDE: '5432'
      FERNET_KEY: ${FERNET_KEY:?FERNET_KEY is required}
      NATS_URL: nats://nats:4222
      LOG_LEVEL: ${LOG_LEVEL:-INFO}
    depends_on:
      postgres:
        condition: service_healthy
      nats:
        condition: service_healthy
      api:
        condition: service_healthy
    networks:
      - backend
    restart: unless-stopped

  chat-thread-live-worker:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-api:${HUDDLE_IMAGE_TAG:-dev}
    entrypoint: ['python', '-m', 'huddle_api.live_outbox_worker', 'chat.threads']
    environment:
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DATABASE_HOST_OVERRIDE: postgres
      DATABASE_PORT_OVERRIDE: '5432'
      FERNET_KEY: ${FERNET_KEY:?FERNET_KEY is required}
      NATS_URL: nats://nats:4222
      LOG_LEVEL: ${LOG_LEVEL:-INFO}
    depends_on:
      postgres:
        condition: service_healthy
      nats:
        condition: service_healthy
      api:
        condition: service_healthy
    networks:
      - backend
    restart: unless-stopped

  scheduled-task-worker:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-api:${HUDDLE_IMAGE_TAG:-dev}
    entrypoint: ['python', '-m', 'huddle_api.scheduled_worker', 'worker']
    environment:
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DATABASE_HOST_OVERRIDE: postgres
      DATABASE_PORT_OVERRIDE: '5432'
      FERNET_KEY: ${FERNET_KEY:?FERNET_KEY is required}
      ARTIFACT_BLOB_ROOT: /var/lib/huddle/artifacts
      ARTIFACT_VISION_MAX_EDGE_PX: ${ARTIFACT_VISION_MAX_EDGE_PX:-2048}
      ARTIFACT_SVG_MAX_BYTES: ${ARTIFACT_SVG_MAX_BYTES:-5242880}
      ARTIFACT_HTML_MAX_BYTES: ${ARTIFACT_HTML_MAX_BYTES:-5242880}
      ARTIFACT_HTML_MAX_IMAGES: ${ARTIFACT_HTML_MAX_IMAGES:-10}
      ARTIFACT_HTML_MIN_IMAGE_DIMENSION_PX: ${ARTIFACT_HTML_MIN_IMAGE_DIMENSION_PX:-32}
      # Public HTTPS fetches (get_web_page). The byte limit follows the upload limit unless set.
      WEB_PAGE_MAX_BYTES: ${WEB_PAGE_MAX_BYTES:-${ARTIFACT_MAX_UPLOAD_BYTES:-26214400}}
      WEB_PAGE_CONNECT_TIMEOUT_SECONDS: ${WEB_PAGE_CONNECT_TIMEOUT_SECONDS:-5}
      WEB_PAGE_TIMEOUT_SECONDS: ${WEB_PAGE_TIMEOUT_SECONDS:-60}
      WEB_PAGE_MAX_REDIRECTS: ${WEB_PAGE_MAX_REDIRECTS:-5}
      WEB_PAGE_USER_AGENT: ${WEB_PAGE_USER_AGENT:-Huddle/0.1.0 (+https://huddlehub.ai)}
      WEB_PAGE_MAX_TEXT_BYTES: ${WEB_PAGE_MAX_TEXT_BYTES:-${ARTIFACT_TEXT_MAX_BYTES:-2097152}}
      WEB_PAGE_IMAGE_MAX_BYTES: ${WEB_PAGE_IMAGE_MAX_BYTES:-8388608}
      WEB_PAGE_RENDER_ENABLED: ${WEB_PAGE_RENDER_ENABLED:-true}
      WEB_PAGE_RENDER_WS_URL: ${WEB_PAGE_RENDER_WS_URL-ws://web-renderer:3000/}
      WEB_PAGE_RENDER_TIMEOUT_SECONDS: ${WEB_PAGE_RENDER_TIMEOUT_SECONDS:-30}
      WEB_PAGE_RENDER_SETTLE_SECONDS: ${WEB_PAGE_RENDER_SETTLE_SECONDS:-5}
      WEB_PAGE_RENDER_MAX_CONCURRENCY: ${WEB_PAGE_RENDER_MAX_CONCURRENCY:-2}
      WEB_PAGE_RENDER_MAX_REQUESTS: ${WEB_PAGE_RENDER_MAX_REQUESTS:-150}
      WEB_PAGE_RENDER_MAX_RESOURCE_BYTES: ${WEB_PAGE_RENDER_MAX_RESOURCE_BYTES:-5242880}
      WEB_PAGE_RENDER_MAX_TOTAL_BYTES: ${WEB_PAGE_RENDER_MAX_TOTAL_BYTES:-26214400}
      WEB_PAGE_VIEW_MAX_SEGMENTS: ${WEB_PAGE_VIEW_MAX_SEGMENTS:-3}
      WEB_PAGE_VIEW_CACHE_TTL_SECONDS: ${WEB_PAGE_VIEW_CACHE_TTL_SECONDS:-3600}
      WEB_PAGE_VIEW_MAX_OUTPUT_TOKENS: ${WEB_PAGE_VIEW_MAX_OUTPUT_TOKENS:-2000}
      NATS_URL: nats://nats:4222
      LOG_LEVEL: ${LOG_LEVEL:-INFO}
      HUDDLE_PUBLIC_ORIGIN: ${HUDDLE_PUBLIC_ORIGIN:-https://localhost}
      REDIS_HOST: redis:6379
      REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX:-huddle:}
      VLLM_API_KEY: ${VLLM_API_KEY:-EMPTY}
      HTTP_TIMEOUT: ${HTTP_TIMEOUT:-300}
      HTTPX_MAX_CONNECTIONS: ${HTTPX_MAX_CONNECTIONS:-100}
      HTTP_MAX_KEEPALIVE: ${HTTP_MAX_KEEPALIVE:-20}
      CHAT_HEIC_MODE: ${CHAT_HEIC_MODE:-passthrough}
      CHAT_CONTEXT_USAGE_TIMEOUT_SECONDS: ${CHAT_CONTEXT_USAGE_TIMEOUT_SECONDS:-2.0}
      EXA_API_KEY: ${EXA_API_KEY:-}
      GOOGLE_PLACES_API_KEY: ${GOOGLE_PLACES_API_KEY:-}
      EBAY_ENVIRONMENT: ${EBAY_ENVIRONMENT:-sandbox}
      EBAY_CLIENT_ID: ${EBAY_CLIENT_ID:-}
      EBAY_CLIENT_SECRET: ${EBAY_CLIENT_SECRET:-}
      EBAY_REDIRECT_URI: ${EBAY_REDIRECT_URI:-}
      EBAY_ALLOW_PRODUCTION: ${EBAY_ALLOW_PRODUCTION:-false}
      EBAY_SANDBOX_API_URL: ${EBAY_SANDBOX_API_URL:-https://api.sandbox.ebay.com}
      EBAY_PRODUCTION_API_URL: ${EBAY_PRODUCTION_API_URL:-https://api.ebay.com}
      EBAY_SANDBOX_INVENTORY_API_URL: ${EBAY_SANDBOX_INVENTORY_API_URL:-https://api.sandbox.ebay.com/sell/inventory/v1}
      EBAY_PRODUCTION_INVENTORY_API_URL: ${EBAY_PRODUCTION_INVENTORY_API_URL:-https://api.ebay.com/sell/inventory/v1}
    depends_on:
      postgres:
        condition: service_healthy
      nats:
        condition: service_healthy
      api:
        condition: service_healthy
    volumes:
      - artifact_data:/var/lib/huddle/artifacts
    networks:
      - renderer
      - backend
      - frontend
      - webhook_backend
    restart: unless-stopped

  artifact-index-worker:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-api:${HUDDLE_IMAGE_TAG:-dev}
    entrypoint: ['python', '-m', 'huddle_api.artifact_index_worker']
    environment:
      DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required}
      DATABASE_HOST_OVERRIDE: postgres
      DATABASE_PORT_OVERRIDE: '5432'
      FERNET_KEY: ${FERNET_KEY:?FERNET_KEY is required}
      ARTIFACT_BLOB_ROOT: /var/lib/huddle/artifacts
      ARTIFACT_VISION_MAX_EDGE_PX: ${ARTIFACT_VISION_MAX_EDGE_PX:-2048}
      ARTIFACT_SVG_MAX_BYTES: ${ARTIFACT_SVG_MAX_BYTES:-5242880}
      ARTIFACT_HTML_MAX_BYTES: ${ARTIFACT_HTML_MAX_BYTES:-5242880}
      ARTIFACT_HTML_MAX_IMAGES: ${ARTIFACT_HTML_MAX_IMAGES:-10}
      ARTIFACT_HTML_MIN_IMAGE_DIMENSION_PX: ${ARTIFACT_HTML_MIN_IMAGE_DIMENSION_PX:-32}
      NATS_URL: nats://nats:4222
      LOG_LEVEL: ${LOG_LEVEL:-INFO}
      HUDDLE_PUBLIC_ORIGIN: ${HUDDLE_PUBLIC_ORIGIN:-https://localhost}
      REDIS_HOST: redis:6379
      REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX:-huddle:}
      VLLM_API_KEY: ${VLLM_API_KEY:-EMPTY}
      HTTP_TIMEOUT: ${HTTP_TIMEOUT:-300}
      HTTPX_MAX_CONNECTIONS: ${HTTPX_MAX_CONNECTIONS:-100}
      HTTP_MAX_KEEPALIVE: ${HTTP_MAX_KEEPALIVE:-20}
      CHAT_HEIC_MODE: ${CHAT_HEIC_MODE:-passthrough}
      CHAT_CONTEXT_USAGE_TIMEOUT_SECONDS: ${CHAT_CONTEXT_USAGE_TIMEOUT_SECONDS:-2.0}
      EXA_API_KEY: ${EXA_API_KEY:-}
      GOOGLE_PLACES_API_KEY: ${GOOGLE_PLACES_API_KEY:-}
      EBAY_ENVIRONMENT: ${EBAY_ENVIRONMENT:-sandbox}
      EBAY_CLIENT_ID: ${EBAY_CLIENT_ID:-}
      EBAY_CLIENT_SECRET: ${EBAY_CLIENT_SECRET:-}
      EBAY_REDIRECT_URI: ${EBAY_REDIRECT_URI:-}
      EBAY_ALLOW_PRODUCTION: ${EBAY_ALLOW_PRODUCTION:-false}
      EBAY_SANDBOX_API_URL: ${EBAY_SANDBOX_API_URL:-https://api.sandbox.ebay.com}
      EBAY_PRODUCTION_API_URL: ${EBAY_PRODUCTION_API_URL:-https://api.ebay.com}
      EBAY_SANDBOX_INVENTORY_API_URL: ${EBAY_SANDBOX_INVENTORY_API_URL:-https://api.sandbox.ebay.com/sell/inventory/v1}
      EBAY_PRODUCTION_INVENTORY_API_URL: ${EBAY_PRODUCTION_INVENTORY_API_URL:-https://api.ebay.com/sell/inventory/v1}
    depends_on:
      postgres:
        condition: service_healthy
      nats:
        condition: service_healthy
      api:
        condition: service_healthy
    volumes:
      - artifact_data:/var/lib/huddle/artifacts
      - hf_cache:/var/lib/huddle/hf-cache
    networks:
      - backend
      - frontend
      - webhook_backend
    restart: unless-stopped

  web:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-web:${HUDDLE_IMAGE_TAG:-dev}
    networks:
      - frontend
    healthcheck:
      test: ['CMD', 'wget', '--quiet', '--output-document=/dev/null', 'http://127.0.0.1/healthz']
      interval: 10s
      timeout: 5s
      retries: 12
    restart: unless-stopped

  landing:
    image: docker.io/${DOCKER_HUB_NAMESPACE:-local}/huddle-landing-web:${HUDDLE_IMAGE_TAG:-dev}
    environment:
      HUDDLE_APP_ORIGIN: ${HUDDLE_PUBLIC_ORIGIN:-https://localhost}
    networks:
      - frontend
    healthcheck:
      test: ['CMD', 'wget', '--quiet', '--output-document=/dev/null', 'http://127.0.0.1/healthz']
      interval: 10s
      timeout: 5s
      retries: 12
    restart: unless-stopped

  caddy:
    image: caddy:2-alpine
    environment:
      HUDDLE_HOSTNAME: ${HUDDLE_HOSTNAME:-localhost}
      HUDDLE_LANDING_HOSTNAME: ${HUDDLE_LANDING_HOSTNAME:-landing.localhost}
      HUDDLE_AUTH_HOSTNAME: ${HUDDLE_AUTH_HOSTNAME:-auth.localhost}
      HUDDLE_TLS_ISSUER: ${HUDDLE_TLS_ISSUER:-internal}
    configs:
      - source: caddyfile
        target: /etc/caddy/Caddyfile
    ports:
      - name: http
        target: 80
        published: ${HUDDLE_HTTP_PORT:-80}
        protocol: tcp
      - name: https
        target: 443
        published: ${HUDDLE_HTTPS_PORT:-443}
        protocol: tcp
      - name: http3
        target: 443
        published: ${HUDDLE_HTTPS_PORT:-443}
        protocol: udp
    volumes:
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      api:
        condition: service_healthy
      web:
        condition: service_healthy
      landing:
        condition: service_healthy
    networks:
      - frontend
    healthcheck:
      test:
        ['CMD', 'wget', '--quiet', '--output-document=/dev/null', 'http://127.0.0.1:2019/config/']
      interval: 10s
      timeout: 5s
      retries: 12
    restart: unless-stopped

  # The eBay webhook tunnel is for environments without a public domain (local
  # and LAN staging). Start it with `--profile tunnel`; production does not need it.
  ebay-webhook-gateway:
    image: caddy:2-alpine
    profiles: ['tunnel']
    configs:
      - source: ebay_webhook_caddyfile
        target: /etc/caddy/Caddyfile
    depends_on:
      api:
        condition: service_healthy
    networks:
      - webhook_edge
      - webhook_backend
    healthcheck:
      test:
        ['CMD', 'wget', '--quiet', '--output-document=/dev/null', 'http://127.0.0.1:2019/config/']
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 5s
    restart: unless-stopped

  cloudflared:
    image: cloudflare/cloudflared:latest
    profiles: ['tunnel']
    command: ['tunnel', '--no-autoupdate', '--url', 'http://ebay-webhook-gateway:8080']
    depends_on:
      ebay-webhook-gateway:
        condition: service_healthy
    networks:
      - webhook_edge
    restart: unless-stopped

configs:
  zitadel_caddyfile:
    file: ./zitadel.Caddyfile
  caddyfile:
    file: ./Caddyfile
  ebay_webhook_caddyfile:
    file: ./ebay-webhook.Caddyfile

networks:
  renderer:
    internal: true
  frontend:
  webhook_edge:
  webhook_backend:
    internal: true
  backend:
    internal: true

volumes:
  zitadel_bootstrap:
  postgres_data:
  redis_data:
  nats_data:
  caddy_data:
  caddy_config:
  artifact_data:
  artifact_staging:
  hf_cache:
services:
  api:
    image: docker.io/harlenalvarez/huddle-api:2026.10.23-1@sha256:6adc15fec15e7bd6490b1ae822d6cd0e5443aeb97ce296f58ab2a909d19e8e4f
  artifact-index-worker:
    image: docker.io/harlenalvarez/huddle-api:2026.10.23-1@sha256:6adc15fec15e7bd6490b1ae822d6cd0e5443aeb97ce296f58ab2a909d19e8e4f
  artifact-storage-init:
    image: docker.io/harlenalvarez/huddle-api:2026.10.23-1@sha256:6adc15fec15e7bd6490b1ae822d6cd0e5443aeb97ce296f58ab2a909d19e8e4f
  caddy:
    image: docker.io/library/caddy:2-alpine@sha256:eb4d35b7452d1ab6171fbdbe8d9275b2301dad6dad92115d91e19fb9b24d510f
  chat-thread-live-worker:
    image: docker.io/harlenalvarez/huddle-api:2026.10.23-1@sha256:6adc15fec15e7bd6490b1ae822d6cd0e5443aeb97ce296f58ab2a909d19e8e4f
  cloudflared:
    image: docker.io/cloudflare/cloudflared:latest@sha256:faa8e0a1475f137c910be6025f43c8e4092fdd35461b31e19fd1390657a1ff39
  ebay-webhook-gateway:
    image: docker.io/library/caddy:2-alpine@sha256:eb4d35b7452d1ab6171fbdbe8d9275b2301dad6dad92115d91e19fb9b24d510f
  landing:
    image: docker.io/harlenalvarez/huddle-landing-web:2026.10.23-1@sha256:39073052130460a93f741527f0f216cf688df87fe4c09db4fa6633d161b3b311
  nats:
    image: docker.io/library/nats:2-alpine@sha256:7ae34c25dfd6fe2e01c6483eabef20d796690fdcac6c4d94ca780a6cf9877fde
  nats-tools:
    image: docker.io/natsio/nats-box:latest@sha256:dd57687c32199867bfc42823e935174f37754a4281de957d6717469948b5b18b
  notification-live-worker:
    image: docker.io/harlenalvarez/huddle-api:2026.10.23-1@sha256:6adc15fec15e7bd6490b1ae822d6cd0e5443aeb97ce296f58ab2a909d19e8e4f
  postgres:
    image: docker.io/harlenalvarez/huddle-postgres:2026.10.23-1@sha256:3a4fd7da9bbc2c1b7e8482d9d239fb6d336283a17b1da358d0db43e838d6d3a2
  redis:
    image: docker.io/library/redis:8-alpine@sha256:0bf6577d7774e592b29d093e3ac9390eb91e13d72c21e506afce736de5e0eeb0
  schedule-live-worker:
    image: docker.io/harlenalvarez/huddle-api:2026.10.23-1@sha256:6adc15fec15e7bd6490b1ae822d6cd0e5443aeb97ce296f58ab2a909d19e8e4f
  schedule-publisher:
    image: docker.io/harlenalvarez/huddle-api:2026.10.23-1@sha256:6adc15fec15e7bd6490b1ae822d6cd0e5443aeb97ce296f58ab2a909d19e8e4f
  scheduled-task-worker:
    image: docker.io/harlenalvarez/huddle-api:2026.10.23-1@sha256:6adc15fec15e7bd6490b1ae822d6cd0e5443aeb97ce296f58ab2a909d19e8e4f
  web:
    image: docker.io/harlenalvarez/huddle-web:2026.10.23-1@sha256:650c718f9cc6335ff8849885d803e17592bc9c49a759f2747ca4ee5af6eb1c2a
  web-renderer:
    image: docker.io/harlenalvarez/huddle-web-renderer:2026.10.23-1@sha256:71f6fba5ccce5678ea72e79840ac864ee8ac84113db0d075534a47f3ee6a9e15
  zitadel:
    image: docker.io/library/caddy:2-alpine@sha256:eb4d35b7452d1ab6171fbdbe8d9275b2301dad6dad92115d91e19fb9b24d510f
  zitadel-api:
    image: ghcr.io/zitadel/zitadel:v4.19.3@sha256:d8430210beea5319bce553a0e6ff77167e30633535c3e072938c6b675f3c115e
  zitadel-db-init:
    image: docker.io/harlenalvarez/huddle-postgres:2026.10.23-1@sha256:3a4fd7da9bbc2c1b7e8482d9d239fb6d336283a17b1da358d0db43e838d6d3a2
  zitadel-login:
    image: ghcr.io/zitadel/zitadel-login:v4.19.3@sha256:f4fb0eb8acb474e8abba0698dfb69b8b67e0856f5cda9e787c2c67b8205976f3

Docker commands

docker compose -f oci://harlenalvarez/huddle-stack:latest up

Use the above command to pull and run the Compose file. Learn more⁠.

Images used

Image + 1 more

Redis is the world’s fastest data platform for caching, vector search, and NoSQL databases.


Pulls

1B+

Stars

13624

Last Updated

1 day

Image

Caddy 2 is a powerful, enterprise-ready, open source web server with automatic HTTPS written in Go.


Pulls

500M+

Stars

993

Last Updated

1 day

Image + 1 more

NATS is an open-source, high-performance, cloud native messaging system.


Pulls

100M+

Stars

522

Last Updated

1 day

Image

Client for Cloudflare Tunnel, a daemon that exposes private services through the Cloudflare edge.


Pulls

100M+

Stars

591

Last Updated

3 days

Image

Lightweight container with NATS and NATS Streaming utilities


Pulls

50M+

Stars

5

Last Updated

3 days

Image


Pulls

2.5K

Stars

0

Last Updated

about 6 hours

Image


Pulls

2.2K

Stars

0

Last Updated

about 6 hours

Image


Pulls

1.8K

Stars

0

Last Updated

about 6 hours

Image


Pulls

991

Stars

0

Last Updated

about 6 hours

Image


Pulls

0

Stars

0

Last Updated

about 6 hours