02 / Install & self-host

Quickstart & Self-Hosting

On this page 17

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.

shell
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.

shell
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-ai profile
  • ~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 (+ 1025 SMTP), 3307, 6333, 9999, 11435 (local-ai profile only), 10200 (TTS, localhost-only), 8080/8443 (oidc profile only)

Apple Silicon (M1–M4) — build, don't pull. make up builds the backend and worker locally from a multi-arch base image, so PHP/FrankenPHP runs natively on arm64 — that is the default and by far the fastest setup. (The published ghcr.io/metadist/synaplan image 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:

shell
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 AI bge-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:
shell
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 (import them with Models & keys → Local AI → Import pulled models):
text
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.)

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:

  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:

text
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).

shell
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.

shell
# 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 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-platform repository.

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:

shell
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:

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:

yaml
  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:

cron
* * * * * 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 development compose starts the same scheduler role 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#

shell
# 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:

shell
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.

shell
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#