mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-03-21 22:56:47 +00:00
fix: standalone on ubuntu (#865)
* Standalone on ubuntu * fix: use port 3043 for Caddy, disable rooms, remove dead Caddyfile - Caddy mapped to host port 3043 instead of 80/443 to avoid conflicts - FEATURE_ROOMS=false in standalone web service - Removed scripts/standalone/Caddyfile (dead code on this branch) - Updated all URLs, port checks, docs to reference :3043 --------- Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
This commit is contained in:
42
Caddyfile.standalone.example
Normal file
42
Caddyfile.standalone.example
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Reflector standalone — HTTPS via Caddy (droplet / IP access)
|
||||||
|
# Copy to Caddyfile: cp Caddyfile.standalone.example Caddyfile
|
||||||
|
# Run: docker compose -f docker-compose.standalone.yml --profile ollama-cpu up -d
|
||||||
|
#
|
||||||
|
# :443 = catch-all inside container; Docker maps host port 3043 → container 443
|
||||||
|
# on_demand = generate self-signed cert for IP/SNI on first request (required for bare IP access)
|
||||||
|
# Browser will warn. Click Advanced → Proceed.
|
||||||
|
# Access at https://localhost:3043 (or https://YOUR_IP:3043 on droplet)
|
||||||
|
# Update www/.env.local with: API_URL=https://YOUR_IP:3043, WEBSOCKET_URL=wss://YOUR_IP:3043, SITE_URL=https://YOUR_IP:3043, NEXTAUTH_URL=https://YOUR_IP:3043
|
||||||
|
|
||||||
|
:443 {
|
||||||
|
tls internal {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
handle /v1/* {
|
||||||
|
reverse_proxy server:1250
|
||||||
|
}
|
||||||
|
handle /health {
|
||||||
|
reverse_proxy server:1250
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
reverse_proxy web:3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Option B: localhost (comment Option A, uncomment this)
|
||||||
|
# app.localhost {
|
||||||
|
# tls internal
|
||||||
|
# reverse_proxy web:3000
|
||||||
|
# }
|
||||||
|
# api.localhost {
|
||||||
|
# tls internal
|
||||||
|
# reverse_proxy server:1250
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Option C: Real domain (uncomment and replace example.com)
|
||||||
|
# app.example.com {
|
||||||
|
# reverse_proxy web:3000
|
||||||
|
# }
|
||||||
|
# api.example.com {
|
||||||
|
# reverse_proxy server:1250
|
||||||
|
# }
|
||||||
@@ -14,9 +14,12 @@ services:
|
|||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
- ./scripts/standalone/Caddyfile:/etc/caddy/Caddyfile:ro
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
- caddy_config:/config
|
- caddy_config:/config
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
- server
|
||||||
|
|
||||||
server:
|
server:
|
||||||
build:
|
build:
|
||||||
@@ -123,19 +126,18 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
command: ["node", "server.js"]
|
command: ["node", "server.js"]
|
||||||
|
env_file:
|
||||||
|
- ./www/.env.local
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
# Browser-facing URLs (host-accessible ports)
|
# API_URL, WEBSOCKET_URL, SITE_URL, NEXTAUTH_URL from www/.env.local (allows HTTPS)
|
||||||
API_URL: /server-api
|
|
||||||
WEBSOCKET_URL: auto
|
|
||||||
SITE_URL: http://localhost:3000
|
|
||||||
# Server-side URLs (docker-network internal)
|
# Server-side URLs (docker-network internal)
|
||||||
SERVER_API_URL: http://server:1250
|
SERVER_API_URL: http://server:1250
|
||||||
KV_URL: redis://redis:6379
|
KV_URL: redis://redis:6379
|
||||||
KV_USE_TLS: "false"
|
KV_USE_TLS: "false"
|
||||||
# Standalone: no external auth provider
|
# Standalone: no external auth provider
|
||||||
FEATURE_REQUIRE_LOGIN: "false"
|
FEATURE_REQUIRE_LOGIN: "false"
|
||||||
NEXTAUTH_URL: http://localhost:3000
|
FEATURE_ROOMS: "false"
|
||||||
NEXTAUTH_SECRET: standalone-local-secret
|
NEXTAUTH_SECRET: standalone-local-secret
|
||||||
# Nullify partial auth vars inherited from base env_file
|
# Nullify partial auth vars inherited from base env_file
|
||||||
AUTHENTIK_ISSUER: ""
|
AUTHENTIK_ISSUER: ""
|
||||||
|
|||||||
@@ -13,15 +13,27 @@ cd reflector
|
|||||||
./scripts/setup-standalone.sh
|
./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.
|
The script is idempotent — safe to re-run at any time. It detects what's already set up and skips completed steps.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Docker / OrbStack / Docker Desktop (any)
|
- Docker with Compose V2 plugin (Docker Desktop, OrbStack, or Docker Engine + compose plugin)
|
||||||
- Mac (Apple Silicon) or Linux
|
- Mac (Apple Silicon) or Linux
|
||||||
- 16GB+ RAM (32GB recommended for 14B LLM models)
|
- 16GB+ RAM (32GB recommended for 14B LLM models)
|
||||||
- **Mac only**: [Ollama](https://ollama.com/download) installed (`brew install ollama`)
|
- **Mac only**: [Ollama](https://ollama.com/download) 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/install-docker-ubuntu.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mac**: Install [Docker Desktop](https://www.docker.com/products/docker-desktop/) or [OrbStack](https://orbstack.dev/).
|
||||||
|
|
||||||
## What the script does
|
## What the script does
|
||||||
|
|
||||||
### 1. LLM inference via Ollama
|
### 1. LLM inference via Ollama
|
||||||
@@ -36,28 +48,28 @@ Generates `server/.env` and `www/.env.local` with standalone defaults:
|
|||||||
|
|
||||||
**`server/.env`** — key settings:
|
**`server/.env`** — key settings:
|
||||||
|
|
||||||
| Variable | Value | Why |
|
| Variable | Value | Why |
|
||||||
|----------|-------|-----|
|
| --------------------- | -------------------------------------------------- | ----------------------------------- |
|
||||||
| `DATABASE_URL` | `postgresql+asyncpg://...@postgres:5432/reflector` | Docker-internal hostname |
|
| `DATABASE_URL` | `postgresql+asyncpg://...@postgres:5432/reflector` | Docker-internal hostname |
|
||||||
| `REDIS_HOST` | `redis` | Docker-internal hostname |
|
| `REDIS_HOST` | `redis` | Docker-internal hostname |
|
||||||
| `CELERY_BROKER_URL` | `redis://redis:6379/1` | Docker-internal hostname |
|
| `CELERY_BROKER_URL` | `redis://redis:6379/1` | Docker-internal hostname |
|
||||||
| `AUTH_BACKEND` | `none` | No Authentik in standalone |
|
| `AUTH_BACKEND` | `none` | No Authentik in standalone |
|
||||||
| `TRANSCRIPT_BACKEND` | `modal` | HTTP API to self-hosted CPU service |
|
| `TRANSCRIPT_BACKEND` | `modal` | HTTP API to self-hosted CPU service |
|
||||||
| `TRANSCRIPT_URL` | `http://cpu:8000` | Docker-internal CPU service |
|
| `TRANSCRIPT_URL` | `http://cpu:8000` | Docker-internal CPU service |
|
||||||
| `DIARIZATION_BACKEND` | `modal` | HTTP API to self-hosted CPU service |
|
| `DIARIZATION_BACKEND` | `modal` | HTTP API to self-hosted CPU service |
|
||||||
| `DIARIZATION_URL` | `http://cpu:8000` | Docker-internal CPU service |
|
| `DIARIZATION_URL` | `http://cpu:8000` | Docker-internal CPU service |
|
||||||
| `TRANSLATION_BACKEND` | `passthrough` | No Modal |
|
| `TRANSLATION_BACKEND` | `passthrough` | No Modal |
|
||||||
| `LLM_URL` | `http://host.docker.internal:11434/v1` (Mac) | Ollama endpoint |
|
| `LLM_URL` | `http://host.docker.internal:11434/v1` (Mac) | Ollama endpoint |
|
||||||
|
|
||||||
**`www/.env.local`** — key settings:
|
**`www/.env.local`** — key settings:
|
||||||
|
|
||||||
| Variable | Value |
|
| Variable | Value |
|
||||||
|----------|-------|
|
| ----------------------- | ------------------------------------------ |
|
||||||
| `API_URL` | `http://localhost:1250` |
|
| `API_URL` | `https://localhost:3043` or `https://YOUR_IP:3043` (Linux) |
|
||||||
| `SERVER_API_URL` | `http://server:1250` |
|
| `SERVER_API_URL` | `http://server:1250` |
|
||||||
| `WEBSOCKET_URL` | `ws://localhost:1250` |
|
| `WEBSOCKET_URL` | `auto` |
|
||||||
| `FEATURE_REQUIRE_LOGIN` | `false` |
|
| `FEATURE_REQUIRE_LOGIN` | `false` |
|
||||||
| `NEXTAUTH_SECRET` | `standalone-dev-secret-not-for-production` |
|
| `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.
|
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.
|
||||||
|
|
||||||
@@ -67,14 +79,14 @@ Standalone uses [Garage](https://garagehq.deuxfleurs.fr/) — a lightweight S3-c
|
|||||||
|
|
||||||
**`server/.env`** — storage settings added by the script:
|
**`server/.env`** — storage settings added by the script:
|
||||||
|
|
||||||
| Variable | Value | Why |
|
| Variable | Value | Why |
|
||||||
|----------|-------|-----|
|
| ------------------------------------------ | -------------------- | ------------------------------------- |
|
||||||
| `TRANSCRIPT_STORAGE_BACKEND` | `aws` | Uses the S3-compatible storage driver |
|
| `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_ENDPOINT_URL` | `http://garage:3900` | Docker-internal Garage S3 API |
|
||||||
| `TRANSCRIPT_STORAGE_AWS_BUCKET_NAME` | `reflector-media` | Created by the script |
|
| `TRANSCRIPT_STORAGE_AWS_BUCKET_NAME` | `reflector-media` | Created by the script |
|
||||||
| `TRANSCRIPT_STORAGE_AWS_REGION` | `garage` | Must match Garage config |
|
| `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_ACCESS_KEY_ID` | _(auto-generated)_ | Created by `garage key create` |
|
||||||
| `TRANSCRIPT_STORAGE_AWS_SECRET_ACCESS_KEY` | *(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.
|
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.
|
||||||
|
|
||||||
@@ -107,23 +119,25 @@ Run automatically by the `server` container on startup (`runserver.sh` calls `al
|
|||||||
### 7. Health check
|
### 7. Health check
|
||||||
|
|
||||||
Verifies:
|
Verifies:
|
||||||
|
|
||||||
- CPU service responds (transcription + diarization ready)
|
- CPU service responds (transcription + diarization ready)
|
||||||
- Server responds at `http://localhost:1250/health`
|
- Server responds at `http://localhost:1250/health`
|
||||||
- Frontend serves at `http://localhost:3000`
|
- Frontend serves at `http://localhost:3000` (or via Caddy at `https://localhost:3043`)
|
||||||
- LLM endpoint reachable from inside containers
|
- LLM endpoint reachable from inside containers
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
| Service | Port | Purpose |
|
| Service | Port | Purpose |
|
||||||
|---------|------|---------|
|
| ---------- | ---------- | -------------------------------------------------- |
|
||||||
| `server` | 1250 | FastAPI backend (runs migrations on start) |
|
| `caddy` | 3043 | Reverse proxy (HTTPS, self-signed cert) |
|
||||||
| `web` | 3000 | Next.js frontend |
|
| `server` | 1250 | FastAPI backend (runs migrations on start) |
|
||||||
| `postgres` | 5432 | PostgreSQL database |
|
| `web` | 3000 | Next.js frontend |
|
||||||
| `redis` | 6379 | Cache + Celery broker |
|
| `postgres` | 5432 | PostgreSQL database |
|
||||||
| `garage` | 3900, 3903 | S3-compatible object storage (S3 API + admin API) |
|
| `redis` | 6379 | Cache + Celery broker |
|
||||||
| `cpu` | — | Self-hosted transcription + diarization (CPU-only) |
|
| `garage` | 3900, 3903 | S3-compatible object storage (S3 API + admin API) |
|
||||||
| `worker` | — | Celery worker (live pipeline post-processing) |
|
| `cpu` | — | Self-hosted transcription + diarization (CPU-only) |
|
||||||
| `beat` | — | Celery beat (scheduled tasks) |
|
| `worker` | — | Celery worker (live pipeline post-processing) |
|
||||||
|
| `beat` | — | Celery beat (scheduled tasks) |
|
||||||
|
|
||||||
## Testing programmatically
|
## Testing programmatically
|
||||||
|
|
||||||
@@ -157,8 +171,89 @@ Expected result: status `ended`, auto-generated `title`, `short_summary`, `long_
|
|||||||
|
|
||||||
CPU-only processing is slow (~15 min for a 3 min audio file). Diarization finishes in ~3 min, transcription takes the rest.
|
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):
|
||||||
|
|
||||||
|
1. **Copy the Caddyfile** (no edits needed — `:443` catches all HTTPS inside container, mapped to host port 3043):
|
||||||
|
```bash
|
||||||
|
cp Caddyfile.standalone.example Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update `www/.env.local`** with HTTPS URLs (port 3043):
|
||||||
|
```env
|
||||||
|
API_URL=https://YOUR_IP:3043
|
||||||
|
WEBSOCKET_URL=wss://YOUR_IP:3043
|
||||||
|
SITE_URL=https://YOUR_IP:3043
|
||||||
|
NEXTAUTH_URL=https://YOUR_IP:3043
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Restart services**:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.standalone.yml --profile ollama-cpu up -d
|
||||||
|
```
|
||||||
|
(Use `ollama-gpu` instead of `ollama-cpu` if you have an NVIDIA GPU.)
|
||||||
|
|
||||||
|
4. **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
|
## 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:
|
||||||
|
|
||||||
|
1. **Caddyfile** — must use the `:443` catch-all (container-internal; Docker maps host 3043 → container 443):
|
||||||
|
```bash
|
||||||
|
cp Caddyfile.standalone.example Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Firewall** — allow port 3043 (common on DigitalOcean):
|
||||||
|
```bash
|
||||||
|
sudo ufw allow 3043
|
||||||
|
sudo ufw status
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Caddy running** — verify and restart:
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Test from the droplet** — if this works, the issue is external (firewall, network):
|
||||||
|
```bash
|
||||||
|
curl -vk https://localhost:3043
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **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:
|
||||||
|
```bash
|
||||||
|
./scripts/setup-standalone.sh
|
||||||
|
```
|
||||||
|
Or manually create `Caddyfile` with 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`
|
||||||
|
|
||||||
|
6. **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 access `http://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)
|
### 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**:
|
If the frontend or backend behaves unexpectedly (e.g., env vars seem ignored, changes don't take effect), **check for port conflicts first**:
|
||||||
@@ -176,6 +271,7 @@ lsof -ti :3000 | xargs kill
|
|||||||
```
|
```
|
||||||
|
|
||||||
Common causes:
|
Common causes:
|
||||||
|
|
||||||
- A stale `next dev` or `pnpm dev` process from another terminal/worktree
|
- A stale `next dev` or `pnpm dev` process 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 ps` to find them, `docker stop` to kill them)
|
- 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 ps` to find them, `docker stop` to kill them)
|
||||||
|
|
||||||
|
|||||||
87
scripts/install-docker-ubuntu.sh
Executable file
87
scripts/install-docker-ubuntu.sh
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Install Docker Engine + Compose plugin on Ubuntu.
|
||||||
|
# Ubuntu's default repos don't include docker-compose-plugin, so we add Docker's official repo.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/install-docker-ubuntu.sh
|
||||||
|
#
|
||||||
|
# Requires: root or sudo
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# --- Colors ---
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${CYAN}==>${NC} $*"; }
|
||||||
|
ok() { echo -e "${GREEN} ✓${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW} !${NC} $*"; }
|
||||||
|
err() { echo -e "${RED} ✗${NC} $*" >&2; }
|
||||||
|
|
||||||
|
# Use sudo if available and not root; otherwise run directly
|
||||||
|
if [[ $(id -u) -eq 0 ]]; then
|
||||||
|
MAYBE_SUDO=""
|
||||||
|
elif command -v sudo &>/dev/null; then
|
||||||
|
MAYBE_SUDO="sudo "
|
||||||
|
else
|
||||||
|
err "Need root. Run as root or install sudo: apt install sudo"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Ubuntu
|
||||||
|
if [[ ! -f /etc/os-release ]]; then
|
||||||
|
err "Cannot detect OS. This script is for Ubuntu."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
source /etc/os-release
|
||||||
|
if [[ "${ID:-}" != "ubuntu" ]] && [[ "${ID_LIKE:-}" != *"ubuntu"* ]]; then
|
||||||
|
err "This script is for Ubuntu. Detected: ${ID:-unknown}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Adding Docker's official repository..."
|
||||||
|
${MAYBE_SUDO}apt update
|
||||||
|
${MAYBE_SUDO}apt install -y ca-certificates curl
|
||||||
|
${MAYBE_SUDO}install -m 0755 -d /etc/apt/keyrings
|
||||||
|
${MAYBE_SUDO}rm -f /etc/apt/sources.list.d/docker.list /etc/apt/sources.list.d/docker.sources
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | ${MAYBE_SUDO}tee /etc/apt/keyrings/docker.asc > /dev/null
|
||||||
|
${MAYBE_SUDO}chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
CODENAME="$(. /etc/os-release && echo "${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}")"
|
||||||
|
[[ -z "$CODENAME" ]] && { err "Could not detect Ubuntu version codename."; exit 1; }
|
||||||
|
${MAYBE_SUDO}tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
|
||||||
|
Types: deb
|
||||||
|
URIs: https://download.docker.com/linux/ubuntu
|
||||||
|
Suites: ${CODENAME}
|
||||||
|
Components: stable
|
||||||
|
Signed-By: /etc/apt/keyrings/docker.asc
|
||||||
|
EOF
|
||||||
|
|
||||||
|
info "Installing Docker Engine and Compose plugin..."
|
||||||
|
${MAYBE_SUDO}apt update
|
||||||
|
${MAYBE_SUDO}apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
if [[ -d /run/systemd/system ]]; then
|
||||||
|
info "Enabling and starting Docker..."
|
||||||
|
${MAYBE_SUDO}systemctl enable --now docker
|
||||||
|
else
|
||||||
|
err "No systemd. This script requires Ubuntu with systemd (e.g. DigitalOcean droplet)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOCKER_USER="${SUDO_USER:-${USER:-root}}"
|
||||||
|
if [[ "$DOCKER_USER" != "root" ]]; then
|
||||||
|
info "Adding $DOCKER_USER to docker group..."
|
||||||
|
${MAYBE_SUDO}usermod -aG docker "$DOCKER_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "Docker installed successfully."
|
||||||
|
echo ""
|
||||||
|
echo " Log out and back in (or run: newgrp docker) so the group change takes effect."
|
||||||
|
echo " Then verify with: docker compose version"
|
||||||
|
echo ""
|
||||||
@@ -308,15 +308,24 @@ step_storage() {
|
|||||||
# Generate garage.toml from template (fill in RPC secret)
|
# Generate garage.toml from template (fill in RPC secret)
|
||||||
GARAGE_TOML="$ROOT_DIR/scripts/garage.toml"
|
GARAGE_TOML="$ROOT_DIR/scripts/garage.toml"
|
||||||
GARAGE_TOML_RUNTIME="$ROOT_DIR/data/garage.toml"
|
GARAGE_TOML_RUNTIME="$ROOT_DIR/data/garage.toml"
|
||||||
|
mkdir -p "$ROOT_DIR/data"
|
||||||
|
if [[ -d "$GARAGE_TOML_RUNTIME" ]]; then
|
||||||
|
rm -rf "$GARAGE_TOML_RUNTIME"
|
||||||
|
fi
|
||||||
if [[ ! -f "$GARAGE_TOML_RUNTIME" ]]; then
|
if [[ ! -f "$GARAGE_TOML_RUNTIME" ]]; then
|
||||||
mkdir -p "$ROOT_DIR/data"
|
|
||||||
RPC_SECRET=$(openssl rand -hex 32)
|
RPC_SECRET=$(openssl rand -hex 32)
|
||||||
sed "s|__GARAGE_RPC_SECRET__|${RPC_SECRET}|" "$GARAGE_TOML" > "$GARAGE_TOML_RUNTIME"
|
sed "s|__GARAGE_RPC_SECRET__|${RPC_SECRET}|" "$GARAGE_TOML" > "$GARAGE_TOML_RUNTIME"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
compose_cmd up -d garage
|
compose_cmd up -d garage
|
||||||
|
|
||||||
wait_for_url "http://localhost:3903/health" "Garage admin API"
|
# Use /metrics for readiness — /health returns 503 until layout is applied
|
||||||
|
if ! wait_for_url "http://localhost:3903/metrics" "Garage admin API"; then
|
||||||
|
echo ""
|
||||||
|
err "Garage container logs:"
|
||||||
|
compose_cmd logs garage --tail 30 2>&1 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Layout: get node ID, assign, apply (skip if already applied)
|
# Layout: get node ID, assign, apply (skip if already applied)
|
||||||
@@ -376,11 +385,17 @@ ENVEOF
|
|||||||
ok "Created www/.env.local"
|
ok "Created www/.env.local"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
env_set "$WWW_ENV" "SITE_URL" "http://localhost:3000"
|
# Caddyfile.standalone.example serves API at /v1, /health — use base URL
|
||||||
env_set "$WWW_ENV" "NEXTAUTH_URL" "http://localhost:3000"
|
if [[ -n "${PRIMARY_IP:-}" ]]; then
|
||||||
|
BASE_URL="https://$PRIMARY_IP:3043"
|
||||||
|
else
|
||||||
|
BASE_URL="https://localhost:3043"
|
||||||
|
fi
|
||||||
|
env_set "$WWW_ENV" "SITE_URL" "$BASE_URL"
|
||||||
|
env_set "$WWW_ENV" "NEXTAUTH_URL" "$BASE_URL"
|
||||||
env_set "$WWW_ENV" "NEXTAUTH_SECRET" "standalone-dev-secret-not-for-production"
|
env_set "$WWW_ENV" "NEXTAUTH_SECRET" "standalone-dev-secret-not-for-production"
|
||||||
env_set "$WWW_ENV" "API_URL" "http://localhost:1250"
|
env_set "$WWW_ENV" "API_URL" "$BASE_URL"
|
||||||
env_set "$WWW_ENV" "WEBSOCKET_URL" "ws://localhost:1250"
|
env_set "$WWW_ENV" "WEBSOCKET_URL" "auto"
|
||||||
env_set "$WWW_ENV" "SERVER_API_URL" "http://server:1250"
|
env_set "$WWW_ENV" "SERVER_API_URL" "http://server:1250"
|
||||||
env_set "$WWW_ENV" "FEATURE_REQUIRE_LOGIN" "false"
|
env_set "$WWW_ENV" "FEATURE_REQUIRE_LOGIN" "false"
|
||||||
|
|
||||||
@@ -533,21 +548,38 @@ main() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure Docker Compose V2 plugin is available.
|
# Docker: Compose plugin, buildx, and daemon. On Ubuntu, auto-install if missing.
|
||||||
# Check output for "Compose" — without the plugin, `docker compose version`
|
docker_ready() {
|
||||||
# may still exit 0 (falling through to `docker version`).
|
docker compose version 2>/dev/null | grep -qi compose \
|
||||||
if ! docker compose version 2>/dev/null | grep -qi compose; then
|
&& docker buildx version &>/dev/null \
|
||||||
err "Docker Compose plugin not found."
|
&& docker info &>/dev/null
|
||||||
err "Install Docker Desktop, OrbStack, or: brew install docker-compose"
|
}
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Dockerfiles use RUN --mount which requires BuildKit.
|
if ! docker_ready; then
|
||||||
# Docker Desktop/OrbStack bundle it; Colima/bare engine need docker-buildx.
|
RAN_INSTALL=false
|
||||||
if ! docker buildx version &>/dev/null; then
|
if [[ "$OS" == "Linux" ]] && [[ -f /etc/os-release ]] && (source /etc/os-release 2>/dev/null; [[ "${ID:-}" == "ubuntu" || "${ID_LIKE:-}" == *"ubuntu"* ]]); then
|
||||||
err "Docker BuildKit (buildx) not found."
|
info "Docker not ready. Running install-docker-ubuntu.sh..."
|
||||||
err "Install Docker Desktop, OrbStack, or: brew install docker-buildx"
|
"$SCRIPT_DIR/install-docker-ubuntu.sh" || true
|
||||||
exit 1
|
RAN_INSTALL=true
|
||||||
|
[[ -d /run/systemd/system ]] && command -v systemctl &>/dev/null && systemctl start docker 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
if ! docker_ready; then
|
||||||
|
# Docker may be installed but current shell lacks docker group (needs newgrp)
|
||||||
|
if [[ "$RAN_INSTALL" == "true" ]] && [[ $(id -u) -ne 0 ]] && command -v sg &>/dev/null && getent group docker &>/dev/null; then
|
||||||
|
info "Re-running with docker group..."
|
||||||
|
exec sg docker -c "$(printf '%q' "$0" && printf ' %q' "$@")"
|
||||||
|
fi
|
||||||
|
if [[ "$OS" == "Darwin" ]]; then
|
||||||
|
err "Docker not ready. Install Docker Desktop or OrbStack."
|
||||||
|
elif [[ "$OS" == "Linux" ]]; then
|
||||||
|
err "Docker not ready. Run: ./scripts/install-docker-ubuntu.sh"
|
||||||
|
err "Then run: newgrp docker (or log out and back in), then run this script again."
|
||||||
|
else
|
||||||
|
err "Docker not ready. Install Docker with Compose V2 and buildx."
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# LLM_URL_VALUE is set by step_llm, used by later steps
|
# LLM_URL_VALUE is set by step_llm, used by later steps
|
||||||
@@ -558,6 +590,57 @@ main() {
|
|||||||
# touch them so compose_cmd works before the steps that populate them.
|
# touch them so compose_cmd works before the steps that populate them.
|
||||||
touch "$SERVER_ENV" "$WWW_ENV"
|
touch "$SERVER_ENV" "$WWW_ENV"
|
||||||
|
|
||||||
|
# Ensure garage.toml exists before any compose up (step_llm starts all services including garage)
|
||||||
|
GARAGE_TOML="$ROOT_DIR/scripts/garage.toml"
|
||||||
|
GARAGE_TOML_RUNTIME="$ROOT_DIR/data/garage.toml"
|
||||||
|
mkdir -p "$ROOT_DIR/data"
|
||||||
|
if [[ -d "$GARAGE_TOML_RUNTIME" ]]; then
|
||||||
|
rm -rf "$GARAGE_TOML_RUNTIME"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$GARAGE_TOML_RUNTIME" ]]; then
|
||||||
|
RPC_SECRET=$(openssl rand -hex 32)
|
||||||
|
sed "s|__GARAGE_RPC_SECRET__|${RPC_SECRET}|" "$GARAGE_TOML" > "$GARAGE_TOML_RUNTIME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove containers that may have bad mounts (was directory); force recreate
|
||||||
|
compose_cmd rm -f -s garage caddy 2>/dev/null || true
|
||||||
|
|
||||||
|
# Detect primary IP for droplet (used for Caddyfile, step_www_env, success message)
|
||||||
|
PRIMARY_IP=""
|
||||||
|
if [[ "$OS" == "Linux" ]]; then
|
||||||
|
PRIMARY_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || true)
|
||||||
|
if [[ "$PRIMARY_IP" == "127."* ]] || [[ -z "$PRIMARY_IP" ]]; then
|
||||||
|
PRIMARY_IP=$(ip -4 route get 1 2>/dev/null | sed -n 's/.*src \([0-9.]*\).*/\1/p' || true)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure Caddyfile exists before any compose up (step_llm starts caddy)
|
||||||
|
# On droplet: explicit IP + localhost so Caddy provisions cert at startup (avoids on_demand/SNI issues)
|
||||||
|
CADDYFILE="$ROOT_DIR/Caddyfile"
|
||||||
|
if [[ -d "$CADDYFILE" ]]; then
|
||||||
|
rm -rf "$CADDYFILE"
|
||||||
|
fi
|
||||||
|
if [[ -n "$PRIMARY_IP" ]]; then
|
||||||
|
cat > "$CADDYFILE" << CADDYEOF
|
||||||
|
# Generated by setup-standalone.sh — explicit IP for droplet (provisions cert at startup)
|
||||||
|
https://$PRIMARY_IP, localhost {
|
||||||
|
tls internal
|
||||||
|
handle /v1/* {
|
||||||
|
reverse_proxy server:1250
|
||||||
|
}
|
||||||
|
handle /health {
|
||||||
|
reverse_proxy server:1250
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
reverse_proxy web:3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CADDYEOF
|
||||||
|
ok "Created Caddyfile for $PRIMARY_IP and localhost"
|
||||||
|
elif [[ ! -f "$CADDYFILE" ]]; then
|
||||||
|
cp "$ROOT_DIR/Caddyfile.standalone.example" "$CADDYFILE"
|
||||||
|
fi
|
||||||
|
|
||||||
step_llm
|
step_llm
|
||||||
echo ""
|
echo ""
|
||||||
step_server_env
|
step_server_env
|
||||||
@@ -575,8 +658,14 @@ main() {
|
|||||||
echo -e " ${GREEN}Reflector is running!${NC}"
|
echo -e " ${GREEN}Reflector is running!${NC}"
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo ""
|
echo ""
|
||||||
echo " App: https://localhost:3043 (accept self-signed cert in browser)"
|
if [[ -n "$PRIMARY_IP" ]]; then
|
||||||
echo " API: https://localhost:3043/server-api"
|
echo " App: https://$PRIMARY_IP:3043 (accept self-signed cert in browser)"
|
||||||
|
echo " API: https://$PRIMARY_IP:3043/v1/"
|
||||||
|
echo " Local: https://localhost:3043"
|
||||||
|
else
|
||||||
|
echo " App: https://localhost:3043 (accept self-signed cert in browser)"
|
||||||
|
echo " API: https://localhost:3043/v1/"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo " To stop: docker compose down"
|
echo " To stop: docker compose down"
|
||||||
echo " To re-run: ./scripts/setup-standalone.sh"
|
echo " To re-run: ./scripts/setup-standalone.sh"
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# Standalone Caddyfile — single-origin reverse proxy for local development.
|
|
||||||
# Routes:
|
|
||||||
# /server-api/* → FastAPI backend (server:1250)
|
|
||||||
# /* → Next.js frontend (web:3000)
|
|
||||||
|
|
||||||
localhost {
|
|
||||||
# API + WebSocket: server has ROOT_PATH=/server-api so path is preserved
|
|
||||||
handle /server-api/* {
|
|
||||||
reverse_proxy server:1250
|
|
||||||
}
|
|
||||||
|
|
||||||
# Everything else → frontend
|
|
||||||
handle {
|
|
||||||
reverse_proxy web:3000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user