# Docker Deployment Run SynaptoMind as a Docker container for production or development. --- ## Quick Start ```bash git clone https://github.com/zumik3-del/synaptomind.git && cd synaptomind cp config.json.example config.json # required: compose bind-mounts ./config.json cp .env.example .env # then set SYNAPTOMIND_SECRET in .env docker compose up -d ``` Both files must exist before `docker compose up`: `docker-compose.yml` bind-mounts `./config.json` and `./data`. Without `config.json`, Docker creates a directory at that path and the server refuses to start (`config.json … is a directory`). `.env.example` deliberately ships no secret — set `SYNAPTOMIND_SECRET` in `.env`, or the server fails closed and rejects authenticated requests with `401` (see [CONFIG.md](CONFIG.md#authentication)). Server starts on `http://127.0.0.1:3005`. MCP endpoint: `http://127.0.0.1:3006/mcp`. --- ## Production Deployment ### Using published image (recommended) Edit `docker-compose.yml`: ```yaml services: synaptomind: image: ghcr.io/zumik3-del/synaptomind:latest # build: . # comment out for production ``` ```bash docker compose pull && docker compose up -d ``` ### Systemd install (not a container path) The `deploy/` framework installs the published binary under systemd by default (`DIST="binary"` in `deploy/app.env`); it does **not** manage containers. For that path see [DEPLOY.md](DEPLOY.md). This guide covers Docker only — manage the stack with `docker compose`. --- ## Configuration ### Environment variables Create `.env` file: ```bash cp .env.example .env ``` | Variable | Default | Description | |----------|---------|-------------| | `SYNAPTOMIND_SECRET` | (required) | Auth token for the HTTP ops endpoints and MCP | | `SYNAPTOMIND_PORT` | 3005 | HTTP ops port (health, health-check) | | `SYNAPTOMIND_HOST` | 0.0.0.0 (in container) | Server bind address. Pinned to `0.0.0.0` by `docker-compose.yml` so published ports are reachable from the host | | `SYNAPTOMIND_MCP_HTTP_PORT` | 3006 | MCP HTTP transport port | | `SYNAPTOMIND_DB_PATH` | ./data/synaptomind.db | SQLite database path | | `SYNAPTOMIND_EMBEDDER_MODEL` | Xenova/multilingual-e5-small | Embedding model | | `BIND_ADDR` | 127.0.0.1 | Docker port bind address (host side; default keeps ports local-only) | Exposure model: inside the container the server binds `0.0.0.0`; what the host — and therefore the network — can reach is controlled by `BIND_ADDR`. Without `SYNAPTOMIND_SECRET` in single-user mode (or a per-user token when `config.json` has a `users` list), the server fails closed: authenticated API and MCP requests are rejected with `401`. `SYNAPTOMIND_ALLOW_INSECURE=true` disables auth for local development only (single-user mode). For full configuration reference (all settings, env vars, defaults), see [CONFIG.md](CONFIG.md). ### Config file Mount a custom `config.json`: ```yaml volumes: - ./config.json:/app/config.json:ro ``` Priority: env vars > config.json > defaults. See `config.json.example` for all options. **Port resolution:** the effective HTTP ops port is `SYNAPTOMIND_PORT` > `config.json` `server.port` > `3005` (the same rule the server applies, and what `update.sh` uses to locate `/health`). Inside the container the port is pinned by `docker-compose.yml` (`3005:3005`) and the image healthcheck; if you change the port you must update both to match, or the container will report unhealthy. ### Exposing to network By default, ports bind to `127.0.0.1` (local only). To expose to network: ```bash echo "BIND_ADDR=0.0.0.0" >> .env docker compose up -d ``` Or edit `docker-compose.yml` directly: ```yaml ports: - "0.0.0.0:3005:3005" - "0.0.0.0:3006:3006" ``` --- ## Updating ### Update the container The `deploy/` framework targets systemd, not containers. Update a Docker install with the manual procedure below. ### Manual update The compose file resolves the image via `SYNAPTOMIND_IMAGE` (see `docker-compose.yml`): ```bash cd /opt/synaptomind git fetch --tags -f origin git checkout -f v0.6.1 # force: discard diverged tracked files export SYNAPTOMIND_IMAGE=ghcr.io/zumik3-del/synaptomind:0.6.1 # optional: persist for later manual `docker compose up` runs grep -q '^SYNAPTOMIND_IMAGE=' .env && sed -i "s|^SYNAPTOMIND_IMAGE=.*|SYNAPTOMIND_IMAGE=${SYNAPTOMIND_IMAGE}|" .env || echo "SYNAPTOMIND_IMAGE=${SYNAPTOMIND_IMAGE}" >> .env sudo chown -R 10001:10001 data # required once when upgrading from root-run images docker compose pull 2>/dev/null || true docker compose up -d ``` ### Rollback Schema migrations are **forward-only** (`src/db/init.ts:127`): the server applies every migration it has not seen yet and never reverses one. Running an older tag against a database that a newer version has already migrated is therefore **unsafe** — the old code cannot undo the schema change and may operate on tables or columns it does not understand. The supported rollback is: restore the database backup taken *before* the upgrade, then run the older version. Take one first (see [Backup](#backup)): ```bash # 1. Before upgrading: stop the container and keep the current database docker compose stop cp data/synaptomind.db data/synaptomind.db.preupgrade docker compose start # 2. Upgrading happens here (deploy framework or manual) # ... # 3. Roll back: stop, restore the pre-upgrade database, deploy the older tag cd /opt/synaptomind docker compose stop cp data/synaptomind.db.preupgrade data/synaptomind.db rm -f data/synaptomind.db-wal data/synaptomind.db-shm git fetch --tags -f origin git checkout -f v0.6.0 SYNAPTOMIND_IMAGE=ghcr.io/zumik3-del/synaptomind:0.6.0 docker compose pull && docker compose up -d ``` ### Check running version ```bash curl -s http://127.0.0.1:3005/health | jq -r .version # {"status":"ok","version":"0.6.1","checks":{"database":"ok","embedder":"ok"}} ``` --- ## Data Volumes Archived thoughts are retained until the retention-review process removes them; retention-review is the sole deletion path. There is no TTL-based cleanup. | Container path | Host mount | Description | |----------------|------------|-------------| | `/app/data` | `./data` | Database + embeddings cache | | `/app/config.json` | `./config.json` | Configuration (read-only) | ### Database files - `data/synaptomind.db` — main database (thoughts, edges, projects) - `data/huggingface/` — cached embedding models (~113 MB with the default `q8` dtype, ~449 MB with `fp32`, after first run) --- ## Backup ```bash # Stop container docker compose stop # Backup database cp data/synaptomind.db data/synaptomind.db.bak # Restart docker compose start ``` Or hot backup without stopping: ```bash docker compose exec synaptomind sqlite3 /app/data/synaptomind.db ".backup '/app/data/backup.db'" docker compose cp synaptomind:/app/data/backup.db ./backup.db ``` --- ## Health Check ```bash curl http://127.0.0.1:3005/health ``` Response: ```json { "status": "ok", "version": "0.6.1", "checks": { "database": "ok", "embedder": "ok" } } ``` Docker health check is configured — container shows `(healthy)` in `docker compose ps`. --- ## Logs Application logs are written to stdout/stderr (warning and error to stderr, info/debug to stdout) and, when configured, an optional size-rotated file via `log.file`/`SYNAPTOMIND_LOG_FILE`. ```bash # Follow logs docker compose logs -f # Last 100 lines docker compose logs --tail 100 # Since specific time docker compose logs --since 10m ``` --- ## Troubleshooting ### Container won't start ```bash docker compose logs ``` Common issues: - Missing `.env` file → create with `SYNAPTOMIND_SECRET=your-token` - Port already in use → change `BIND_ADDR` or ports in `docker-compose.yml` - Permission denied on `./data` → `chmod 777 ./data` ### Embedder not loading First run downloads the model (~113 MB with the default `q8` dtype, ~449 MB with `fp32`). Check progress: ```bash docker compose logs -f | grep -i embedder ``` ### Reset database ```bash docker compose stop rm -f data/synaptomind.db data/synaptomind.db-wal data/synaptomind.db-shm docker compose start ``` ### Container shows unhealthy ```bash docker compose exec synaptomind curl -s http://127.0.0.1:3005/health ``` Check if the process is running: ```bash docker compose exec synaptomind ps aux ``` --- ## Docker Compose Reference ```yaml services: synaptomind: image: ${SYNAPTOMIND_IMAGE:-ghcr.io/zumik3-del/synaptomind:local} build: context: . args: VERSION: ${GIT_DESCRIBE:-dev} COMMIT_SHA: ${GIT_COMMIT:-} container_name: synaptomind restart: unless-stopped ports: - "${BIND_ADDR:-127.0.0.1}:3005:3005" - "${BIND_ADDR:-127.0.0.1}:3006:3006" volumes: - ./data:/app/data - ./config.json:/app/config.json:ro env_file: - .env environment: - SYNAPTOMIND_DB_PATH=/app/data/synaptomind.db - SYNAPTOMIND_EMBEDDER_CACHE_DIR=/app/data/huggingface healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1:3005/health"] interval: 30s timeout: 5s retries: 3 start_period: 10s ``` This mirrors `docker-compose.yml`; `SYNAPTOMIND_IMAGE` defaults to a local build from source — set it to a published GHCR tag to run a release image (see [Using published image](#using-published-image-recommended)).