02 / Install & self-host
Quickstart & Self-Hosting
On this page 17
- TL;DR — the smooth path
- Closed network
- Prerequisites
- Install options
- Pitfall 1 — the local-AI model download
- Pitfall 2 — do I have to manage Qdrant?
- Pitfall 3 — Text-to-Speech starts with the stack
- Pitfall 4 — Office documents need LibreOffice (Collabora sidecar)
- Pitfall 5 — the database (Galera) cluster is a production concern only
- Saved Tasks & the scheduler — one extra container (or one cron line)
- Option A — the scheduler role (recommended)
- Option B — classic host cron
- Good to know
- Verify it's healthy
- Configuration in one place
- File work starts with the stack
- Next steps
We produce AI freedom. This page is the longer install: what starts with
make up, what stays off, and the four things that trip people up — the optional
local-AI download, Qdrant, the office engine (Collabora), and a production
Galera cluster. Spoken answers have started with the stack since 4.9.
make up starts the whole standard stack: chat, file work (the compute
sidecar) and spoken answers (Piper TTS) — no profiles, no tokens, no extra .env.
Add one provider key and everything works. Local models stay opt-in (see Pitfall 1).
If you only want to use the API or the widget against the hosted instance, you don't need any of this — sign up at web.synaplan.com with a paid plan or an invitation code, create a key under Apps → API & developers (API keys come with the paid plans) and jump to Code Examples. This page is for running your own instance.
TL;DR — the smooth path#
The standard install is cloud-AI only: no AI weights are downloaded, the first boot is
dominated by the Docker images and npm ci, and chat works as soon as one provider key is
in place. Local models are an opt-in profile you add later.
git clone https://github.com/metadist/synaplan.git
cd synaplan
make up
make up starts the status page on :5173 first, then pulls and starts the rest. A plain docker compose up -d starts the same stack, but :5173 stays silent until every image is pulled.
Open http://localhost:5173 immediately. The status screen shows every boot step and switches to the app when it is ready. Log in with [email protected] / admin123. Until a provider is chosen, chat is already open: the note above the input says your first message starts the Synaplan Demo (no API key, 40 messages/day for 14 days) and where that message will go. Set up Ollama and add your own key in that note open Operate → AI infrastructure → Models & keys (Ollama on your machine, your own AI key, free: console.groq.com). Keys are tested live, stored encrypted, and the next message already uses them.
Closed network#
Opt-in, and off by default. Use it when the network has no route to the public internet and people open Synaplan by this machine's address. Chat needs https://<address>/. The machine creates the certificate.
cp deploy/selfhost.env.example deploy/.env
deploy/scripts/local-tls.sh 10.0.0.15
Any unrouted IPv4 address works (10.x, 172.16–31.x, 192.168.x, 100.64.x, 169.254.x, or a block you assigned and do not announce). The rest of the start is in Local network. A public name keeps your own HTTPS proxy and leaves this off.
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 what each optional service (local models, Qdrant, the office engine, Galera) actually buys you, how spoken answers (on by default) fit in, and how to add or hide each.
Prerequisites#
- Docker + Docker Compose v2 (Docker Desktop, or Docker Engine + the Compose plugin)
- Git
- 8 GB RAM minimum — 16 GB recommended once you add the
local-aiprofile - ~4 GB free disk for the standard install (includes file work + spoken answers; +~1 GB for
local-ai, +~14 GB if you also enable the local chat model) - Free TCP ports:
5173,8000,8082,8025(+1025SMTP),3307,6333,9999,11435(local-aiprofile only),10200(TTS, localhost-only),8080/8443(oidcprofile only)
Apple Silicon (M1–M4) — build, don't pull.
make upbuilds the backend and worker locally from a multi-arch base image, so PHP/FrankenPHP runs natively onarm64— that is the default and by far the fastest setup. (The publishedghcr.io/metadist/synaplanimage is multi-arch too, so pulling it also runs natively.) Two optional dev tools (phpMyAdmin, MailHog) are amd64-only upstream images — if you keep them, enable Docker Desktop → Settings → General → "Use Rosetta for x86/amd64 emulation on Apple Silicon" (macOS 13+) so those two emulate quickly.
Install options#
| Mode | Command | Size | Best for |
|---|---|---|---|
| One-liner | curl -fsSL https://raw.githubusercontent.com/metadist/synaplan/main/install.sh | bash |
~4 GB | Checks prerequisites, fetches and starts the standard stack (--mode server for production) |
| Standard | make up |
~4 GB | Local try-out: status page first, then the rest — chat, file work, spoken answers; add one provider key and it works |
| + local AI | COMPOSE_PROFILES=local-ai make up |
~5 GB | Adds Ollama and the bge-m3 embedding model on your own hardware (local chat model optional, +~14 GB) |
| Production | install.sh --mode server or the deploy/ contract |
published image | Self-host on a Linux server — Production Deployment |
| Local network | deploy/scripts/local-tls.sh <address> |
opt-in | Closed network, any unrouted IPv4 address — Local network |
| Kubernetes | synaplan-charts | published image | Helm-based cluster deployments |
COMPOSE_PROFILES=local-ai is the same switch a self-hosted install uses in deploy/.env.
Combine profiles with a comma (local-ai,office). On the development stack the remaining
opt-in profiles are local-ai, office, docling, searxng and oidc — file work and
TTS are no longer profiles there, they start with the stack. Production deploy/ additionally
carries file work as an opt-in compute profile (COMPOSE_PROFILES=compute in deploy/.env;
see Run the compute sidecar). The older docker-compose-minimal.yml
still starts a leaner stack but is no longer needed — the standard stack is
the cloud-AI-only stack. (install.sh --minimal is deprecated and simply starts
the standard stack.)
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#
Local AI is opt-in. With COMPOSE_PROFILES=local-ai the stack adds an ollama
container and pulls models in the background:
| Model | Role | When | Approx size |
|---|---|---|---|
bge-m3 |
Embeddings (RAG / semantic search) | always with local-ai |
~1 GB |
gpt-oss:20b |
Local chat | only with ENABLE_LOCAL_GPT_OSS=true |
~14 GB |
The web UI is up long before the pull finishes, and chat that uses a local model only
starts working once the download completes — a GPU or a strong CPU box is recommended for
gpt-oss:20b. The UI looking ready before the models are ready is exactly what makes this
feel broken when it isn't. The boot status screen and the AI provider setup show the
download progress; so do the logs:
docker compose logs -f backend # shows model pull progress
docker compose logs -f ollama # raw Ollama download logs
Ways to avoid the wait:
- Stay cloud-only (the default). Document search then needs an embedding model from a
cloud provider — OpenAI
text-embedding-3, Cloudflare Workers AIbge-m3, or any OpenAI-compatible endpoint you import — bound as the account's embedding default. - Pull a smaller chat model yourself after startup and select it under Manage → Assistants → Models:
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/.envand you reuse models you already downloaded (import them with Models & keys → Local AI → Import pulled models):
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_KEYinbackend/.envstill works too: they are imported into the encrypted store on first use.)
Which models show up in the picker, and how a fully local install stays limited to pulled Ollama models: AI providers & models, Air-gapped installation.
Pitfall 2 — do I have to manage Qdrant?#
No. Qdrant starts automatically with the standard compose file 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:
- RAG document search — semantic search over indexed documents
- AI Memories — long-term user context and profiling
- 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 starts with the stack#
Voice output (TTS) runs in synaplan-tts (image), a separate container 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).
make up # the tts service starts with the stack
docker compose stop tts # hide spoken answers again
Synaplan finds it via SYNAPLAN_TTS_URL (default http://tts:10200, the compose
service). Prefer a cloud voice? Set ELEVENLABS_API_KEY or pick a catalog TTS model instead.
Running the minimal stack
or a standalone speaker on another host? Then the URL points there instead
(http://host.docker.internal:10200 for a container on your laptop,
http://10.0.1.10:10200 for a LAN box).
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
make up 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_URLandDATABASE_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 maintained deployment configs rather than hand-rolling Galera:
- synaplan-charts — public Helm charts for Kubernetes, including the Galera and inference-server wiring.
- The reference multi-node layout (Galera, shared Redis, NFS, GPU and compute nodes) is
described on Hosting Platform; the compose files we run it with live in
the private
synaplan-platformrepository.
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 / ENABLEDis off, the tick is a no-op. New installs are seeded with the flag on. - Testing locally? The development compose starts the same
schedulerrole as production, so due tasks fire on their own. To force a run right now: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. A run that would change something outside Synaplan pauses under Manage → Automations → Approvals until you approve it (Tools & approvals).
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.
Feature flags (people & sharing, assistants, tools & approvals, saved-task steps,
Desktop, linked platforms, module gates, file work) are all on on a fresh install. Switch one off
under Operate → System configuration → Features, or pin it for a deployment with
FEATURE_<GROUP>_<SETTING>=false in backend/.env — see
Administration → Feature flags.
File work starts with the stack#
The assistant can run short Python or Node jobs on copies of files you pick — the
compute sidecar starts with make up, no profile, no token, no image
pin. Compose builds the sidecar and the Python/Node runtimes from the repo, creates
.compute-data/, injects a local token, and turns the feature (plus per-user
workspaces) on. On a new database the seed rows land as on; on an older database
the compose FEATURE_COMPUTE_ENABLED=true pin does the same job.
make up # app + file work + spoken answers
COMPUTE_URL=disabled make up # hide file work again
Never publish port 8080. A production cluster uses a separate gVisor box, not
this sidecar on the web nodes; a single production host adds compute to
COMPOSE_PROFILES in deploy/.env and lets prepare.sh write the token.
Full paths (laptop, one-box production, dedicated host): Run the compute sidecar. What people see: Secure compute.
Next steps#
- Synaplan Demo — how the zero-key onboarding Demo works and how to leave it
- Get your own AI keys — free tiers and setup for Groq, Cerebras, Gemini, Mistral, TrustedTokens, OpenAI and Anthropic
- Run the compute sidecar — file work for developers and operators
- Developer FAQ — install options,
.envreference, adding API keys, troubleshooting - Architecture & Realtime — the full service map, Redis, Centrifugo, clustering
- Code Examples — call the API in cURL, JavaScript, PHP
- Widget Integration — embed the chat widget on any site
- Plugins & Integrations — Synaads, Synaform, TTS, Nextcloud, OpenCloud