15 KiB
sidebar_position, title
| sidebar_position | title |
|---|---|
| 2 | Standalone Local Setup |
Standalone Local Setup
The goal: a clueless user clones the repo, runs one script, and has a working Reflector instance locally. No cloud accounts, no API keys, no manual env file editing.
git clone https://github.com/monadical-sas/reflector.git
cd reflector
./scripts/setup-standalone.sh
On Ubuntu, the setup script installs Docker automatically if missing.
The script is idempotent — safe to re-run at any time. It detects what's already set up and skips completed steps.
Prerequisites
- Docker with Compose V2 plugin (Docker Desktop, OrbStack, or Docker Engine + compose plugin)
- Mac (Apple Silicon) or Linux
- 16GB+ RAM (32GB recommended for 14B LLM models)
- Mac only: Ollama installed (
brew install ollama)
Installing Docker (if not already installed)
Ubuntu: The setup script runs install-docker-ubuntu.sh automatically when Docker is missing. Or run it manually:
./scripts/install-docker-ubuntu.sh
Mac: Install Docker Desktop or OrbStack.
What the script does
1. LLM inference via Ollama
Mac: starts Ollama natively (Metal GPU acceleration). Pulls the LLM model. Docker containers reach it via host.docker.internal:11435.
Linux: starts containerized Ollama via docker-compose.standalone.yml profile (ollama-gpu with NVIDIA, ollama-cpu without). Pulls model inside the container.
2. Environment files
Generates server/.env and www/.env.local with standalone defaults:
server/.env — key settings:
| Variable | Value | Why |
|---|---|---|
DATABASE_URL |
postgresql+asyncpg://...@postgres:5432/reflector |
Docker-internal hostname |
REDIS_HOST |
redis |
Docker-internal hostname |
CELERY_BROKER_URL |
redis://redis:6379/1 |
Docker-internal hostname |
AUTH_BACKEND |
none |
No Authentik in standalone |
TRANSCRIPT_BACKEND |
modal |
HTTP API to self-hosted CPU service |
TRANSCRIPT_URL |
http://cpu:8000 |
Docker-internal CPU service |
DIARIZATION_BACKEND |
modal |
HTTP API to self-hosted CPU service |
DIARIZATION_URL |
http://cpu:8000 |
Docker-internal CPU service |
TRANSLATION_BACKEND |
passthrough |
No Modal |
LLM_URL |
http://host.docker.internal:11435/v1 (Mac) |
Ollama endpoint |
www/.env.local — key settings:
| Variable | Value |
|---|---|
API_URL |
https://localhost:3043 or https://YOUR_IP:3043 (Linux) |
SERVER_API_URL |
http://server:1250 |
WEBSOCKET_URL |
auto |
FEATURE_REQUIRE_LOGIN |
false |
NEXTAUTH_SECRET |
standalone-dev-secret-not-for-production |
If env files already exist (including symlinks from worktree setup), the script resolves symlinks and ensures all standalone-critical vars are set. Existing vars not related to standalone are preserved.
3. Object storage (Garage)
Standalone uses Garage — a lightweight S3-compatible object store running in Docker. The setup script starts Garage, initializes the layout, creates a bucket and access key, and writes the credentials to server/.env.
server/.env — storage settings added by the script:
| Variable | Value | Why |
|---|---|---|
TRANSCRIPT_STORAGE_BACKEND |
aws |
Uses the S3-compatible storage driver |
TRANSCRIPT_STORAGE_AWS_ENDPOINT_URL |
http://garage:3900 |
Docker-internal Garage S3 API |
TRANSCRIPT_STORAGE_AWS_BUCKET_NAME |
reflector-media |
Created by the script |
TRANSCRIPT_STORAGE_AWS_REGION |
garage |
Must match Garage config |
TRANSCRIPT_STORAGE_AWS_ACCESS_KEY_ID |
(auto-generated) | Created by garage key create |
TRANSCRIPT_STORAGE_AWS_SECRET_ACCESS_KEY |
(auto-generated) | Created by garage key create |
The TRANSCRIPT_STORAGE_AWS_ENDPOINT_URL setting enables S3-compatible backends. When set, the storage driver uses path-style addressing and routes all requests to the custom endpoint. When unset (production AWS), behavior is unchanged.
Garage config template lives at scripts/garage.toml. The setup script generates data/garage.toml (gitignored) with a random RPC secret and mounts it read-only into the container. Single-node, replication_factor=1.
Note
: Presigned URLs embed the Garage Docker hostname (
http://garage:3900). This is fine — the server proxies S3 responses to the browser. Modal GPU workers cannot reach internal Garage, but standalone doesn't use Modal.
4. Transcription and diarization
Standalone runs the self-hosted ML service (gpu/self_hosted/) in a CPU-only Docker container named cpu. This is the same FastAPI service used for Modal.com GPU deployments, but built with Dockerfile.cpu (no NVIDIA CUDA dependencies). The compose service is named cpu (not gpu) to make clear it runs without GPU acceleration; the source code lives in gpu/self_hosted/ because it's shared with the GPU deployment.
The modal backend name is reused — it just means "HTTP API client". Setting TRANSCRIPT_URL / DIARIZATION_URL to http://cpu:8000 routes requests to the local container instead of Modal.com.
On first start, the service downloads pyannote speaker diarization models (~1GB) from a public S3 bundle. Models are cached in a Docker volume (gpu_cache) so subsequent starts are fast. No HuggingFace token or API key needed.
Performance: CPU-only transcription and diarization work but are slow (~15 min for a 3 min file). For faster processing on Linux with NVIDIA GPU, use
--profile gpu-nvidiainstead (seedocker-compose.standalone.yml).
5. Docker services
docker compose up -d postgres redis garage cpu server worker beat web
All services start in a single command. Garage and cpu are already started by earlier steps but included for idempotency. No Hatchet in standalone mode — LLM processing (summaries, topics, titles) runs via Celery tasks.
6. Database migrations
Run automatically by the server container on startup (runserver.sh calls alembic upgrade head). No manual step needed.
7. Health check
Verifies:
- CPU service responds (transcription + diarization ready)
- Server responds at
http://localhost:1250/health - Frontend serves at
http://localhost:3000(or via Caddy athttps://localhost:3043) - LLM endpoint reachable from inside containers
Services
| Service | Port | Purpose |
|---|---|---|
caddy |
3043 | Reverse proxy (HTTPS, self-signed cert) |
server |
1250 | FastAPI backend (runs migrations on start) |
web |
3000 | Next.js frontend |
postgres |
5432 | PostgreSQL database |
redis |
6379 | Cache + Celery broker |
garage |
3900, 3903 | S3-compatible object storage (S3 API + admin API) |
cpu |
— | Self-hosted transcription + diarization (CPU-only) |
worker |
— | Celery worker (live pipeline post-processing) |
beat |
— | Celery beat (scheduled tasks) |
Testing programmatically
After the setup script completes, verify the full pipeline (upload, transcription, diarization, LLM summary) via the API:
# 1. Create a transcript
TRANSCRIPT_ID=$(curl -s -X POST 'http://localhost:1250/v1/transcripts' \
-H 'Content-Type: application/json' \
-d '{"name":"test-upload"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Created: $TRANSCRIPT_ID"
# 2. Upload an audio file (single-chunk upload)
curl -s "http://localhost:1250/v1/transcripts/${TRANSCRIPT_ID}/record/upload?chunk_number=0&total_chunks=1" \
-X POST -F "chunk=@/path/to/audio.mp3"
# 3. Poll until processing completes (status: ended or error)
while true; do
STATUS=$(curl -s "http://localhost:1250/v1/transcripts/${TRANSCRIPT_ID}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
echo "Status: $STATUS"
case "$STATUS" in ended|error) break;; esac
sleep 10
done
# 4. Check the result
curl -s "http://localhost:1250/v1/transcripts/${TRANSCRIPT_ID}" | python3 -m json.tool
Expected result: status ended, auto-generated title, short_summary, long_summary, and transcript text with Speaker 0 / Speaker 1 labels.
CPU-only processing is slow (~15 min for a 3 min audio file). Diarization finishes in ~3 min, transcription takes the rest.
Enabling HTTPS (droplet via IP)
To serve Reflector over HTTPS on a droplet accessed by IP (self-signed certificate):
-
Copy the Caddyfile (no edits needed —
:443catches all HTTPS inside container, mapped to host port 3043):cp Caddyfile.standalone.example Caddyfile -
Update
www/.env.localwith HTTPS URLs (port 3043):API_URL=https://YOUR_IP:3043 WEBSOCKET_URL=wss://YOUR_IP:3043 SITE_URL=https://YOUR_IP:3043 NEXTAUTH_URL=https://YOUR_IP:3043 -
Restart services:
docker compose -f docker-compose.standalone.yml --profile ollama-cpu up -d(Use
ollama-gpuinstead ofollama-cpuif you have an NVIDIA GPU.) -
Access at
https://YOUR_IP:3043. The browser will warn about the self-signed cert — click Advanced → Proceed to YOUR_IP (unsafe). All traffic (page, API, WebSocket) uses the same origin, so accepting once is enough.
Troubleshooting
ERR_SSL_PROTOCOL_ERROR when accessing https://YOUR_IP
You do not need a domain — the setup works with an IP address. This error usually means Caddy isn't serving TLS on port 3043. Check in order:
-
Caddyfile — must use the
:443catch-all (container-internal; Docker maps host 3043 → container 443):cp Caddyfile.standalone.example Caddyfile -
Firewall — allow port 3043 (common on DigitalOcean):
sudo ufw allow 3043 sudo ufw status -
Caddy running — verify and restart:
docker compose -f docker-compose.standalone.yml ps docker compose -f docker-compose.standalone.yml logs caddy --tail 20 docker compose -f docker-compose.standalone.yml --profile ollama-cpu up -d -
Test from the droplet — if this works, the issue is external (firewall, network):
curl -vk https://localhost:3043 -
localhost works but external IP fails — Re-run the setup script; it generates a Caddyfile with your droplet IP explicitly, so Caddy provisions the cert at startup:
./scripts/setup-standalone.shOr manually create
Caddyfilewith your IP (replace 138.197.162.116):https://138.197.162.116, localhost { tls internal handle /v1/* { reverse_proxy server:1250 } handle /health { reverse_proxy server:1250 } handle { reverse_proxy web:3000 } }Then restart:
docker compose -f docker-compose.standalone.yml --profile ollama-cpu up -d -
Still failing? Try HTTP (no TLS) — create
Caddyfile::80 { handle /v1/* { reverse_proxy server:1250 } handle /health { reverse_proxy server:1250 } handle { reverse_proxy web:3000 } }Update
www/.env.local:API_URL=http://YOUR_IP:3043,WEBSOCKET_URL=ws://YOUR_IP:3043,SITE_URL=http://YOUR_IP:3043,NEXTAUTH_URL=http://YOUR_IP:3043. Restart, then accesshttp://YOUR_IP:3043.
Docker not ready
If setup fails with "Docker not ready", on Ubuntu run ./scripts/install-docker-ubuntu.sh. If Docker is installed but you're not root, run newgrp docker then run the setup script again.
Port conflicts (most common issue)
If the frontend or backend behaves unexpectedly (e.g., env vars seem ignored, changes don't take effect), check for port conflicts first:
# Check what's listening on key ports
lsof -i :3000 # frontend
lsof -i :1250 # backend
lsof -i :5432 # postgres
lsof -i :3900 # Garage S3 API
lsof -i :6379 # Redis
# Kill stale processes on a port
lsof -ti :3000 | xargs kill
Common causes:
- A stale
next devorpnpm devprocess from another terminal/worktree - Another Docker Compose project (different worktree) with containers on the same ports — the setup script only manages its own project; containers from other projects must be stopped manually (
docker psto find them,docker stopto kill them)
The setup script checks ports 3000, 1250, 5432, 6379, 3900, 3903 for conflicts before starting services. It ignores OrbStack/Docker Desktop port forwarding processes (which always bind these ports but are not real conflicts).
OrbStack false port-conflict warnings (Mac)
If you use OrbStack as your Docker runtime, lsof will show OrbStack binding ports like 3000, 1250, etc. even when no containers are running. This is OrbStack's port forwarding mechanism — not a real conflict. The setup script filters these out automatically.
Re-enabling authentication
Standalone runs without authentication (FEATURE_REQUIRE_LOGIN=false, AUTH_BACKEND=none). To re-enable:
- In
www/.env.local: setFEATURE_REQUIRE_LOGIN=true, uncommentAUTHENTIK_ISSUERandAUTHENTIK_REFRESH_TOKEN_URL - In
server/.env: setAUTH_BACKEND=authentik(or your backend), configureAUTH_JWT_AUDIENCE - Restart:
docker compose -f docker-compose.standalone.yml up -d --force-recreate web server
What's NOT covered
These require external accounts and infrastructure that can't be scripted:
- Live meeting rooms — requires Daily.co account, S3 bucket, IAM roles
- Authentication — requires Authentik deployment and OAuth configuration
- Hatchet workflows — requires separate Hatchet setup for multitrack processing
- Production deployment — see Deployment Guide
Current status
All steps implemented. The setup script handles everything end-to-end:
- Step 1 (Ollama/LLM) — implemented
- Step 2 (environment files) — implemented
- Step 3 (object storage / Garage) — implemented
- Step 4 (transcription/diarization) — implemented (self-hosted GPU service)
- Steps 5-7 (Docker, migrations, health) — implemented
- Unified script:
scripts/setup-standalone.sh