mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
chore: refactor setup scrpts (#288)
* refactor setup scrpts * update docker compose to use dograh-init * avoid creating unnecessary conf files * fix local setup script * add agents.md
This commit is contained in:
parent
4ff1f576f0
commit
87699f2dee
18 changed files with 1321 additions and 1178 deletions
82
deploy/templates/nginx.remote.conf.template
Normal file
82
deploy/templates/nginx.remote.conf.template
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
__DOGRAH_UPSTREAM_BLOCK__
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name __DOGRAH_PUBLIC_HOST__;
|
||||||
|
|
||||||
|
# Redirect all HTTP to HTTPS
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name __DOGRAH_PUBLIC_HOST__;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/local.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/local.key;
|
||||||
|
|
||||||
|
# Basic TLS settings
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
# Backend API and WebSockets - bypass the UI, go straight to the
|
||||||
|
# api workers via the least_conn upstream defined above.
|
||||||
|
location /api/v1/ {
|
||||||
|
proxy_pass http://dograh_api;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# Retry on a dead/restarting worker
|
||||||
|
proxy_next_upstream error timeout http_502 http_503 http_504;
|
||||||
|
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
|
||||||
|
# Long-lived WebSockets (audio streaming, signaling)
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
|
||||||
|
# Don't buffer streamed responses
|
||||||
|
proxy_buffering off;
|
||||||
|
client_max_body_size 100M;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://ui:3010;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# Important for WebSockets / hot reload etc.
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
|
||||||
|
# Rewrite localhost MinIO URLs in API responses to use current domain
|
||||||
|
sub_filter 'http://localhost:9000/voice-audio/' 'https://$host/voice-audio/';
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types application/json text/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /voice-audio/ {
|
||||||
|
proxy_pass http://minio:9000/voice-audio/;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# Headers for file downloads from MinIO
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
|
||||||
|
# Allow large file downloads
|
||||||
|
proxy_buffering off;
|
||||||
|
client_max_body_size 100M;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
deploy/templates/turnserver.remote.conf.template
Normal file
28
deploy/templates/turnserver.remote.conf.template
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Coturn TURN Server - Docker Configuration
|
||||||
|
# Auto-generated by Dograh remote config renderer.
|
||||||
|
|
||||||
|
# Listener ports
|
||||||
|
listening-port=3478
|
||||||
|
tls-listening-port=5349
|
||||||
|
|
||||||
|
# Relay port range
|
||||||
|
min-port=49152
|
||||||
|
max-port=49200
|
||||||
|
|
||||||
|
# Network - external IP / host for NAT traversal
|
||||||
|
external-ip=__DOGRAH_TURN_EXTERNAL_IP__
|
||||||
|
|
||||||
|
# Realm
|
||||||
|
realm=dograh.com
|
||||||
|
|
||||||
|
# Authentication (TURN REST API with time-limited credentials)
|
||||||
|
use-auth-secret
|
||||||
|
static-auth-secret=__DOGRAH_TURN_SECRET__
|
||||||
|
|
||||||
|
# Security
|
||||||
|
fingerprint
|
||||||
|
no-cli
|
||||||
|
no-multicast-peers
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log-file=stdout
|
||||||
|
|
@ -59,21 +59,69 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
|
dograh-init:
|
||||||
|
image: bash:5.2
|
||||||
|
container_name: dograh_init
|
||||||
|
profiles: ["remote", "local-turn"]
|
||||||
|
environment:
|
||||||
|
ENVIRONMENT: "${ENVIRONMENT:-local}"
|
||||||
|
SERVER_IP: "${SERVER_IP:-}"
|
||||||
|
PUBLIC_HOST: "${PUBLIC_HOST:-}"
|
||||||
|
PUBLIC_BASE_URL: "${PUBLIC_BASE_URL:-}"
|
||||||
|
BACKEND_API_ENDPOINT: "${BACKEND_API_ENDPOINT:-http://localhost:8000}"
|
||||||
|
MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT:-http://localhost:9000}"
|
||||||
|
TURN_HOST: "${TURN_HOST:-}"
|
||||||
|
TURN_SECRET: "${TURN_SECRET:-}"
|
||||||
|
FASTAPI_WORKERS: "${FASTAPI_WORKERS:-1}"
|
||||||
|
volumes:
|
||||||
|
- ./scripts:/workspace/scripts:ro
|
||||||
|
- ./deploy:/workspace/deploy:ro
|
||||||
|
- ./certs:/certs:ro
|
||||||
|
- nginx-generated:/generated/nginx
|
||||||
|
- coturn-generated:/generated/coturn
|
||||||
|
command:
|
||||||
|
- /workspace/scripts/run_dograh_init.sh
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
container_name: nginx_https
|
container_name: nginx_https
|
||||||
profiles: ["remote"]
|
profiles: ["remote"]
|
||||||
depends_on:
|
depends_on:
|
||||||
- ui
|
dograh-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
ui:
|
||||||
|
condition: service_started
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
- nginx-generated:/etc/nginx/conf.d:ro
|
||||||
- ./certs:/etc/nginx/certs:ro
|
- ./certs:/etc/nginx/certs:ro
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
|
coturn:
|
||||||
|
image: coturn/coturn:4.8.0
|
||||||
|
container_name: coturn
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles: ["remote", "local-turn"]
|
||||||
|
depends_on:
|
||||||
|
dograh-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
ports:
|
||||||
|
- "3478:3478/udp"
|
||||||
|
- "3478:3478/tcp"
|
||||||
|
- "5349:5349/udp"
|
||||||
|
- "5349:5349/tcp"
|
||||||
|
- "49152-49200:49152-49200/udp"
|
||||||
|
volumes:
|
||||||
|
- coturn-generated:/etc/coturn:ro
|
||||||
|
command:
|
||||||
|
- -c
|
||||||
|
- /etc/coturn/turnserver.conf
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
api:
|
api:
|
||||||
image: ${REGISTRY:-dograhai}/dograh-api:latest
|
image: ${REGISTRY:-dograhai}/dograh-api:latest
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -108,8 +156,8 @@ services:
|
||||||
MINIO_SECURE: "false"
|
MINIO_SECURE: "false"
|
||||||
|
|
||||||
# Number of uvicorn worker processes (each is its own process bound to a
|
# Number of uvicorn worker processes (each is its own process bound to a
|
||||||
# distinct port starting at 8000). nginx load-balances across them with
|
# distinct port starting at 8000). dograh-init renders nginx upstreams
|
||||||
# least_conn — see setup_remote.sh.
|
# from this value and nginx load-balances across them with least_conn.
|
||||||
FASTAPI_WORKERS: "${FASTAPI_WORKERS:-1}"
|
FASTAPI_WORKERS: "${FASTAPI_WORKERS:-1}"
|
||||||
|
|
||||||
# Langfuse — credentials can be set here or per-organization via the UI
|
# Langfuse — credentials can be set here or per-organization via the UI
|
||||||
|
|
@ -195,25 +243,6 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
coturn:
|
|
||||||
image: coturn/coturn:4.8.0
|
|
||||||
container_name: coturn
|
|
||||||
restart: unless-stopped
|
|
||||||
profiles: ["remote", "local-turn"]
|
|
||||||
ports:
|
|
||||||
- "3478:3478/udp"
|
|
||||||
- "3478:3478/tcp"
|
|
||||||
- "5349:5349/udp"
|
|
||||||
- "5349:5349/tcp"
|
|
||||||
- "49152-49200:49152-49200/udp"
|
|
||||||
volumes:
|
|
||||||
- ./turnserver.conf:/etc/coturn/turnserver.conf:ro
|
|
||||||
command:
|
|
||||||
- -c
|
|
||||||
- /etc/coturn/turnserver.conf
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
|
@ -221,6 +250,10 @@ volumes:
|
||||||
driver: local
|
driver: local
|
||||||
shared-tmp:
|
shared-tmp:
|
||||||
driver: local
|
driver: local
|
||||||
|
nginx-generated:
|
||||||
|
driver: local
|
||||||
|
coturn-generated:
|
||||||
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
app-network:
|
app-network:
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,10 @@ It will automatically:
|
||||||
- Verify DNS configuration
|
- Verify DNS configuration
|
||||||
- Install Certbot
|
- Install Certbot
|
||||||
- Generate Let's Encrypt SSL certificates
|
- Generate Let's Encrypt SSL certificates
|
||||||
- Update nginx configuration
|
- Update the canonical public host/base URL settings in `.env`
|
||||||
|
- Validate the runtime config that `dograh-init` will render from `.env`
|
||||||
- Configure automatic certificate renewal
|
- Configure automatic certificate renewal
|
||||||
- Restart Dograh services
|
- Restart Dograh services through the validated startup wrapper
|
||||||
|
|
||||||
Once complete, your application will be available at `https://voice.yourcompany.com`.
|
Once complete, your application will be available at `https://voice.yourcompany.com`.
|
||||||
|
|
||||||
|
|
@ -130,7 +131,7 @@ Replace `voice.yourcompany.com` with your actual domain name.
|
||||||
Certbot will:
|
Certbot will:
|
||||||
1. Verify that you control the domain
|
1. Verify that you control the domain
|
||||||
2. Generate SSL certificates
|
2. Generate SSL certificates
|
||||||
3. Store them in `/etc/letsencrypt/archive/voice.yourcompany.com/`
|
3. Store them in `/etc/letsencrypt/live/voice.yourcompany.com/`
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
You'll be prompted to enter an email address for renewal notifications and agree to the terms of service.
|
You'll be prompted to enter an email address for renewal notifications and agree to the terms of service.
|
||||||
|
|
@ -142,44 +143,31 @@ Copy the generated certificates to the dograh certs directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd dograh
|
cd dograh
|
||||||
sudo cp /etc/letsencrypt/archive/voice.yourcompany.com/fullchain1.pem certs/local.crt
|
sudo cp /etc/letsencrypt/live/voice.yourcompany.com/fullchain.pem certs/local.crt
|
||||||
sudo cp /etc/letsencrypt/archive/voice.yourcompany.com/privkey1.pem certs/local.key
|
sudo cp /etc/letsencrypt/live/voice.yourcompany.com/privkey.pem certs/local.key
|
||||||
sudo chmod 644 certs/local.crt certs/local.key
|
sudo chmod 644 certs/local.crt certs/local.key
|
||||||
```
|
```
|
||||||
|
|
||||||
### Update nginx Configuration
|
### Update Canonical Public URL Settings
|
||||||
|
|
||||||
Update the nginx configuration to use your domain name. Open the nginx configuration file:
|
Update `.env` so the canonical remote settings point at your domain:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nano dograh/nginx.conf
|
nano dograh/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
Update the `server_name` directive with your domain:
|
```bash
|
||||||
|
PUBLIC_HOST=voice.yourcompany.com
|
||||||
```nginx
|
PUBLIC_BASE_URL=https://voice.yourcompany.com
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name voice.yourcompany.com;
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/local.crt;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/local.key;
|
|
||||||
|
|
||||||
# ... rest of the configuration remains the same
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Add environment variable
|
|
||||||
|
|
||||||
Replace `BACKEND_API_ENDPOINT` environment variable the `docker-compose.yaml` with your custom domain with the scheme.
|
|
||||||
|
|
||||||
### Start Dograh Services
|
### Start Dograh Services
|
||||||
|
|
||||||
Start Dograh with the updated configuration:
|
Start Dograh through the validated startup wrapper so `dograh-init` regenerates nginx and coturn runtime config before Docker starts:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd dograh
|
cd dograh
|
||||||
sudo docker compose --profile remote up -d --pull always
|
./remote_up.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Access Your Application
|
### Access Your Application
|
||||||
|
|
@ -207,8 +195,8 @@ Add the following content (replace paths as needed):
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Copy renewed certificates to dograh certs directory
|
# Copy renewed certificates to dograh certs directory
|
||||||
cp /etc/letsencrypt/archive/voice.yourcompany.com/fullchain1.pem /home/ubuntu/dograh/certs/local.crt
|
cp /etc/letsencrypt/live/voice.yourcompany.com/fullchain.pem /home/ubuntu/dograh/certs/local.crt
|
||||||
cp /etc/letsencrypt/archive/voice.yourcompany.com/privkey1.pem /home/ubuntu/dograh/certs/local.key
|
cp /etc/letsencrypt/live/voice.yourcompany.com/privkey.pem /home/ubuntu/dograh/certs/local.key
|
||||||
chmod 644 /home/ubuntu/dograh/certs/local.crt /home/ubuntu/dograh/certs/local.key
|
chmod 644 /home/ubuntu/dograh/certs/local.crt /home/ubuntu/dograh/certs/local.key
|
||||||
|
|
||||||
# Restart nginx to load new certificates
|
# Restart nginx to load new certificates
|
||||||
|
|
@ -243,7 +231,7 @@ If Certbot fails to generate certificates:
|
||||||
If you see SSL errors after setup:
|
If you see SSL errors after setup:
|
||||||
|
|
||||||
1. Verify the certificates were copied correctly: `ls -la dograh/certs/`
|
1. Verify the certificates were copied correctly: `ls -la dograh/certs/`
|
||||||
2. Check that `nginx.conf` points to `/etc/nginx/certs/local.crt` and `/etc/nginx/certs/local.key`
|
2. Run `./remote_up.sh --preflight-only` in `dograh/` to verify the `dograh-init` runtime render matches `.env`
|
||||||
3. Restart the nginx container: `sudo docker compose --profile remote restart nginx`
|
3. Restart the nginx container: `sudo docker compose --profile remote restart nginx`
|
||||||
|
|
||||||
### WebRTC Connection Issues
|
### WebRTC Connection Issues
|
||||||
|
|
@ -251,5 +239,4 @@ If you see SSL errors after setup:
|
||||||
If voice calls don't connect after domain setup:
|
If voice calls don't connect after domain setup:
|
||||||
|
|
||||||
1. Ensure TCP/UDP ports 3478, 5349, and UDP 49152-49200 are still open
|
1. Ensure TCP/UDP ports 3478, 5349, and UDP 49152-49200 are still open
|
||||||
2. Update the `.env` file with your domain name if needed for TURN server configuration
|
2. Check that `PUBLIC_HOST` / `PUBLIC_BASE_URL` in `.env` match your domain, then re-run `./remote_up.sh`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ The script will prompt you for:
|
||||||
- The host browsers should use to reach TURN (press Enter for `127.0.0.1`; use your LAN IP if testing from another device on the same network)
|
- The host browsers should use to reach TURN (press Enter for `127.0.0.1`; use your LAN IP if testing from another device on the same network)
|
||||||
- A shared secret for the TURN server (press Enter to generate a random one)
|
- A shared secret for the TURN server (press Enter to generate a random one)
|
||||||
|
|
||||||
It creates `docker-compose.yaml`, `turnserver.conf`, and a `.env` file with TURN credentials. Start the stack with the `local-turn` profile so coturn comes up alongside the other services:
|
It creates `docker-compose.yaml`, a `.env` file with TURN credentials, and the small helper bundle that `dograh-init` uses to render coturn config at startup. Start the stack with the `local-turn` profile so coturn comes up alongside the other services:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose --profile local-turn up --pull always
|
docker compose --profile local-turn up --pull always
|
||||||
|
|
@ -118,9 +118,10 @@ The script will prompt you for:
|
||||||
|
|
||||||
It will automatically:
|
It will automatically:
|
||||||
- Get the source — `docker-compose.yaml` only (prebuilt mode), or clone the full repo (build mode)
|
- 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
|
- Download the validated remote deployment helper bundle
|
||||||
- Generate SSL certificates
|
- Generate SSL certificates
|
||||||
- Create an environment file with TURN server configuration
|
- Create an environment file with TURN server configuration
|
||||||
|
- Validate the runtime config that `dograh-init` will render from `.env`
|
||||||
- Write a `docker-compose.override.yaml` with build directives (build mode only)
|
- Write a `docker-compose.override.yaml` with build directives (build mode only)
|
||||||
|
|
||||||
### Start the Application
|
### Start the Application
|
||||||
|
|
@ -134,11 +135,11 @@ After the setup script completes, start Dograh. The script prints the exact comm
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```bash Prebuilt mode
|
```bash Prebuilt mode
|
||||||
cd dograh
|
cd dograh
|
||||||
sudo docker compose --profile remote up --pull always
|
./remote_up.sh
|
||||||
```
|
```
|
||||||
```bash Build mode
|
```bash Build mode
|
||||||
cd dograh
|
cd dograh
|
||||||
sudo docker compose --profile remote up -d --build
|
./remote_up.sh --build
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
|
|
@ -174,12 +175,14 @@ The setup script creates the following files in the `dograh/` directory:
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `docker-compose.yaml` | Main Docker Compose configuration |
|
| `docker-compose.yaml` | Main Docker Compose configuration |
|
||||||
| `docker-compose.override.yaml` | Build directives for `api` and `ui` (**build mode only**) |
|
| `docker-compose.override.yaml` | Build directives for `api` and `ui` (**build mode only**) |
|
||||||
| `turnserver.conf` | Configuration for TURN server |
|
| `remote_up.sh` | Validated startup wrapper for the remote stack |
|
||||||
| `nginx.conf` | nginx reverse proxy configuration with your IP |
|
| `scripts/run_dograh_init.sh` | One-shot init renderer/validator used by Docker Compose |
|
||||||
|
| `scripts/lib/setup_common.sh` | Shared deployment helper library |
|
||||||
|
| `deploy/templates/` | nginx and coturn runtime config templates |
|
||||||
| `generate_certificate.sh` | Script to regenerate SSL certificates |
|
| `generate_certificate.sh` | Script to regenerate SSL certificates |
|
||||||
| `certs/local.crt` | Self-signed SSL certificate |
|
| `certs/local.crt` | Self-signed SSL certificate |
|
||||||
| `certs/local.key` | SSL private key |
|
| `certs/local.key` | SSL private key |
|
||||||
| `.env` | Environment variables (TURN secret, JWT secret, FastAPI worker count) |
|
| `.env` | Single source of truth for deployment settings (TURN secret, JWT secret, FastAPI worker count, public host/base URL) |
|
||||||
|
|
||||||
### Building from source
|
### Building from source
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ By default, the Dograh API container runs a single uvicorn worker. For productio
|
||||||
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.
|
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.
|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
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.
|
Multi-worker support requires **Dograh v1.29.0 or newer**. Earlier releases used `uvicorn --workers` and a different remote deployment layout. If your stack is older, [update first](/deployment/update) and then come back to this page.
|
||||||
</Warning>
|
</Warning>
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
@ -58,23 +58,11 @@ Press Enter for the default (`4`) or enter a different positive integer. Non-int
|
||||||
SERVER_IP=... TURN_SECRET=... FASTAPI_WORKERS=8 ./setup_remote.sh
|
SERVER_IP=... TURN_SECRET=... FASTAPI_WORKERS=8 ./setup_remote.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The script wires the value into two places:
|
The script stores the value in **`.env`** (`FASTAPI_WORKERS=N`). The supported startup path (`./remote_up.sh`) preflights the `dograh-init` render from that value before every remote start, so nginx and the API worker count stay aligned.
|
||||||
|
|
||||||
- **`.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
|
## 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:
|
Once Dograh is running, increasing or decreasing the worker count is a one-file edit plus a restart. Change `.env`, then start through `./remote_up.sh` so `dograh-init` regenerates nginx runtime config before Docker starts the stack.
|
||||||
|
|
||||||
1. **`.env`** — controls how many uvicorn processes the API container spawns.
|
|
||||||
2. **`nginx.conf`** — controls which worker ports nginx forwards to.
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
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.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
### Steps
|
### Steps
|
||||||
|
|
||||||
|
|
@ -90,41 +78,21 @@ FASTAPI_WORKERS=4
|
||||||
FASTAPI_WORKERS=8
|
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`:
|
**2. Recreate the stack through the validated wrapper.** The simplest path — brief downtime, no surprises:
|
||||||
|
|
||||||
```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
|
```bash
|
||||||
sudo docker compose --profile remote down
|
./remote_up.sh
|
||||||
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:
|
If you want to avoid downtime and your stack is healthy, you can recreate only the `api` and `nginx` containers:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo docker compose --profile remote up -d --force-recreate api nginx
|
./remote_up.sh -- 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).
|
`remote_up.sh` validates `.env`, runs the same `dograh-init` render that Compose will use at startup, runs `docker compose config -q`, and then starts the requested services.
|
||||||
|
|
||||||
**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:
|
**3. 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
|
```bash
|
||||||
sudo docker compose --profile remote top api | grep uvicorn
|
sudo docker compose --profile remote top api | grep uvicorn
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ Always update **`dograh-api`** and **`dograh-ui`** to the **same tag**. The two
|
||||||
|
|
||||||
- Asks for a target version (defaults to the latest release tag on GitHub).
|
- 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.
|
- 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.
|
- Refreshes the remote helper bundle (`remote_up.sh` plus shared templates/helpers).
|
||||||
- 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.
|
- Synchronizes the canonical remote keys in `.env` and validates the runtime config that `dograh-init` will render from it.
|
||||||
- Backs up every file it changes with a `.bak.<timestamp>` suffix.
|
- Backs up every file it changes with a `.bak.<timestamp>` suffix.
|
||||||
|
|
||||||
From your install directory:
|
From your install directory:
|
||||||
|
|
@ -55,15 +55,14 @@ You'll be prompted for the target version, defaulting to the most recent release
|
||||||
TARGET_VERSION=1.28.0 DOGRAH_UPDATE_YES=1 bash update_remote.sh
|
TARGET_VERSION=1.28.0 DOGRAH_UPDATE_YES=1 bash update_remote.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
After the script finishes, apply the update by recreating the stack:
|
After the script finishes, apply the update through the validated startup wrapper:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo docker compose --profile remote down
|
./remote_up.sh
|
||||||
sudo docker compose --profile remote up -d --pull always
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
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.<timestamp>` files after the update and re-apply your edits.
|
The script overwrites `docker-compose.yaml` and the remote helper bundle (`remote_up.sh`, `scripts/run_dograh_init.sh`, `scripts/lib/setup_common.sh`, and `deploy/templates/*`) from the shared upstream deployment bundle. If you've made local edits to any of these, check the `.bak.<timestamp>` files after the update and re-apply your edits.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
## Local deployment
|
## Local deployment
|
||||||
|
|
@ -100,11 +99,10 @@ curl http://localhost:8000/api/v1/health # local
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd dograh
|
cd dograh
|
||||||
for f in docker-compose.yaml nginx.conf turnserver.conf .env; do
|
for f in docker-compose.yaml nginx.conf turnserver.conf .env remote_up.sh scripts/run_dograh_init.sh scripts/lib/setup_common.sh deploy/templates/nginx.remote.conf.template deploy/templates/turnserver.remote.conf.template; do
|
||||||
[[ -f "$f.bak.<timestamp>" ]] && cp "$f.bak.<timestamp>" "$f"
|
[[ -f "$f.bak.<timestamp>" ]] && cp "$f.bak.<timestamp>" "$f"
|
||||||
done
|
done
|
||||||
sudo docker compose --profile remote down
|
./remote_up.sh
|
||||||
sudo docker compose --profile remote up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Your Postgres data volume persists across `down`/`up` cycles, so agents and call history are preserved.
|
Your Postgres data volume persists across `down`/`up` cycles, so agents and call history are preserved.
|
||||||
|
|
@ -138,6 +136,6 @@ 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 update the `pipecat` submodule, you **must** run `git submodule update --init --recursive` before rebuilding, or the Docker build will not pick up `pipecat` changes.
|
||||||
</Warning>
|
</Warning>
|
||||||
|
|
||||||
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.
|
If you maintain a fork with local customizations on top of upstream, merging conflicts in `docker-compose.yaml`, `remote_up.sh`, `scripts/run_dograh_init.sh`, `deploy/templates/*`, 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.
|
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.
|
||||||
|
|
|
||||||
72
remote_up.sh
Executable file
72
remote_up.sh
Executable file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LIB_PATH="$SCRIPT_DIR/scripts/lib/setup_common.sh"
|
||||||
|
BOOTSTRAP_LIB=""
|
||||||
|
|
||||||
|
if [[ ! -f "$LIB_PATH" ]]; then
|
||||||
|
BOOTSTRAP_LIB="$(mktemp)"
|
||||||
|
curl -fsSL -o "$BOOTSTRAP_LIB" "https://raw.githubusercontent.com/dograh-hq/dograh/main/scripts/lib/setup_common.sh"
|
||||||
|
LIB_PATH="$BOOTSTRAP_LIB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "$BOOTSTRAP_LIB" ]]; then
|
||||||
|
rm -f "$BOOTSTRAP_LIB"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$LIB_PATH"
|
||||||
|
|
||||||
|
DOGRAH_DEPLOY_PROJECT_DIR="$SCRIPT_DIR"
|
||||||
|
|
||||||
|
VALIDATE_ONLY=0
|
||||||
|
MODE="pull"
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--build)
|
||||||
|
MODE="build"
|
||||||
|
;;
|
||||||
|
--preflight-only|--validate-only)
|
||||||
|
VALIDATE_ONLY=1
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
EXTRA_ARGS=("$@")
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
EXTRA_ARGS+=("$1")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
dograh_info "Running Dograh remote preflight..."
|
||||||
|
dograh_prepare_remote_install "$SCRIPT_DIR"
|
||||||
|
docker compose config -q
|
||||||
|
dograh_success "✓ dograh-init preflight validated"
|
||||||
|
|
||||||
|
if [[ "$VALIDATE_ONLY" == "1" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $EUID -eq 0 ]] || ! command -v sudo >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD=(docker compose)
|
||||||
|
else
|
||||||
|
COMPOSE_CMD=(sudo docker compose)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$MODE" == "build" ]]; then
|
||||||
|
exec "${COMPOSE_CMD[@]}" --profile remote up -d --build --force-recreate "${EXTRA_ARGS[@]}"
|
||||||
|
else
|
||||||
|
exec "${COMPOSE_CMD[@]}" --profile remote up -d --pull always --force-recreate "${EXTRA_ARGS[@]}"
|
||||||
|
fi
|
||||||
55
scripts/AGENTS.md
Normal file
55
scripts/AGENTS.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# scripts/
|
||||||
|
|
||||||
|
## Bash ↔ PowerShell parity — keep them in sync
|
||||||
|
|
||||||
|
Most contributor-facing scripts ship as a `.sh` + `.ps1` pair so macOS/Linux and Windows users get the same workflow. **When you edit one, edit the other in the same change.** Env-var names, defaults, flags, and behavior should match — if `start_services_dev.sh` reads `HEALTH_MAX_ATTEMPTS`, so should `start_services_dev.ps1`.
|
||||||
|
|
||||||
|
Current pairs:
|
||||||
|
|
||||||
|
- `setup_fork.{sh,ps1}` — contributor bootstrap (git remotes, submodule, venv, env files)
|
||||||
|
- `setup_requirements.{sh,ps1}` — Python + pipecat dependency install
|
||||||
|
- `start_services_dev.{sh,ps1}` — local backend launcher (auto-reload + health-check wait)
|
||||||
|
- `stop_services.{sh,ps1}`
|
||||||
|
- `makemigrate.{sh,ps1}` / `migrate.{sh,ps1}` — Alembic helpers
|
||||||
|
|
||||||
|
Bash-only (deployment / CI / OSS-user setup — not intended for Windows contributors):
|
||||||
|
|
||||||
|
- `start_services.sh` — VM production
|
||||||
|
- `start_services_docker.sh` — Docker image CMD
|
||||||
|
- `rolling_update.sh` — zero-downtime VM redeploy
|
||||||
|
- `setup_local.sh` / `setup_remote.sh` — OSS Docker-compose setup
|
||||||
|
- `format.sh` / `lint.sh` / `pre_commit.sh`
|
||||||
|
- `generate_sdk.sh` / `release_sdks.sh` / `dump_docs_openapi.py`
|
||||||
|
|
||||||
|
## Deployment Memory — current OSS Docker state
|
||||||
|
|
||||||
|
This directory now has a shared deployment model for OSS Docker installs. If you touch any of the scripts below, assume they are coupled and review them together:
|
||||||
|
|
||||||
|
- `scripts/lib/setup_common.sh` is the shared deployment helper library. It is sourced by `setup_local.sh`, `setup_remote.sh`, `update_remote.sh`, `setup_custom_domain.sh`, `run_dograh_init.sh`, and repo-root `remote_up.sh`.
|
||||||
|
- `setup_common.sh` must stay safe to source. It should not set shell options like `set -u` for callers.
|
||||||
|
- `.env` is the single operator-owned source of truth for remote deployment settings. Remote/runtime config should derive from it, not the other way around.
|
||||||
|
- Canonical remote keys in `.env`: `ENVIRONMENT`, `SERVER_IP`, `PUBLIC_HOST`, `PUBLIC_BASE_URL`, `BACKEND_API_ENDPOINT`, `MINIO_PUBLIC_ENDPOINT`, `TURN_HOST`, `TURN_SECRET`, `FASTAPI_WORKERS`, `OSS_JWT_SECRET`.
|
||||||
|
- `remote_up.sh` is the supported remote startup entrypoint. It runs preflight via `dograh_prepare_remote_install`, runs `docker compose config -q`, then starts the stack.
|
||||||
|
- `docker-compose.yaml` uses a one-shot `dograh-init` service for profiles `remote` and `local-turn`.
|
||||||
|
- `dograh-init` executes `scripts/run_dograh_init.sh`, which renders nginx/coturn runtime config into named volumes consumed by `nginx` and `coturn`.
|
||||||
|
- Remote nginx/coturn config is runtime-generated. Host-managed `nginx.conf` / `turnserver.conf` are legacy only; update flow may back them up and delete them, but current installs should not depend on them.
|
||||||
|
- `setup_remote.sh` writes `.env`, downloads the deployment helper bundle, generates self-signed certs, validates the init-based config, and tells operators to start via `./remote_up.sh` or `./remote_up.sh --build`.
|
||||||
|
- `update_remote.sh` is the migration/upgrade path for prebuilt remote installs. It refreshes `docker-compose.yaml`, `remote_up.sh`, `scripts/run_dograh_init.sh`, `scripts/lib/setup_common.sh`, and `deploy/templates/*`, backs up touched files, removes legacy host `nginx.conf` / `turnserver.conf`, and revalidates the init-based path.
|
||||||
|
- `setup_custom_domain.sh` is certificate/domain glue only. It must not own nginx config. It updates canonical public URL keys in `.env`, copies Let's Encrypt certs into `certs/`, installs renewal hook, and restarts through `./remote_up.sh`.
|
||||||
|
- `setup_local.sh` has an interactive `Enable coturn? [y/N]` prompt unless `ENABLE_COTURN` is preset. If coturn is enabled, it downloads the minimal helper bundle needed for `local-turn` (`setup_common.sh`, `run_dograh_init.sh`, templates) and relies on `dograh-init` to render coturn config.
|
||||||
|
- `setup_local.sh` must remain safe under unset env vars; use `${VAR:-}` guards for optional inputs like `ENABLE_COTURN`, `TURN_HOST`, `TURN_SECRET`, `DOGRAH_SKIP_DOWNLOAD`.
|
||||||
|
- `run_dograh_init.sh` is an executable entrypoint, not a library. Compose runs it directly. If it ever gets refactored, keep the distinction between sourced helper logic (`lib/`) and executable entrypoints.
|
||||||
|
- `dograh_prepare_remote_install` in `setup_common.sh` currently does three things: sync canonical `.env` keys, reject legacy compose layouts that do not use `dograh-init`, and preflight the init render in a temp directory.
|
||||||
|
- `dograh_uses_init_compose_layout` / `dograh_require_init_compose_layout` are the guardrails for old installs. If a remote install still bind-mounts host `nginx.conf` / `turnserver.conf`, the intended fix path is `./update_remote.sh`.
|
||||||
|
- Templates live under `deploy/templates/`. `nginx.remote.conf.template` contains the static shape and `dograh_render_remote_nginx_conf` expands the multi-worker upstream block dynamically. `turnserver.remote.conf.template` is also rendered from env.
|
||||||
|
- If you rename/move any of these deployment files, update all of: bootstrap curl URLs inside scripts, helper-bundle download paths in `setup_common.sh`, backup lists in `update_remote.sh`, docs under `docs/deployment/`, and any existence checks in `setup_local.sh` / `setup_custom_domain.sh`.
|
||||||
|
|
||||||
|
## The three "start" scripts — pick the right one
|
||||||
|
|
||||||
|
| Script | Where it runs | Key behavior |
|
||||||
|
| -------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `start_services_dev.sh` | Local dev shell | `uvicorn --reload`, exits after launching, restart by re-running, single arq worker, waits for `/api/v1/health` before exiting. |
|
||||||
|
| `start_services.sh` | VM production | Multi-port uvicorn behind nginx, `sudo nginx -t && systemctl reload`, writes `run/active_band` for `rolling_update.sh`. |
|
||||||
|
| `start_services_docker.sh` | Docker image `CMD` | PID 1: traps SIGTERM, uvicorn `--workers $FASTAPI_WORKERS`, `wait -n` so a dying child tears the container down. |
|
||||||
|
|
||||||
|
If you find yourself adding nginx/sudo logic to the dev script, or `--reload` to the production/Docker scripts, stop — you probably want a different file.
|
||||||
|
|
@ -1,30 +1 @@
|
||||||
# scripts/
|
@AGENTS.md
|
||||||
|
|
||||||
## Bash ↔ PowerShell parity — keep them in sync
|
|
||||||
|
|
||||||
Most contributor-facing scripts ship as a `.sh` + `.ps1` pair so macOS/Linux and Windows users get the same workflow. **When you edit one, edit the other in the same change.** Env-var names, defaults, flags, and behavior should match — if `start_services_dev.sh` reads `HEALTH_MAX_ATTEMPTS`, so should `start_services_dev.ps1`.
|
|
||||||
|
|
||||||
Current pairs:
|
|
||||||
- `setup_fork.{sh,ps1}` — contributor bootstrap (git remotes, submodule, venv, env files)
|
|
||||||
- `setup_requirements.{sh,ps1}` — Python + pipecat dependency install
|
|
||||||
- `start_services_dev.{sh,ps1}` — local backend launcher (auto-reload + health-check wait)
|
|
||||||
- `stop_services.{sh,ps1}`
|
|
||||||
- `makemigrate.{sh,ps1}` / `migrate.{sh,ps1}` — Alembic helpers
|
|
||||||
|
|
||||||
Bash-only (deployment / CI / OSS-user setup — not intended for Windows contributors):
|
|
||||||
- `start_services.sh` — VM production
|
|
||||||
- `start_services_docker.sh` — Docker image CMD
|
|
||||||
- `rolling_update.sh` — zero-downtime VM redeploy
|
|
||||||
- `setup_local.sh` / `setup_remote.sh` — OSS Docker-compose setup
|
|
||||||
- `format.sh` / `lint.sh` / `pre_commit.sh`
|
|
||||||
- `generate_sdk.sh` / `release_sdks.sh` / `dump_docs_openapi.py`
|
|
||||||
|
|
||||||
## The three "start" scripts — pick the right one
|
|
||||||
|
|
||||||
| Script | Where it runs | Key behavior |
|
|
||||||
| ----------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `start_services_dev.sh` | Local dev shell | `uvicorn --reload`, exits after launching, restart by re-running, single arq worker, waits for `/api/v1/health` before exiting. |
|
|
||||||
| `start_services.sh` | VM production | Multi-port uvicorn behind nginx, `sudo nginx -t && systemctl reload`, writes `run/active_band` for `rolling_update.sh`. |
|
|
||||||
| `start_services_docker.sh` | Docker image `CMD` | PID 1: traps SIGTERM, uvicorn `--workers $FASTAPI_WORKERS`, `wait -n` so a dying child tears the container down. |
|
|
||||||
|
|
||||||
If you find yourself adding nginx/sudo logic to the dev script, or `--reload` to the production/Docker scripts, stop — you probably want a different file.
|
|
||||||
|
|
|
||||||
425
scripts/lib/setup_common.sh
Normal file
425
scripts/lib/setup_common.sh
Normal file
|
|
@ -0,0 +1,425 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
DOGRAH_DEPLOY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DOGRAH_DEPLOY_REPO_ROOT="$(cd "$DOGRAH_DEPLOY_LIB_DIR/../.." 2>/dev/null && pwd || true)"
|
||||||
|
|
||||||
|
: "${RED:=\033[0;31m}"
|
||||||
|
: "${GREEN:=\033[0;32m}"
|
||||||
|
: "${YELLOW:=\033[1;33m}"
|
||||||
|
: "${BLUE:=\033[0;34m}"
|
||||||
|
: "${NC:=\033[0m}"
|
||||||
|
|
||||||
|
dograh_info() {
|
||||||
|
echo -e "${BLUE}$*${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_success() {
|
||||||
|
echo -e "${GREEN}$*${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_warn() {
|
||||||
|
echo -e "${YELLOW}$*${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_fail() {
|
||||||
|
echo -e "${RED}Error: $*${NC}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_project_dir() {
|
||||||
|
if [[ -n "${DOGRAH_DEPLOY_PROJECT_DIR:-}" ]]; then
|
||||||
|
printf '%s\n' "$DOGRAH_DEPLOY_PROJECT_DIR"
|
||||||
|
else
|
||||||
|
pwd
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_template_path() {
|
||||||
|
local template_name=$1
|
||||||
|
local candidate=""
|
||||||
|
local project_dir
|
||||||
|
|
||||||
|
project_dir="$(dograh_project_dir)"
|
||||||
|
|
||||||
|
for candidate in \
|
||||||
|
"$project_dir/deploy/templates/$template_name" \
|
||||||
|
"$DOGRAH_DEPLOY_REPO_ROOT/deploy/templates/$template_name"
|
||||||
|
do
|
||||||
|
if [[ -f "$candidate" ]]; then
|
||||||
|
printf '%s\n' "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
dograh_fail "Template '$template_name' not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_init_script_path() {
|
||||||
|
local candidate=""
|
||||||
|
local project_dir
|
||||||
|
|
||||||
|
project_dir="$(dograh_project_dir)"
|
||||||
|
|
||||||
|
for candidate in \
|
||||||
|
"$project_dir/scripts/run_dograh_init.sh" \
|
||||||
|
"$DOGRAH_DEPLOY_REPO_ROOT/scripts/run_dograh_init.sh"
|
||||||
|
do
|
||||||
|
if [[ -f "$candidate" ]]; then
|
||||||
|
printf '%s\n' "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
dograh_fail "run_dograh_init.sh not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_load_env_file() {
|
||||||
|
local env_file=${1:-.env}
|
||||||
|
|
||||||
|
[[ -f "$env_file" ]] || dograh_fail "$env_file not found"
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$env_file"
|
||||||
|
set +a
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_host_from_url() {
|
||||||
|
local url=$1
|
||||||
|
|
||||||
|
url="${url#https://}"
|
||||||
|
url="${url#http://}"
|
||||||
|
url="${url%%/*}"
|
||||||
|
|
||||||
|
printf '%s\n' "$url"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_is_ipv4() {
|
||||||
|
[[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_infer_server_ip() {
|
||||||
|
local project_dir=${1:-$(dograh_project_dir)}
|
||||||
|
local turn_conf="$project_dir/turnserver.conf"
|
||||||
|
local ip=""
|
||||||
|
|
||||||
|
if [[ -n "${SERVER_IP:-}" ]]; then
|
||||||
|
printf '%s\n' "$SERVER_IP"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$turn_conf" ]]; then
|
||||||
|
ip="$(sed -n 's/^external-ip=//p' "$turn_conf" | head -1)"
|
||||||
|
if [[ -n "$ip" ]]; then
|
||||||
|
printf '%s\n' "$ip"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${TURN_HOST:-}" ]] && dograh_is_ipv4 "$TURN_HOST"; then
|
||||||
|
printf '%s\n' "$TURN_HOST"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${PUBLIC_HOST:-}" ]] && dograh_is_ipv4 "$PUBLIC_HOST"; then
|
||||||
|
printf '%s\n' "$PUBLIC_HOST"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_infer_public_base_url() {
|
||||||
|
if [[ -n "${PUBLIC_BASE_URL:-}" ]]; then
|
||||||
|
printf '%s\n' "${PUBLIC_BASE_URL%/}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${BACKEND_API_ENDPOINT:-}" ]]; then
|
||||||
|
printf '%s\n' "${BACKEND_API_ENDPOINT%/}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${PUBLIC_HOST:-}" ]]; then
|
||||||
|
printf 'https://%s\n' "$PUBLIC_HOST"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${SERVER_IP:-}" ]]; then
|
||||||
|
printf 'https://%s\n' "$SERVER_IP"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_infer_public_host() {
|
||||||
|
local public_base_url=""
|
||||||
|
|
||||||
|
if [[ -n "${PUBLIC_HOST:-}" ]]; then
|
||||||
|
printf '%s\n' "$PUBLIC_HOST"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
public_base_url="$(dograh_infer_public_base_url 2>/dev/null || true)"
|
||||||
|
if [[ -n "$public_base_url" ]]; then
|
||||||
|
dograh_host_from_url "$public_base_url"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${TURN_HOST:-}" ]]; then
|
||||||
|
printf '%s\n' "$TURN_HOST"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_set_env_key() {
|
||||||
|
local env_file=$1
|
||||||
|
local key=$2
|
||||||
|
local value=$3
|
||||||
|
local tmp_file="${env_file}.tmp.$$"
|
||||||
|
|
||||||
|
awk -v key="$key" -v value="$value" '
|
||||||
|
BEGIN { updated = 0 }
|
||||||
|
$0 ~ "^" key "=" {
|
||||||
|
print key "=" value
|
||||||
|
updated = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
END {
|
||||||
|
if (!updated) {
|
||||||
|
print key "=" value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$env_file" > "$tmp_file"
|
||||||
|
|
||||||
|
mv "$tmp_file" "$env_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_delete_env_key() {
|
||||||
|
local env_file=$1
|
||||||
|
local key=$2
|
||||||
|
local tmp_file="${env_file}.tmp.$$"
|
||||||
|
|
||||||
|
awk -v key="$key" '$0 !~ "^" key "=" { print }' "$env_file" > "$tmp_file"
|
||||||
|
mv "$tmp_file" "$env_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_sync_remote_env_file() {
|
||||||
|
local env_file=${1:-.env}
|
||||||
|
local project_dir
|
||||||
|
local public_base_url=""
|
||||||
|
local public_host=""
|
||||||
|
local server_ip=""
|
||||||
|
|
||||||
|
project_dir="$(cd "$(dirname "$env_file")" && pwd)"
|
||||||
|
dograh_load_env_file "$env_file"
|
||||||
|
|
||||||
|
public_base_url="$(dograh_infer_public_base_url)" || dograh_fail "Could not determine PUBLIC_BASE_URL"
|
||||||
|
public_base_url="${public_base_url%/}"
|
||||||
|
public_host="$(dograh_infer_public_host)" || dograh_fail "Could not determine PUBLIC_HOST"
|
||||||
|
server_ip="$(dograh_infer_server_ip "$project_dir")" || dograh_fail "Could not determine SERVER_IP"
|
||||||
|
|
||||||
|
[[ "$public_base_url" =~ ^https?:// ]] || dograh_fail "PUBLIC_BASE_URL must include http:// or https://"
|
||||||
|
dograh_is_ipv4 "$server_ip" || dograh_fail "SERVER_IP must be an IPv4 address (got: $server_ip)"
|
||||||
|
|
||||||
|
dograh_set_env_key "$env_file" ENVIRONMENT "${ENVIRONMENT:-production}"
|
||||||
|
dograh_set_env_key "$env_file" SERVER_IP "$server_ip"
|
||||||
|
dograh_set_env_key "$env_file" PUBLIC_HOST "$public_host"
|
||||||
|
dograh_set_env_key "$env_file" PUBLIC_BASE_URL "$public_base_url"
|
||||||
|
dograh_set_env_key "$env_file" BACKEND_API_ENDPOINT "$public_base_url"
|
||||||
|
dograh_set_env_key "$env_file" MINIO_PUBLIC_ENDPOINT "$public_base_url"
|
||||||
|
dograh_set_env_key "$env_file" TURN_HOST "$public_host"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_validate_remote_runtime_env() {
|
||||||
|
[[ "${FASTAPI_WORKERS:-}" =~ ^[1-9][0-9]*$ ]] || dograh_fail "FASTAPI_WORKERS must be a positive integer"
|
||||||
|
[[ -n "${TURN_SECRET:-}" ]] || dograh_fail "TURN_SECRET is missing"
|
||||||
|
[[ -n "${PUBLIC_HOST:-}" ]] || dograh_fail "PUBLIC_HOST is missing"
|
||||||
|
[[ -n "${PUBLIC_BASE_URL:-}" ]] || dograh_fail "PUBLIC_BASE_URL is missing"
|
||||||
|
[[ -n "${BACKEND_API_ENDPOINT:-}" ]] || dograh_fail "BACKEND_API_ENDPOINT is missing"
|
||||||
|
[[ -n "${MINIO_PUBLIC_ENDPOINT:-}" ]] || dograh_fail "MINIO_PUBLIC_ENDPOINT is missing"
|
||||||
|
[[ -n "${TURN_HOST:-}" ]] || dograh_fail "TURN_HOST is missing"
|
||||||
|
dograh_is_ipv4 "${SERVER_IP:-}" || dograh_fail "SERVER_IP must be a valid IPv4 address"
|
||||||
|
[[ "${PUBLIC_BASE_URL}" =~ ^https?:// ]] || dograh_fail "PUBLIC_BASE_URL must include http:// or https://"
|
||||||
|
[[ "${BACKEND_API_ENDPOINT}" == "${PUBLIC_BASE_URL}" ]] || dograh_fail "BACKEND_API_ENDPOINT must match PUBLIC_BASE_URL"
|
||||||
|
[[ "${MINIO_PUBLIC_ENDPOINT}" == "${PUBLIC_BASE_URL}" ]] || dograh_fail "MINIO_PUBLIC_ENDPOINT must match PUBLIC_BASE_URL"
|
||||||
|
[[ "${TURN_HOST}" == "${PUBLIC_HOST}" ]] || dograh_fail "TURN_HOST must match PUBLIC_HOST"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_uses_init_compose_layout() {
|
||||||
|
local project_dir=${1:-$(dograh_project_dir)}
|
||||||
|
local compose_file="$project_dir/docker-compose.yaml"
|
||||||
|
|
||||||
|
[[ -f "$compose_file" ]] || return 1
|
||||||
|
grep -q "dograh-init:" "$compose_file" \
|
||||||
|
&& grep -q "nginx-generated:/etc/nginx/conf.d:ro" "$compose_file" \
|
||||||
|
&& grep -q "coturn-generated:/etc/coturn:ro" "$compose_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_require_init_compose_layout() {
|
||||||
|
local project_dir=${1:-$(dograh_project_dir)}
|
||||||
|
|
||||||
|
if ! dograh_uses_init_compose_layout "$project_dir"; then
|
||||||
|
dograh_fail "This install uses the legacy remote compose layout. Run ./update_remote.sh first so Docker uses dograh-init generated config."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_render_remote_nginx_conf() {
|
||||||
|
local project_dir=${1:-$(dograh_project_dir)}
|
||||||
|
local destination=${2:-"$project_dir/nginx.conf"}
|
||||||
|
local template=""
|
||||||
|
local tmp_upstream=""
|
||||||
|
|
||||||
|
template="$(dograh_template_path "nginx.remote.conf.template")"
|
||||||
|
tmp_upstream="$(mktemp)"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Backend API workers - one uvicorn process per port, balanced by least_conn."
|
||||||
|
echo "# Auto-generated by Dograh remote config renderer. Do not edit manually."
|
||||||
|
echo "upstream dograh_api {"
|
||||||
|
echo " least_conn;"
|
||||||
|
for ((i=0; i<FASTAPI_WORKERS; i++)); do
|
||||||
|
printf ' server api:%d max_fails=3 fail_timeout=10s;\n' "$((8000 + i))"
|
||||||
|
done
|
||||||
|
echo " keepalive 32;"
|
||||||
|
echo "}"
|
||||||
|
} > "$tmp_upstream"
|
||||||
|
|
||||||
|
awk -v public_host="$PUBLIC_HOST" -v upstream_file="$tmp_upstream" '
|
||||||
|
BEGIN {
|
||||||
|
while ((getline line < upstream_file) > 0) {
|
||||||
|
upstream = upstream line ORS
|
||||||
|
}
|
||||||
|
close(upstream_file)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
gsub(/__DOGRAH_PUBLIC_HOST__/, public_host)
|
||||||
|
if ($0 == "__DOGRAH_UPSTREAM_BLOCK__") {
|
||||||
|
printf "%s", upstream
|
||||||
|
} else {
|
||||||
|
print
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$template" > "$destination"
|
||||||
|
|
||||||
|
rm -f "$tmp_upstream"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_render_remote_turn_conf() {
|
||||||
|
local project_dir=${1:-$(dograh_project_dir)}
|
||||||
|
local destination=${2:-"$project_dir/turnserver.conf"}
|
||||||
|
local template=""
|
||||||
|
local external_ip="${TURN_EXTERNAL_IP:-${SERVER_IP:-}}"
|
||||||
|
|
||||||
|
template="$(dograh_template_path "turnserver.remote.conf.template")"
|
||||||
|
[[ -n "$external_ip" ]] || dograh_fail "TURN external IP/host is missing"
|
||||||
|
|
||||||
|
awk \
|
||||||
|
-v external_ip="$external_ip" \
|
||||||
|
-v turn_secret="$TURN_SECRET" \
|
||||||
|
'
|
||||||
|
{
|
||||||
|
gsub(/__DOGRAH_TURN_EXTERNAL_IP__/, external_ip)
|
||||||
|
gsub(/__DOGRAH_TURN_SECRET__/, turn_secret)
|
||||||
|
print
|
||||||
|
}
|
||||||
|
' "$template" > "$destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_preflight_remote_init_render() {
|
||||||
|
local project_dir=${1:-$(dograh_project_dir)}
|
||||||
|
local env_file="$project_dir/.env"
|
||||||
|
local cert_dir="$project_dir/certs"
|
||||||
|
local init_script=""
|
||||||
|
local tmp_root=""
|
||||||
|
local nginx_conf=""
|
||||||
|
local turn_conf=""
|
||||||
|
local nginx_workers=0
|
||||||
|
local rendered_secret=""
|
||||||
|
local rendered_ip=""
|
||||||
|
local rendered_server_name=""
|
||||||
|
|
||||||
|
dograh_load_env_file "$env_file"
|
||||||
|
dograh_validate_remote_runtime_env
|
||||||
|
[[ -f "$cert_dir/local.crt" ]] || dograh_fail "certs/local.crt not found"
|
||||||
|
[[ -f "$cert_dir/local.key" ]] || dograh_fail "certs/local.key not found"
|
||||||
|
|
||||||
|
init_script="$(dograh_init_script_path)"
|
||||||
|
tmp_root="$(mktemp -d)"
|
||||||
|
nginx_conf="$tmp_root/nginx/default.conf"
|
||||||
|
turn_conf="$tmp_root/coturn/turnserver.conf"
|
||||||
|
|
||||||
|
(
|
||||||
|
export ENVIRONMENT SERVER_IP PUBLIC_HOST PUBLIC_BASE_URL BACKEND_API_ENDPOINT MINIO_PUBLIC_ENDPOINT TURN_HOST TURN_SECRET FASTAPI_WORKERS
|
||||||
|
export DOGRAH_INIT_WORKSPACE_DIR="$project_dir"
|
||||||
|
export DOGRAH_INIT_OUTPUT_ROOT="$tmp_root"
|
||||||
|
export DOGRAH_INIT_CERTS_DIR="$cert_dir"
|
||||||
|
bash "$init_script" >/dev/null
|
||||||
|
)
|
||||||
|
|
||||||
|
[[ -f "$nginx_conf" ]] || dograh_fail "dograh-init did not render nginx config"
|
||||||
|
[[ -f "$turn_conf" ]] || dograh_fail "dograh-init did not render coturn config"
|
||||||
|
|
||||||
|
nginx_workers=$(awk '/^[[:space:]]*server api:[0-9]+/ { count += 1 } END { print count + 0 }' "$nginx_conf")
|
||||||
|
[[ "$nginx_workers" -eq "$FASTAPI_WORKERS" ]] || dograh_fail "FASTAPI_WORKERS=$FASTAPI_WORKERS but nginx.conf has $nginx_workers upstream servers"
|
||||||
|
|
||||||
|
rendered_server_name="$(awk '/^[[:space:]]*server_name / { print $2; exit }' "$nginx_conf" | sed 's/;$//')"
|
||||||
|
[[ "$rendered_server_name" == "$PUBLIC_HOST" ]] || dograh_fail "nginx.conf server_name ($rendered_server_name) does not match PUBLIC_HOST ($PUBLIC_HOST)"
|
||||||
|
|
||||||
|
rendered_secret="$(sed -n 's/^static-auth-secret=//p' "$turn_conf" | head -1)"
|
||||||
|
[[ "$rendered_secret" == "$TURN_SECRET" ]] || dograh_fail "TURN_SECRET in .env does not match turnserver.conf"
|
||||||
|
|
||||||
|
rendered_ip="$(sed -n 's/^external-ip=//p' "$turn_conf" | head -1)"
|
||||||
|
[[ "$rendered_ip" == "$SERVER_IP" ]] || dograh_fail "SERVER_IP in .env does not match turnserver.conf"
|
||||||
|
|
||||||
|
rm -rf "$tmp_root"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_prepare_remote_install() {
|
||||||
|
local project_dir=${1:-$(dograh_project_dir)}
|
||||||
|
local env_file="$project_dir/.env"
|
||||||
|
|
||||||
|
dograh_sync_remote_env_file "$env_file"
|
||||||
|
dograh_require_init_compose_layout "$project_dir"
|
||||||
|
dograh_preflight_remote_init_render "$project_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_download_bundle_file_for_ref() {
|
||||||
|
local destination=$1
|
||||||
|
local remote_path=$2
|
||||||
|
local ref=${3:-main}
|
||||||
|
local raw_base="https://raw.githubusercontent.com/dograh-hq/dograh/$ref"
|
||||||
|
local fallback_base="https://raw.githubusercontent.com/dograh-hq/dograh/main"
|
||||||
|
|
||||||
|
if ! curl -fsSL -o "$destination" "$raw_base/$remote_path"; then
|
||||||
|
dograh_warn "Warning: '$remote_path' not found at '$ref' - falling back to main"
|
||||||
|
curl -fsSL -o "$destination" "$fallback_base/$remote_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_download_init_support_bundle() {
|
||||||
|
local project_dir=$1
|
||||||
|
local ref=${2:-main}
|
||||||
|
|
||||||
|
mkdir -p "$project_dir/scripts/lib" "$project_dir/deploy/templates"
|
||||||
|
|
||||||
|
mkdir -p "$project_dir/scripts"
|
||||||
|
dograh_download_bundle_file_for_ref "$project_dir/scripts/lib/setup_common.sh" "scripts/lib/setup_common.sh" "$ref"
|
||||||
|
dograh_download_bundle_file_for_ref "$project_dir/scripts/run_dograh_init.sh" "scripts/run_dograh_init.sh" "$ref"
|
||||||
|
chmod +x "$project_dir/scripts/run_dograh_init.sh"
|
||||||
|
dograh_download_bundle_file_for_ref "$project_dir/deploy/templates/nginx.remote.conf.template" "deploy/templates/nginx.remote.conf.template" "$ref"
|
||||||
|
dograh_download_bundle_file_for_ref "$project_dir/deploy/templates/turnserver.remote.conf.template" "deploy/templates/turnserver.remote.conf.template" "$ref"
|
||||||
|
}
|
||||||
|
|
||||||
|
dograh_download_remote_support_bundle() {
|
||||||
|
local project_dir=$1
|
||||||
|
local ref=${2:-main}
|
||||||
|
|
||||||
|
dograh_download_bundle_file_for_ref "$project_dir/remote_up.sh" "remote_up.sh" "$ref"
|
||||||
|
chmod +x "$project_dir/remote_up.sh"
|
||||||
|
dograh_download_init_support_bundle "$project_dir" "$ref"
|
||||||
|
}
|
||||||
38
scripts/run_dograh_init.sh
Executable file
38
scripts/run_dograh_init.sh
Executable file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
WORKSPACE_DIR="${DOGRAH_INIT_WORKSPACE_DIR:-/workspace}"
|
||||||
|
OUTPUT_ROOT="${DOGRAH_INIT_OUTPUT_ROOT:-/generated}"
|
||||||
|
NGINX_OUTPUT_DIR="$OUTPUT_ROOT/nginx"
|
||||||
|
COTURN_OUTPUT_DIR="$OUTPUT_ROOT/coturn"
|
||||||
|
CERTS_DIR="${DOGRAH_INIT_CERTS_DIR:-/certs}"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. "$SCRIPT_DIR/lib/setup_common.sh"
|
||||||
|
|
||||||
|
DOGRAH_DEPLOY_PROJECT_DIR="$WORKSPACE_DIR"
|
||||||
|
|
||||||
|
mkdir -p "$NGINX_OUTPUT_DIR" "$COTURN_OUTPUT_DIR"
|
||||||
|
|
||||||
|
if [[ "${ENVIRONMENT:-local}" == "production" ]]; then
|
||||||
|
dograh_validate_remote_runtime_env
|
||||||
|
[[ -f "$CERTS_DIR/local.crt" ]] || dograh_fail "certs/local.crt not found"
|
||||||
|
[[ -f "$CERTS_DIR/local.key" ]] || dograh_fail "certs/local.key not found"
|
||||||
|
|
||||||
|
export TURN_EXTERNAL_IP="$SERVER_IP"
|
||||||
|
dograh_render_remote_nginx_conf "$WORKSPACE_DIR" "$NGINX_OUTPUT_DIR/default.conf"
|
||||||
|
dograh_render_remote_turn_conf "$WORKSPACE_DIR" "$COTURN_OUTPUT_DIR/turnserver.conf"
|
||||||
|
dograh_success "✓ dograh-init rendered remote nginx and coturn config"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${TURN_SECRET:-}" && -n "${TURN_HOST:-}" ]]; then
|
||||||
|
export TURN_EXTERNAL_IP="$TURN_HOST"
|
||||||
|
dograh_render_remote_turn_conf "$WORKSPACE_DIR" "$COTURN_OUTPUT_DIR/turnserver.conf"
|
||||||
|
dograh_success "✓ dograh-init rendered local TURN config"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
dograh_success "✓ dograh-init no-op for current profile"
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -euo pipefail
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
|
@ -8,6 +8,26 @@ YELLOW='\033[1;33m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LIB_PATH="$SCRIPT_DIR/lib/setup_common.sh"
|
||||||
|
BOOTSTRAP_LIB=""
|
||||||
|
|
||||||
|
if [[ ! -f "$LIB_PATH" ]]; then
|
||||||
|
BOOTSTRAP_LIB="$(mktemp)"
|
||||||
|
curl -fsSL -o "$BOOTSTRAP_LIB" "https://raw.githubusercontent.com/dograh-hq/dograh/main/scripts/lib/setup_common.sh"
|
||||||
|
LIB_PATH="$BOOTSTRAP_LIB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "$BOOTSTRAP_LIB" ]]; then
|
||||||
|
rm -f "$BOOTSTRAP_LIB"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$LIB_PATH"
|
||||||
|
|
||||||
echo -e "${BLUE}"
|
echo -e "${BLUE}"
|
||||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ Dograh Custom Domain Setup ║"
|
echo "║ Dograh Custom Domain Setup ║"
|
||||||
|
|
@ -15,13 +35,10 @@ echo "║ Automated Let's Encrypt SSL certificate setup ║"
|
||||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||||
echo -e "${NC}"
|
echo -e "${NC}"
|
||||||
|
|
||||||
# Check if running as root or with sudo
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
echo -e "${RED}Error: This script must be run as root or with sudo${NC}"
|
dograh_fail "This script must be run as root or with sudo"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if dograh directory exists
|
|
||||||
if [[ ! -d "dograh" ]]; then
|
if [[ ! -d "dograh" ]]; then
|
||||||
echo -e "${RED}Error: 'dograh' directory not found.${NC}"
|
echo -e "${RED}Error: 'dograh' directory not found.${NC}"
|
||||||
echo -e "${YELLOW}Please run this script from the directory containing your Dograh installation.${NC}"
|
echo -e "${YELLOW}Please run this script from the directory containing your Dograh installation.${NC}"
|
||||||
|
|
@ -30,29 +47,17 @@ if [[ ! -d "dograh" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get the domain name
|
|
||||||
echo -e "${YELLOW}Enter your domain name (e.g., voice.yourcompany.com):${NC}"
|
echo -e "${YELLOW}Enter your domain name (e.g., voice.yourcompany.com):${NC}"
|
||||||
read -p "> " DOMAIN_NAME
|
read -p "> " DOMAIN_NAME
|
||||||
|
[[ -n "$DOMAIN_NAME" ]] || dograh_fail "Domain name cannot be empty"
|
||||||
|
|
||||||
if [[ -z "$DOMAIN_NAME" ]]; then
|
|
||||||
echo -e "${RED}Error: Domain name cannot be empty${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Basic domain validation
|
|
||||||
if ! [[ "$DOMAIN_NAME" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$ ]]; then
|
if ! [[ "$DOMAIN_NAME" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$ ]]; then
|
||||||
echo -e "${RED}Error: Invalid domain name format${NC}"
|
dograh_fail "Invalid domain name format"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get email for Let's Encrypt notifications
|
|
||||||
echo -e "${YELLOW}Enter your email address for SSL certificate notifications:${NC}"
|
echo -e "${YELLOW}Enter your email address for SSL certificate notifications:${NC}"
|
||||||
read -p "> " EMAIL_ADDRESS
|
read -p "> " EMAIL_ADDRESS
|
||||||
|
[[ -n "$EMAIL_ADDRESS" ]] || dograh_fail "Email address cannot be empty (required by Let's Encrypt)"
|
||||||
if [[ -z "$EMAIL_ADDRESS" ]]; then
|
|
||||||
echo -e "${RED}Error: Email address cannot be empty (required by Let's Encrypt)${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}Configuration:${NC}"
|
echo -e "${GREEN}Configuration:${NC}"
|
||||||
|
|
@ -60,13 +65,12 @@ echo -e " Domain: ${BLUE}$DOMAIN_NAME${NC}"
|
||||||
echo -e " Email: ${BLUE}$EMAIL_ADDRESS${NC}"
|
echo -e " Email: ${BLUE}$EMAIL_ADDRESS${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Verify DNS is pointing to this server
|
|
||||||
echo -e "${BLUE}[1/7] Verifying DNS configuration...${NC}"
|
echo -e "${BLUE}[1/7] Verifying DNS configuration...${NC}"
|
||||||
SERVER_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "")
|
SERVER_IP="$(curl -s ifconfig.me || curl -s icanhazip.com || echo "")"
|
||||||
RESOLVED_IP=$(dig +short "$DOMAIN_NAME" | tail -1)
|
RESOLVED_IP="$(dig +short "$DOMAIN_NAME" | tail -1)"
|
||||||
|
|
||||||
if [[ -z "$SERVER_IP" ]]; then
|
if [[ -z "$SERVER_IP" ]]; then
|
||||||
echo -e "${YELLOW}Warning: Could not detect server's public IP${NC}"
|
dograh_warn "Warning: Could not detect server's public IP"
|
||||||
elif [[ "$RESOLVED_IP" != "$SERVER_IP" ]]; then
|
elif [[ "$RESOLVED_IP" != "$SERVER_IP" ]]; then
|
||||||
echo -e "${YELLOW}Warning: Domain '$DOMAIN_NAME' resolves to '$RESOLVED_IP' but this server's IP is '$SERVER_IP'${NC}"
|
echo -e "${YELLOW}Warning: Domain '$DOMAIN_NAME' resolves to '$RESOLVED_IP' but this server's IP is '$SERVER_IP'${NC}"
|
||||||
echo -e "${YELLOW}Make sure your DNS A record points to this server before proceeding.${NC}"
|
echo -e "${YELLOW}Make sure your DNS A record points to this server before proceeding.${NC}"
|
||||||
|
|
@ -80,7 +84,6 @@ else
|
||||||
echo -e "${GREEN}✓ DNS is correctly configured (${RESOLVED_IP})${NC}"
|
echo -e "${GREEN}✓ DNS is correctly configured (${RESOLVED_IP})${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Detect package manager and install certbot
|
|
||||||
echo -e "${BLUE}[2/7] Installing Certbot...${NC}"
|
echo -e "${BLUE}[2/7] Installing Certbot...${NC}"
|
||||||
if command -v apt-get &> /dev/null; then
|
if command -v apt-get &> /dev/null; then
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
|
|
@ -90,14 +93,20 @@ elif command -v yum &> /dev/null; then
|
||||||
elif command -v dnf &> /dev/null; then
|
elif command -v dnf &> /dev/null; then
|
||||||
dnf install -y -q certbot
|
dnf install -y -q certbot
|
||||||
else
|
else
|
||||||
echo -e "${RED}Error: Could not detect package manager. Please install certbot manually.${NC}"
|
dograh_fail "Could not detect package manager. Please install certbot manually."
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
echo -e "${GREEN}✓ Certbot installed${NC}"
|
echo -e "${GREEN}✓ Certbot installed${NC}"
|
||||||
|
|
||||||
# Stop Dograh services to free port 80
|
|
||||||
echo -e "${BLUE}[3/7] Stopping Dograh services...${NC}"
|
echo -e "${BLUE}[3/7] Stopping Dograh services...${NC}"
|
||||||
cd dograh
|
cd dograh
|
||||||
|
DOGRAH_DEPLOY_PROJECT_DIR="$(pwd)"
|
||||||
|
|
||||||
|
if [[ ! -f remote_up.sh || ! -f scripts/lib/setup_common.sh ]]; then
|
||||||
|
dograh_download_remote_support_bundle "$(pwd)" "main"
|
||||||
|
fi
|
||||||
|
|
||||||
|
dograh_require_init_compose_layout "$(pwd)"
|
||||||
|
|
||||||
if docker compose --profile remote ps --quiet 2>/dev/null | grep -q .; then
|
if docker compose --profile remote ps --quiet 2>/dev/null | grep -q .; then
|
||||||
docker compose --profile remote down
|
docker compose --profile remote down
|
||||||
echo -e "${GREEN}✓ Dograh services stopped${NC}"
|
echo -e "${GREEN}✓ Dograh services stopped${NC}"
|
||||||
|
|
@ -105,7 +114,6 @@ else
|
||||||
echo -e "${YELLOW}⚠ No running services found${NC}"
|
echo -e "${YELLOW}⚠ No running services found${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate SSL certificate
|
|
||||||
echo -e "${BLUE}[4/7] Generating Let's Encrypt SSL certificate...${NC}"
|
echo -e "${BLUE}[4/7] Generating Let's Encrypt SSL certificate...${NC}"
|
||||||
CERTBOT_OUTPUT=$(certbot certonly --standalone \
|
CERTBOT_OUTPUT=$(certbot certonly --standalone \
|
||||||
--non-interactive \
|
--non-interactive \
|
||||||
|
|
@ -115,7 +123,6 @@ CERTBOT_OUTPUT=$(certbot certonly --standalone \
|
||||||
echo -e "${RED}✗ Certificate generation failed${NC}"
|
echo -e "${RED}✗ Certificate generation failed${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check for common errors and provide helpful hints
|
|
||||||
if echo "$CERTBOT_OUTPUT" | grep -qi "timeout\|firewall\|connection"; then
|
if echo "$CERTBOT_OUTPUT" | grep -qi "timeout\|firewall\|connection"; then
|
||||||
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}"
|
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}"
|
||||||
echo -e "${YELLOW} Port 80 appears to be blocked by a firewall.${NC}"
|
echo -e "${YELLOW} Port 80 appears to be blocked by a firewall.${NC}"
|
||||||
|
|
@ -123,14 +130,6 @@ CERTBOT_OUTPUT=$(certbot certonly --standalone \
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "Let's Encrypt needs to connect to port 80 to verify domain ownership."
|
echo -e "Let's Encrypt needs to connect to port 80 to verify domain ownership."
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}If using AWS EC2:${NC}"
|
|
||||||
echo " 1. Go to AWS Console → EC2 → Security Groups"
|
|
||||||
echo " 2. Find the security group for your instance"
|
|
||||||
echo " 3. Add inbound rule: HTTP | TCP | Port 80 | 0.0.0.0/0"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}If using another cloud provider:${NC}"
|
|
||||||
echo " • Ensure port 80 (TCP) is open for inbound traffic from all sources"
|
|
||||||
echo ""
|
|
||||||
elif echo "$CERTBOT_OUTPUT" | grep -qi "too many\|rate.limit"; then
|
elif echo "$CERTBOT_OUTPUT" | grep -qi "too many\|rate.limit"; then
|
||||||
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}"
|
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}"
|
||||||
echo -e "${YELLOW} Let's Encrypt rate limit reached.${NC}"
|
echo -e "${YELLOW} Let's Encrypt rate limit reached.${NC}"
|
||||||
|
|
@ -160,161 +159,60 @@ CERTBOT_OUTPUT=$(certbot certonly --standalone \
|
||||||
}
|
}
|
||||||
echo -e "${GREEN}✓ SSL certificate generated${NC}"
|
echo -e "${GREEN}✓ SSL certificate generated${NC}"
|
||||||
|
|
||||||
# Verify and display certificate location
|
|
||||||
CERT_PATH="/etc/letsencrypt/live/$DOMAIN_NAME"
|
CERT_PATH="/etc/letsencrypt/live/$DOMAIN_NAME"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}Certificate location:${NC}"
|
echo -e "${BLUE}Certificate location:${NC}"
|
||||||
echo -e " ${CERT_PATH}/"
|
echo -e " ${CERT_PATH}/"
|
||||||
if [[ -f "$CERT_PATH/fullchain.pem" ]]; then
|
[[ -f "$CERT_PATH/fullchain.pem" ]] && echo -e " ${GREEN}✓${NC} fullchain.pem exists" || echo -e " ${RED}✗${NC} fullchain.pem NOT FOUND"
|
||||||
echo -e " ${GREEN}✓${NC} fullchain.pem exists"
|
[[ -f "$CERT_PATH/privkey.pem" ]] && echo -e " ${GREEN}✓${NC} privkey.pem exists" || echo -e " ${RED}✗${NC} privkey.pem NOT FOUND"
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} fullchain.pem NOT FOUND"
|
|
||||||
fi
|
|
||||||
if [[ -f "$CERT_PATH/privkey.pem" ]]; then
|
|
||||||
echo -e " ${GREEN}✓${NC} privkey.pem exists"
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} privkey.pem NOT FOUND"
|
|
||||||
fi
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Copy certificates to dograh/certs directory
|
mkdir -p certs
|
||||||
cp /etc/letsencrypt/archive/$DOMAIN_NAME/fullchain1.pem certs/local.crt
|
cp "$CERT_PATH/fullchain.pem" certs/local.crt
|
||||||
cp /etc/letsencrypt/archive/$DOMAIN_NAME/privkey1.pem certs/local.key
|
cp "$CERT_PATH/privkey.pem" certs/local.key
|
||||||
chmod 644 certs/local.crt certs/local.key
|
chmod 644 certs/local.crt certs/local.key
|
||||||
echo -e "${GREEN}✓${NC} Certificates copied to certs/ directory"
|
echo -e "${GREEN}✓${NC} Certificates copied to certs/ directory"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Update nginx.conf
|
echo -e "${BLUE}[5/7] Updating canonical remote settings and validating init-based config...${NC}"
|
||||||
echo -e "${BLUE}[5/7] Updating nginx configuration...${NC}"
|
dograh_load_env_file .env
|
||||||
cat > nginx.conf << NGINX_EOF
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name $DOMAIN_NAME;
|
|
||||||
|
|
||||||
# Redirect all HTTP to HTTPS
|
if [[ -z "${SERVER_IP:-}" ]]; then
|
||||||
return 301 https://\$host\$request_uri;
|
SERVER_IP="$(dograh_infer_server_ip "$(pwd)" || true)"
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name $DOMAIN_NAME;
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/local.crt;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/local.key;
|
|
||||||
|
|
||||||
# TLS settings
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_prefer_server_ciphers on;
|
|
||||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
|
||||||
|
|
||||||
# Backend API and WebSockets — bypass the UI, go straight to api:8000
|
|
||||||
location /api/v1/ {
|
|
||||||
proxy_pass http://api:8000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
|
||||||
|
|
||||||
# Long-lived WebSockets (audio streaming, signaling)
|
|
||||||
proxy_read_timeout 3600s;
|
|
||||||
proxy_send_timeout 3600s;
|
|
||||||
|
|
||||||
# Don't buffer streamed responses
|
|
||||||
proxy_buffering off;
|
|
||||||
client_max_body_size 100M;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://ui:3010;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# Important for WebSockets / hot reload etc.
|
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
|
||||||
|
|
||||||
# Rewrite localhost MinIO URLs in API responses to use current domain
|
|
||||||
sub_filter 'http://localhost:9000/voice-audio/' 'https://\$host/voice-audio/';
|
|
||||||
sub_filter_once off;
|
|
||||||
sub_filter_types application/json text/html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /voice-audio/ {
|
|
||||||
proxy_pass http://minio:9000/voice-audio/;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# Headers for file downloads from MinIO
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
|
||||||
|
|
||||||
# Allow large file downloads
|
|
||||||
proxy_buffering off;
|
|
||||||
client_max_body_size 100M;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX_EOF
|
|
||||||
echo -e "${GREEN}✓ nginx.conf updated${NC}"
|
|
||||||
|
|
||||||
# Update .env file with domain name
|
|
||||||
echo -e "${BLUE}[6/8] Updating environment variables...${NC}"
|
|
||||||
if [[ -f ".env" ]]; then
|
|
||||||
# Update BACKEND_API_ENDPOINT to use domain (public URL the backend advertises)
|
|
||||||
sed -i.bak "s|^BACKEND_API_ENDPOINT=.*|BACKEND_API_ENDPOINT=https://$DOMAIN_NAME|" .env
|
|
||||||
# Drop any stale BACKEND_URL override — the ui container should use the
|
|
||||||
# internal Docker URL (http://api:8000) from docker-compose defaults.
|
|
||||||
sed -i.bak "/^BACKEND_URL=/d" .env
|
|
||||||
sed -i.bak "/^# Backend URL for UI$/d" .env
|
|
||||||
# Update TURN_HOST to use domain
|
|
||||||
sed -i.bak "s|^TURN_HOST=.*|TURN_HOST=$DOMAIN_NAME|" .env
|
|
||||||
# Update MINIO_PUBLIC_ENDPOINT to use domain (browsers fetch /voice-audio/* here)
|
|
||||||
if grep -q "^MINIO_PUBLIC_ENDPOINT=" .env; then
|
|
||||||
sed -i.bak "s|^MINIO_PUBLIC_ENDPOINT=.*|MINIO_PUBLIC_ENDPOINT=https://$DOMAIN_NAME|" .env
|
|
||||||
else
|
|
||||||
echo "MINIO_PUBLIC_ENDPOINT=https://$DOMAIN_NAME" >> .env
|
|
||||||
fi
|
|
||||||
rm -f .env.bak
|
|
||||||
echo -e "${GREEN}✓ .env updated with domain name${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}⚠ .env file not found - skipping environment update${NC}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Setup auto-renewal
|
[[ -n "${SERVER_IP:-}" ]] || dograh_fail "Could not determine SERVER_IP from the existing install"
|
||||||
echo -e "${BLUE}[7/8] Setting up automatic certificate renewal...${NC}"
|
|
||||||
DOGRAH_PATH=$(pwd)
|
dograh_set_env_key .env SERVER_IP "$SERVER_IP"
|
||||||
|
dograh_set_env_key .env PUBLIC_HOST "$DOMAIN_NAME"
|
||||||
|
dograh_set_env_key .env PUBLIC_BASE_URL "https://$DOMAIN_NAME"
|
||||||
|
dograh_delete_env_key .env BACKEND_URL
|
||||||
|
dograh_prepare_remote_install "$(pwd)"
|
||||||
|
echo -e "${GREEN}✓ .env synchronized and init-based config validated${NC}"
|
||||||
|
|
||||||
|
echo -e "${BLUE}[6/7] Setting up automatic certificate renewal...${NC}"
|
||||||
|
DOGRAH_PATH="$(pwd)"
|
||||||
|
|
||||||
# Create renewal hook script that copies new certificates and restarts nginx
|
|
||||||
cat > /etc/letsencrypt/renewal-hooks/deploy/dograh-reload.sh << HOOK_EOF
|
cat > /etc/letsencrypt/renewal-hooks/deploy/dograh-reload.sh << HOOK_EOF
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Copy renewed certificates to dograh certs directory
|
cp /etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem $DOGRAH_PATH/certs/local.crt
|
||||||
cp /etc/letsencrypt/archive/$DOMAIN_NAME/fullchain1.pem $DOGRAH_PATH/certs/local.crt
|
cp /etc/letsencrypt/live/$DOMAIN_NAME/privkey.pem $DOGRAH_PATH/certs/local.key
|
||||||
cp /etc/letsencrypt/archive/$DOMAIN_NAME/privkey1.pem $DOGRAH_PATH/certs/local.key
|
|
||||||
chmod 644 $DOGRAH_PATH/certs/local.crt $DOGRAH_PATH/certs/local.key
|
chmod 644 $DOGRAH_PATH/certs/local.crt $DOGRAH_PATH/certs/local.key
|
||||||
|
|
||||||
# Restart nginx to load new certificates
|
|
||||||
cd $DOGRAH_PATH
|
cd $DOGRAH_PATH
|
||||||
docker compose --profile remote restart nginx 2>/dev/null || true
|
docker compose --profile remote restart nginx 2>/dev/null || true
|
||||||
HOOK_EOF
|
HOOK_EOF
|
||||||
chmod +x /etc/letsencrypt/renewal-hooks/deploy/dograh-reload.sh
|
chmod +x /etc/letsencrypt/renewal-hooks/deploy/dograh-reload.sh
|
||||||
|
|
||||||
# Test renewal
|
if certbot renew --dry-run --quiet; then
|
||||||
certbot renew --dry-run --quiet && echo -e "${GREEN}✓ Auto-renewal configured and tested${NC}" || echo -e "${YELLOW}⚠ Auto-renewal test had issues, but certificates are installed${NC}"
|
echo -e "${GREEN}✓ Auto-renewal configured and tested${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ Auto-renewal test had issues, but certificates are installed${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Start Dograh services
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}[8/8] Starting Dograh services...${NC}"
|
echo -e "${BLUE}[7/7] Starting Dograh services through validated startup wrapper...${NC}"
|
||||||
docker compose --profile remote up -d --pull always
|
./remote_up.sh
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
|
@ -331,8 +229,7 @@ echo -e " Private Key: $DOGRAH_PATH/certs/local.key"
|
||||||
echo -e " Auto-renewal: Enabled (certificates renew automatically)"
|
echo -e " Auto-renewal: Enabled (certificates renew automatically)"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Files modified:${NC}"
|
echo -e "${YELLOW}Files modified:${NC}"
|
||||||
echo " - dograh/nginx.conf (updated with domain name)"
|
echo " - dograh/.env (canonical public host/base URL updated)"
|
||||||
echo " - dograh/.env (BACKEND_API_ENDPOINT and TURN_HOST updated)"
|
|
||||||
echo " - dograh/certs/local.crt (SSL certificate)"
|
echo " - dograh/certs/local.crt (SSL certificate)"
|
||||||
echo " - dograh/certs/local.key (SSL private key)"
|
echo " - dograh/certs/local.key (SSL private key)"
|
||||||
echo " - /etc/letsencrypt/renewal-hooks/deploy/dograh-reload.sh (renewal hook)"
|
echo " - /etc/letsencrypt/renewal-hooks/deploy/dograh-reload.sh (renewal hook)"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,26 @@ YELLOW='\033[1;33m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LIB_PATH="$SCRIPT_DIR/lib/setup_common.sh"
|
||||||
|
BOOTSTRAP_LIB=""
|
||||||
|
|
||||||
|
if [[ ! -f "$LIB_PATH" ]]; then
|
||||||
|
BOOTSTRAP_LIB="$(mktemp)"
|
||||||
|
curl -fsSL -o "$BOOTSTRAP_LIB" "https://raw.githubusercontent.com/dograh-hq/dograh/main/scripts/lib/setup_common.sh"
|
||||||
|
LIB_PATH="$BOOTSTRAP_LIB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "$BOOTSTRAP_LIB" ]]; then
|
||||||
|
rm -f "$BOOTSTRAP_LIB"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$LIB_PATH"
|
||||||
|
|
||||||
echo -e "${BLUE}"
|
echo -e "${BLUE}"
|
||||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ Dograh Local Setup ║"
|
echo "║ Dograh Local Setup ║"
|
||||||
|
|
@ -16,7 +36,7 @@ echo "╚═══════════════════════
|
||||||
echo -e "${NC}"
|
echo -e "${NC}"
|
||||||
|
|
||||||
# Ask whether to enable coturn (skip prompt if ENABLE_COTURN is already set)
|
# Ask whether to enable coturn (skip prompt if ENABLE_COTURN is already set)
|
||||||
if [[ -z "$ENABLE_COTURN" ]]; then
|
if [[ -z "${ENABLE_COTURN:-}" ]]; then
|
||||||
echo -e "${YELLOW}Enable coturn (TURN server) for WebRTC NAT traversal? [y/N]:${NC}"
|
echo -e "${YELLOW}Enable coturn (TURN server) for WebRTC NAT traversal? [y/N]:${NC}"
|
||||||
read -p "> " ENABLE_COTURN_INPUT
|
read -p "> " ENABLE_COTURN_INPUT
|
||||||
if [[ "$ENABLE_COTURN_INPUT" =~ ^[Yy] ]]; then
|
if [[ "$ENABLE_COTURN_INPUT" =~ ^[Yy] ]]; then
|
||||||
|
|
@ -26,7 +46,7 @@ if [[ -z "$ENABLE_COTURN" ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$ENABLE_COTURN" == "true" ]]; then
|
if [[ "${ENABLE_COTURN:-false}" == "true" ]]; then
|
||||||
# Pick a TURN_HOST that's reachable from BOTH the browser (running on the
|
# Pick a TURN_HOST that's reachable from BOTH the browser (running on the
|
||||||
# host) and the API container (running in docker). 127.0.0.1 is tempting
|
# host) and the API container (running in docker). 127.0.0.1 is tempting
|
||||||
# but doesn't work for the api container — its own loopback isn't where
|
# but doesn't work for the api container — its own loopback isn't where
|
||||||
|
|
@ -54,7 +74,7 @@ if [[ "$ENABLE_COTURN" == "true" ]]; then
|
||||||
DEFAULT_TURN_HOST="${DEFAULT_TURN_HOST:-127.0.0.1}"
|
DEFAULT_TURN_HOST="${DEFAULT_TURN_HOST:-127.0.0.1}"
|
||||||
|
|
||||||
# Get the host browsers/peers will use to reach the TURN server
|
# Get the host browsers/peers will use to reach the TURN server
|
||||||
if [[ -z "$TURN_HOST" ]]; then
|
if [[ -z "${TURN_HOST:-}" ]]; then
|
||||||
echo -e "${YELLOW}Enter the host browsers AND the API container will use to reach TURN${NC}"
|
echo -e "${YELLOW}Enter the host browsers AND the API container will use to reach TURN${NC}"
|
||||||
echo -e "${YELLOW}(press Enter for ${DEFAULT_TURN_HOST}):${NC}"
|
echo -e "${YELLOW}(press Enter for ${DEFAULT_TURN_HOST}):${NC}"
|
||||||
read -p "> " TURN_HOST
|
read -p "> " TURN_HOST
|
||||||
|
|
@ -68,13 +88,13 @@ if [[ "$ENABLE_COTURN" == "true" ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get the TURN secret (skip prompt if TURN_SECRET is already set)
|
# Get the TURN secret (skip prompt if TURN_SECRET is already set)
|
||||||
if [[ -z "$TURN_SECRET" ]]; then
|
if [[ -z "${TURN_SECRET:-}" ]]; then
|
||||||
echo -e "${YELLOW}Enter a shared secret for the TURN server (press Enter to generate a random one):${NC}"
|
echo -e "${YELLOW}Enter a shared secret for the TURN server (press Enter to generate a random one):${NC}"
|
||||||
read -sp "> " TURN_SECRET
|
read -sp "> " TURN_SECRET
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$TURN_SECRET" ]]; then
|
if [[ -z "${TURN_SECRET:-}" ]]; then
|
||||||
TURN_SECRET=$(openssl rand -hex 32)
|
TURN_SECRET=$(openssl rand -hex 32)
|
||||||
echo -e "${BLUE}Generated random TURN secret${NC}"
|
echo -e "${BLUE}Generated random TURN secret${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
@ -88,8 +108,8 @@ REGISTRY="${REGISTRY:-ghcr.io/dograh-hq}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}Configuration:${NC}"
|
echo -e "${GREEN}Configuration:${NC}"
|
||||||
echo -e " Coturn: ${BLUE}$ENABLE_COTURN${NC}"
|
echo -e " Coturn: ${BLUE}${ENABLE_COTURN:-false}${NC}"
|
||||||
if [[ "$ENABLE_COTURN" == "true" ]]; then
|
if [[ "${ENABLE_COTURN:-false}" == "true" ]]; then
|
||||||
echo -e " TURN Host: ${BLUE}$TURN_HOST${NC}"
|
echo -e " TURN Host: ${BLUE}$TURN_HOST${NC}"
|
||||||
echo -e " TURN Secret: ${BLUE}********${NC}"
|
echo -e " TURN Secret: ${BLUE}********${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
@ -99,52 +119,26 @@ echo ""
|
||||||
|
|
||||||
# Download compose file (skip when DOGRAH_SKIP_DOWNLOAD=1 — e.g. local repo testing).
|
# Download compose file (skip when DOGRAH_SKIP_DOWNLOAD=1 — e.g. local repo testing).
|
||||||
TOTAL_STEPS=2
|
TOTAL_STEPS=2
|
||||||
if [[ "$ENABLE_COTURN" == "true" ]]; then
|
|
||||||
TOTAL_STEPS=3
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$DOGRAH_SKIP_DOWNLOAD" != "1" ]]; then
|
if [[ "${DOGRAH_SKIP_DOWNLOAD:-}" != "1" ]]; then
|
||||||
echo -e "${BLUE}[1/$TOTAL_STEPS] Downloading docker-compose.yaml...${NC}"
|
if [[ "${ENABLE_COTURN:-false}" == "true" ]]; then
|
||||||
|
echo -e "${BLUE}[1/$TOTAL_STEPS] Downloading docker-compose.yaml and TURN helper bundle...${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${BLUE}[1/$TOTAL_STEPS] Downloading docker-compose.yaml...${NC}"
|
||||||
|
fi
|
||||||
curl -sS -o docker-compose.yaml https://raw.githubusercontent.com/dograh-hq/dograh/main/docker-compose.yaml
|
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}"
|
if [[ "${ENABLE_COTURN:-false}" == "true" ]]; then
|
||||||
|
dograh_download_init_support_bundle "$(pwd)" "main"
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✓ Deployment files downloaded${NC}"
|
||||||
else
|
else
|
||||||
echo -e "${BLUE}[1/$TOTAL_STEPS] Using docker-compose.yaml in current directory${NC}"
|
echo -e "${BLUE}[1/$TOTAL_STEPS] Using docker-compose.yaml in current directory${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate turnserver.conf if coturn is enabled
|
if [[ "${ENABLE_COTURN:-false}" == "true" ]]; then
|
||||||
if [[ "$ENABLE_COTURN" == "true" ]]; then
|
[[ -f scripts/run_dograh_init.sh ]] || dograh_fail "scripts/run_dograh_init.sh not found. Re-run setup_local.sh without DOGRAH_SKIP_DOWNLOAD=1, or use a full repo checkout."
|
||||||
echo -e "${BLUE}[2/$TOTAL_STEPS] Creating TURN server configuration...${NC}"
|
[[ -f scripts/lib/setup_common.sh ]] || dograh_fail "scripts/lib/setup_common.sh not found. Re-run setup_local.sh without DOGRAH_SKIP_DOWNLOAD=1, or use a full repo checkout."
|
||||||
cat > turnserver.conf << TURN_EOF
|
[[ -f deploy/templates/turnserver.remote.conf.template ]] || dograh_fail "deploy/templates/turnserver.remote.conf.template not found. Re-run setup_local.sh without DOGRAH_SKIP_DOWNLOAD=1, or use a full repo checkout."
|
||||||
# Coturn TURN Server - Docker Configuration (local)
|
|
||||||
# Auto-generated by setup_local.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=$TURN_HOST
|
|
||||||
|
|
||||||
# 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 created${NC}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate .env
|
# Generate .env
|
||||||
|
|
@ -163,7 +157,7 @@ OSS_JWT_SECRET=$OSS_JWT_SECRET
|
||||||
ENABLE_TELEMETRY=$ENABLE_TELEMETRY
|
ENABLE_TELEMETRY=$ENABLE_TELEMETRY
|
||||||
ENV_EOF
|
ENV_EOF
|
||||||
|
|
||||||
if [[ "$ENABLE_COTURN" == "true" ]]; then
|
if [[ "${ENABLE_COTURN:-false}" == "true" ]]; then
|
||||||
cat >> .env << ENV_EOF
|
cat >> .env << ENV_EOF
|
||||||
|
|
||||||
# TURN Server Configuration (time-limited credentials via TURN REST API)
|
# TURN Server Configuration (time-limited credentials via TURN REST API)
|
||||||
|
|
@ -181,11 +175,13 @@ echo ""
|
||||||
echo -e "Files created in ${BLUE}$(pwd)${NC}:"
|
echo -e "Files created in ${BLUE}$(pwd)${NC}:"
|
||||||
echo " - docker-compose.yaml"
|
echo " - docker-compose.yaml"
|
||||||
echo " - .env"
|
echo " - .env"
|
||||||
if [[ "$ENABLE_COTURN" == "true" ]]; then
|
if [[ "${ENABLE_COTURN:-false}" == "true" ]]; then
|
||||||
echo " - turnserver.conf"
|
echo " - scripts/run_dograh_init.sh"
|
||||||
|
echo " - scripts/lib/setup_common.sh"
|
||||||
|
echo " - deploy/templates/"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
if [[ "$ENABLE_COTURN" == "true" ]]; then
|
if [[ "${ENABLE_COTURN:-false}" == "true" ]]; then
|
||||||
echo -e "${YELLOW}To start Dograh with TURN, run:${NC}"
|
echo -e "${YELLOW}To start Dograh with TURN, run:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BLUE}docker compose --profile local-turn up --pull always${NC}"
|
echo -e " ${BLUE}docker compose --profile local-turn up --pull always${NC}"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -euo pipefail
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
|
@ -8,6 +8,26 @@ YELLOW='\033[1;33m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LIB_PATH="$SCRIPT_DIR/lib/setup_common.sh"
|
||||||
|
BOOTSTRAP_LIB=""
|
||||||
|
|
||||||
|
if [[ ! -f "$LIB_PATH" ]]; then
|
||||||
|
BOOTSTRAP_LIB="$(mktemp)"
|
||||||
|
curl -fsSL -o "$BOOTSTRAP_LIB" "https://raw.githubusercontent.com/dograh-hq/dograh/main/scripts/lib/setup_common.sh"
|
||||||
|
LIB_PATH="$BOOTSTRAP_LIB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "$BOOTSTRAP_LIB" ]]; then
|
||||||
|
rm -f "$BOOTSTRAP_LIB"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$LIB_PATH"
|
||||||
|
|
||||||
echo -e "${BLUE}"
|
echo -e "${BLUE}"
|
||||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ Dograh Remote Setup ║"
|
echo "║ Dograh Remote Setup ║"
|
||||||
|
|
@ -16,24 +36,21 @@ echo "╚═══════════════════════
|
||||||
echo -e "${NC}"
|
echo -e "${NC}"
|
||||||
|
|
||||||
# Get the public IP address (skip prompt if SERVER_IP is already set)
|
# Get the public IP address (skip prompt if SERVER_IP is already set)
|
||||||
if [[ -z "$SERVER_IP" ]]; then
|
if [[ -z "${SERVER_IP:-}" ]]; then
|
||||||
echo -e "${YELLOW}Enter your server's public IP address:${NC}"
|
echo -e "${YELLOW}Enter your server's public IP address:${NC}"
|
||||||
read -p "> " SERVER_IP
|
read -p "> " SERVER_IP
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$SERVER_IP" ]]; then
|
if [[ -z "$SERVER_IP" ]]; then
|
||||||
echo -e "${RED}Error: IP address cannot be empty${NC}"
|
dograh_fail "IP address cannot be empty"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Validate IP address format (basic validation)
|
if ! dograh_is_ipv4 "$SERVER_IP"; then
|
||||||
if ! [[ "$SERVER_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
dograh_fail "Invalid IP address format"
|
||||||
echo -e "${RED}Error: Invalid IP address format${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get the TURN secret (skip prompt if TURN_SECRET is already set)
|
# Get the TURN secret (skip prompt if TURN_SECRET is already set)
|
||||||
if [[ -z "$TURN_SECRET" ]]; then
|
if [[ -z "${TURN_SECRET:-}" ]]; then
|
||||||
echo -e "${YELLOW}Enter a shared secret for the TURN server (press Enter to generate a random one):${NC}"
|
echo -e "${YELLOW}Enter a shared secret for the TURN server (press Enter to generate a random one):${NC}"
|
||||||
read -sp "> " TURN_SECRET
|
read -sp "> " TURN_SECRET
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -45,10 +62,8 @@ if [[ -z "$TURN_SECRET" ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Deployment mode. Skip prompt if DEPLOY_MODE is already set. Non-interactive
|
# Deployment mode. Skip prompt if DEPLOY_MODE is already set. Non-interactive
|
||||||
# callers (cloud-init, CI, terraform) without a TTY default to "prebuilt" so
|
# callers without a TTY default to "prebuilt" to keep automation stable.
|
||||||
# existing automation keeps working without changes - explicitly set
|
if [[ -z "${DEPLOY_MODE:-}" ]]; then
|
||||||
# DEPLOY_MODE=build to opt into source builds from a non-interactive context.
|
|
||||||
if [[ -z "$DEPLOY_MODE" ]]; then
|
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Deployment mode:${NC}"
|
echo -e "${YELLOW}Deployment mode:${NC}"
|
||||||
|
|
@ -58,19 +73,16 @@ if [[ -z "$DEPLOY_MODE" ]]; then
|
||||||
mode_choice="${mode_choice:-1}"
|
mode_choice="${mode_choice:-1}"
|
||||||
case "$mode_choice" in
|
case "$mode_choice" in
|
||||||
1|prebuilt) DEPLOY_MODE="prebuilt" ;;
|
1|prebuilt) DEPLOY_MODE="prebuilt" ;;
|
||||||
2|build) DEPLOY_MODE="build" ;;
|
2|build) DEPLOY_MODE="build" ;;
|
||||||
*) echo -e "${RED}Error: invalid choice '$mode_choice'${NC}"; exit 1 ;;
|
*) dograh_fail "invalid choice '$mode_choice'" ;;
|
||||||
esac
|
esac
|
||||||
else
|
else
|
||||||
DEPLOY_MODE="prebuilt"
|
DEPLOY_MODE="prebuilt"
|
||||||
fi
|
fi
|
||||||
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 [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
if [[ -z "$REPO_SOURCE" ]]; then
|
if [[ -z "${REPO_SOURCE:-}" ]]; then
|
||||||
if [[ -d ".git" ]] && [[ -f "docker-compose.yaml" ]]; then
|
if [[ -d ".git" ]] && [[ -f "docker-compose.yaml" ]]; then
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -91,7 +103,7 @@ if [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$REPO_SOURCE" == "clone" ]]; then
|
if [[ "$REPO_SOURCE" == "clone" ]]; then
|
||||||
if [[ -z "$FORK_REPO" ]]; then
|
if [[ -z "${FORK_REPO:-}" ]]; then
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}GitHub repo to clone (format: owner/name):${NC}"
|
echo -e "${YELLOW}GitHub repo to clone (format: owner/name):${NC}"
|
||||||
|
|
@ -101,7 +113,8 @@ if [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
FORK_REPO="dograh-hq/dograh"
|
FORK_REPO="dograh-hq/dograh"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [[ -z "$BRANCH" ]]; then
|
|
||||||
|
if [[ -z "${BRANCH:-}" ]]; then
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
echo -e "${YELLOW}Branch:${NC}"
|
echo -e "${YELLOW}Branch:${NC}"
|
||||||
read -p "[main]: " BRANCH
|
read -p "[main]: " BRANCH
|
||||||
|
|
@ -113,13 +126,9 @@ if [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Telemetry opt-out (default: true)
|
|
||||||
ENABLE_TELEMETRY="${ENABLE_TELEMETRY:-true}"
|
ENABLE_TELEMETRY="${ENABLE_TELEMETRY:-true}"
|
||||||
|
FASTAPI_WORKERS="${FASTAPI_WORKERS:-}"
|
||||||
|
|
||||||
# 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 [[ -z "$FASTAPI_WORKERS" ]]; then
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -131,26 +140,15 @@ if [[ -z "$FASTAPI_WORKERS" ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! [[ "$FASTAPI_WORKERS" =~ ^[1-9][0-9]*$ ]]; then
|
[[ "$FASTAPI_WORKERS" =~ ^[1-9][0-9]*$ ]] || dograh_fail "FASTAPI_WORKERS must be a positive integer (got: $FASTAPI_WORKERS)"
|
||||||
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
|
if [[ "$DEPLOY_MODE" == "build" && "${REPO_SOURCE:-}" == "existing" ]]; then
|
||||||
# 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="."
|
TARGET_DIR="."
|
||||||
else
|
else
|
||||||
TARGET_DIR="dograh"
|
TARGET_DIR="dograh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Refuse to overwrite an existing install - re-running this script would
|
if [[ "${DOGRAH_FORCE_OVERWRITE:-}" != "1" && "${DOGRAH_SKIP_DOWNLOAD:-}" != "1" ]]; then
|
||||||
# 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 [[ -f "$TARGET_DIR/.env" ]]; then
|
||||||
if [[ "$TARGET_DIR" == "." ]]; then
|
if [[ "$TARGET_DIR" == "." ]]; then
|
||||||
existing_path="$(pwd)/.env"
|
existing_path="$(pwd)/.env"
|
||||||
|
|
@ -164,7 +162,7 @@ if [[ "$DOGRAH_FORCE_OVERWRITE" != "1" && "$DOGRAH_SKIP_DOWNLOAD" != "1" ]]; the
|
||||||
echo -e "${RED}Refusing to continue - re-running setup would:${NC}"
|
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} - overwrite .env (invalidates sessions, breaks TURN auth)${NC}"
|
||||||
echo -e "${RED} - regenerate SSL certificates${NC}"
|
echo -e "${RED} - regenerate SSL certificates${NC}"
|
||||||
echo -e "${RED} - reset nginx.conf and turnserver.conf customizations${NC}"
|
echo -e "${RED} - replace the validated remote deployment bundle${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}To upgrade an existing install, follow:${NC}"
|
echo -e "${BLUE}To upgrade an existing install, follow:${NC}"
|
||||||
echo -e " ${BLUE}https://docs.dograh.com/deployment/update${NC}"
|
echo -e " ${BLUE}https://docs.dograh.com/deployment/update${NC}"
|
||||||
|
|
@ -176,11 +174,10 @@ if [[ "$DOGRAH_FORCE_OVERWRITE" != "1" && "$DOGRAH_SKIP_DOWNLOAD" != "1" ]]; the
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Total step count depends on mode (build adds the override-file step)
|
|
||||||
if [[ "$DEPLOY_MODE" == "build" ]]; then
|
if [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
TOTAL=7
|
|
||||||
else
|
|
||||||
TOTAL=6
|
TOTAL=6
|
||||||
|
else
|
||||||
|
TOTAL=5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -190,24 +187,20 @@ echo -e " TURN Secret: ${BLUE}********${NC}"
|
||||||
echo -e " Deploy mode: ${BLUE}$DEPLOY_MODE${NC}"
|
echo -e " Deploy mode: ${BLUE}$DEPLOY_MODE${NC}"
|
||||||
echo -e " FastAPI workers: ${BLUE}$FASTAPI_WORKERS${NC} (ports 8000..$((8000 + FASTAPI_WORKERS - 1)))"
|
echo -e " FastAPI workers: ${BLUE}$FASTAPI_WORKERS${NC} (ports 8000..$((8000 + FASTAPI_WORKERS - 1)))"
|
||||||
if [[ "$DEPLOY_MODE" == "build" ]]; then
|
if [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
if [[ "$REPO_SOURCE" == "clone" ]]; then
|
if [[ "${REPO_SOURCE:-}" == "clone" ]]; then
|
||||||
echo -e " Source: ${BLUE}clone $FORK_REPO@$BRANCH${NC}"
|
echo -e " Source: ${BLUE}clone $FORK_REPO@$BRANCH${NC}"
|
||||||
else
|
else
|
||||||
echo -e " Source: ${BLUE}existing repo at $(pwd)${NC}"
|
echo -e " Source: ${BLUE}existing repo at $(pwd)${NC}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo ""
|
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 [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
if [[ "$DOGRAH_SKIP_DOWNLOAD" == "1" ]]; then
|
if [[ "${DOGRAH_SKIP_DOWNLOAD:-}" == "1" ]]; then
|
||||||
echo -e "${BLUE}[1/$TOTAL] Using existing repo in current directory${NC}"
|
echo -e "${BLUE}[1/$TOTAL] Using existing repo in current directory${NC}"
|
||||||
elif [[ "$REPO_SOURCE" == "clone" ]]; then
|
elif [[ "${REPO_SOURCE:-}" == "clone" ]]; then
|
||||||
if [[ -e "dograh" ]]; 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}"
|
dograh_fail "'dograh' directory already exists. Remove it or re-run with REPO_SOURCE=existing from inside it."
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
echo -e "${BLUE}[1/$TOTAL] Cloning $FORK_REPO (branch: $BRANCH)...${NC}"
|
echo -e "${BLUE}[1/$TOTAL] Cloning $FORK_REPO (branch: $BRANCH)...${NC}"
|
||||||
git clone --branch "$BRANCH" --recurse-submodules "https://github.com/$FORK_REPO.git" dograh
|
git clone --branch "$BRANCH" --recurse-submodules "https://github.com/$FORK_REPO.git" dograh
|
||||||
|
|
@ -217,123 +210,26 @@ if [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
echo -e "${BLUE}[1/$TOTAL] Using existing repo at $(pwd)${NC}"
|
echo -e "${BLUE}[1/$TOTAL] Using existing repo at $(pwd)${NC}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [[ "$DOGRAH_SKIP_DOWNLOAD" != "1" ]]; then
|
if [[ "${DOGRAH_SKIP_DOWNLOAD:-}" != "1" ]]; then
|
||||||
mkdir -p dograh 2>/dev/null || true
|
mkdir -p dograh 2>/dev/null || true
|
||||||
cd dograh
|
cd dograh
|
||||||
|
|
||||||
echo -e "${BLUE}[1/$TOTAL] Downloading docker-compose.yaml...${NC}"
|
echo -e "${BLUE}[1/$TOTAL] Downloading deployment bundle...${NC}"
|
||||||
curl -sS -o docker-compose.yaml https://raw.githubusercontent.com/dograh-hq/dograh/main/docker-compose.yaml
|
curl -fsSL -o docker-compose.yaml "https://raw.githubusercontent.com/dograh-hq/dograh/main/docker-compose.yaml"
|
||||||
echo -e "${GREEN}✓ docker-compose.yaml downloaded${NC}"
|
dograh_download_remote_support_bundle "$(pwd)" "main"
|
||||||
|
echo -e "${GREEN}✓ Deployment bundle downloaded${NC}"
|
||||||
else
|
else
|
||||||
echo -e "${BLUE}[1/$TOTAL] Using docker-compose.yaml in current directory${NC}"
|
echo -e "${BLUE}[1/$TOTAL] Using deployment files in current directory${NC}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${BLUE}[2/$TOTAL] Creating nginx.conf...${NC}"
|
DOGRAH_DEPLOY_PROJECT_DIR="$(pwd)"
|
||||||
# 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<FASTAPI_WORKERS; i++)); do
|
|
||||||
port=$((8000 + i))
|
|
||||||
echo " server api:$port max_fails=3 fail_timeout=10s;"
|
|
||||||
done
|
|
||||||
echo " keepalive 32;"
|
|
||||||
echo "}"
|
|
||||||
echo ""
|
|
||||||
cat << 'NGINX_EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name SERVER_IP_PLACEHOLDER;
|
|
||||||
|
|
||||||
# Redirect all HTTP to HTTPS
|
if [[ "$DEPLOY_MODE" != "prebuilt" ]]; then
|
||||||
return 301 https://$host$request_uri;
|
chmod +x remote_up.sh
|
||||||
}
|
fi
|
||||||
|
|
||||||
server {
|
echo -e "${BLUE}[2/$TOTAL] Creating SSL certificate generation script...${NC}"
|
||||||
listen 443 ssl;
|
|
||||||
server_name SERVER_IP_PLACEHOLDER;
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/local.crt;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/local.key;
|
|
||||||
|
|
||||||
# Basic TLS settings
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_prefer_server_ciphers on;
|
|
||||||
|
|
||||||
# Backend API and WebSockets - bypass the UI, go straight to the
|
|
||||||
# api workers via the least_conn upstream defined above.
|
|
||||||
location /api/v1/ {
|
|
||||||
proxy_pass http://dograh_api;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# Retry on a dead/restarting worker
|
|
||||||
proxy_next_upstream error timeout http_502 http_503 http_504;
|
|
||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
|
||||||
|
|
||||||
# Long-lived WebSockets (audio streaming, signaling)
|
|
||||||
proxy_read_timeout 3600s;
|
|
||||||
proxy_send_timeout 3600s;
|
|
||||||
|
|
||||||
# Don't buffer streamed responses
|
|
||||||
proxy_buffering off;
|
|
||||||
client_max_body_size 100M;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://ui:3010;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# Important for WebSockets / hot reload etc.
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
|
||||||
|
|
||||||
# Rewrite localhost MinIO URLs in API responses to use current domain
|
|
||||||
sub_filter 'http://localhost:9000/voice-audio/' 'https://$host/voice-audio/';
|
|
||||||
sub_filter_once off;
|
|
||||||
sub_filter_types application/json text/html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /voice-audio/ {
|
|
||||||
proxy_pass http://minio:9000/voice-audio/;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# Headers for file downloads from MinIO
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
|
||||||
|
|
||||||
# Allow large file downloads
|
|
||||||
proxy_buffering off;
|
|
||||||
client_max_body_size 100M;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX_EOF
|
|
||||||
} > 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/$TOTAL] Creating SSL certificate generation script...${NC}"
|
|
||||||
cat > generate_certificate.sh << CERT_EOF
|
cat > generate_certificate.sh << CERT_EOF
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
mkdir -p certs
|
mkdir -p certs
|
||||||
|
|
@ -346,50 +242,22 @@ CERT_EOF
|
||||||
chmod +x generate_certificate.sh
|
chmod +x generate_certificate.sh
|
||||||
echo -e "${GREEN}✓ generate_certificate.sh created${NC}"
|
echo -e "${GREEN}✓ generate_certificate.sh created${NC}"
|
||||||
|
|
||||||
echo -e "${BLUE}[4/$TOTAL] Generating SSL certificates...${NC}"
|
echo -e "${BLUE}[3/$TOTAL] Generating SSL certificates...${NC}"
|
||||||
./generate_certificate.sh
|
./generate_certificate.sh
|
||||||
echo -e "${GREEN}✓ SSL certificates generated${NC}"
|
echo -e "${GREEN}✓ SSL certificates generated${NC}"
|
||||||
|
|
||||||
echo -e "${BLUE}[5/$TOTAL] Creating TURN server configuration...${NC}"
|
echo -e "${BLUE}[4/$TOTAL] Creating environment file...${NC}"
|
||||||
cat > turnserver.conf << TURN_EOF
|
|
||||||
# Coturn TURN Server - Docker Configuration
|
|
||||||
# Auto-generated by setup_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 created${NC}"
|
|
||||||
|
|
||||||
echo -e "${BLUE}[6/$TOTAL] Creating environment file...${NC}"
|
|
||||||
OSS_JWT_SECRET=$(openssl rand -hex 32)
|
OSS_JWT_SECRET=$(openssl rand -hex 32)
|
||||||
|
|
||||||
cat > .env << ENV_EOF
|
cat > .env << ENV_EOF
|
||||||
# Change environment from local to production so that coturn filters local IPs
|
# Change environment from local to production so that coturn filters local IPs
|
||||||
ENVIRONMENT=production
|
ENVIRONMENT=production
|
||||||
|
|
||||||
|
# Canonical public host/base URL for this install.
|
||||||
|
SERVER_IP=$SERVER_IP
|
||||||
|
PUBLIC_HOST=$SERVER_IP
|
||||||
|
PUBLIC_BASE_URL=https://$SERVER_IP
|
||||||
|
|
||||||
# Backend API endpoint (public URL the backend uses to build webhook/embed links)
|
# Backend API endpoint (public URL the backend uses to build webhook/embed links)
|
||||||
BACKEND_API_ENDPOINT=https://$SERVER_IP
|
BACKEND_API_ENDPOINT=https://$SERVER_IP
|
||||||
|
|
||||||
|
|
@ -407,18 +275,16 @@ OSS_JWT_SECRET=$OSS_JWT_SECRET
|
||||||
ENABLE_TELEMETRY=$ENABLE_TELEMETRY
|
ENABLE_TELEMETRY=$ENABLE_TELEMETRY
|
||||||
|
|
||||||
# Number of uvicorn worker processes; nginx load-balances across them
|
# 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
|
FASTAPI_WORKERS=$FASTAPI_WORKERS
|
||||||
ENV_EOF
|
ENV_EOF
|
||||||
echo -e "${GREEN}✓ .env file created${NC}"
|
echo -e "${GREEN}✓ .env file created${NC}"
|
||||||
|
|
||||||
# In build mode, write the override file that swaps prebuilt images for
|
echo -e "${BLUE}[5/$TOTAL] Validating remote init configuration...${NC}"
|
||||||
# local builds. Compose auto-loads docker-compose.override.yaml, so no -f flag
|
dograh_prepare_remote_install "$(pwd)"
|
||||||
# is needed at runtime.
|
echo -e "${GREEN}✓ Remote init configuration validated${NC}"
|
||||||
|
|
||||||
if [[ "$DEPLOY_MODE" == "build" ]]; then
|
if [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
echo -e "${BLUE}[7/$TOTAL] Creating docker-compose.override.yaml...${NC}"
|
echo -e "${BLUE}[6/$TOTAL] Creating docker-compose.override.yaml...${NC}"
|
||||||
cat > docker-compose.override.yaml << 'OVERRIDE_EOF'
|
cat > docker-compose.override.yaml << 'OVERRIDE_EOF'
|
||||||
# Auto-generated by setup_remote.sh (build mode).
|
# Auto-generated by setup_remote.sh (build mode).
|
||||||
# Overrides docker-compose.yaml to build api and ui images from local source
|
# Overrides docker-compose.yaml to build api and ui images from local source
|
||||||
|
|
@ -452,8 +318,9 @@ echo " - docker-compose.yaml"
|
||||||
if [[ "$DEPLOY_MODE" == "build" ]]; then
|
if [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
echo " - docker-compose.override.yaml (build directives)"
|
echo " - docker-compose.override.yaml (build directives)"
|
||||||
fi
|
fi
|
||||||
echo " - nginx.conf"
|
echo " - remote_up.sh"
|
||||||
echo " - turnserver.conf"
|
echo " - scripts/run_dograh_init.sh"
|
||||||
|
echo " - deploy/templates/"
|
||||||
echo " - generate_certificate.sh"
|
echo " - generate_certificate.sh"
|
||||||
echo " - certs/local.crt"
|
echo " - certs/local.crt"
|
||||||
echo " - certs/local.key"
|
echo " - certs/local.key"
|
||||||
|
|
@ -461,28 +328,17 @@ echo " - .env"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}To start Dograh, run:${NC}"
|
echo -e "${YELLOW}To start Dograh, run:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
# The script's own cd into dograh/ doesn't persist to the user's shell, so
|
if [[ "$DEPLOY_MODE" != "build" || "${REPO_SOURCE:-}" != "existing" ]]; then
|
||||||
# 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}"
|
echo -e " ${BLUE}cd $(pwd)${NC}"
|
||||||
fi
|
fi
|
||||||
if [[ "$DEPLOY_MODE" == "build" ]]; then
|
if [[ "$DEPLOY_MODE" == "build" ]]; then
|
||||||
echo -e " ${BLUE}sudo docker compose --profile remote up -d --build${NC}"
|
echo -e " ${BLUE}./remote_up.sh --build${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}A docker-compose.override.yaml has been created alongside${NC}"
|
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}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 -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
|
else
|
||||||
echo -e " ${BLUE}sudo docker compose --profile remote up --pull always${NC}"
|
echo -e " ${BLUE}./remote_up.sh${NC}"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Your application will be available at:${NC}"
|
echo -e "${YELLOW}Your application will be available at:${NC}"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -euo pipefail
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
|
@ -8,34 +8,39 @@ YELLOW='\033[1;33m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LIB_PATH="$SCRIPT_DIR/lib/setup_common.sh"
|
||||||
|
BOOTSTRAP_LIB=""
|
||||||
|
|
||||||
|
if [[ ! -f "$LIB_PATH" ]]; then
|
||||||
|
BOOTSTRAP_LIB="$(mktemp)"
|
||||||
|
curl -fsSL -o "$BOOTSTRAP_LIB" "https://raw.githubusercontent.com/dograh-hq/dograh/main/scripts/lib/setup_common.sh"
|
||||||
|
LIB_PATH="$BOOTSTRAP_LIB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "$BOOTSTRAP_LIB" ]]; then
|
||||||
|
rm -f "$BOOTSTRAP_LIB"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$LIB_PATH"
|
||||||
|
|
||||||
REPO="dograh-hq/dograh"
|
REPO="dograh-hq/dograh"
|
||||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||||
|
|
||||||
echo -e "${BLUE}"
|
echo -e "${BLUE}"
|
||||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ Dograh Remote Update ║"
|
echo "║ Dograh Remote Update ║"
|
||||||
echo "║ Refresh host-side configs and pin api/ui image versions ║"
|
echo "║ Refresh deployment files and validate runtime config ║"
|
||||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||||
echo -e "${NC}"
|
echo -e "${NC}"
|
||||||
|
|
||||||
# Refuse outside an install — nothing to update if these aren't here.
|
[[ -f docker-compose.yaml ]] || dograh_fail "docker-compose.yaml not found in $(pwd)"
|
||||||
if [[ ! -f docker-compose.yaml ]]; then
|
[[ -f .env ]] || dograh_fail ".env not found in $(pwd)"
|
||||||
echo -e "${RED}Error: docker-compose.yaml not found in $(pwd)${NC}"
|
|
||||||
echo -e "${RED}Run this script from your Dograh install directory${NC}"
|
|
||||||
echo -e "${RED}(the 'dograh/' folder created by setup_remote.sh).${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f .env ]]; then
|
|
||||||
echo -e "${RED}Error: .env not found in $(pwd)${NC}"
|
|
||||||
echo -e "${RED}This script updates an existing install — there is nothing here to update.${NC}"
|
|
||||||
echo -e "${RED}For a fresh install, see https://docs.dograh.com/deployment/docker${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build-mode installs update via git, not via this script. The presence of an
|
|
||||||
# override file is the definitive marker (created by setup_remote.sh in build
|
|
||||||
# mode and not in prebuilt mode).
|
|
||||||
if [[ -f docker-compose.override.yaml ]]; then
|
if [[ -f docker-compose.override.yaml ]]; then
|
||||||
echo -e "${YELLOW}Build-mode install detected (docker-compose.override.yaml present).${NC}"
|
echo -e "${YELLOW}Build-mode install detected (docker-compose.override.yaml present).${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -44,61 +49,47 @@ if [[ -f docker-compose.override.yaml ]]; then
|
||||||
echo -e " ${BLUE}git fetch${NC}"
|
echo -e " ${BLUE}git fetch${NC}"
|
||||||
echo -e " ${BLUE}git checkout <tag> # or: git pull${NC}"
|
echo -e " ${BLUE}git checkout <tag> # or: git pull${NC}"
|
||||||
echo -e " ${BLUE}git submodule update --init --recursive${NC}"
|
echo -e " ${BLUE}git submodule update --init --recursive${NC}"
|
||||||
echo -e " ${BLUE}sudo docker compose --profile remote build${NC}"
|
echo -e " ${BLUE}./remote_up.sh --build${NC}"
|
||||||
echo -e " ${BLUE}sudo docker compose --profile remote up -d${NC}"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}See https://docs.dograh.com/deployment/update#updating-a-source-build${NC}"
|
echo -e "${YELLOW}See https://docs.dograh.com/deployment/update#updating-a-source-build${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
###############################################################################
|
_caller_FASTAPI_WORKERS="${FASTAPI_WORKERS:-}"
|
||||||
### Discover existing config from .env
|
_caller_TARGET_VERSION="${TARGET_VERSION:-}"
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
# Save anything the caller exported before we overwrite from .env.
|
DOGRAH_DEPLOY_PROJECT_DIR="$(pwd)"
|
||||||
_caller_FASTAPI_WORKERS="$FASTAPI_WORKERS"
|
dograh_load_env_file .env
|
||||||
_caller_TARGET_VERSION="$TARGET_VERSION"
|
|
||||||
|
|
||||||
set -a
|
[[ -n "${TURN_SECRET:-}" ]] || dograh_fail "TURN_SECRET not found in .env"
|
||||||
# shellcheck disable=SC1091
|
|
||||||
. ./.env
|
|
||||||
set +a
|
|
||||||
|
|
||||||
# SERVER_IP isn't a literal key in .env — derive it from BACKEND_API_ENDPOINT.
|
if [[ -n "$_caller_FASTAPI_WORKERS" ]]; then
|
||||||
if [[ -z "$SERVER_IP" ]]; then
|
FASTAPI_WORKERS="$_caller_FASTAPI_WORKERS"
|
||||||
if [[ -n "$BACKEND_API_ENDPOINT" ]]; then
|
fi
|
||||||
SERVER_IP="${BACKEND_API_ENDPOINT#https://}"
|
|
||||||
SERVER_IP="${SERVER_IP#http://}"
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$SERVER_IP" ]]; then
|
[[ "$FASTAPI_WORKERS" =~ ^[1-9][0-9]*$ ]] || dograh_fail "FASTAPI_WORKERS must be a positive integer (got: $FASTAPI_WORKERS)"
|
||||||
echo -e "${RED}Error: could not determine SERVER_IP from .env${NC}"
|
|
||||||
echo -e "${RED}Expected BACKEND_API_ENDPOINT=https://<ip> in .env${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$TURN_SECRET" ]]; then
|
TARGET_VERSION="${_caller_TARGET_VERSION:-${TARGET_VERSION:-}}"
|
||||||
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
|
if [[ -z "$TARGET_VERSION" ]]; then
|
||||||
echo -e "${BLUE}Fetching latest release tag from GitHub...${NC}"
|
dograh_info "Fetching latest release tag from GitHub..."
|
||||||
LATEST_TAG=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null \
|
LATEST_TAG=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null \
|
||||||
| grep -E '"tag_name":' | head -1 \
|
| grep -E '"tag_name":' | head -1 \
|
||||||
| sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/' || true)
|
| sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/' || true)
|
||||||
|
|
||||||
if [[ -z "$LATEST_TAG" ]]; then
|
if [[ -z "$LATEST_TAG" ]]; then
|
||||||
echo -e "${YELLOW}Could not auto-discover latest tag — defaulting to 'main'.${NC}"
|
dograh_warn "Could not auto-discover latest tag - defaulting to 'main'."
|
||||||
LATEST_TAG="main"
|
LATEST_TAG="main"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -113,28 +104,19 @@ if [[ -z "$TARGET_VERSION" ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# "latest" isn't a real ref on GitHub — treat it as "latest release".
|
|
||||||
if [[ "$TARGET_VERSION" == "latest" ]]; then
|
if [[ "$TARGET_VERSION" == "latest" ]]; then
|
||||||
TARGET_VERSION=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null \
|
TARGET_VERSION=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null \
|
||||||
| grep -E '"tag_name":' | head -1 \
|
| grep -E '"tag_name":' | head -1 \
|
||||||
| sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/' || true)
|
| sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/' || true)
|
||||||
if [[ -z "$TARGET_VERSION" ]]; then
|
[[ -n "$TARGET_VERSION" ]] || dograh_fail "could not resolve 'latest' to a release tag"
|
||||||
echo -e "${RED}Error: could not resolve 'latest' to a release tag${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
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")
|
TRY_TAGS=("$TARGET_VERSION")
|
||||||
case "$TARGET_VERSION" in
|
case "$TARGET_VERSION" in
|
||||||
main|HEAD)
|
main|HEAD)
|
||||||
;; # branch refs — leave as-is
|
;;
|
||||||
dograh-*)
|
dograh-*)
|
||||||
;; # already in the full tag form
|
;;
|
||||||
v*)
|
v*)
|
||||||
TRY_TAGS+=("dograh-$TARGET_VERSION")
|
TRY_TAGS+=("dograh-$TARGET_VERSION")
|
||||||
;;
|
;;
|
||||||
|
|
@ -143,7 +125,7 @@ case "$TARGET_VERSION" in
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo -e "${BLUE}Validating target version: $TARGET_VERSION...${NC}"
|
dograh_info "Validating target version: $TARGET_VERSION..."
|
||||||
RESOLVED_TAG=""
|
RESOLVED_TAG=""
|
||||||
for tag in "${TRY_TAGS[@]}"; do
|
for tag in "${TRY_TAGS[@]}"; do
|
||||||
if curl -fsI "https://raw.githubusercontent.com/$REPO/$tag/docker-compose.yaml" >/dev/null 2>&1; then
|
if curl -fsI "https://raw.githubusercontent.com/$REPO/$tag/docker-compose.yaml" >/dev/null 2>&1; then
|
||||||
|
|
@ -152,81 +134,49 @@ for tag in "${TRY_TAGS[@]}"; do
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z "$RESOLVED_TAG" ]]; then
|
[[ -n "$RESOLVED_TAG" ]] || dograh_fail "could not find a git tag matching '$TARGET_VERSION'"
|
||||||
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
|
if [[ "$RESOLVED_TAG" != "$TARGET_VERSION" ]]; then
|
||||||
echo -e "${GREEN}✓ Resolved '$TARGET_VERSION' to git tag '$RESOLVED_TAG'${NC}"
|
dograh_success "✓ Resolved '$TARGET_VERSION' to git tag '$RESOLVED_TAG'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TARGET_VERSION="$RESOLVED_TAG"
|
TARGET_VERSION="$RESOLVED_TAG"
|
||||||
RAW_BASE="https://raw.githubusercontent.com/$REPO/$TARGET_VERSION"
|
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=""
|
IMAGE_TAG=""
|
||||||
|
|
||||||
case "$TARGET_VERSION" in
|
case "$TARGET_VERSION" in
|
||||||
dograh-v*) IMAGE_TAG="${TARGET_VERSION#dograh-v}" ;;
|
dograh-v*) IMAGE_TAG="${TARGET_VERSION#dograh-v}" ;;
|
||||||
v*) IMAGE_TAG="${TARGET_VERSION#v}" ;;
|
v*) IMAGE_TAG="${TARGET_VERSION#v}" ;;
|
||||||
main|HEAD) IMAGE_TAG="" ;;
|
main|HEAD) IMAGE_TAG="" ;;
|
||||||
*) [[ "$TARGET_VERSION" =~ ^[0-9] ]] && IMAGE_TAG="$TARGET_VERSION" ;;
|
*) [[ "$TARGET_VERSION" =~ ^[0-9] ]] && IMAGE_TAG="$TARGET_VERSION" ;;
|
||||||
esac
|
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 [[ -n "$IMAGE_TAG" ]]; then
|
||||||
if curl -fsI "https://hub.docker.com/v2/repositories/dograhai/dograh-api/tags/$IMAGE_TAG/" >/dev/null 2>&1; 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}"
|
dograh_success "✓ Image tag :$IMAGE_TAG found on Docker Hub"
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}Warning: image tag :$IMAGE_TAG not found on Docker Hub — leaving images at :latest${NC}"
|
dograh_warn "Warning: image tag :$IMAGE_TAG not found on Docker Hub - leaving images at :latest"
|
||||||
IMAGE_TAG=""
|
IMAGE_TAG=""
|
||||||
fi
|
fi
|
||||||
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 ""
|
||||||
echo -e "${GREEN}Update plan:${NC}"
|
echo -e "${GREEN}Update plan:${NC}"
|
||||||
echo -e " Server IP: ${BLUE}$SERVER_IP${NC}"
|
echo -e " Server IP: ${BLUE}$(dograh_infer_server_ip "$(pwd)" || echo "unknown")${NC}"
|
||||||
echo -e " Target version: ${BLUE}$TARGET_VERSION${NC}"
|
echo -e " Target version: ${BLUE}$TARGET_VERSION${NC}"
|
||||||
echo -e " FastAPI workers: ${BLUE}$FASTAPI_WORKERS${NC} (ports 8000..$((8000 + FASTAPI_WORKERS - 1)))"
|
echo -e " FastAPI workers: ${BLUE}$FASTAPI_WORKERS${NC} (ports 8000..$((8000 + FASTAPI_WORKERS - 1)))"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Files that will be replaced (backups saved with suffix .bak.$TIMESTAMP):${NC}"
|
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 " - docker-compose.yaml (pulled from GitHub at $TARGET_VERSION)"
|
||||||
echo " - nginx.conf (regenerated from this script's template)"
|
echo " - remote_up.sh (startup wrapper / preflight)"
|
||||||
echo " - turnserver.conf (regenerated from this script's template)"
|
echo " - scripts/run_dograh_init.sh"
|
||||||
echo " - .env (existing values preserved; missing keys appended)"
|
echo " - scripts/lib/setup_common.sh"
|
||||||
echo ""
|
echo " - deploy/templates/*.template"
|
||||||
echo -e "${YELLOW}Any local customizations to these files will be overwritten — check the backup${NC}"
|
echo " - .env (canonical remote keys synchronized)"
|
||||||
echo -e "${YELLOW}files if you need to re-apply edits afterwards.${NC}"
|
echo " - legacy nginx.conf / turnserver.conf backups will be kept if those files still exist"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ -t 0 && "$DOGRAH_UPDATE_YES" != "1" ]]; then
|
if [[ -t 0 && "${DOGRAH_UPDATE_YES:-}" != "1" ]]; then
|
||||||
read -p "Proceed? [y/N]: " confirm
|
read -p "Proceed? [y/N]: " confirm
|
||||||
if ! [[ "$confirm" =~ ^[Yy] ]]; then
|
if ! [[ "$confirm" =~ ^[Yy] ]]; then
|
||||||
echo -e "${RED}Aborted.${NC}"
|
echo -e "${RED}Aborted.${NC}"
|
||||||
|
|
@ -234,198 +184,44 @@ if [[ -t 0 && "$DOGRAH_UPDATE_YES" != "1" ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
### Step 1 — backups
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}[1/5] Backing up existing files...${NC}"
|
echo -e "${BLUE}[1/3] Backing up existing files...${NC}"
|
||||||
for f in docker-compose.yaml nginx.conf turnserver.conf .env; do
|
for f in \
|
||||||
|
docker-compose.yaml \
|
||||||
|
nginx.conf \
|
||||||
|
turnserver.conf \
|
||||||
|
.env \
|
||||||
|
remote_up.sh \
|
||||||
|
scripts/run_dograh_init.sh \
|
||||||
|
scripts/lib/setup_common.sh \
|
||||||
|
deploy/templates/nginx.remote.conf.template \
|
||||||
|
deploy/templates/turnserver.remote.conf.template
|
||||||
|
do
|
||||||
if [[ -f "$f" ]]; then
|
if [[ -f "$f" ]]; then
|
||||||
|
mkdir -p "$(dirname "$f")"
|
||||||
cp -p "$f" "$f.bak.$TIMESTAMP"
|
cp -p "$f" "$f.bak.$TIMESTAMP"
|
||||||
echo -e " ${GREEN}✓ $f → $f.bak.$TIMESTAMP${NC}"
|
echo -e " ${GREEN}✓ $f → $f.bak.$TIMESTAMP${NC}"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
###############################################################################
|
echo -e "${BLUE}[2/3] Downloading deployment bundle at $TARGET_VERSION...${NC}"
|
||||||
### 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"
|
curl -fsSL -o docker-compose.yaml "$RAW_BASE/docker-compose.yaml"
|
||||||
|
dograh_download_remote_support_bundle "$(pwd)" "$TARGET_VERSION"
|
||||||
|
rm -f nginx.conf turnserver.conf
|
||||||
|
|
||||||
# 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
|
if [[ -n "$IMAGE_TAG" ]]; then
|
||||||
sed -i.tmp -E "s#(dograh-(api|ui)):latest#\1:$IMAGE_TAG#g" docker-compose.yaml
|
sed -i.tmp -E "s#(dograh-(api|ui)):latest#\1:$IMAGE_TAG#g" docker-compose.yaml
|
||||||
rm -f docker-compose.yaml.tmp
|
rm -f docker-compose.yaml.tmp
|
||||||
echo -e "${GREEN}✓ docker-compose.yaml updated; images pinned to :$IMAGE_TAG${NC}"
|
dograh_success "✓ docker-compose.yaml updated; images pinned to :$IMAGE_TAG"
|
||||||
else
|
else
|
||||||
echo -e "${GREEN}✓ docker-compose.yaml updated (image tags left at :latest)${NC}"
|
dograh_success "✓ docker-compose.yaml updated (image tags left at :latest)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
###############################################################################
|
echo -e "${BLUE}[3/3] Synchronizing environment and validating init-based remote config...${NC}"
|
||||||
### Step 3 — nginx.conf (regenerate from embedded template)
|
dograh_set_env_key .env FASTAPI_WORKERS "$FASTAPI_WORKERS"
|
||||||
###############################################################################
|
dograh_prepare_remote_install "$(pwd)"
|
||||||
|
docker compose config -q
|
||||||
echo -e "${BLUE}[3/5] Regenerating nginx.conf...${NC}"
|
dograh_success "✓ Remote init configuration validated"
|
||||||
{
|
|
||||||
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<FASTAPI_WORKERS; i++)); do
|
|
||||||
port=$((8000 + i))
|
|
||||||
echo " server api:$port max_fails=3 fail_timeout=10s;"
|
|
||||||
done
|
|
||||||
echo " keepalive 32;"
|
|
||||||
echo "}"
|
|
||||||
echo ""
|
|
||||||
cat << 'NGINX_EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name SERVER_IP_PLACEHOLDER;
|
|
||||||
|
|
||||||
# Redirect all HTTP to HTTPS
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name SERVER_IP_PLACEHOLDER;
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/local.crt;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/local.key;
|
|
||||||
|
|
||||||
# Basic TLS settings
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_prefer_server_ciphers on;
|
|
||||||
|
|
||||||
# Backend API and WebSockets - bypass the UI, go straight to the
|
|
||||||
# api workers via the least_conn upstream defined above.
|
|
||||||
location /api/v1/ {
|
|
||||||
proxy_pass http://dograh_api;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# Retry on a dead/restarting worker
|
|
||||||
proxy_next_upstream error timeout http_502 http_503 http_504;
|
|
||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
|
||||||
|
|
||||||
# Long-lived WebSockets (audio streaming, signaling)
|
|
||||||
proxy_read_timeout 3600s;
|
|
||||||
proxy_send_timeout 3600s;
|
|
||||||
|
|
||||||
# Don't buffer streamed responses
|
|
||||||
proxy_buffering off;
|
|
||||||
client_max_body_size 100M;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://ui:3010;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# Important for WebSockets / hot reload etc.
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
|
||||||
|
|
||||||
# Rewrite localhost MinIO URLs in API responses to use current domain
|
|
||||||
sub_filter 'http://localhost:9000/voice-audio/' 'https://$host/voice-audio/';
|
|
||||||
sub_filter_once off;
|
|
||||||
sub_filter_types application/json text/html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /voice-audio/ {
|
|
||||||
proxy_pass http://minio:9000/voice-audio/;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# Headers for file downloads from MinIO
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
|
||||||
|
|
||||||
# Allow large file downloads
|
|
||||||
proxy_buffering off;
|
|
||||||
client_max_body_size 100M;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX_EOF
|
|
||||||
} > 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 ""
|
||||||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
|
@ -434,15 +230,14 @@ echo -e "${GREEN}╚════════════════════
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "Backups: ${BLUE}*.bak.$TIMESTAMP${NC}"
|
echo -e "Backups: ${BLUE}*.bak.$TIMESTAMP${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}To apply, recreate the stack:${NC}"
|
echo -e "${YELLOW}To apply, restart through the validated wrapper:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BLUE}sudo docker compose --profile remote down${NC}"
|
echo -e " ${BLUE}./remote_up.sh${NC}"
|
||||||
echo -e " ${BLUE}sudo docker compose --profile remote up -d --pull always${NC}"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}To roll back, restore the backups and recreate:${NC}"
|
echo -e "${YELLOW}To roll back, restore the backups and re-run the wrapper:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BLUE}for f in docker-compose.yaml nginx.conf turnserver.conf .env; do${NC}"
|
echo -e " ${BLUE}for f in docker-compose.yaml nginx.conf turnserver.conf .env remote_up.sh scripts/run_dograh_init.sh scripts/lib/setup_common.sh deploy/templates/nginx.remote.conf.template deploy/templates/turnserver.remote.conf.template; do${NC}"
|
||||||
echo -e " ${BLUE} [[ -f \"\$f.bak.$TIMESTAMP\" ]] && cp \"\$f.bak.$TIMESTAMP\" \"\$f\"${NC}"
|
echo -e " ${BLUE} [[ -f \"\$f.bak.$TIMESTAMP\" ]] && cp \"\$f.bak.$TIMESTAMP\" \"\$f\"${NC}"
|
||||||
echo -e " ${BLUE}done${NC}"
|
echo -e " ${BLUE}done${NC}"
|
||||||
echo -e " ${BLUE}sudo docker compose --profile remote down && sudo docker compose --profile remote up -d${NC}"
|
echo -e " ${BLUE}./remote_up.sh${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
|
|
@ -62,15 +62,13 @@ const AppLayout: React.FC<AppLayoutProps> = ({
|
||||||
// Hide sidebar for root (/), /handler routes (Stack Auth routes), and /auth routes
|
// Hide sidebar for root (/), /handler routes (Stack Auth routes), and /auth routes
|
||||||
const shouldShowSidebar = pathname !== "/" && !pathname.startsWith("/handler") && !pathname.startsWith("/auth");
|
const shouldShowSidebar = pathname !== "/" && !pathname.startsWith("/handler") && !pathname.startsWith("/auth");
|
||||||
|
|
||||||
// Check if we're in workflow editor mode or superadmin runs - collapse sidebar by default
|
|
||||||
// Only match the exact editor page /workflow/<id>, not sub-routes like /workflow/<id>/runs
|
// Only match the exact editor page /workflow/<id>, not sub-routes like /workflow/<id>/runs
|
||||||
const isWorkflowEditor = /^\/workflow\/\d+$/.test(pathname);
|
const isWorkflowEditor = /^\/workflow\/\d+$/.test(pathname);
|
||||||
const isSuperadmin = pathname.startsWith("/superadmin");
|
|
||||||
|
|
||||||
// Always render SidebarProvider to keep the component tree shape consistent
|
// Always render SidebarProvider to keep the component tree shape consistent
|
||||||
// across route changes (avoids React hooks ordering violations during navigation).
|
// across route changes (avoids React hooks ordering violations during navigation).
|
||||||
return (
|
return (
|
||||||
<SidebarProvider defaultOpen={!isWorkflowEditor && !isSuperadmin}>
|
<SidebarProvider defaultOpen>
|
||||||
{shouldShowSidebar ? (
|
{shouldShowSidebar ? (
|
||||||
<div className="flex min-h-screen w-full">
|
<div className="flex min-h-screen w-full">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
Home,
|
Home,
|
||||||
Key,
|
Key,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
type LucideIcon,
|
||||||
Megaphone,
|
Megaphone,
|
||||||
Phone,
|
Phone,
|
||||||
Settings,
|
Settings,
|
||||||
|
|
@ -49,12 +50,7 @@ import {
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import {
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { useAppConfig } from "@/context/AppConfigContext";
|
import { useAppConfig } from "@/context/AppConfigContext";
|
||||||
import { useTelephonyConfigWarnings } from "@/context/TelephonyConfigWarningsContext";
|
import { useTelephonyConfigWarnings } from "@/context/TelephonyConfigWarningsContext";
|
||||||
import { useLatestReleaseVersion } from "@/hooks/useLatestReleaseVersion";
|
import { useLatestReleaseVersion } from "@/hooks/useLatestReleaseVersion";
|
||||||
|
|
@ -62,6 +58,94 @@ import type { LocalUser } from "@/lib/auth";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type SidebarNavItem = {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
showsTelephonyWarning?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SidebarNavSection = {
|
||||||
|
label?: string;
|
||||||
|
items: SidebarNavItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const TELEPHONY_WARNING_DEADLINE = "15 May 2026";
|
||||||
|
const TELEPHONY_WARNING_COPY = `Action required before ${TELEPHONY_WARNING_DEADLINE}`;
|
||||||
|
|
||||||
|
const NAV_SECTIONS: SidebarNavSection[] = [
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Overview",
|
||||||
|
url: "/overview",
|
||||||
|
icon: Home,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "BUILD",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Voice Agents",
|
||||||
|
url: "/workflow",
|
||||||
|
icon: Workflow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Campaigns",
|
||||||
|
url: "/campaigns",
|
||||||
|
icon: Megaphone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Models",
|
||||||
|
url: "/model-configurations",
|
||||||
|
icon: Brain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Telephony",
|
||||||
|
url: "/telephony-configurations",
|
||||||
|
icon: Phone,
|
||||||
|
showsTelephonyWarning: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tools",
|
||||||
|
url: "/tools",
|
||||||
|
icon: Wrench,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Files",
|
||||||
|
url: "/files",
|
||||||
|
icon: Database,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Recordings",
|
||||||
|
url: "/recordings",
|
||||||
|
icon: AudioLines,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Developers",
|
||||||
|
url: "/api-keys",
|
||||||
|
icon: Key,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "OBSERVE",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Agent Runs",
|
||||||
|
url: "/usage",
|
||||||
|
icon: TrendingUp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Reports",
|
||||||
|
url: "/reports",
|
||||||
|
icon: FileText,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Lazy load SelectedTeamSwitcher - we'll pass selectedTeam from our context
|
// Lazy load SelectedTeamSwitcher - we'll pass selectedTeam from our context
|
||||||
const StackTeamSwitcher = React.lazy(() =>
|
const StackTeamSwitcher = React.lazy(() =>
|
||||||
import("@stackframe/stack").then((mod) => ({
|
import("@stackframe/stack").then((mod) => ({
|
||||||
|
|
@ -77,10 +161,7 @@ export function AppSidebar() {
|
||||||
const { config } = useAppConfig();
|
const { config } = useAppConfig();
|
||||||
const { telnyxMissingWebhookPublicKeyCount } = useTelephonyConfigWarnings();
|
const { telnyxMissingWebhookPublicKeyCount } = useTelephonyConfigWarnings();
|
||||||
const hasTelephonyWarning = telnyxMissingWebhookPublicKeyCount > 0;
|
const hasTelephonyWarning = telnyxMissingWebhookPublicKeyCount > 0;
|
||||||
|
const isCollapsed = !isMobile && state === "collapsed";
|
||||||
// On mobile the sidebar renders as a full-width sheet overlay, so treat it
|
|
||||||
// as always "expanded" regardless of the desktop collapsed/expanded state.
|
|
||||||
const effectiveState = isMobile ? "expanded" : state;
|
|
||||||
|
|
||||||
// Get selected team for Stack auth (cast to Team type from Stack)
|
// Get selected team for Stack auth (cast to Team type from Stack)
|
||||||
// Stabilize the reference so SelectedTeamSwitcher only sees a change when the team ID changes,
|
// Stabilize the reference so SelectedTeamSwitcher only sees a change when the team ID changes,
|
||||||
|
|
@ -101,90 +182,7 @@ export function AppSidebar() {
|
||||||
{ enabled: config?.deploymentMode === "oss" },
|
{ enabled: config?.deploymentMode === "oss" },
|
||||||
);
|
);
|
||||||
|
|
||||||
const isActive = (path: string) => {
|
const isActive = (path: string) => pathname.startsWith(path);
|
||||||
return pathname.startsWith(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Organize navigation into sections
|
|
||||||
const overviewSection = [
|
|
||||||
{
|
|
||||||
title: "Overview",
|
|
||||||
url: "/overview",
|
|
||||||
icon: Home,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const buildSection = [
|
|
||||||
{
|
|
||||||
title: "Voice Agents",
|
|
||||||
url: "/workflow",
|
|
||||||
icon: Workflow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Campaigns",
|
|
||||||
url: "/campaigns",
|
|
||||||
icon: Megaphone,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: "Automation",
|
|
||||||
// url: "/automation",
|
|
||||||
// icon: Zap,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: "Models",
|
|
||||||
url: "/model-configurations",
|
|
||||||
icon: Brain,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Telephony",
|
|
||||||
url: "/telephony-configurations",
|
|
||||||
icon: Phone,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Tools",
|
|
||||||
url: "/tools",
|
|
||||||
icon: Wrench,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Files",
|
|
||||||
url: "/files",
|
|
||||||
icon: Database,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Recordings",
|
|
||||||
url: "/recordings",
|
|
||||||
icon: AudioLines,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: "Integrations",
|
|
||||||
// url: "/integrations",
|
|
||||||
// icon: Plug,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: "Developers",
|
|
||||||
url: "/api-keys",
|
|
||||||
icon: Key,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const observeSection = [
|
|
||||||
{
|
|
||||||
title: "Agent Runs",
|
|
||||||
url: "/usage",
|
|
||||||
icon: TrendingUp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Reports",
|
|
||||||
url: "/reports",
|
|
||||||
icon: FileText,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: "LoopTalk",
|
|
||||||
// url: "/looptalk",
|
|
||||||
// icon: MessageSquare,
|
|
||||||
// },
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleMobileNavClick = () => {
|
const handleMobileNavClick = () => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
|
@ -192,79 +190,65 @@ export function AppSidebar() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const SidebarLink = ({ item }: { item: typeof overviewSection[0] }) => {
|
const SidebarLink = ({ item }: { item: SidebarNavItem }) => {
|
||||||
const isItemActive = isActive(item.url);
|
const isItemActive = isActive(item.url);
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const showWarningDot =
|
const showWarningDot = item.showsTelephonyWarning && hasTelephonyWarning;
|
||||||
item.url === "/telephony-configurations" && hasTelephonyWarning;
|
const tooltip = {
|
||||||
|
children: (
|
||||||
if (effectiveState === "collapsed") {
|
<div className="notranslate" translate="no">
|
||||||
return (
|
<p>{item.title}</p>
|
||||||
<TooltipProvider delayDuration={0}>
|
{showWarningDot && (
|
||||||
<Tooltip>
|
<p className="text-amber-600 dark:text-amber-400">{TELEPHONY_WARNING_COPY}</p>
|
||||||
<TooltipTrigger asChild>
|
)}
|
||||||
<SidebarMenuButton
|
</div>
|
||||||
asChild
|
),
|
||||||
className={cn(
|
};
|
||||||
"hover:bg-accent hover:text-accent-foreground",
|
const warningIndicator = (
|
||||||
isItemActive && "bg-accent text-accent-foreground"
|
<AlertTriangle
|
||||||
)}
|
aria-label={`Action required on a telephony configuration before ${TELEPHONY_WARNING_DEADLINE}`}
|
||||||
>
|
className={cn(
|
||||||
<Link href={item.url} onClick={handleMobileNavClick} className="relative">
|
"text-amber-500",
|
||||||
<Icon className="h-4 w-4" />
|
isCollapsed ? "absolute -right-0.5 -top-0.5 h-3 w-3" : "ml-auto h-3.5 w-3.5"
|
||||||
{showWarningDot && (
|
)}
|
||||||
<AlertTriangle
|
/>
|
||||||
aria-hidden
|
);
|
||||||
className="absolute -right-0.5 -top-0.5 h-3 w-3 text-amber-500"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span className="sr-only">
|
|
||||||
{item.title}
|
|
||||||
{showWarningDot && " — action required before 15 May 2026"}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p>
|
|
||||||
{item.title}
|
|
||||||
{showWarningDot && (
|
|
||||||
<span className="block text-amber-600 dark:text-amber-400">
|
|
||||||
Action required before 15 May 2026
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
|
tooltip={tooltip}
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-accent hover:text-accent-foreground",
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
isItemActive && "bg-accent text-accent-foreground"
|
isItemActive && "bg-accent text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link href={item.url} onClick={handleMobileNavClick}>
|
<Link
|
||||||
<Icon className="h-4 w-4" />
|
href={item.url}
|
||||||
<span>{item.title}</span>
|
onClick={handleMobileNavClick}
|
||||||
|
className={cn("relative", isCollapsed && "justify-center")}
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4 shrink-0" />
|
||||||
|
<span
|
||||||
|
className={cn("notranslate min-w-0 flex-1 truncate", isCollapsed && "sr-only")}
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
{showWarningDot && (
|
{showWarningDot && (
|
||||||
<TooltipProvider delayDuration={0}>
|
isCollapsed ? (
|
||||||
|
warningIndicator
|
||||||
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<AlertTriangle
|
{warningIndicator}
|
||||||
aria-label="Action required on a telephony configuration before 15 May 2026"
|
|
||||||
className="ml-auto h-3.5 w-3.5 text-amber-500"
|
|
||||||
/>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
<p>Action required before 15 May 2026</p>
|
<p>{TELEPHONY_WARNING_COPY}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
)
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
|
|
@ -273,77 +257,70 @@ export function AppSidebar() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" className="border-r">
|
<Sidebar collapsible="icon" className="border-r">
|
||||||
<SidebarHeader className="border-b px-2 py-3">
|
<SidebarHeader className="border-b px-2 py-3 notranslate" translate="no">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Logo - only show when expanded */}
|
<div className={cn("flex items-center gap-2", isCollapsed && "hidden")}>
|
||||||
{effectiveState === "expanded" && (
|
<Link
|
||||||
<div className="flex items-center gap-2">
|
href="/"
|
||||||
<Link
|
className="notranslate flex items-center gap-2 px-2 text-xl font-bold"
|
||||||
href="/"
|
translate="no"
|
||||||
className="flex items-center gap-2 px-2 text-xl font-bold"
|
>
|
||||||
>
|
Dograh
|
||||||
Dograh
|
{versionInfo && (
|
||||||
{versionInfo && (
|
<span
|
||||||
<span className="text-xs font-normal text-muted-foreground">
|
className="notranslate text-xs font-normal text-muted-foreground"
|
||||||
v{versionInfo.ui}
|
translate="no"
|
||||||
|
>
|
||||||
|
v{versionInfo.ui}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
{isBehind && latestRelease && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<a
|
||||||
|
href="https://docs.dograh.com/deployment/update"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 rounded-md border bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-amber-900 transition-opacity hover:opacity-80 dark:bg-amber-950 dark:text-amber-200"
|
||||||
|
>
|
||||||
|
<ArrowUpCircle className="h-3 w-3" />
|
||||||
|
Update
|
||||||
|
</a>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">
|
||||||
|
<p>Latest: {latestRelease} — click to see the update guide</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{isLatest && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="inline-flex items-center rounded-md border bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">
|
||||||
|
Latest
|
||||||
</span>
|
</span>
|
||||||
)}
|
</TooltipTrigger>
|
||||||
</Link>
|
<TooltipContent side="bottom">
|
||||||
{isBehind && latestRelease && (
|
<p>You're running the latest release</p>
|
||||||
<TooltipProvider delayDuration={0}>
|
</TooltipContent>
|
||||||
<Tooltip>
|
</Tooltip>
|
||||||
<TooltipTrigger asChild>
|
)}
|
||||||
<a
|
</div>
|
||||||
href="https://docs.dograh.com/deployment/update"
|
|
||||||
target="_blank"
|
<SidebarTrigger className={cn("hover:bg-accent", isCollapsed && "mx-auto")}>
|
||||||
rel="noopener noreferrer"
|
{isCollapsed ? (
|
||||||
className="inline-flex items-center gap-1 rounded-md border bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-amber-900 transition-opacity hover:opacity-80 dark:bg-amber-950 dark:text-amber-200"
|
|
||||||
>
|
|
||||||
<ArrowUpCircle className="h-3 w-3" />
|
|
||||||
Update
|
|
||||||
</a>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
<p>Latest: {latestRelease} — click to see the update guide</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
{isLatest && (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span className="inline-flex items-center rounded-md border bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">
|
|
||||||
Latest
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
<p>You're running the latest release</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Toggle button - center it when collapsed */}
|
|
||||||
<SidebarTrigger className={cn(
|
|
||||||
"hover:bg-accent",
|
|
||||||
effectiveState === "collapsed" && "mx-auto"
|
|
||||||
)}>
|
|
||||||
{effectiveState === "expanded" ? (
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</SidebarTrigger>
|
</SidebarTrigger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Team Switcher for Stack Auth - at the top */}
|
{provider === "stack" && (
|
||||||
{provider === "stack" && effectiveState === "expanded" && (
|
<div className={cn("mt-3 notranslate", isCollapsed && "hidden")} translate="no">
|
||||||
<div className="mt-3">
|
|
||||||
<React.Suspense
|
<React.Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div className="h-9 w-full animate-pulse bg-muted rounded" />
|
<div className="h-9 w-full animate-pulse rounded bg-muted" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackTeamSwitcher
|
<StackTeamSwitcher
|
||||||
|
|
@ -355,73 +332,46 @@ export function AppSidebar() {
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarContent className={cn(
|
<SidebarContent className={cn("notranslate", isCollapsed && "px-0")} translate="no">
|
||||||
effectiveState === "collapsed" && "px-0"
|
{NAV_SECTIONS.map((section, index) => (
|
||||||
)}>
|
<SidebarGroup
|
||||||
{/* Overview Section */}
|
key={section.label ?? "overview"}
|
||||||
<SidebarGroup className="mt-2">
|
className={index === 0 ? "mt-2" : "mt-6"}
|
||||||
<SidebarMenu>
|
>
|
||||||
{overviewSection.map((item) => (
|
{section.label && (
|
||||||
<SidebarMenuItem key={item.title}>
|
<SidebarGroupLabel
|
||||||
<SidebarLink item={item} />
|
className={cn(
|
||||||
</SidebarMenuItem>
|
"notranslate text-xs font-semibold uppercase tracking-wider text-muted-foreground",
|
||||||
))}
|
isCollapsed && "hidden"
|
||||||
</SidebarMenu>
|
)}
|
||||||
</SidebarGroup>
|
translate="no"
|
||||||
|
>
|
||||||
{/* BUILD Section */}
|
{section.label}
|
||||||
{buildSection.length > 0 && (
|
|
||||||
<SidebarGroup className="mt-6">
|
|
||||||
{effectiveState === "expanded" && (
|
|
||||||
<SidebarGroupLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
||||||
BUILD
|
|
||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
)}
|
)}
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{buildSection.map((item) => (
|
{section.items.map((item) => (
|
||||||
<SidebarMenuItem key={item.title}>
|
<SidebarMenuItem key={item.title}>
|
||||||
<SidebarLink item={item} />
|
<SidebarLink item={item} />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
)}
|
))}
|
||||||
|
|
||||||
{/* OBSERVE Section */}
|
|
||||||
<SidebarGroup className="mt-6">
|
|
||||||
{effectiveState === "expanded" && (
|
|
||||||
<SidebarGroupLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
||||||
OBSERVE
|
|
||||||
</SidebarGroupLabel>
|
|
||||||
)}
|
|
||||||
<SidebarMenu>
|
|
||||||
{observeSection.map((item) => (
|
|
||||||
<SidebarMenuItem key={item.title}>
|
|
||||||
<SidebarLink item={item} />
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter className={cn(
|
<SidebarFooter
|
||||||
"border-t p-4",
|
className={cn("border-t p-4 notranslate", isCollapsed && "p-2")}
|
||||||
effectiveState === "collapsed" && "p-2"
|
translate="no"
|
||||||
)}>
|
>
|
||||||
{/* Bottom Actions */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* User Button - for local/OSS mode */}
|
|
||||||
{provider !== "stack" && (
|
{provider !== "stack" && (
|
||||||
<div className={cn(
|
<div className={cn("flex", isCollapsed ? "justify-center" : "justify-start")}>
|
||||||
"flex",
|
|
||||||
effectiveState === "collapsed" ? "justify-center" : "justify-start"
|
|
||||||
)}>
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="rounded-full h-8 w-8 cursor-pointer">
|
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer rounded-full">
|
||||||
<span className="text-xs font-medium">
|
<span className="text-xs font-medium">
|
||||||
{(user?.displayName || (user as LocalUser | undefined)?.email || "")
|
{(user?.displayName || (user as LocalUser | undefined)?.email || "")
|
||||||
.split(/[\s@]/)
|
.split(/[\s@]/)
|
||||||
|
|
@ -455,15 +405,11 @@ export function AppSidebar() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* User Button - for Stack auth */}
|
|
||||||
{provider === "stack" && (
|
{provider === "stack" && (
|
||||||
<div className={cn(
|
<div className={cn("flex", isCollapsed ? "justify-center" : "justify-start")}>
|
||||||
"flex",
|
|
||||||
effectiveState === "collapsed" ? "justify-center" : "justify-start"
|
|
||||||
)}>
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="rounded-full h-8 w-8 cursor-pointer">
|
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer rounded-full">
|
||||||
<span className="text-xs font-medium">
|
<span className="text-xs font-medium">
|
||||||
{(user?.displayName || (user as { primaryEmail?: string })?.primaryEmail || "")
|
{(user?.displayName || (user as { primaryEmail?: string })?.primaryEmail || "")
|
||||||
.split(/[\s@]/)
|
.split(/[\s@]/)
|
||||||
|
|
@ -508,35 +454,30 @@ export function AppSidebar() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Theme Toggle - at the very bottom */}
|
<div className={cn("mt-2 border-t pt-2", isCollapsed && "flex justify-center")}>
|
||||||
<div className={cn(
|
{isCollapsed ? (
|
||||||
"mt-2 pt-2 border-t",
|
<Tooltip>
|
||||||
effectiveState === "collapsed" ? "flex justify-center" : ""
|
<TooltipTrigger asChild>
|
||||||
)}>
|
<div className="notranslate" translate="no">
|
||||||
{effectiveState === "collapsed" ? (
|
<ThemeToggle
|
||||||
<TooltipProvider delayDuration={0}>
|
showLabel={false}
|
||||||
<Tooltip>
|
className="hover:bg-accent hover:text-accent-foreground"
|
||||||
<TooltipTrigger asChild>
|
/>
|
||||||
<div>
|
</div>
|
||||||
<ThemeToggle
|
</TooltipTrigger>
|
||||||
showLabel={false}
|
<TooltipContent side="right">
|
||||||
className="hover:bg-accent hover:text-accent-foreground"
|
<p>Toggle theme</p>
|
||||||
/>
|
</TooltipContent>
|
||||||
</div>
|
</Tooltip>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p>Toggle theme</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
) : (
|
) : (
|
||||||
<ThemeToggle
|
<div className="notranslate" translate="no">
|
||||||
showLabel={true}
|
<ThemeToggle
|
||||||
className="hover:bg-accent hover:text-accent-foreground"
|
showLabel={true}
|
||||||
/>
|
className="hover:bg-accent hover:text-accent-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue