From 59619e9eaad4313b76e4daa436ecdbb6088b33c1 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 13 May 2026 17:22:14 +0530 Subject: [PATCH] feat: an option to setup remote server with docker compose build (#280) * feat: remote setup with docker build option * chore: update documentation * chore: make script run in non tty * chore: add warning about slow build * chore: add more documentation * feat: add FASTAPI_WORKERS parameter * feat: add scaling docs * feat: add update script * fix: fix semver options in update_remote.sh --- docker-compose.yaml | 6 +- docs/deployment/docker.mdx | 99 +++- docs/deployment/scaling.mdx | 159 +++++++ docs/deployment/update.mdx | 134 +++--- docs/docs.json | 1 + docs/getting-started/index.mdx | 5 +- docs/integrations/telephony/agent-stream.mdx | 74 +-- scripts/setup_remote.sh | 294 ++++++++++-- scripts/start_services_docker.sh | 11 +- scripts/update_remote.sh | 448 +++++++++++++++++++ 10 files changed, 1086 insertions(+), 145 deletions(-) create mode 100644 docs/deployment/scaling.mdx create mode 100755 scripts/update_remote.sh diff --git a/docker-compose.yaml b/docker-compose.yaml index 653427b..307c6d6 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -107,8 +107,10 @@ services: MINIO_BUCKET: "voice-audio" MINIO_SECURE: "false" - # FastAPI workers count - FASTAPI_WORKERS: 1 + # Number of uvicorn worker processes (each is its own process bound to a + # distinct port starting at 8000). nginx load-balances across them with + # least_conn — see setup_remote.sh. + FASTAPI_WORKERS: "${FASTAPI_WORKERS:-1}" # Langfuse — credentials can be set here or per-organization via the UI # at /settings. Tracing is automatically active when credentials are diff --git a/docs/deployment/docker.mdx b/docs/deployment/docker.mdx index 7e07516..f235d5c 100644 --- a/docs/deployment/docker.mdx +++ b/docs/deployment/docker.mdx @@ -6,7 +6,7 @@ description: "Deploy Dograh AI using Docker for local development and remote ser Dograh AI can be deployed using Docker in two main configurations. Choose the option that best fits your needs: - **Option 1**: For local development and testing on your own machine -- **Option 2**: For remote server deployment with HTTPS (using IP address) +- **Option 2**: For remote server deployment with HTTPS (using IP address). If you also have a custom domain, you can first deploy Dograh stack on your server using steps in this document and then proceed to the [Custom Domain](deployment/custom-domain) section. ## Option 1: Local Docker Deployment @@ -52,7 +52,7 @@ The Quick Start above relies on direct peer-to-peer WebRTC between your browser - The call connects but no audio flows in either direction - The browser console reports `iceConnectionState: failed` -- You are testing from a phone or another device on your LAN against the laptop running Docker +- You are testing from a browser on your smartphone or another device on your LAN against the laptop running Docker - A VPN, corporate firewall, or strict NAT sits between the browser and Docker For these cases, use the alternate local setup script which configures a coturn TURN server alongside the rest of the stack: @@ -90,17 +90,17 @@ Watch the video tutorial below for a step-by-step walkthrough of deploying Dogra allowFullScreen > -Deploy Dograh AI on a remote server to make it accessible from anywhere using your server's IP address. This setup includes HTTPS support via nginx reverse proxy with self-signed certificates. We need to serve the application over HTTPS, since modern browsers only allow microphone permissions for websites being served over HTTPS. +Deploy Dograh AI on a remote server to make it accessible from anywhere using your server's IP address. This setup includes HTTPS support via nginx reverse proxy with self-signed certificates. **We need to serve the application over HTTPS, since modern browsers only allow microphone permissions for websites being served over HTTPS**. -**We highly recommend you set up the platform on a fresh server, so that there are less chances of confliciting dependencies, and ports from other applications.** +We highly recommend you set up the platform on a fresh server, so that there are less chances of confliciting dependencies, and ports from other applications. ### Prerequisites - A server with Docker and Docker Compose installed. It should have minimum of 8 GB RAM and 4 vCPUs. -- Public IP address for your server +- Public IP address for your server. You can also access the server using a local IP address in your VPC as long as its reachable from your browser. - TCP Ports 80, 443, 3478, 5349 and UDP Ports 3478, 5349 and 49152:49200 reachable from Internet (Port 80 and 443 to access the UI and rest of the ports for WebRTC Signaling) -Please refer to your server hosting provider's documentaion on how you can open these ports in the firewall of the server. +IMPORTANT: Please double check the ports on your firewall. Please refer to your server hosting provider's documentaion on how you can open these ports in the firewall of the server. ### Quick Setup @@ -113,12 +113,15 @@ curl -o setup_remote.sh https://raw.githubusercontent.com/dograh-hq/dograh/main/ The script will prompt you for: - Your server's public IP address - A password for the TURN server (optional, press Enter for default) +- Deployment mode — press Enter for **prebuilt** (pulls official images, the recommended default) or pick **build** to compile images from source. Use **build** when you maintain a fork or want to deploy local customizations — see [Building from source](#building-from-source) below. +- Number of FastAPI workers (uvicorn processes nginx will load-balance) — press Enter for the default of `4`, or pick a value that suits your server's CPU count. See [Scaling](/deployment/scaling) for sizing guidance and how to change this on a running stack. It will automatically: -- Download the docker-compose.yaml file +- Get the source — `docker-compose.yaml` only (prebuilt mode), or clone the full repo (build mode) - Create and configure nginx.conf with your IP address - Generate SSL certificates - Create an environment file with TURN server configuration +- Write a `docker-compose.override.yaml` with build directives (build mode only) ### Start the Application @@ -126,12 +129,20 @@ It will automatically: Please ensure that Docker Compose is installed on your machine before proceeding further. You can check whether its installed by running `docker compose version` command. If its not installed, please install it by following your server provider documentation. -After the setup script completes, start Dograh: +After the setup script completes, start Dograh. The script prints the exact command to run at the end — it differs slightly between modes: -```bash + +```bash Prebuilt mode cd dograh sudo docker compose --profile remote up --pull always ``` +```bash Build mode +cd dograh +sudo docker compose --profile remote up -d --build +``` + + +First boot in build mode takes several minutes — Docker has to build both the API and UI images before the stack comes up. ### Access Your Application @@ -162,12 +173,80 @@ The setup script creates the following files in the `dograh/` directory: | File | Purpose | |------|---------| | `docker-compose.yaml` | Main Docker Compose configuration | +| `docker-compose.override.yaml` | Build directives for `api` and `ui` (**build mode only**) | | `turnserver.conf` | Configuration for TURN server | | `nginx.conf` | nginx reverse proxy configuration with your IP | | `generate_certificate.sh` | Script to regenerate SSL certificates | | `certs/local.crt` | Self-signed SSL certificate | | `certs/local.key` | SSL private key | -| `.env` | Environment variables for TURN server | +| `.env` | Environment variables (TURN secret, JWT secret, FastAPI worker count) | + +### Building from source + +If you maintain a fork or want to ship local code changes without waiting for an official image release, pick **build** when the setup script prompts for deployment mode. + +In this mode the script: + +1. Clones the repo into `dograh/` (default: `dograh-hq/dograh@main`; the script will ask for an `owner/name` and `branch` so a fork can point at its own). +2. If you run the script from inside an existing dograh checkout, offers to use that checkout instead of cloning. +3. Generates a `docker-compose.override.yaml` next to `docker-compose.yaml` that swaps the `image:` directives for local `build:` directives. Docker Compose auto-loads the override file — you don't need to pass any `-f` flags. + +#### Updating after a code change + +Whenever the codebase changes — a `git pull`, a local edit, or a submodule bump — rebuild the `api` and `ui` images and recreate the running containers so they pick up the new code. + +**TL;DR (rebuilds both services and restarts in one shot):** + +```bash +cd dograh +sudo docker compose --profile remote up -d --build +``` + +`--build` rebuilds any service whose `build:` context has changed (both `api` and `ui` are wired through the `docker-compose.override.yaml` written by `setup_remote.sh`), and `up -d` recreates the affected containers so they run off the new image. + +**Step-by-step** — use this when you're pulling fresh code from a remote, or want to be explicit: + +```bash +cd dograh + +# 1. Pull the latest source. +git pull +git submodule update --init --recursive # picks up pipecat submodule bumps + +# 2. Rebuild both images. +sudo docker compose --profile remote build api ui + +# 3. Recreate the running containers so they use the new images. +sudo docker compose --profile remote up -d +``` + +If you update `pipecat` submodule, you must do `git submodule update --init --recursive`, or the Docker build step will not pick up `pipecat` changes + +**Rebuild a single service** — faster when you only changed one side: + +```bash +sudo docker compose --profile remote build api # or: build ui +sudo docker compose --profile remote up -d api # or: up -d ui +``` + +**Force a clean rebuild** — needed when a base image changed, a `pip install` was added, or you suspect a stale layer: + +```bash +sudo docker compose --profile remote build --no-cache api ui +sudo docker compose --profile remote up -d +``` + +**Verify the containers were actually recreated** — check the `CREATED` column shows a recent timestamp, not minutes/hours ago: + +```bash +sudo docker compose --profile remote ps +``` + +To revert to pulling official images, delete `docker-compose.override.yaml` and start the stack with `--pull always` as in the [prebuilt flow](#start-the-application). + + +Build mode is for **fork maintainers and self-hosters who want to deploy customized images** — not for active development on the code itself. For day-to-day contributor work (fast reload, IDE/LSP integration), use the [contributor setup](/contribution/setup) instead. + ### Next Steps Now that you are able to create and test a voice agent, you can setup a custom domain and setup SSL using letsencrypt. Checkout [Custom Domain](custom-domain) for instructions on how to do that. diff --git a/docs/deployment/scaling.mdx b/docs/deployment/scaling.mdx new file mode 100644 index 0000000..b544539 --- /dev/null +++ b/docs/deployment/scaling.mdx @@ -0,0 +1,159 @@ +--- +title: "Scaling" +description: "Run multiple FastAPI worker processes behind nginx for higher throughput" +--- + +By default, the Dograh API container runs a single uvicorn worker. For production traffic — especially with many concurrent voice calls (long-lived WebSockets) — you'll want multiple workers. Dograh ships with built-in support for this: nginx load-balances across N independent uvicorn processes using a `least_conn` strategy. + +This page covers how the multi-worker setup works, how to choose a worker count at install time, and how to change it on a running stack. + + +Multi-worker support requires **Dograh v1.29.0 or newer**. Earlier releases used `uvicorn --workers` and ship a different `setup_remote.sh` / `start_services_docker.sh` / `nginx.conf` layout — the steps below will not work on them. If your stack is older, [update first](/deployment/update) and then come back to this page. + + +## How it works + +The API container starts `FASTAPI_WORKERS` separate uvicorn processes, each bound to its own port (`8000`, `8001`, `8002`, …). nginx exposes a single upstream `dograh_api` that includes all worker ports and routes new requests to whichever worker currently has the **fewest active connections**. + +``` + ┌───────────────────────────────────┐ + │ api container │ + │ uvicorn worker 0 → :8000 │ + browser ──► nginx ──► │ uvicorn worker 1 → :8001 │ + (443) (least_conn) uvicorn worker 2 → :8002 │ + │ uvicorn worker 3 → :8003 │ + └───────────────────────────────────┘ +``` + + +This is intentionally **not** `uvicorn --workers N` (the built-in pre-fork mode). With pre-fork, the Linux kernel distributes new TCP connections across workers via `accept()` — fine for short HTTP requests, but long-lived WebSockets stick to whichever worker first accepted them. A handful of unlucky workers end up handling most of the streaming traffic while the others idle. Routing at the nginx layer with `least_conn` knows the actual per-worker connection count and distributes WebSockets evenly. + + +The `ari_manager` and `campaign_orchestrator` processes inside the API container stay as **singletons** regardless of `FASTAPI_WORKERS` — they coordinate global state (Asterisk channels, campaign scheduling) and should not be duplicated. ARQ background workers are controlled separately via `ARQ_WORKERS`. + +## Choosing a worker count + +A safe starting point is **one worker per available vCPU**, capped at 8 unless you've profiled your workload. The [Remote Server Deployment prerequisites](/deployment/docker#prerequisites) ask for a minimum of 4 vCPUs, so: + +| vCPUs | Suggested `FASTAPI_WORKERS` | +|-------|-----------------------------| +| 4 | 4 | +| 8 | 6–8 | +| 16+ | profile first | + +Each worker holds its own Python process and memory — budget roughly **300–500 MB RAM per worker** in addition to the postgres/redis/minio overhead. If you're near the 8 GB RAM minimum and see OOMs, drop the worker count before adding more. + +## Setting the worker count at install time + +`setup_remote.sh` prompts for the worker count alongside the other configuration: + +``` +Number of FastAPI workers (uvicorn processes nginx will load-balance): +[4]: +``` + +Press Enter for the default (`4`) or enter a different positive integer. Non-interactive callers (cloud-init, CI, Terraform) can set the value via environment variable instead: + +```bash +SERVER_IP=... TURN_SECRET=... FASTAPI_WORKERS=8 ./setup_remote.sh +``` + +The script wires the value into two places: + +- **`.env`** — sets `FASTAPI_WORKERS=N`, which `docker-compose.yaml` substitutes into the API container's environment. +- **`nginx.conf`** — generates an `upstream dograh_api` block with one `server api:800X` entry per worker. + +Both must agree, which is why the script generates them together. + +## Changing the worker count on a running stack + +Once Dograh is running, increasing or decreasing the worker count is a two-file edit plus a restart. You'll touch: + +1. **`.env`** — controls how many uvicorn processes the API container spawns. +2. **`nginx.conf`** — controls which worker ports nginx forwards to. + + +Both files must stay in sync. If `.env` says `FASTAPI_WORKERS=8` but `nginx.conf` only lists 4 upstream servers, half your workers will be idle. If `nginx.conf` lists more upstreams than there are workers, those upstreams will throw connection errors and trip the `proxy_next_upstream` fallback. + + +### Steps + +All commands run from your `dograh/` directory (the one with `docker-compose.yaml`). + +**1. Edit `.env`** and change the `FASTAPI_WORKERS` line: + +```bash +# Before +FASTAPI_WORKERS=4 + +# After +FASTAPI_WORKERS=8 +``` + +**2. Edit `nginx.conf`** and update the `upstream dograh_api` block so it has exactly one `server api:800X` line per worker, with ports starting at `8000`: + +```nginx +upstream dograh_api { + least_conn; + server api:8000 max_fails=3 fail_timeout=10s; + server api:8001 max_fails=3 fail_timeout=10s; + server api:8002 max_fails=3 fail_timeout=10s; + server api:8003 max_fails=3 fail_timeout=10s; + server api:8004 max_fails=3 fail_timeout=10s; # ← new + server api:8005 max_fails=3 fail_timeout=10s; # ← new + server api:8006 max_fails=3 fail_timeout=10s; # ← new + server api:8007 max_fails=3 fail_timeout=10s; # ← new + keepalive 32; +} +``` + +To **scale down**, remove the trailing `server` lines so the list matches the new `FASTAPI_WORKERS` value. + +**3. Recreate the affected containers.** The simplest path — brief downtime, no surprises: + +```bash +sudo docker compose --profile remote down +sudo docker compose --profile remote up -d +``` + +If you want to avoid downtime and your stack is healthy, you can recreate only the `api` and `nginx` containers: + +```bash +sudo docker compose --profile remote up -d --force-recreate api nginx +``` + +`--force-recreate` ensures the api container picks up the new `FASTAPI_WORKERS` value and nginx re-reads the updated `nginx.conf` (which is mounted read-only from disk). + +**4. Verify.** Confirm the right number of uvicorn processes are running. The API image is slim and doesn't include `ps`, so use Docker's host-side view instead: + +```bash +sudo docker compose --profile remote top api | grep uvicorn +``` + +You should see one line per worker. To confirm the bound ports, check the startup logs — each worker logs an `Uvicorn running on http://0.0.0.0:800X` line on boot: + +```bash +sudo docker compose --profile remote logs api | grep "Uvicorn running" +``` + +Then hit the API through nginx to confirm requests still flow: + +```bash +curl -k https://YOUR_SERVER_IP/api/v1/health +``` + +### Why not just re-run `setup_remote.sh`? + +`setup_remote.sh` refuses to overwrite an existing install by design — re-running it would regenerate `OSS_JWT_SECRET` (logging everyone out), reset the TURN shared secret (breaking WebRTC auth on connected clients), and regenerate SSL certificates. The two-file edit above is the supported way to change worker count after install. + +If you genuinely want a clean reinstall, see the `DOGRAH_FORCE_OVERWRITE=1` escape hatch documented in the script. + +## What this does not scale + +Multi-worker mode scales the HTTP/WebSocket API surface. It does **not** scale: + +- **ARQ background workers** — controlled by `ARQ_WORKERS` (defaults to 1). Increase this in the API container's environment if your background job queue backs up. +- **`ari_manager` / `campaign_orchestrator`** — singletons by design; they don't benefit from extra processes. +- **Postgres, Redis, MinIO** — each runs as a single container in the stack. For production-scale Postgres you'd run a managed service and point `DATABASE_URL` at it; the same applies to Redis and S3-compatible storage. + +For multi-machine horizontal scaling (separate API containers across hosts), see the [Custom Domain](/deployment/custom-domain) guide for the load-balancer-in-front-of-multiple-hosts pattern — it's the same idea as the in-container `least_conn` upstream, just one layer higher. diff --git a/docs/deployment/update.mdx b/docs/deployment/update.mdx index d4dc3a1..bbea9a0 100644 --- a/docs/deployment/update.mdx +++ b/docs/deployment/update.mdx @@ -5,6 +5,12 @@ description: "Update your self-hosted Dograh stack to a newer image version" This guide covers updating a Dograh stack you've already deployed with [Docker](/deployment/docker) or a [custom domain](/deployment/custom-domain). You run commands from the same directory that contains your `docker-compose.yaml` (this is the `dograh/` directory if you used `setup_remote.sh`). +There are three update flows depending on how you deployed: + +- **Remote, prebuilt mode** (most users) — use [`update_remote.sh`](#remote-prebuilt-mode-recommended) below. +- **Local Docker** — pull images and restart; see [Local deployment](#local-deployment). +- **Remote, build mode** (you have a `docker-compose.override.yaml`) — update via git; see [Updating a source build](#updating-a-source-build). + ## Find an image version Dograh publishes two images — `dograh-api` and `dograh-ui` — to both container registries: @@ -12,81 +18,64 @@ Dograh publishes two images — `dograh-api` and `dograh-ui` — to both contain - **GitHub Container Registry** — [github.com/orgs/dograh-hq/packages](https://github.com/orgs/dograh-hq/packages) - **Docker Hub** — [hub.docker.com/u/dograhai](https://hub.docker.com/u/dograhai) -Each release is published under two kinds of tags: +Each release is published under two kinds of tags. Note the formats differ between GitHub releases and the Docker image tags — `update_remote.sh` understands both and normalizes for you. -| Tag style | Example | When to use | -|-----------|---------|-------------| -| **Release tag** | `v0.8.2` | Stable, recommended for production | -| **Git commit SHA** | `a1b2c3d` | Bleeding edge — any commit merged to `main` | -| `latest` | `latest` | Tracks the most recent release tag | +| Where | Tag format | Example | When to use | +|-------|-----------|---------|-------------| +| GitHub release tag | `dograh-vX.Y.Z` | `dograh-v1.28.0` | What you see at [github.com/dograh-hq/dograh/releases](https://github.com/dograh-hq/dograh/releases) | +| Docker image tag (semver) | `X.Y.Z` | `1.28.0` | Stable, recommended for production | +| Docker image tag (SHA) | short SHA | `a1b2c3d` | Bleeding edge — any commit merged to `main` | +| Docker image tag (`latest`) | `latest` | `latest` | Tracks the most recent release tag | -Always update **`dograh-api`** and **`dograh-ui`** to the **same tag**. The two images are built from the same commit and the UI expects API responses in a matching shape — mixing versions will break the app. +Always update **`dograh-api`** and **`dograh-ui`** to the **same tag**. The two images are built from the same commit and the UI expects API responses in a matching shape — mixing versions will break the app. `update_remote.sh` handles this for you automatically. -## Option A: Update to the latest release +## Remote, prebuilt mode (recommended) -If your `docker-compose.yaml` uses `:latest` (the default), just pull and restart: +`update_remote.sh` is the supported path for updating a stack created with `setup_remote.sh`. In one shot it: - -```bash Local deployment -docker compose down -docker compose up --pull always -``` -```bash Remote deployment -cd dograh -sudo docker compose --profile remote down -sudo docker compose --profile remote up --pull always -``` - +- Asks for a target version (defaults to the latest release tag on GitHub). +- Pulls `docker-compose.yaml` at that version and pins both `api` and `ui` images to it. +- Regenerates `nginx.conf` and `turnserver.conf` from the upstream templates, so newer features (like [multi-worker scaling](/deployment/scaling)) are wired up correctly without manual editing. +- Reads your existing `.env` and appends any new required keys with safe defaults — your `OSS_JWT_SECRET`, `TURN_SECRET`, and other values are never touched. +- Backs up every file it changes with a `.bak.` suffix. -`--pull always` forces Docker to fetch the latest `:latest` from the registry instead of reusing your cached image. - -## Option B: Pin a specific tag - -To update (or roll back) to a specific release or commit, edit `docker-compose.yaml` and change the `image:` lines for both `api` and `ui` services to the same tag. - -Open the file: +From your install directory: ```bash -nano docker-compose.yaml +cd dograh +curl -o update_remote.sh https://raw.githubusercontent.com/dograh-hq/dograh/main/scripts/update_remote.sh +bash update_remote.sh ``` -Find these two lines: +You'll be prompted for the target version, defaulting to the most recent release. Accepted forms: bare semver (`1.28.0`), v-prefixed (`v1.28.0`), the full GitHub tag (`dograh-v1.28.0`), or `main` for bleeding edge — the script normalizes them. Non-interactive callers can set it via environment variable and skip the confirmation prompt: -```yaml - api: - image: ${REGISTRY:-dograhai}/dograh-api:latest - ui: - image: ${REGISTRY:-dograhai}/dograh-ui:latest +```bash +TARGET_VERSION=1.28.0 DOGRAH_UPDATE_YES=1 bash update_remote.sh ``` -Replace `:latest` with your chosen tag on **both** services — for example: +After the script finishes, apply the update by recreating the stack: -```yaml - api: - image: ${REGISTRY:-dograhai}/dograh-api:v0.8.2 - ui: - image: ${REGISTRY:-dograhai}/dograh-ui:v0.8.2 +```bash +sudo docker compose --profile remote down +sudo docker compose --profile remote up -d --pull always ``` -You can use either registry. Leave `REGISTRY` unset for Docker Hub (`dograhai`), or export `REGISTRY=ghcr.io/dograh-hq` to pull from GitHub Container Registry. +The script overwrites `docker-compose.yaml`, `nginx.conf`, and `turnserver.conf` from upstream templates. If you've made local edits to any of these (extra environment variables, custom ports, modified nginx routes), check the `.bak.` files after the update and re-apply your edits. -Then bring the stack down and back up: +## Local deployment - -```bash Local deployment +For local Docker installs (the [Quick Start](/deployment/docker#quick-start) flow or `setup_local.sh`), there are no host-side config files to refresh — pull new images and restart: + +```bash docker compose down docker compose up --pull always ``` -```bash Remote deployment -cd dograh -sudo docker compose --profile remote down -sudo docker compose --profile remote up --pull always -``` - + +To pin a specific version instead of `latest`, edit `docker-compose.yaml` and change both `image:` lines for `api` and `ui` to the same tag (e.g. `:1.28.0` — Docker image tags use bare semver, no `v` prefix), then run the commands above. ## Verify the update @@ -101,13 +90,54 @@ You should see the API and UI both running the tag you pinned. Hit the health endpoint to confirm the API is responding: ```bash -curl http://localhost:8000/api/v1/health +curl -k https://YOUR_SERVER_IP/api/v1/health # remote +curl http://localhost:8000/api/v1/health # local ``` ## Roll back -If something breaks, roll back by pinning the previous tag using the same process in **Option B** and restarting. Your Postgres data volume persists across `down`/`up` cycles, so agents and call history are preserved. +`update_remote.sh` saves backups of every file it touched. To roll back, restore them and recreate the stack — the exact commands (including the timestamp it used) are printed at the end of the script's output. The generic form: + +```bash +cd dograh +for f in docker-compose.yaml nginx.conf turnserver.conf .env; do + [[ -f "$f.bak." ]] && cp "$f.bak." "$f" +done +sudo docker compose --profile remote down +sudo docker compose --profile remote up -d +``` + +Your Postgres data volume persists across `down`/`up` cycles, so agents and call history are preserved. Rolling back across a database migration is not always safe — if the newer release ran a schema migration, downgrading may leave the DB in a state the older API doesn't understand. If in doubt, [open an issue](https://github.com/dograh-hq/dograh/issues) before rolling back. + +## Updating a source build + +If you deployed in **build mode** (you'll have a `docker-compose.override.yaml` in your install directory), `update_remote.sh` deliberately refuses to run — you already have the full repo locally and update via git: + +```bash +cd dograh +git fetch + +# Track latest main: +git pull +# Or pin to a specific release (git tag format is dograh-vX.Y.Z): +git checkout dograh-v1.28.0 + +# Pick up pipecat and other submodule bumps +git submodule update --init --recursive + +# Rebuild and restart +sudo docker compose --profile remote build +sudo docker compose --profile remote up -d +``` + + +If you update the `pipecat` submodule, you **must** run `git submodule update --init --recursive` before rebuilding, or the Docker build will not pick up `pipecat` changes. + + +If you maintain a fork with local customizations on top of upstream, merging conflicts in `docker-compose.yaml`, `nginx.conf`, `turnserver.conf`, or `setup_remote.sh` is up to you — resolve them as you would any other git merge. Leave `OSS_JWT_SECRET` and `TURN_SECRET` in `.env` unchanged across updates to preserve sessions and WebRTC auth. + +The same migration warning above applies: rolling back across a schema change can leave the DB in a state the older API can't read. diff --git a/docs/docs.json b/docs/docs.json index 8ed591e..09b55f5 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -145,6 +145,7 @@ "deployment/introduction", "deployment/docker", "deployment/custom-domain", + "deployment/scaling", "deployment/update", "deployment/heroku" ] diff --git a/docs/getting-started/index.mdx b/docs/getting-started/index.mdx index 14d9cd2..7dfebff 100644 --- a/docs/getting-started/index.mdx +++ b/docs/getting-started/index.mdx @@ -1,11 +1,10 @@ --- title: "Introduction" -description: "Open-source alternative to Vapi - build voice AI agents with full control & transparency" --- ## About Dograh -**Dograh is an open-source alternative to Vapi** - Dograh helps you build voice AI agents with an easy drag-and-drop workflow builder. Unlike proprietary solutions like Vapi, Dograh gives you: +**Dograh is an open-source alternative to platforms like Vapi and Retell** - Dograh helps you build voice AI agents with an easy drag-and-drop workflow builder. Unlike proprietary solutions like Vapi, Dograh gives you: - **100% open source** - no vendor lock-in, full transparency - **Self-hostable** - deploy anywhere, own your infrastructure - **Complete control** - every line of code is open and customizable @@ -25,7 +24,7 @@ Watch the following video to learn about Dograh’s capabilities. ## Setting up -Get the platform up and running using Docker with a single command on your local computer. If you are looking to deploy the platform on a server, please check the [Deployment](deployment/introduction) section. +Get the platform up and running using Docker with a single command on your local computer. If you are looking to deploy the platform on a server different than your local machine, please check the [Deployment](deployment/introduction) section. We collect anonymous usage data to improve the product. You can opt out by setting the `ENABLE_TELEMETRY` to `false` in the below command. diff --git a/docs/integrations/telephony/agent-stream.mdx b/docs/integrations/telephony/agent-stream.mdx index fcc18d5..432db9e 100644 --- a/docs/integrations/telephony/agent-stream.mdx +++ b/docs/integrations/telephony/agent-stream.mdx @@ -1,19 +1,16 @@ --- title: "Agent Stream" -description: "Stream audio to a Dograh agent over a WebSocket using inline provider credentials" +description: "Stream audio to a Dograh agent over a WebSocket" --- ## Overview -Agent Stream is a WebSocket endpoint that lets an external caller drive a Dograh agent run by passing everything inline in the query string — including the telephony provider's credentials. Unlike the standard inbound webhook flow, this path does **not** require a stored telephony configuration in your Dograh organization. You bring the credentials with you when you connect. +Agent Stream is a WebSocket endpoint that lets a telephony provider point its media stream at a single URL and drive a Dograh agent run. The agent UUID in the URL selects the agent; provider-specific identifiers in the query string (for Cloudonix: `Domain`) tell Dograh which stored telephony configuration to use for that call. The bearer token and other credentials are never passed in the URL — they live in the stored configuration and are used by Dograh to validate the session and to issue provider API calls (hangup, transfer) during the call. This is useful when: -- You manage your telephony tenant outside Dograh and only want Dograh as the agent runtime -- You're integrating Dograh into a SIP gateway or in-house dialer -- You need a single endpoint that doesn't need per-tenant DB rows for every caller - -The endpoint authenticates with a Dograh **API key** (so the connection itself is authorized against your organization), and routes the audio through the named provider's serializer. +- You're integrating Dograh into a SIP gateway or in-house dialer that already speaks a supported provider's streaming protocol +- You want one stable endpoint per agent rather than wiring up an inbound webhook per phone number Agent Stream currently supports the **Cloudonix** provider only. Other providers @@ -26,16 +23,15 @@ The endpoint authenticates with a Dograh **API key** (so the connection itself i ## Endpoint ``` -wss://app.dograh.com/api/v1/agent-stream/{workflow_uuid} +wss://app.dograh.com/api/v1/agent-stream/{agent_uuid} ``` -`{workflow_uuid}` is the agent's stable UUID (see [Get the Agent UUID](#get-the-agent-uuid) below). On self-hosted deployments, replace `app.dograh.com` with your backend host. +`{agent_uuid}` is the agent's stable UUID (see [Get the Agent UUID](#get-the-agent-uuid) below). On self-hosted deployments, replace `app.dograh.com` with your backend host. ## Prerequisites - A Dograh agent (workflow) — published or in draft is fine -- A Dograh API key with access to the organization that owns the agent -- Provider credentials (for Cloudonix: bearer token + domain ID) +- A Cloudonix telephony configuration in your Dograh organization whose `domain_id` matches the `Domain` you pass on the URL. Dograh uses the bearer token from this configuration to validate the call session and to issue provider API calls (hangup, transfer). ## Get the Agent UUID @@ -53,47 +49,25 @@ The Agent UUID is the workflow's stable identifier — it doesn't change when ve 2. Scroll to the **Agent UUID** section (also linked in the right-side nav) 3. Click the UUID code block, or use the **Copy UUID** button -## Create a Dograh API key - -Agent Stream auth uses your Dograh API key — not your provider's bearer token. The provider credentials go in separate query params (see [URL parameters](#url-parameters) below). - -1. From the dashboard, navigate to **API Keys** -2. Click **Create API key**, give it a name, and copy the generated key (it starts with `dg_`) -3. Store it securely — Dograh shows the full key only once at creation time - -For programmatic management, see the [API Keys reference](/api-reference/api-keys). - - - API keys are scoped to an organization. The agent referenced by the URL must - belong to the same organization as the key, otherwise the connection is - rejected with WebSocket close code `1008`. - - ## Connect to the WebSocket ### URL parameters | Param | Required | Description | | --- | --- | --- | -| `api_key` | Yes | Your Dograh API key (`dg_...`). Authorizes the connection. | | `provider` | Yes | Provider name. Currently only `cloudonix` is supported. | -| `session` | Yes (cloudonix) | Cloudonix domain bearer token used to drive the call (hangup, etc.). | -| `AccountSid` | Yes (cloudonix) | Cloudonix domain ID. | -| `CallSid` | Yes (cloudonix) | Cloudonix call SID for this session. | -| `callId` | No | SIP-side call identifier; persisted on the workflow run for record-keeping. | +| `Domain` | Yes (cloudonix) | Cloudonix domain ID. Dograh uses this to look up the matching stored telephony configuration and retrieve the bearer token used for provider API calls. | +| `callId` / `CallSid` | No | Call identifier from your side; persisted on the workflow run's `gathered_context` as `call_id`. The Cloudonix call SID used for streaming is taken from the `start` event payload, not this param. | | `from` | No | Caller phone number, persisted on the workflow run as `caller_number`. | | `to` | No | Called phone number, persisted on the workflow run as `called_number`. | ### Cloudonix example ``` -wss://app.dograh.com/api/v1/agent-stream/{workflow_uuid} - ?api_key=dg_xxxxxxxxxxxxxxxx - &provider=cloudonix - &session={CLOUDONIX_BEARER_TOKEN} - &AccountSid={CLOUDONIX_DOMAIN_ID} - &CallSid={CALL_SID} - &callId={SIP_CALL_ID} +wss://app.dograh.com/api/v1/agent-stream/{agent_uuid} + ?provider=cloudonix + &Domain={CLOUDONIX_DOMAIN_ID} + &callId={CALL_ID} &from=+15555550100 &to=+15555550199 ``` @@ -104,23 +78,22 @@ Use this URL inside the CXML `` your Cloudonix Voice Application returns - + ``` -The first two messages on the socket should be Cloudonix's standard `connected` and `start` events (Twilio-compatible framing). Dograh extracts `streamSid` and `callSid` from the `start` event and begins streaming audio. +The first two messages on the socket should be Cloudonix's standard `connected` and `start` events (Twilio-compatible framing). Dograh extracts `streamSid` and `callSid` from the `start` event payload, validates the session against Cloudonix using the bearer token from the stored telephony configuration matched by `Domain`, and then begins streaming audio. ## Workflow run lifecycle When the WebSocket is accepted, Dograh: -1. Resolves your API key to a user and organization -2. Looks up the workflow by `workflow_uuid`, scoped to that organization -3. Runs a quota check -4. Creates a new `WorkflowRun` (`call_type=inbound`, `mode=cloudonix`, name `WR-AGS-XXXXXXXX`) with the `from`/`to`/`callId`/`AccountSid` fields stamped on `initial_context` -5. Transitions the run to `running` and starts the agent pipeline +1. Looks up the workflow by `agent_uuid` +2. Runs a quota check against the workflow's owning user +3. Creates a new `WorkflowRun` (`call_type=inbound`, `mode=cloudonix`, name `WR-AGS-XXXXXXXX`) with the `from`/`to` numbers stamped on `initial_context`, the `callId`/`CallSid` stored as `call_id` on `gathered_context`, and `Domain` recorded under the run's `inbound_webhook` log +4. Transitions the run to `running` and starts the agent pipeline The run is visible under the agent's **Runs** tab as soon as it's minted, just like an inbound or outbound call. @@ -128,12 +101,11 @@ The run is visible under the agent's **Runs** tab as soon as it's minted, just l | Code | Reason | | --- | --- | -| `1008` | Auth or routing failure — invalid API key, unknown provider, workflow not found in your organization, or quota exceeded | +| `1008` | Routing failure — unknown provider, workflow not found, or quota exceeded | | `1011` | Server-side failure or unsupported provider for Agent Stream | -| `4400` | Provider-level handshake error — for cloudonix, missing `session`/`AccountSid` or malformed `connected`/`start` events | +| `4400` | Provider-level handshake error — for cloudonix, missing `Domain`, no matching telephony configuration, missing bearer token on the configuration, malformed `connected`/`start` events, or session validation failed against Cloudonix | ## Security notes -- Treat the URL as a secret — both the Dograh API key and the provider bearer token sit in the query string. Store and transmit it only over TLS, and avoid logging the raw URL in places where access is broader than your operations team. -- Rotate the Dograh API key from the dashboard if you suspect it has leaked. -- The Dograh API key authorizes the connection against your organization. The provider bearer token is only used by Dograh to drive provider-side actions (e.g. hangup) — it is not stored on the workflow run. +- Treat the URL as a secret — the agent UUID itself authorizes the connection. Store and transmit it only over TLS, and avoid logging the raw URL in places where access is broader than your operations team. +- No bearer tokens or provider secrets are passed in the URL. Provider credentials live in the stored telephony configuration (matched by `Domain` for Cloudonix) and are used server-side by Dograh to validate the session and issue provider API calls. diff --git a/scripts/setup_remote.sh b/scripts/setup_remote.sh index 7958d1e..334a516 100755 --- a/scripts/setup_remote.sh +++ b/scripts/setup_remote.sh @@ -44,30 +44,208 @@ if [[ -z "$TURN_SECRET" ]]; then echo -e "${BLUE}Generated random TURN secret${NC}" fi +# Deployment mode. Skip prompt if DEPLOY_MODE is already set. Non-interactive +# callers (cloud-init, CI, terraform) without a TTY default to "prebuilt" so +# existing automation keeps working without changes - explicitly set +# DEPLOY_MODE=build to opt into source builds from a non-interactive context. +if [[ -z "$DEPLOY_MODE" ]]; then + if [[ -t 0 ]]; then + echo "" + echo -e "${YELLOW}Deployment mode:${NC}" + echo " 1) prebuilt - pull official dograh images (recommended, fastest)" + echo " 2) build - build images from source (for forks or local customizations)" + read -p "Choose [1]: " mode_choice + mode_choice="${mode_choice:-1}" + case "$mode_choice" in + 1|prebuilt) DEPLOY_MODE="prebuilt" ;; + 2|build) DEPLOY_MODE="build" ;; + *) echo -e "${RED}Error: invalid choice '$mode_choice'${NC}"; exit 1 ;; + esac + else + DEPLOY_MODE="prebuilt" + fi +fi + +# Build mode needs source code - either use existing repo or clone fresh. +# Same TTY rule: prompt interactively, otherwise pick sensible defaults so +# automation that sets DEPLOY_MODE=build doesn't need to spell everything out. +if [[ "$DEPLOY_MODE" == "build" ]]; then + if [[ -z "$REPO_SOURCE" ]]; then + if [[ -d ".git" ]] && [[ -f "docker-compose.yaml" ]]; then + if [[ -t 0 ]]; then + echo "" + echo -e "${YELLOW}Detected a git repo with docker-compose.yaml in $(pwd).${NC}" + read -p "Build from this repo? [Y/n]: " use_existing + use_existing="${use_existing:-Y}" + if [[ "$use_existing" =~ ^[Yy] ]]; then + REPO_SOURCE="existing" + else + REPO_SOURCE="clone" + fi + else + REPO_SOURCE="existing" + fi + else + REPO_SOURCE="clone" + fi + fi + + if [[ "$REPO_SOURCE" == "clone" ]]; then + if [[ -z "$FORK_REPO" ]]; then + if [[ -t 0 ]]; then + echo "" + echo -e "${YELLOW}GitHub repo to clone (format: owner/name):${NC}" + read -p "[dograh-hq/dograh]: " FORK_REPO + FORK_REPO="${FORK_REPO:-dograh-hq/dograh}" + else + FORK_REPO="dograh-hq/dograh" + fi + fi + if [[ -z "$BRANCH" ]]; then + if [[ -t 0 ]]; then + echo -e "${YELLOW}Branch:${NC}" + read -p "[main]: " BRANCH + BRANCH="${BRANCH:-main}" + else + BRANCH="main" + fi + fi + fi +fi + # Telemetry opt-out (default: true) ENABLE_TELEMETRY="${ENABLE_TELEMETRY:-true}" -echo "" -echo -e "${GREEN}Configuration:${NC}" -echo -e " Server IP: ${BLUE}$SERVER_IP${NC}" -echo -e " TURN Secret: ${BLUE}********${NC}" -echo "" - -# Create project directory and download compose file (skip when -# DOGRAH_SKIP_DOWNLOAD=1 — e.g. e2e tests that already have a cloned repo). -if [[ "$DOGRAH_SKIP_DOWNLOAD" != "1" ]]; then - mkdir -p dograh 2>/dev/null || true - cd dograh - - echo -e "${BLUE}[1/5] Downloading docker-compose.yaml...${NC}" - curl -sS -o docker-compose.yaml https://raw.githubusercontent.com/dograh-hq/dograh/main/docker-compose.yaml - echo -e "${GREEN}✓ docker-compose.yaml downloaded${NC}" -else - echo -e "${BLUE}[1/5] Using docker-compose.yaml in current directory${NC}" +# Number of uvicorn worker processes. Each runs as its own process on a +# distinct port (8000, 8001, ...) and nginx balances across them with +# least_conn. Better than uvicorn --workers for long-lived WebSocket +# connections, which would otherwise stick to whichever worker accepted them. +if [[ -z "$FASTAPI_WORKERS" ]]; then + if [[ -t 0 ]]; then + echo "" + echo -e "${YELLOW}Number of FastAPI workers (uvicorn processes nginx will load-balance):${NC}" + read -p "[4]: " FASTAPI_WORKERS + FASTAPI_WORKERS="${FASTAPI_WORKERS:-4}" + else + FASTAPI_WORKERS="4" + fi fi -echo -e "${BLUE}[2/5] Creating nginx.conf...${NC}" -cat > nginx.conf << 'NGINX_EOF' +if ! [[ "$FASTAPI_WORKERS" =~ ^[1-9][0-9]*$ ]]; then + echo -e "${RED}Error: FASTAPI_WORKERS must be a positive integer (got: $FASTAPI_WORKERS)${NC}" + exit 1 +fi + +# Where setup artifacts (.env, certs, nginx.conf, etc.) will land. Build mode +# with an existing repo writes them next to docker-compose.yaml in cwd; +# everything else writes into a fresh dograh/ subdirectory. +if [[ "$DEPLOY_MODE" == "build" && "$REPO_SOURCE" == "existing" ]]; then + TARGET_DIR="." +else + TARGET_DIR="dograh" +fi + +# Refuse to overwrite an existing install - re-running this script would +# regenerate OSS_JWT_SECRET (invalidating logged-in sessions), reset the +# TURN secret (breaking WebRTC auth), and overwrite nginx.conf customizations. +# Set DOGRAH_FORCE_OVERWRITE=1 to bypass; DOGRAH_SKIP_DOWNLOAD=1 (used by e2e) +# also bypasses since those flows manage state themselves. +if [[ "$DOGRAH_FORCE_OVERWRITE" != "1" && "$DOGRAH_SKIP_DOWNLOAD" != "1" ]]; then + if [[ -f "$TARGET_DIR/.env" ]]; then + if [[ "$TARGET_DIR" == "." ]]; then + existing_path="$(pwd)/.env" + else + existing_path="$(pwd)/$TARGET_DIR/.env" + fi + echo "" + echo -e "${YELLOW}Detected an existing Dograh install:${NC}" + echo -e " ${YELLOW}$existing_path${NC}" + echo "" + echo -e "${RED}Refusing to continue - re-running setup would:${NC}" + echo -e "${RED} - overwrite .env (invalidates sessions, breaks TURN auth)${NC}" + echo -e "${RED} - regenerate SSL certificates${NC}" + echo -e "${RED} - reset nginx.conf and turnserver.conf customizations${NC}" + echo "" + echo -e "${BLUE}To upgrade an existing install, follow:${NC}" + echo -e " ${BLUE}https://docs.dograh.com/deployment/update${NC}" + echo "" + echo -e "${BLUE}To wipe state and reinstall from scratch, re-run with:${NC}" + echo -e " ${BLUE}DOGRAH_FORCE_OVERWRITE=1 ${NC}" + echo "" + exit 1 + fi +fi + +# Total step count depends on mode (build adds the override-file step) +if [[ "$DEPLOY_MODE" == "build" ]]; then + TOTAL=7 +else + TOTAL=6 +fi + +echo "" +echo -e "${GREEN}Configuration:${NC}" +echo -e " Server IP: ${BLUE}$SERVER_IP${NC}" +echo -e " TURN Secret: ${BLUE}********${NC}" +echo -e " Deploy mode: ${BLUE}$DEPLOY_MODE${NC}" +echo -e " FastAPI workers: ${BLUE}$FASTAPI_WORKERS${NC} (ports 8000..$((8000 + FASTAPI_WORKERS - 1)))" +if [[ "$DEPLOY_MODE" == "build" ]]; then + if [[ "$REPO_SOURCE" == "clone" ]]; then + echo -e " Source: ${BLUE}clone $FORK_REPO@$BRANCH${NC}" + else + echo -e " Source: ${BLUE}existing repo at $(pwd)${NC}" + fi +fi +echo "" + +# Step 1: get the source - either the standalone compose file (prebuilt mode) +# or the full repo (build mode). Skip the download/clone when +# DOGRAH_SKIP_DOWNLOAD=1 (e.g. e2e tests that already have everything in place). +if [[ "$DEPLOY_MODE" == "build" ]]; then + if [[ "$DOGRAH_SKIP_DOWNLOAD" == "1" ]]; then + echo -e "${BLUE}[1/$TOTAL] Using existing repo in current directory${NC}" + elif [[ "$REPO_SOURCE" == "clone" ]]; then + if [[ -e "dograh" ]]; then + echo -e "${RED}Error: 'dograh' directory already exists. Remove it or re-run with REPO_SOURCE=existing from inside it.${NC}" + exit 1 + fi + echo -e "${BLUE}[1/$TOTAL] Cloning $FORK_REPO (branch: $BRANCH)...${NC}" + git clone --branch "$BRANCH" --recurse-submodules "https://github.com/$FORK_REPO.git" dograh + cd dograh + echo -e "${GREEN}✓ Repo cloned${NC}" + else + echo -e "${BLUE}[1/$TOTAL] Using existing repo at $(pwd)${NC}" + fi +else + if [[ "$DOGRAH_SKIP_DOWNLOAD" != "1" ]]; then + mkdir -p dograh 2>/dev/null || true + cd dograh + + echo -e "${BLUE}[1/$TOTAL] Downloading docker-compose.yaml...${NC}" + curl -sS -o docker-compose.yaml https://raw.githubusercontent.com/dograh-hq/dograh/main/docker-compose.yaml + echo -e "${GREEN}✓ docker-compose.yaml downloaded${NC}" + else + echo -e "${BLUE}[1/$TOTAL] Using docker-compose.yaml in current directory${NC}" + fi +fi + +echo -e "${BLUE}[2/$TOTAL] Creating nginx.conf...${NC}" +# Build the upstream block first (needs shell interpolation for the server +# lines), then append the static server blocks via a quoted heredoc. The +# SERVER_IP_PLACEHOLDER gets replaced by sed below. +{ + echo "# Backend API workers — one uvicorn process per port, balanced by least_conn." + echo "# Generated by setup_remote.sh; regenerate to change worker count." + echo "upstream dograh_api {" + echo " least_conn;" + for ((i=0; i nginx.conf # Replace placeholder with actual IP sed -i.bak "s/SERVER_IP_PLACEHOLDER/$SERVER_IP/g" nginx.conf && rm -f nginx.conf.bak echo -e "${GREEN}✓ nginx.conf created${NC}" -echo -e "${BLUE}[3/5] Creating SSL certificate generation script...${NC}" +echo -e "${BLUE}[3/$TOTAL] Creating SSL certificate generation script...${NC}" cat > generate_certificate.sh << CERT_EOF #!/bin/bash mkdir -p certs @@ -163,11 +346,11 @@ CERT_EOF chmod +x generate_certificate.sh echo -e "${GREEN}✓ generate_certificate.sh created${NC}" -echo -e "${BLUE}[4/5] Generating SSL certificates...${NC}" +echo -e "${BLUE}[4/$TOTAL] Generating SSL certificates...${NC}" ./generate_certificate.sh echo -e "${GREEN}✓ SSL certificates generated${NC}" -echo -e "${BLUE}[5/6] Creating TURN server configuration...${NC}" +echo -e "${BLUE}[5/$TOTAL] Creating TURN server configuration...${NC}" cat > turnserver.conf << TURN_EOF # Coturn TURN Server - Docker Configuration # Auto-generated by setup_remote.sh @@ -200,7 +383,7 @@ log-file=stdout TURN_EOF echo -e "${GREEN}✓ turnserver.conf created${NC}" -echo -e "${BLUE}[6/6] Creating environment file...${NC}" +echo -e "${BLUE}[6/$TOTAL] Creating environment file...${NC}" OSS_JWT_SECRET=$(openssl rand -hex 32) cat > .env << ENV_EOF @@ -222,9 +405,43 @@ OSS_JWT_SECRET=$OSS_JWT_SECRET # Telemetry (set to false to disable) ENABLE_TELEMETRY=$ENABLE_TELEMETRY + +# Number of uvicorn worker processes; nginx load-balances across them +# (ports 8000..$((8000 + FASTAPI_WORKERS - 1))) with least_conn. +# Must match the upstream block in nginx.conf — re-run setup_remote.sh +# (with DOGRAH_FORCE_OVERWRITE=1) to change. +FASTAPI_WORKERS=$FASTAPI_WORKERS ENV_EOF echo -e "${GREEN}✓ .env file created${NC}" +# In build mode, write the override file that swaps prebuilt images for +# local builds. Compose auto-loads docker-compose.override.yaml, so no -f flag +# is needed at runtime. +if [[ "$DEPLOY_MODE" == "build" ]]; then + echo -e "${BLUE}[7/$TOTAL] Creating docker-compose.override.yaml...${NC}" + cat > docker-compose.override.yaml << 'OVERRIDE_EOF' +# Auto-generated by setup_remote.sh (build mode). +# Overrides docker-compose.yaml to build api and ui images from local source +# instead of pulling them from a registry. Remove this file to revert to +# pulling prebuilt images. +services: + api: + build: + context: . + dockerfile: api/Dockerfile + image: dograh-local/dograh-api:local + pull_policy: never + + ui: + build: + context: . + dockerfile: ui/Dockerfile + image: dograh-local/dograh-ui:local + pull_policy: never +OVERRIDE_EOF + echo -e "${GREEN}✓ docker-compose.override.yaml created${NC}" +fi + echo "" echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ Setup Complete! ║${NC}" @@ -232,6 +449,9 @@ echo -e "${GREEN}╚════════════════════ echo "" echo -e "Files created in ${BLUE}$(pwd)${NC}:" echo " - docker-compose.yaml" +if [[ "$DEPLOY_MODE" == "build" ]]; then + echo " - docker-compose.override.yaml (build directives)" +fi echo " - nginx.conf" echo " - turnserver.conf" echo " - generate_certificate.sh" @@ -241,7 +461,29 @@ echo " - .env" echo "" echo -e "${YELLOW}To start Dograh, run:${NC}" echo "" -echo -e " ${BLUE}sudo docker compose --profile remote up --pull always${NC}" +# The script's own cd into dograh/ doesn't persist to the user's shell, so +# remind them to cd themselves — except when they're already there (build mode +# with REPO_SOURCE=existing, which writes into cwd). +if [[ "$DEPLOY_MODE" != "build" || "$REPO_SOURCE" != "existing" ]]; then + echo -e " ${BLUE}cd $(pwd)${NC}" +fi +if [[ "$DEPLOY_MODE" == "build" ]]; then + echo -e " ${BLUE}sudo docker compose --profile remote up -d --build${NC}" + echo "" + echo -e "${YELLOW}A docker-compose.override.yaml has been created alongside${NC}" + echo -e "${YELLOW}docker-compose.yaml. Compose auto-loads it, so no -f flag is${NC}" + echo -e "${YELLOW}needed — it swaps the prebuilt images for local builds.${NC}" + echo "" + echo -e "${YELLOW}The first build can take several minutes${NC}" + echo -e "${YELLOW}(downloading base images, installing dependencies).${NC}" + echo -e "${YELLOW}If you know how to speed this up, we would love a pull request.${NC}" + echo "" + echo -e "${YELLOW}To rebuild after editing api/ or ui/ code:${NC}" + echo "" + echo -e " ${BLUE}sudo docker compose --profile remote build && sudo docker compose --profile remote up -d${NC}" +else + echo -e " ${BLUE}sudo docker compose --profile remote up --pull always${NC}" +fi echo "" echo -e "${YELLOW}Your application will be available at:${NC}" echo "" diff --git a/scripts/start_services_docker.sh b/scripts/start_services_docker.sh index 48965c9..435c1cb 100755 --- a/scripts/start_services_docker.sh +++ b/scripts/start_services_docker.sh @@ -61,7 +61,16 @@ start() { start ari_manager python -m api.services.telephony.ari_manager start campaign_orchestrator python -m api.services.campaign.campaign_orchestrator -start uvicorn uvicorn api.app:app --host 0.0.0.0 --port "$UVICORN_BASE_PORT" --workers "$FASTAPI_WORKERS" + +# Spawn FASTAPI_WORKERS independent uvicorn processes on consecutive ports +# starting at UVICORN_BASE_PORT. nginx upstream (configured in setup_remote.sh) +# balances across them with least_conn — better than uvicorn --workers for +# long-lived WebSocket connections, which would otherwise stick to whichever +# worker accepted them first. +for ((i=0; i # or: git pull${NC}" + echo -e " ${BLUE}git submodule update --init --recursive${NC}" + echo -e " ${BLUE}sudo docker compose --profile remote build${NC}" + echo -e " ${BLUE}sudo docker compose --profile remote up -d${NC}" + echo "" + echo -e "${YELLOW}See https://docs.dograh.com/deployment/update#updating-a-source-build${NC}" + exit 1 +fi + +############################################################################### +### Discover existing config from .env +############################################################################### + +# Save anything the caller exported before we overwrite from .env. +_caller_FASTAPI_WORKERS="$FASTAPI_WORKERS" +_caller_TARGET_VERSION="$TARGET_VERSION" + +set -a +# shellcheck disable=SC1091 +. ./.env +set +a + +# SERVER_IP isn't a literal key in .env — derive it from BACKEND_API_ENDPOINT. +if [[ -z "$SERVER_IP" ]]; then + if [[ -n "$BACKEND_API_ENDPOINT" ]]; then + SERVER_IP="${BACKEND_API_ENDPOINT#https://}" + SERVER_IP="${SERVER_IP#http://}" + fi +fi + +if [[ -z "$SERVER_IP" ]]; then + echo -e "${RED}Error: could not determine SERVER_IP from .env${NC}" + echo -e "${RED}Expected BACKEND_API_ENDPOINT=https:// in .env${NC}" + exit 1 +fi + +if [[ -z "$TURN_SECRET" ]]; then + echo -e "${RED}Error: TURN_SECRET not found in .env${NC}" + exit 1 +fi + +# Reapply caller overrides on top of sourced .env so e.g. FASTAPI_WORKERS=8 ./update_remote.sh works. +[[ -n "$_caller_FASTAPI_WORKERS" ]] && FASTAPI_WORKERS="$_caller_FASTAPI_WORKERS" +[[ -n "$_caller_TARGET_VERSION" ]] && TARGET_VERSION="$_caller_TARGET_VERSION" + +############################################################################### +### Determine target version +############################################################################### + +if [[ -z "$TARGET_VERSION" ]]; then + echo -e "${BLUE}Fetching latest release tag from GitHub...${NC}" + LATEST_TAG=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null \ + | grep -E '"tag_name":' | head -1 \ + | sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/' || true) + + if [[ -z "$LATEST_TAG" ]]; then + echo -e "${YELLOW}Could not auto-discover latest tag — defaulting to 'main'.${NC}" + LATEST_TAG="main" + fi + + if [[ -t 0 ]]; then + echo "" + echo -e "${YELLOW}Target version. Accepted forms: bare semver (1.28.0), v-prefixed (v1.28.0),${NC}" + echo -e "${YELLOW}full git tag (dograh-v1.28.0), or 'main' for bleeding edge.${NC}" + read -p "[$LATEST_TAG]: " TARGET_VERSION + TARGET_VERSION="${TARGET_VERSION:-$LATEST_TAG}" + else + TARGET_VERSION="$LATEST_TAG" + fi +fi + +# "latest" isn't a real ref on GitHub — treat it as "latest release". +if [[ "$TARGET_VERSION" == "latest" ]]; then + TARGET_VERSION=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null \ + | grep -E '"tag_name":' | head -1 \ + | sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/' || true) + if [[ -z "$TARGET_VERSION" ]]; then + echo -e "${RED}Error: could not resolve 'latest' to a release tag${NC}" + exit 1 + fi +fi + + +# GitHub release tags use a 'dograh-v' prefix (e.g. dograh-v1.28.0); Docker +# image tags on Docker Hub drop both the prefix and the 'v' (e.g. ':1.28.0'). +# Users commonly type shortcuts like '1.28.0' or 'v1.28.0' — try all reasonable +# variants so the script accepts any of those forms. +TRY_TAGS=("$TARGET_VERSION") +case "$TARGET_VERSION" in + main|HEAD) + ;; # branch refs — leave as-is + dograh-*) + ;; # already in the full tag form + v*) + TRY_TAGS+=("dograh-$TARGET_VERSION") + ;; + *) + TRY_TAGS+=("dograh-v$TARGET_VERSION" "v$TARGET_VERSION" "dograh-$TARGET_VERSION") + ;; +esac + +echo -e "${BLUE}Validating target version: $TARGET_VERSION...${NC}" +RESOLVED_TAG="" +for tag in "${TRY_TAGS[@]}"; do + if curl -fsI "https://raw.githubusercontent.com/$REPO/$tag/docker-compose.yaml" >/dev/null 2>&1; then + RESOLVED_TAG="$tag" + break + fi +done + +if [[ -z "$RESOLVED_TAG" ]]; then + echo -e "${RED}Error: could not find a git tag matching '$TARGET_VERSION'${NC}" + echo -e "${RED}Tried: ${TRY_TAGS[*]}${NC}" + echo -e "${RED}See available releases at: https://github.com/$REPO/releases${NC}" + exit 1 +fi + +if [[ "$RESOLVED_TAG" != "$TARGET_VERSION" ]]; then + echo -e "${GREEN}✓ Resolved '$TARGET_VERSION' to git tag '$RESOLVED_TAG'${NC}" +fi +TARGET_VERSION="$RESOLVED_TAG" +RAW_BASE="https://raw.githubusercontent.com/$REPO/$TARGET_VERSION" + +# Derive the Docker image tag from the git tag. Tags on Docker Hub use bare +# semver — strip the 'dograh-' prefix and the leading 'v'. +IMAGE_TAG="" +case "$TARGET_VERSION" in + dograh-v*) IMAGE_TAG="${TARGET_VERSION#dograh-v}" ;; + v*) IMAGE_TAG="${TARGET_VERSION#v}" ;; + main|HEAD) IMAGE_TAG="" ;; + *) [[ "$TARGET_VERSION" =~ ^[0-9] ]] && IMAGE_TAG="$TARGET_VERSION" ;; +esac + +# Verify the image tag actually exists on Docker Hub. If not (e.g. CI hasn't +# published yet), fall back to ':latest' rather than pinning to a missing tag. +if [[ -n "$IMAGE_TAG" ]]; then + if curl -fsI "https://hub.docker.com/v2/repositories/dograhai/dograh-api/tags/$IMAGE_TAG/" >/dev/null 2>&1; then + echo -e "${GREEN}✓ Image tag :$IMAGE_TAG found on Docker Hub${NC}" + else + echo -e "${YELLOW}Warning: image tag :$IMAGE_TAG not found on Docker Hub — leaving images at :latest${NC}" + IMAGE_TAG="" + fi +fi + +############################################################################### +### Reconcile required keys that may be missing on older installs +############################################################################### + +if [[ -z "$FASTAPI_WORKERS" ]]; then + if [[ -t 0 ]]; then + echo "" + echo -e "${YELLOW}FASTAPI_WORKERS not set in .env. Number of uvicorn workers nginx will load-balance:${NC}" + read -p "[4]: " FASTAPI_WORKERS + FASTAPI_WORKERS="${FASTAPI_WORKERS:-4}" + else + FASTAPI_WORKERS="4" + fi +fi + +if ! [[ "$FASTAPI_WORKERS" =~ ^[1-9][0-9]*$ ]]; then + echo -e "${RED}Error: FASTAPI_WORKERS must be a positive integer (got: $FASTAPI_WORKERS)${NC}" + exit 1 +fi + +############################################################################### +### Summary + confirmation +############################################################################### + +echo "" +echo -e "${GREEN}Update plan:${NC}" +echo -e " Server IP: ${BLUE}$SERVER_IP${NC}" +echo -e " Target version: ${BLUE}$TARGET_VERSION${NC}" +echo -e " FastAPI workers: ${BLUE}$FASTAPI_WORKERS${NC} (ports 8000..$((8000 + FASTAPI_WORKERS - 1)))" +echo "" +echo -e "${YELLOW}Files that will be replaced (backups saved with suffix .bak.$TIMESTAMP):${NC}" +echo " - docker-compose.yaml (pulled from GitHub at $TARGET_VERSION)" +echo " - nginx.conf (regenerated from this script's template)" +echo " - turnserver.conf (regenerated from this script's template)" +echo " - .env (existing values preserved; missing keys appended)" +echo "" +echo -e "${YELLOW}Any local customizations to these files will be overwritten — check the backup${NC}" +echo -e "${YELLOW}files if you need to re-apply edits afterwards.${NC}" +echo "" + +if [[ -t 0 && "$DOGRAH_UPDATE_YES" != "1" ]]; then + read -p "Proceed? [y/N]: " confirm + if ! [[ "$confirm" =~ ^[Yy] ]]; then + echo -e "${RED}Aborted.${NC}" + exit 1 + fi +fi + +############################################################################### +### Step 1 — backups +############################################################################### + +echo "" +echo -e "${BLUE}[1/5] Backing up existing files...${NC}" +for f in docker-compose.yaml nginx.conf turnserver.conf .env; do + if [[ -f "$f" ]]; then + cp -p "$f" "$f.bak.$TIMESTAMP" + echo -e " ${GREEN}✓ $f → $f.bak.$TIMESTAMP${NC}" + fi +done + +############################################################################### +### Step 2 — docker-compose.yaml (download + pin image tags) +############################################################################### + +echo -e "${BLUE}[2/5] Downloading docker-compose.yaml at $TARGET_VERSION...${NC}" +curl -fsSL -o docker-compose.yaml "$RAW_BASE/docker-compose.yaml" + +# Pin api/ui image tags when we resolved one. For branch refs (main) IMAGE_TAG +# is empty, so the images stay at ':latest' and `up --pull always` grabs the +# newest build of that branch. +if [[ -n "$IMAGE_TAG" ]]; then + sed -i.tmp -E "s#(dograh-(api|ui)):latest#\1:$IMAGE_TAG#g" docker-compose.yaml + rm -f docker-compose.yaml.tmp + echo -e "${GREEN}✓ docker-compose.yaml updated; images pinned to :$IMAGE_TAG${NC}" +else + echo -e "${GREEN}✓ docker-compose.yaml updated (image tags left at :latest)${NC}" +fi + +############################################################################### +### Step 3 — nginx.conf (regenerate from embedded template) +############################################################################### + +echo -e "${BLUE}[3/5] Regenerating nginx.conf...${NC}" +{ + echo "# Backend API workers — one uvicorn process per port, balanced by least_conn." + echo "# Generated by update_remote.sh; regenerate to change worker count." + echo "upstream dograh_api {" + echo " least_conn;" + for ((i=0; i nginx.conf + +sed -i.tmp "s/SERVER_IP_PLACEHOLDER/$SERVER_IP/g" nginx.conf && rm -f nginx.conf.tmp +echo -e "${GREEN}✓ nginx.conf regenerated${NC}" + +############################################################################### +### Step 4 — turnserver.conf (regenerate from embedded template) +############################################################################### + +echo -e "${BLUE}[4/5] Regenerating turnserver.conf...${NC}" +cat > turnserver.conf << TURN_EOF +# Coturn TURN Server - Docker Configuration +# Auto-generated by update_remote.sh + +# Listener ports +listening-port=3478 +tls-listening-port=5349 + +# Relay port range +min-port=49152 +max-port=49200 + +# Network - external IP for NAT traversal +external-ip=$SERVER_IP + +# Realm +realm=dograh.com + +# Authentication (TURN REST API with time-limited credentials) +use-auth-secret +static-auth-secret=$TURN_SECRET + +# Security +fingerprint +no-cli +no-multicast-peers + +# Logging +log-file=stdout +TURN_EOF +echo -e "${GREEN}✓ turnserver.conf regenerated${NC}" + +############################################################################### +### Step 5 — reconcile .env (append missing keys; never overwrite existing) +############################################################################### + +echo -e "${BLUE}[5/5] Reconciling .env...${NC}" +if ! grep -q "^FASTAPI_WORKERS=" .env; then + { + echo "" + echo "# Number of uvicorn worker processes; nginx load-balances across them" + echo "# (ports 8000..$((8000 + FASTAPI_WORKERS - 1))) with least_conn." + echo "FASTAPI_WORKERS=$FASTAPI_WORKERS" + } >> .env + echo -e "${GREEN}✓ Added FASTAPI_WORKERS=$FASTAPI_WORKERS to .env${NC}" +else + echo -e "${GREEN}✓ .env already has FASTAPI_WORKERS — left unchanged${NC}" +fi + +############################################################################### +### Done — print restart + rollback instructions +############################################################################### + +echo "" +echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Update Prepared! ║${NC}" +echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "Backups: ${BLUE}*.bak.$TIMESTAMP${NC}" +echo "" +echo -e "${YELLOW}To apply, recreate the stack:${NC}" +echo "" +echo -e " ${BLUE}sudo docker compose --profile remote down${NC}" +echo -e " ${BLUE}sudo docker compose --profile remote up -d --pull always${NC}" +echo "" +echo -e "${YELLOW}To roll back, restore the backups and recreate:${NC}" +echo "" +echo -e " ${BLUE}for f in docker-compose.yaml nginx.conf turnserver.conf .env; do${NC}" +echo -e " ${BLUE} [[ -f \"\$f.bak.$TIMESTAMP\" ]] && cp \"\$f.bak.$TIMESTAMP\" \"\$f\"${NC}" +echo -e " ${BLUE}done${NC}" +echo -e " ${BLUE}sudo docker compose --profile remote down && sudo docker compose --profile remote up -d${NC}" +echo ""