Quickstart & Self-Hosting

Get a working Synaplan running on your own machine — and skip the five things that trip people up most: the local-AI model download, Qdrant, Text-to-Speech, the office engine (Collabora), and the production database (Galera) cluster.

If you only want to use the API or the widget against the hosted instance, you don't need any of this — grab a key from web.synaplan.com and jump to Code Examples. This page is for running your own instance.


TL;DR — the smooth path

The single most common complaint about self-hosting is "I ran docker compose up and chat didn't work for ten minutes." That's the standard install pulling ~14 GB of local AI models in the background. If you want a working chat in ~2 minutes, start with the minimal install + a cloud key instead, and add local AI later:

git clone https://github.com/metadist/synaplan.git
cd synaplan
docker compose -f docker-compose-minimal.yml up -d

Open http://localhost:5173, log in with [email protected] / admin123, and go to Operate → AI infrastructure → Models & keys. Paste a cloud key there (free key: console.groq.com) — it's tested live against the provider, stored encrypted in the database, and chat works immediately. No .env editing, no restart.

Scripting the setup instead? Putting GROQ_API_KEY=your_key in backend/.env before the first boot works too — env keys are imported into the encrypted store on first use.

Everything below explains why this is the smooth path, and what each optional service (local models, Qdrant, TTS, the office engine, Galera) actually buys you.


Prerequisites

  • Docker + Docker Compose v2 (Docker Desktop, or Docker Engine + the Compose plugin)
  • Git
  • 8 GB RAM minimum — 16 GB recommended if you run the local-AI (standard) install
  • ~9 GB free disk for the standard install, ~5 GB for minimal
  • Free TCP ports: 5173, 8000, 8082, 8025, 3307, 6333, 11435

Apple Silicon (M1–M4): the images are published for linux/amd64 and run under emulation. In Docker Desktop → Settings → General, enable "Use Rosetta for x86/amd64 emulation on Apple Silicon" for much faster, more stable containers. It works without it — just slower, with a longer first build.


Two ways to install

Mode Command Size Best for
Minimal docker compose -f docker-compose-minimal.yml up -d ~5 GB Cloud AI only (Groq / OpenAI / Anthropic / Gemini). Fast first boot.
Standard docker compose up -d ~9 GB Full features including local AI via Ollama.

Pick minimal for your first run; switch to standard once you actually want offline models. Both modes start the same supporting services (database, Redis, Centrifugo, worker, Qdrant, Tika) — the only difference is whether Ollama is included.

After startup you get:

Service URL
App (Vue SPA) http://localhost:5173
API (Symfony) http://localhost:8000
API Docs (Swagger) http://localhost:8000/api/doc
phpMyAdmin http://localhost:8082
MailHog (email testing) http://localhost:8025

Default logins: [email protected] / admin123 (ADMIN) · [email protected] / demo123 (PRO) · [email protected] / test123 (unverified).


Pitfall 1 — the local-AI model download

This is the big one. The standard install runs an ollama container and, on first boot, pulls two models in the background:

Model Role Approx size
gpt-oss:20b Local chat large
bge-m3 Embeddings (RAG) smaller

Together that's ~14 GB. The web UI is up in ~2 minutes, but chat that uses a local model only starts working once the download finishes — which on a normal connection can take a while. The UI looking ready before the models are ready is exactly what makes this feel broken when it isn't.

Watch the progress instead of guessing:

docker compose logs -f backend     # shows model pull progress
docker compose logs -f ollama      # raw Ollama download logs

Ways to avoid the wait:

  • Use the minimal install and a cloud provider (the TL;DR above). No local download at all — chat works as soon as the key is set.
  • Pull a smaller model yourself after startup and select it in the admin panel:
docker compose exec ollama ollama pull llama3.2:3b
  • Point at an existing Ollama on your host instead of the bundled container — set this in backend/.env and you reuse models you already downloaded:
OLLAMA_BASE_URL=http://host.docker.internal:11434

Adding a cloud key after the fact? Open Operate → AI infrastructure → Models & keys in the UI, paste the key, and it takes effect immediately — validated live, stored encrypted in the database, no restart. The new models then appear in the model selector. (Setting env vars like GROQ_API_KEY in backend/.env still works too: they are imported into the encrypted store on first use.)


Pitfall 2 — do I have to manage Qdrant?

No. Qdrant starts automatically with both compose files and needs zero configuration locally. You do not run, install, or babysit it yourself — it's just another container in the stack.

What it powers:

  1. RAG document search — semantic search over indexed documents
  2. AI Memories — long-term user context and profiling
  3. Feedback system — false-positive detection and learning

And the key relief: Synaplan runs fully without it. If Qdrant is unavailable, memories and vector search are simply disabled — nothing else breaks. For basic vector operations MariaDB VECTOR can serve as an alternative, so a minimal deployment doesn't strictly need a separate vector database at all.

In short: leave the default Qdrant container running and forget about it, or drop it and fall back to MariaDB VECTOR. Either way there's nothing to manage by hand.

The connection is controlled by a single variable if you ever point at an external Qdrant:

QDRANT_URL=http://qdrant:6333

Pitfall 3 — Text-to-Speech is optional and separate

Voice output (TTS) is not part of the core stack. It lives in synaplan-tts (image) so the main install stays small and you can run the speaker on another host. The image already contains five voices (en, de, es, fr, tr). Synaplan auto-enables voice output when the service answers — nothing to toggle. The frontend language selects the voice (detected reply language wins).

# Same compose file
docker compose --profile tts up -d

# Or the published image on this host / another machine
docker run -d --name synaplan-tts -p 127.0.0.1:10200:10200 \
  ghcr.io/metadist/synaplan-tts:latest

Synaplan finds it via SYNAPLAN_TTS_URL (default http://host.docker.internal:10200). Prefer a cloud voice? Set ELEVENLABS_API_KEY or pick a catalog TTS model instead.

Full install, why it is separate, and how to add voices: Text-to-Speech.


Pitfall 4 — Office documents need LibreOffice (Collabora sidecar)

Thumbnails, Download as PDF, inline preview, officemaker PDF output, and conversion of legacy / Apple formats need a Collabora CODE sidecar — not apt install libreoffice on the host, and not Desktop's local LibreOffice. The profile is off by default so docker compose up -d does not grow RAM or pull collabora/code.

# Dev — compose defaults OFFICE_CONVERT_URL to http://collabora:9980
docker compose --profile office up -d

Self-host (deploy/compose.yaml): set COMPOSE_PROFILES=office in deploy/.env (not backend/.env). An existing CODE instance: set OFFICE_CONVERT_URL to that base URL and skip the profile. Collabora never sees Synaplan users — convert-to is server-to-server. Full detail: Office documents.


Pitfall 5 — the database (Galera) cluster is a production concern only

For local development and single-server deployments you use the single MariaDB container that ships in the compose files. There is no Galera cluster to set up to try Synaplan, develop against it, or run a modest instance. Don't let the production architecture scare you off the quickstart.

The MariaDB Galera cluster only enters the picture for high-availability, multi-node production, where it's the reference setup for a synchronously-replicated database behind several backend nodes. It is deliberately kept out of the default install. Two things make clustering far less painful than it sounds:

  • Synaplan already uses a read/write split (DATABASE_WRITE_URL and DATABASE_READ_URL), so pointing writes at the primary and reads at replicas/the cluster endpoint is a config change, not a code change.
  • Redis carries the cross-node coordination (sessions, locks, queues, realtime fan-out), so the database cluster doesn't have to. Scaling out is "add a node, point it at the same Redis and database" — see Architecture & Realtime.

When you're actually ready for HA, use the maintained deployment configs rather than hand-rolling Galera:

Clustered-realtime checklist (the parts people forget): one shared Redis for every node, identical REALTIME_TOKEN_SECRET and REALTIME_API_KEY across nodes, a WebSocket-friendly load balancer (forward Upgrade, idle timeout > 60s), and a real REALTIME_ALLOWED_ORIGINS (never *). Full detail in Architecture & Realtime.


Saved Tasks & the scheduler — one extra container (or one cron line)

Saved Tasks let users pin an AI instruction — including a full multi-step plan like "make an image and save it to Nextcloud" — and run it on a schedule (every 15/30/60 minutes, daily, or on selected weekdays). The tasks are stored in the database; something still has to fire the due ones. That is the console command:

php bin/console app:saved-tasks:tick

Each invocation claims and runs all currently due tasks, then exits. If you host Synaplan yourself, wire it up one of two ways:

Option A — the scheduler role (recommended)

The published image has a built-in scheduler mode: start one extra container from the same image with SYNAPLAN_ROLE=scheduler and the same configuration as your backend:

  scheduler:
    image: ghcr.io/metadist/synaplan:latest
    environment:
      SYNAPLAN_ROLE: scheduler
    env_file: .env                            # same DB/Redis config as backend
    volumes:
      - ./up/:/var/www/backend/var/uploads    # same uploads dir (reapers need it)
    restart: unless-stopped

It loops every 60 seconds (configurable via SYNAPLAN_SCHEDULER_TICK_SECONDS) and runs the whole maintenance suite for you: the Saved Tasks tick, the media-job reaper, an hourly ephemeral-file cleanup and a daily update check. It waits for the web container's migrations before starting and writes a heartbeat for its health check.

Option B — classic host cron

If you prefer cron, call the tick through the running backend container:

* * * * * cd /opt/synaplan && docker compose exec -T backend \
  php bin/console app:saved-tasks:tick --no-interaction >> /var/log/synaplan-tick.log 2>&1

Every minute is the right cadence: daily/weekly schedules carry an HH:MM time, so a coarser cron makes runs late. A 5-minute cron also works if a few minutes of drift are acceptable.

Good to know

  • Safe to run redundantly. The tick takes a cross-node Redis lock and claims each task with a database compare-and-set — run it on every node of a cluster (or keep both a scheduler container and a cron) and nothing executes twice.
  • Gated by a feature flag. While SAVEDTASKS / ENABLED is off, the tick is a no-op. New installs are seeded with the flag on.
  • Testing locally? The dev compose has no scheduler service — fire due tasks by hand: docker compose exec -T backend php bin/console app:saved-tasks:tick.
  • Failure handling is built in. A task that fails three consecutive runs is auto-paused and its owner is emailed; every run is recorded in the task's run history under Manage → Automations → Saved tasks.

Verify it's healthy

# All containers up?
docker compose ps

# Redis is mandatory infrastructure — this must report PONG
docker compose exec redis redis-cli ping

# Health endpoint (reports Redis availability among others)
curl -s http://localhost:8000/api/health

# Background worker should show messenger:consume activity
docker compose logs -f worker

If /api/health returns 503, Redis is unreachable — verify the redis container is up and REDIS_DSN is correct. More symptoms and fixes are in the Developer FAQ → Common Issues.


Configuration in one place

AI provider keys are managed in the app itself: Operate → AI infrastructure → Models & keys validates each key live, stores it encrypted in the database, and applies it without a restart.

Everything else lives in backend/.env, created from a documented template:

cp backend/.env.example backend/.env

You only need the defaults already in the example to get a working install — everything else (email, channels, realtime secrets) is opt-in. Provider keys set as env vars are imported into the encrypted store on first use, so orchestrated deploys keep working. The Developer FAQ breaks down each section and how to add provider keys.


Next steps