# Synaplan lite quickstart: the smallest published-image stack (~3.2 GB). # # docker compose -f compose.lite.yaml up -d then open http://127.0.0.1:8000 # # Starts six runtime containers (backend, worker, scheduler, db, redis, tika) # plus the one-shot secrets-init helper. Spoken answers (tts), Qdrant (memories; # document search stays on MariaDB VECTOR), and live widget takeover (centrifugo) # are omitted to save ~2 GB of images and RAM. Same volume names as compose.yaml, # so switching between compose.lite.yaml and compose.yaml keeps every chat and file. x-app-image: &app-image ghcr.io/metadist/synaplan:${SYNAPLAN_VERSION:-5.6.4} x-app-command: &app-command - | set -a . /run/synaplan-secrets/secrets.env set +a export DB_PASSWORD="$$MARIADB_PASSWORD" exec /usr/local/bin/docker-entrypoint.sh x-app-environment: &app-environment APP_ENV: prod APP_DEBUG: "0" FRANKENPHP_WORKER_NUM: ${FRANKENPHP_WORKER_NUM:-16} FRANKENPHP_STREAM_WORKER_NUM: ${FRANKENPHP_STREAM_WORKER_NUM:-} FRANKENPHP_NUM_THREADS: ${FRANKENPHP_NUM_THREADS:-} FRANKENPHP_MAX_THREADS: ${FRANKENPHP_MAX_THREADS:-} FRANKENPHP_MAX_WAIT_TIME: ${FRANKENPHP_MAX_WAIT_TIME:-15s} SYNAPLAN_PLATFORM: lite APP_VERSION: ${SYNAPLAN_VERSION:-5.6.4} APP_URL: ${SYNAPLAN_URL:-http://127.0.0.1:${SYNAPLAN_HTTP_PORT:-8000}} FRONTEND_URL: ${SYNAPLAN_URL:-http://127.0.0.1:${SYNAPLAN_HTTP_PORT:-8000}} REALTIME_ALLOWED_ORIGINS: ${SYNAPLAN_URL:-http://127.0.0.1:${SYNAPLAN_HTTP_PORT:-8000} http://localhost:${SYNAPLAN_HTTP_PORT:-8000}} BOOTSTRAP_ADMIN_EMAIL: ${BOOTSTRAP_ADMIN_EMAIL:-} BOOTSTRAP_ADMIN_PASSWORD: ${BOOTSTRAP_ADMIN_PASSWORD:-} REGISTRATION_ENABLED: ${REGISTRATION_ENABLED:-} GUEST_CHAT_ENABLED: ${GUEST_CHAT_ENABLED:-} FEATURE_FREEAPI_ENABLED: ${FEATURE_FREEAPI_ENABLED:-true} FREEAPI_BASE_URL: ${FREEAPI_BASE_URL:-https://web.synaplan.com} AI_DEFAULT_PROVIDER: ${AI_DEFAULT_PROVIDER:-groq} GROQ_API_KEY: ${GROQ_API_KEY:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GOOGLE_GEMINI_API_KEY: ${GOOGLE_GEMINI_API_KEY:-} MAILER_DSN: ${MAILER_DSN:-null://null} APP_SENDER_EMAIL: ${APP_SENDER_EMAIL:-} APP_SENDER_NAME: Synaplan DB_HOST: db DB_PORT: "3306" DB_NAME: synaplan DB_USER: synaplan DB_SERVER_VERSION: mariadb-12.3.2 REDIS_DSN: redis://redis:6379 LOCK_DSN: redis://redis:6379 REALTIME_ENABLED: "false" REALTIME_API_URL: "" REALTIME_PUBLIC_WS_URL: "" QDRANT_URL: "" TIKA_BASE_URL: http://tika:9998 SYNAPLAN_TTS_URL: "" OLLAMA_BASE_URL: "" AUTO_DOWNLOAD_MODELS: "false" WHISPER_ENABLED: "false" FFMPEG_BINARY: /usr/bin/ffmpeg LOG_FORMAT: json services: secrets-init: image: alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a restart: "no" environment: APP_SECRET: ${APP_SECRET:-} TOKEN_SECRET: ${TOKEN_SECRET:-} MARIADB_PASSWORD: ${MARIADB_PASSWORD:-} MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-} REALTIME_API_KEY: ${REALTIME_API_KEY:-} REALTIME_TOKEN_SECRET: ${REALTIME_TOKEN_SECRET:-} REALTIME_ADMIN_PASSWORD: ${REALTIME_ADMIN_PASSWORD:-} REALTIME_ADMIN_SECRET: ${REALTIME_ADMIN_SECRET:-} SECRETS_INIT_DB_DIR: /db SECRETS_INIT_FILE_LABEL: the secrets volume (secrets.env) volumes: - secrets:/data - secrets-app:/out/app - secrets-db:/out/db - secrets-realtime:/out/realtime - db:/db:ro entrypoint: ["/bin/sh", "-ec"] command: - | # SECRETS_INIT_SCRIPT_START # Resolve the eight deployment secrets once, then publish a separate file for # each consumer. data/secrets.env is the backup copy and is never rewritten. # A replace-with-* example value is refused. Nothing is written in that case. # A file that already exists but is missing a key is refused too: a value that # lives only in .env would not be in the backup. # # The published files are sourced by the other containers, so each value is # written as a single-quoted shell literal. The backup file stays raw # KEY=value lines, which is what a restore reads back. # # The same script is inlined in deploy/compose.yaml and # deploy/quickstart/compose.yaml (every "$$" doubled) so neither install needs # this path. deploy/scripts/tests/test-secrets-init.sh fails when a copy differs. set -eu DATA="$${SECRETS_INIT_DATA:-/data}" SECRETS="$$DATA/secrets.env" # The quickstart keeps the database in its own volume, not under $$DATA. DB_DIR="$${SECRETS_INIT_DB_DIR:-$$DATA/mariadb}" # Where the person reading an error finds the backup copy. SECRETS_NAME="$${SECRETS_INIT_FILE_LABEL:-data/secrets.env}" # Each consumer mounts only its own directory. The web container never sees # the database root password or the realtime admin credentials. APP_OUT="$${SECRETS_INIT_APP_OUT:-/out/app/secrets.env}" DB_OUT="$${SECRETS_INIT_DB_OUT:-/out/db/secrets.env}" REALTIME_OUT="$${SECRETS_INIT_REALTIME_OUT:-/out/realtime/secrets.env}" KEYS="APP_SECRET TOKEN_SECRET MARIADB_PASSWORD MARIADB_ROOT_PASSWORD REALTIME_API_KEY REALTIME_TOKEN_SECRET REALTIME_ADMIN_PASSWORD REALTIME_ADMIN_SECRET" APP_KEYS="APP_SECRET TOKEN_SECRET MARIADB_PASSWORD REALTIME_API_KEY REALTIME_TOKEN_SECRET" DB_KEYS="MARIADB_PASSWORD MARIADB_ROOT_PASSWORD" REALTIME_KEYS="REALTIME_API_KEY REALTIME_TOKEN_SECRET REALTIME_ADMIN_PASSWORD REALTIME_ADMIN_SECRET" read_key() { file=$$1 key=$$2 val="" while IFS= read -r line || [ -n "$$line" ]; do case "$$line" in ''|\#*) continue ;; esac case "$$line" in "$$key"=*) val=$${line#*=} ;; esac done < "$$file" printf '%s' "$$val" } # One character at a time, so a secret containing *, ? or [ is not treated as a # pattern. A single quote becomes the four-character sequence '\'' . shell_quote() { input=$$1 quoted="'" while [ -n "$$input" ]; do case "$$input" in \'*) quoted="$${quoted}'\\''" ;; *) quoted="$${quoted}$${input%"$${input#?}"}" ;; esac input=$${input#?} done printf '%s' "$${quoted}'" } stack_initialised() { [ -d "$$DB_DIR" ] || return 1 find "$$DB_DIR" -mindepth 1 -print -quit 2>/dev/null | grep -q . } is_placeholder() { case "$$1" in replace-with-*) return 0 ;; esac return 1 } is_adoptable() { case "$$1" in *'$$'*|*' #'*) return 1 ;; \"*\"|\'*\') return 1 ;; [[:space:]]*|*[[:space:]]) return 1 ;; esac return 0 } generate_secret() { val=$$(hexdump -vn 32 -e '/1 "%02x"' /dev/urandom) case "$$val" in *[!0-9a-f]*) val="" ;; esac if [ "$${#val}" -ne 64 ]; then echo "Synaplan did not start and created nothing: a random secret could not be generated." >&2 exit 1 fi printf '%s' "$$val" } refuse() { echo "Synaplan did not start and created nothing: $$1" >&2 exit 1 } list_keys() { printf '%s' "$$1" | sed 's/^ //; s/ /, /g' } publish_quoted() { dest=$$1 key_list=$$2 dest_dir=$$(dirname "$$dest") mkdir -p "$$dest_dir" chmod 755 "$$dest_dir" umask 022 tmp="$$dest_dir/.secrets.env.$$$$" { printf '%s\n' '# Copy for this start. Do not edit.' for key in $$key_list; do printf '%s=%s\n' "$$key" "$$(shell_quote "$$(read_key "$$SECRETS" "$$key")")" done } > "$$tmp" mv "$$tmp" "$$dest" chmod 644 "$$dest" } file_existed=false if [ -f "$$SECRETS" ]; then file_existed=true fi initialised=false if stack_initialised; then initialised=true fi placeholders="" file_placeholders="" unadoptable="" unresolved="" assignments="" for key in $$KEYS; do val="" if [ "$$file_existed" = true ]; then val=$$(read_key "$$SECRETS" "$$key") if [ -z "$$val" ]; then unresolved="$$unresolved $$key" continue fi if is_placeholder "$$val"; then file_placeholders="$$file_placeholders $$key" continue fi else envval=$$(printenv "$$key" || true) if [ -n "$$envval" ]; then if is_placeholder "$$envval"; then placeholders="$$placeholders $$key" continue fi if ! is_adoptable "$$envval"; then unadoptable="$$unadoptable $$key" continue fi val=$$envval fi if [ -z "$$val" ]; then if [ "$$initialised" = true ]; then unresolved="$$unresolved $$key" continue fi val=$$(generate_secret) fi fi assignments="$${assignments}$${key}=$${val} " done if [ -n "$$placeholders" ]; then refuse "$$(list_keys "$$placeholders") still has the example value. Delete that line so Synaplan generates one, or set it to the output of \"openssl rand -hex 32\"." fi if [ -n "$$file_placeholders" ]; then refuse "$$(list_keys "$$file_placeholders") in $$SECRETS_NAME still has the example value. Replace it with the output of \"openssl rand -hex 32\". The file was not rewritten." fi if [ -n "$$unadoptable" ]; then refuse "$$(list_keys "$$unadoptable") cannot be read unchanged. Use only letters, digits, '-' and '_', or delete the line so Synaplan generates one." fi if [ -n "$$unresolved" ]; then if [ "$$file_existed" = true ]; then refuse "$$(list_keys "$$unresolved") has no value in $$SECRETS_NAME. That file was not changed. Add the original value there, or restore the file from a backup. A value that exists only in .env is not used once this file exists." fi refuse "$$(list_keys "$$unresolved") has no value, and this install already has a database. Restore $$SECRETS_NAME from the backup of this install, or put the original values back. A new password would not open the existing database." fi if [ "$$file_existed" = false ]; then mkdir -p "$$DATA" chmod 0700 "$$DATA" umask 077 tmp="$$DATA/.secrets.env.$$$$" { printf '%s\n' '# Synaplan deployment secrets. Generated once. Back this file up with the database. Do not commit it.' printf '%s' "$$assignments" } > "$$tmp" for key in $$KEYS; do got=$$(read_key "$$tmp" "$$key") if ! printf '%s\n' "$$assignments" | grep -Fxq "$${key}=$${got}"; then rm -f "$$tmp" refuse "the generated secrets file could not be read back. Nothing was kept." fi done mv "$$tmp" "$$SECRETS" chmod 600 "$$SECRETS" fi umask 077 publish_quoted "$$APP_OUT" "$$APP_KEYS" publish_quoted "$$DB_OUT" "$$DB_KEYS" publish_quoted "$$REALTIME_OUT" "$$REALTIME_KEYS" # SECRETS_INIT_SCRIPT_END backend: image: *app-image entrypoint: ["/bin/bash", "-ec"] command: *app-command environment: <<: *app-environment SYNAPLAN_ROLE: web volumes: - uploads:/var/www/backend/var/uploads - secrets-app:/run/synaplan-secrets:ro ports: - ${SYNAPLAN_HTTP_BIND:-127.0.0.1}:${SYNAPLAN_HTTP_PORT:-8000}:80 depends_on: secrets-init: condition: service_completed_successfully db: condition: service_healthy redis: condition: service_healthy tika: condition: service_healthy healthcheck: test: ["CMD", "/usr/local/bin/container-healthcheck"] interval: 15s timeout: 10s retries: 5 start_period: 300s restart: unless-stopped worker: image: *app-image entrypoint: ["/bin/bash", "-ec"] command: *app-command environment: <<: *app-environment SYNAPLAN_ROLE: worker volumes: - uploads:/var/www/backend/var/uploads - secrets-app:/run/synaplan-secrets:ro depends_on: secrets-init: condition: service_completed_successfully backend: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "/usr/local/bin/container-healthcheck"] interval: 30s timeout: 5s retries: 3 start_period: 120s restart: unless-stopped scheduler: image: *app-image entrypoint: ["/bin/bash", "-ec"] command: *app-command environment: <<: *app-environment SYNAPLAN_ROLE: scheduler volumes: - uploads:/var/www/backend/var/uploads - secrets-app:/run/synaplan-secrets:ro depends_on: secrets-init: condition: service_completed_successfully backend: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "/usr/local/bin/container-healthcheck"] interval: 60s timeout: 5s retries: 3 start_period: 120s restart: unless-stopped db: image: mariadb:13.0.2@sha256:f4054da26e3a9dd6c8b5cc97363e544408b15c0fb369d7359c25dc04826eb6e1 entrypoint: ["/bin/sh", "-ec"] command: - | set -a . /run/synaplan-secrets/secrets.env set +a exec docker-entrypoint.sh --max-allowed-packet=256M --innodb-buffer-pool-size=512M environment: MARIADB_DATABASE: synaplan MARIADB_USER: synaplan volumes: - db:/var/lib/mysql - secrets-db:/run/synaplan-secrets:ro depends_on: secrets-init: condition: service_completed_successfully healthcheck: test: ["CMD-SHELL", "set -a; . /run/synaplan-secrets/secrets.env; set +a; mariadb-admin ping -h 127.0.0.1 -uroot -p\"$$MARIADB_ROOT_PASSWORD\" --silent"] interval: 15s timeout: 10s retries: 10 start_period: 60s restart: unless-stopped redis: image: redis:7.4-alpine@sha256:8da19c7399dca900c6822a1cf30f9d2285585e2c4b457ff4095b0f4500054d3f command: redis-server --appendonly yes --appendfsync everysec --maxmemory 256mb --maxmemory-policy allkeys-lru volumes: - redis:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 15s timeout: 5s retries: 5 start_period: 5s restart: unless-stopped tika: image: apache/tika:3.3.3.0@sha256:326b238b37904c94ff61fed87afe0af72e6a29fa715933b10af97b7d56f1d4cb command: -h 0.0.0.0 -p 9998 healthcheck: test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/localhost/9998 && printf 'GET /version HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n' >&3 && read -r status <&3 && [[ $$status == *'200 OK'* ]]"] interval: 30s timeout: 10s retries: 5 start_period: 60s restart: unless-stopped volumes: db: redis: qdrant: uploads: secrets: secrets-app: secrets-db: secrets-realtime: