Files
stack/docs/guides/deployment.md
Jason Woltje 237a863dfd
Some checks failed
ci/woodpecker/push/ci Pipeline failed
docs(deploy): add deployment guide and expand .env.example (#153)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-15 19:46:38 +00:00

9.1 KiB

Deployment Guide

This guide covers deploying Mosaic in two modes: Docker Compose (recommended for quick setup) and bare-metal (production, full control).


Prerequisites

Dependency Minimum version Notes
Node.js 22 LTS Required for ESM + --experimental-vm-modules
pnpm 9 npm install -g pnpm
PostgreSQL 17 Must have the pgvector extension
Valkey 8 Redis-compatible; Redis 7+ also works
Docker + Compose v2 For the Docker Compose path only

Docker Compose Deployment (Quick Start)

The docker-compose.yml at the repository root starts PostgreSQL 17 (with pgvector), Valkey 8, an OpenTelemetry Collector, and Jaeger.

1. Clone and configure

git clone <repo-url> mosaic
cd mosaic
cp .env.example .env

Edit .env. The minimum required change is:

BETTER_AUTH_SECRET=<output of: openssl rand -base64 32>

2. Start infrastructure services

docker compose up -d

Services and their ports:

Service Default port
PostgreSQL localhost:5433
Valkey localhost:6380
OTEL Collector (HTTP) localhost:4318
OTEL Collector (gRPC) localhost:4317
Jaeger UI http://localhost:16686

Override host ports via PG_HOST_PORT and VALKEY_HOST_PORT in .env if the defaults conflict.

3. Install dependencies

pnpm install

4. Initialize the database

pnpm --filter @mosaic/db db:migrate

5. Build all packages

pnpm build

6. Start the gateway

pnpm --filter @mosaic/gateway dev

Or for production (after build):

node apps/gateway/dist/main.js

7. Start the web app

# Development
pnpm --filter @mosaic/web dev

# Production (after build)
pnpm --filter @mosaic/web start

The web app runs on port 3000 by default.


Bare-Metal Deployment

Use this path when you want to manage PostgreSQL and Valkey yourself (e.g., existing infrastructure, managed cloud databases).

Step 1 — Install system dependencies

# Node.js 22 via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
nvm install 22
nvm use 22

# pnpm
npm install -g pnpm

# PostgreSQL 17 with pgvector (Debian/Ubuntu example)
sudo apt-get install -y postgresql-17 postgresql-17-pgvector

# Valkey
# Follow https://valkey.io/download/ for your distribution

Step 2 — Create the database

-- Run as the postgres superuser
CREATE USER mosaic WITH PASSWORD 'change-me';
CREATE DATABASE mosaic OWNER mosaic;
\c mosaic
CREATE EXTENSION IF NOT EXISTS vector;

Step 3 — Clone and configure

git clone <repo-url> /opt/mosaic
cd /opt/mosaic
cp .env.example .env

Edit /opt/mosaic/.env. Required fields:

DATABASE_URL=postgresql://mosaic:<password>@localhost:5432/mosaic
VALKEY_URL=redis://localhost:6379
BETTER_AUTH_SECRET=<openssl rand -base64 32>
BETTER_AUTH_URL=https://your-domain.example.com
GATEWAY_CORS_ORIGIN=https://your-domain.example.com
NEXT_PUBLIC_GATEWAY_URL=https://your-domain.example.com

Step 4 — Install dependencies and build

pnpm install
pnpm build

Step 5 — Run database migrations

pnpm --filter @mosaic/db db:migrate

Step 6 — Start the gateway

node apps/gateway/dist/main.js

The gateway reads .env from the monorepo root automatically (via dotenv in main.ts).

Step 7 — Start the web app

# Next.js standalone output
node apps/web/.next/standalone/server.js

The standalone build is self-contained; it does not require node_modules to be present at runtime.

Step 8 — Configure a reverse proxy

Nginx example

# /etc/nginx/sites-available/mosaic

# Gateway API
server {
    listen 443 ssl;
    server_name your-domain.example.com;

    ssl_certificate     /etc/ssl/certs/your-domain.crt;
    ssl_certificate_key /etc/ssl/private/your-domain.key;

    # WebSocket support (for chat.gateway.ts / Socket.IO)
    location /socket.io/ {
        proxy_pass http://127.0.0.1:4000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # REST + auth
    location / {
        proxy_pass http://127.0.0.1:4000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Web app (optional — serve on a subdomain or a separate server block)
server {
    listen 443 ssl;
    server_name app.your-domain.example.com;

    ssl_certificate     /etc/ssl/certs/your-domain.crt;
    ssl_certificate_key /etc/ssl/private/your-domain.key;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Caddy example

# /etc/caddy/Caddyfile

your-domain.example.com {
    reverse_proxy /socket.io/* localhost:4000 {
        header_up Upgrade {http.upgrade}
        header_up Connection {http.connection}
    }
    reverse_proxy localhost:4000
}

app.your-domain.example.com {
    reverse_proxy localhost:3000
}

Production Considerations

systemd Services

Create a service unit for each process.

Gateway/etc/systemd/system/mosaic-gateway.service:

[Unit]
Description=Mosaic Gateway
After=network.target postgresql.service

[Service]
Type=simple
User=mosaic
WorkingDirectory=/opt/mosaic
EnvironmentFile=/opt/mosaic/.env
ExecStart=/usr/bin/node apps/gateway/dist/main.js
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Web app/etc/systemd/system/mosaic-web.service:

[Unit]
Description=Mosaic Web App
After=network.target mosaic-gateway.service

[Service]
Type=simple
User=mosaic
WorkingDirectory=/opt/mosaic/apps/web
EnvironmentFile=/opt/mosaic/.env
ExecStart=/usr/bin/node .next/standalone/server.js
Environment=PORT=3000
Environment=HOSTNAME=127.0.0.1
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now mosaic-gateway mosaic-web

Log Management

Gateway and web app logs go to systemd journal by default. View with:

journalctl -u mosaic-gateway -f
journalctl -u mosaic-web -f

Rotate logs by configuring journald in /etc/systemd/journald.conf:

SystemMaxUse=500M
MaxRetentionSec=30day

Security Checklist

  • Set BETTER_AUTH_SECRET to a cryptographically random value (openssl rand -base64 32).
  • Restrict GATEWAY_CORS_ORIGIN to your exact frontend origin — do not use *.
  • Run services as a dedicated non-root system user (e.g., mosaic).
  • Firewall: only expose ports 80/443 externally; keep 4000 and 3000 bound to 127.0.0.1.
  • Set AGENT_FILE_SANDBOX_DIR to a directory outside the application root to prevent agent tools from accessing source code.
  • If using AGENT_USER_TOOLS, enumerate only the tools non-admin users need.

Troubleshooting

Gateway fails to start — "BETTER_AUTH_SECRET is required"

BETTER_AUTH_SECRET is missing or empty. Set it in .env and restart.

DATABASE_URL connection refused

Verify PostgreSQL is running and the port matches. The Docker Compose default is 5433; bare-metal typically uses 5432.

psql "$DATABASE_URL" -c '\conninfo'

pgvector extension missing

\c mosaic
CREATE EXTENSION IF NOT EXISTS vector;

Valkey / Redis connection refused

Check the URL in VALKEY_URL. The Docker Compose default is port 6380.

redis-cli -u "$VALKEY_URL" ping

WebSocket connections fail in production

Ensure your reverse proxy forwards the Upgrade and Connection headers. See the Nginx/Caddy examples above.

Ollama models not appearing

Set OLLAMA_BASE_URL to the URL where Ollama is running (e.g., http://localhost:11434) and set OLLAMA_MODELS to a comma-separated list of model IDs you have pulled.

ollama pull llama3.2

OTEL traces not appearing in Jaeger

Verify the collector is reachable at OTEL_EXPORTER_OTLP_ENDPOINT. With Docker Compose the default is http://localhost:4318. Check docker compose ps and docker compose logs otel-collector.

Summarization / embedding features not working

These features require OPENAI_API_KEY to be set, or you must point SUMMARIZATION_API_URL / EMBEDDING_API_URL to an OpenAI-compatible endpoint (e.g., a local Ollama instance with an embeddings model).