feat(scripts): free trusted HTTPS via sslip.io for public-IP remote i… (#460)

* feat(scripts): free trusted HTTPS via sslip.io for public-IP remote installs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: refactor setup scripts

* chore: generate sdk

* chore: fix messaging for setup_remote script

* fix: fix ffmpeg download url

* feat: centralise and simplify the url configuration

* fix: force script run as sudo

* fix: fix documentation

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Abhishek 2026-06-27 17:19:29 +05:30 committed by GitHub
parent 3309face2c
commit 78427817a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 838 additions and 392 deletions

View file

@ -29,12 +29,15 @@ This directory now has a shared deployment model for OSS Docker installs. If you
- `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`.
- Canonical remote keys in `.env`: `ENVIRONMENT`, `SERVER_IP`, `PUBLIC_HOST`, `PUBLIC_BASE_URL`, `TURN_SECRET`, `FASTAPI_WORKERS`, `OSS_JWT_SECRET`. `PUBLIC_BASE_URL` (+ `PUBLIC_HOST`, and `SERVER_IP` for coturn's literal `external-ip`) is the single endpoint source of truth.
- `BACKEND_API_ENDPOINT`, `MINIO_PUBLIC_ENDPOINT`, `TURN_HOST` are **derived in-app** from `PUBLIC_BASE_URL` / `PUBLIC_HOST` (`api/constants.py`) and are no longer written to a remote `.env`. `dograh_sync_remote_env_file` neither writes nor deletes them — new installs omit them, and a value an operator sets by hand is left untouched as an explicit override for a split deployment (separate object store / TURN host). `dograh_validate_remote_runtime_env` therefore no longer requires them or asserts they equal `PUBLIC_BASE_URL`.
- `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`.
- `cloudflared` is gated behind a `tunnel` profile (no longer always-on; `api` no longer `depends_on` it). `remote_up.sh` adds `--profile tunnel` when `SERVER_IP` is a private/reserved address (`dograh_is_local_ipv4`) — i.e. the host has no public IP; public-IP installs run `--profile remote` only and start no tunnel. Local installs opt in with `--profile tunnel`. The backend mirrors this exactly: `api/utils/common.py:is_local_or_private_url` decides when `get_backend_endpoints()` resolves the tunnel URL at runtime, so deploy-side and runtime stay in sync (keep the two IP classifiers aligned, incl. CGNAT 100.64.0.0/10).
- `cloudflared` picks a mode by token: with `CLOUDFLARE_TUNNEL_TOKEN` it runs a named tunnel (stable hostname — set `BACKEND_API_ENDPOINT` to it and point its Cloudflare-dashboard ingress at `http://api:8000`); without a token it runs a quick tunnel (ephemeral `*.trycloudflare.com`, discovered via the `:2000` metrics endpoint by `api/utils/tunnel.py`).
- `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`.
- `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`. It hard-requires root (a guard near the top exits non-root with a "re-run with sudo" message) because it provisions Docker, binds :80/:443, and installs a Let's Encrypt cert + system renewal hook. Cloud-init/user-data callers (e.g. `infrastructure/`) already run as root, so they pass; interactive callers must use `sudo`. Docs that invoke it (`docs/deployment/docker.mdx`, `docs/deployment/scaling.mdx`) and the hint printed by `setup_custom_domain.sh` all use `sudo`.
- `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,ps1}` 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.

View file

@ -252,9 +252,11 @@ dograh_sync_remote_env_file() {
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"
# BACKEND_API_ENDPOINT / MINIO_PUBLIC_ENDPOINT / TURN_HOST are derived in-app
# from PUBLIC_BASE_URL / PUBLIC_HOST (see api/constants.py), so sync neither
# writes nor removes them: new installs simply omit them, and any value an
# operator set by hand is left untouched as an explicit override.
}
dograh_validate_remote_runtime_env() {
@ -262,14 +264,12 @@ dograh_validate_remote_runtime_env() {
[[ -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"
# BACKEND_API_ENDPOINT / MINIO_PUBLIC_ENDPOINT / TURN_HOST are derived in-app
# from PUBLIC_BASE_URL / PUBLIC_HOST (see api/constants.py), so they are not
# required here. When an operator sets them explicitly (split deployment),
# their value is honored as-is — no equality check.
}
dograh_uses_init_compose_layout() {
@ -401,6 +401,59 @@ dograh_preflight_remote_init_render() {
rm -rf "$tmp_root"
}
# Reconcile the running Postgres role password with POSTGRES_PASSWORD in .env.
#
# POSTGRES_PASSWORD only takes effect when the postgres data volume is first
# initialized. If the volume was created before .env had a generated password
# (e.g. an early start used the compose fallback `:-postgres`), or the password
# was later rotated, the role keeps its old password while the API connects with
# the .env value over TCP (pg_hba `scram-sha-256`) and dies with "password
# authentication failed for user postgres". start_docker.sh handles this for the
# OSS quickstart; the remote path (remote_up.sh) needs the same reconciliation.
#
# Bring postgres up on its own, then ALTER the role over the trusted local
# socket (pg_hba trusts `local`, so this works even when the password is
# currently mismatched). Idempotent: on a fresh volume it just re-sets the same
# value. Survives the later `--force-recreate` because the password lives in the
# data volume, not the container.
dograh_sync_postgres_password() {
local project_dir=$1
shift
local compose=("$@")
local env_file="$project_dir/.env"
local password=""
local ready=""
local i
[[ ${#compose[@]} -gt 0 ]] || compose=(docker compose)
if [[ -f "$env_file" ]]; then
password="$(awk -F= '/^POSTGRES_PASSWORD=/{sub(/^POSTGRES_PASSWORD=/, ""); print; exit}' "$env_file")"
fi
# No explicit password: the compose fallback (`:-postgres`) governs both the
# DB init and the API's DATABASE_URL, so the two already agree — nothing to do.
[[ -n "$password" ]] || return 0
dograh_info "Syncing Postgres password from .env..."
( cd "$project_dir" && "${compose[@]}" up -d postgres ) >/dev/null
for ((i = 0; i < 30; i++)); do
if ( cd "$project_dir" && "${compose[@]}" exec -T postgres pg_isready -U postgres ) >/dev/null 2>&1; then
ready=1
break
fi
sleep 1
done
[[ -n "$ready" ]] || dograh_fail "Postgres did not become ready while syncing POSTGRES_PASSWORD."
printf '%s\n' "ALTER USER postgres WITH PASSWORD :'pw';" \
| ( cd "$project_dir" && "${compose[@]}" exec -T postgres \
psql -U postgres -d postgres -v ON_ERROR_STOP=1 -v "pw=$password" ) >/dev/null \
|| dograh_fail "Failed to sync Postgres password from .env."
dograh_success "✓ Postgres password synced with .env"
}
dograh_prepare_remote_install() {
local project_dir=${1:-$(dograh_project_dir)}
local env_file="$project_dir/.env"
@ -410,6 +463,101 @@ dograh_prepare_remote_install() {
dograh_preflight_remote_init_render "$project_dir"
}
# ---------------------------------------------------------------------------
# TLS certificate helpers (self-signed bootstrap + Let's Encrypt via webroot)
# ---------------------------------------------------------------------------
# Map an IPv4 address to a public sslip.io / nip.io hostname, e.g.
# 203.0.113.10 -> 203-0-113-10.sslip.io. The hostname resolves back to the
# embedded IP from any public resolver, so Let's Encrypt can validate it over
# the HTTP-01 challenge without the operator owning a domain. Public IPs only:
# Let's Encrypt refuses to validate private/reserved addresses.
dograh_sslip_host_from_ip() {
local ip=$1
local suffix=${2:-sslip.io}
dograh_is_ipv4 "$ip" || dograh_fail "dograh_sslip_host_from_ip: '$ip' is not an IPv4 address"
printf '%s.%s\n' "${ip//./-}" "$suffix"
}
# Install certbot via the host package manager if it is not already present.
# Returns non-zero (instead of exiting) when no supported package manager is
# found or the install fails, so callers can fall back to a self-signed cert.
dograh_install_certbot() {
if command -v certbot >/dev/null 2>&1; then
return 0
fi
dograh_info "Installing Certbot..."
if command -v apt-get >/dev/null 2>&1; then
apt-get update -qq && apt-get install -y -qq certbot
elif command -v dnf >/dev/null 2>&1; then
dnf install -y -q certbot
elif command -v yum >/dev/null 2>&1; then
yum install -y -q certbot
else
dograh_warn "Could not detect a package manager (apt/dnf/yum) to install certbot."
return 1
fi
}
# Obtain (or renew) a Let's Encrypt certificate for $host using the webroot
# challenge served by the running nginx container out of <project>/certs, then
# copy the issued cert to certs/local.{crt,key} (the files nginx reads). This
# needs nginx already running and serving /.well-known/acme-challenge/ on :80.
# Returns non-zero on failure so callers can keep the self-signed cert.
dograh_issue_letsencrypt_webroot() {
local project_dir=$1
local host=$2
local email=${3:-}
local webroot="$project_dir/certs"
local live_dir="/etc/letsencrypt/live/$host"
local -a email_args
if [[ -n "$email" ]]; then
email_args=(--email "$email")
else
email_args=(--register-unsafely-without-email)
fi
mkdir -p "$webroot/.well-known/acme-challenge"
certbot certonly --webroot -w "$webroot" \
--non-interactive --agree-tos --keep-until-expiring \
"${email_args[@]}" \
-d "$host" || return 1
[[ -f "$live_dir/fullchain.pem" && -f "$live_dir/privkey.pem" ]] || return 1
cp "$live_dir/fullchain.pem" "$webroot/local.crt"
cp "$live_dir/privkey.pem" "$webroot/local.key"
chmod 644 "$webroot/local.crt" "$webroot/local.key"
}
# Install a certbot deploy hook so renewed certificates are copied into
# <project>/certs and nginx is restarted to load them. Renewal itself is driven
# by certbot's packaged systemd timer / cron; webroot renewals need no downtime
# because the running nginx serves the challenge.
dograh_install_cert_renewal_hook() {
local project_dir=$1
local host=$2
local hook_dir="/etc/letsencrypt/renewal-hooks/deploy"
local hook_path="$hook_dir/dograh-reload.sh"
mkdir -p "$hook_dir"
cat > "$hook_path" << HOOK_EOF
#!/bin/bash
cp /etc/letsencrypt/live/$host/fullchain.pem $project_dir/certs/local.crt
cp /etc/letsencrypt/live/$host/privkey.pem $project_dir/certs/local.key
chmod 644 $project_dir/certs/local.crt $project_dir/certs/local.key
cd $project_dir
docker compose --profile remote restart nginx 2>/dev/null || true
HOOK_EOF
chmod +x "$hook_path"
}
dograh_download_bundle_file_for_ref() {
local destination=$1
local remote_path=$2

View file

@ -43,7 +43,7 @@ if [[ ! -d "dograh" ]]; then
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}If you haven't set up Dograh yet, run the remote setup first:${NC}"
echo -e "${BLUE} curl -o setup_remote.sh https://raw.githubusercontent.com/dograh-hq/dograh/main/scripts/setup_remote.sh && chmod +x setup_remote.sh && ./setup_remote.sh${NC}"
echo -e "${BLUE} curl -o setup_remote.sh https://raw.githubusercontent.com/dograh-hq/dograh/main/scripts/setup_remote.sh && chmod +x setup_remote.sh && sudo ./setup_remote.sh${NC}"
exit 1
fi
@ -65,7 +65,7 @@ echo -e " Domain: ${BLUE}$DOMAIN_NAME${NC}"
echo -e " Email: ${BLUE}$EMAIL_ADDRESS${NC}"
echo ""
echo -e "${BLUE}[1/7] Verifying DNS configuration...${NC}"
echo -e "${BLUE}[1/6] Verifying DNS configuration...${NC}"
SERVER_IP="$(curl -s ifconfig.me || curl -s icanhazip.com || echo "")"
RESOLVED_IP="$(dig +short "$DOMAIN_NAME" | tail -1)"
@ -84,22 +84,14 @@ else
echo -e "${GREEN}✓ DNS is correctly configured (${RESOLVED_IP})${NC}"
fi
echo -e "${BLUE}[2/7] Installing Certbot...${NC}"
if command -v apt-get &> /dev/null; then
apt-get update -qq
apt-get install -y -qq certbot
elif command -v yum &> /dev/null; then
yum install -y -q certbot
elif command -v dnf &> /dev/null; then
dnf install -y -q certbot
else
dograh_fail "Could not detect package manager. Please install certbot manually."
fi
echo -e "${BLUE}[2/6] Installing Certbot...${NC}"
dograh_install_certbot || dograh_fail "Could not install certbot. Please install it manually and re-run."
echo -e "${GREEN}✓ Certbot installed${NC}"
echo -e "${BLUE}[3/7] Stopping Dograh services...${NC}"
echo -e "${BLUE}[3/6] Pointing .env at $DOMAIN_NAME and starting services...${NC}"
cd dograh
DOGRAH_DEPLOY_PROJECT_DIR="$(pwd)"
DOGRAH_PATH="$(pwd)"
if [[ ! -f remote_up.sh || ! -f scripts/lib/setup_common.sh ]]; then
dograh_download_remote_support_bundle "$(pwd)" "main"
@ -107,113 +99,74 @@ fi
dograh_require_init_compose_layout "$(pwd)"
if docker compose --profile remote ps --quiet 2>/dev/null | grep -q .; then
docker compose --profile remote down
echo -e "${GREEN}✓ Dograh services stopped${NC}"
else
echo -e "${YELLOW}⚠ No running services found${NC}"
fi
echo -e "${BLUE}[4/7] Generating Let's Encrypt SSL certificate...${NC}"
CERTBOT_OUTPUT=$(certbot certonly --standalone \
--non-interactive \
--agree-tos \
--email "$EMAIL_ADDRESS" \
-d "$DOMAIN_NAME" 2>&1) || {
echo -e "${RED}✗ Certificate generation failed${NC}"
echo ""
if echo "$CERTBOT_OUTPUT" | grep -qi "timeout\|firewall\|connection"; then
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${YELLOW} Port 80 appears to be blocked by a firewall.${NC}"
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e "Let's Encrypt needs to connect to port 80 to verify domain ownership."
echo ""
elif echo "$CERTBOT_OUTPUT" | grep -qi "too many\|rate.limit"; then
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${YELLOW} Let's Encrypt rate limit reached.${NC}"
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo "You've requested too many certificates recently."
echo "Please wait before trying again (usually 1 hour)."
echo ""
elif echo "$CERTBOT_OUTPUT" | grep -qi "dns\|resolve\|NXDOMAIN"; then
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${YELLOW} DNS resolution failed.${NC}"
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo "The domain '$DOMAIN_NAME' does not resolve to this server."
echo "Please verify your DNS A record is correctly configured."
echo ""
else
echo -e "${YELLOW}Certbot output:${NC}"
echo "$CERTBOT_OUTPUT"
echo ""
fi
echo -e "After fixing the issue, re-run this script:"
echo -e " ${BLUE}sudo ./setup_custom_domain.sh${NC}"
echo ""
exit 1
}
echo -e "${GREEN}✓ SSL certificate generated${NC}"
CERT_PATH="/etc/letsencrypt/live/$DOMAIN_NAME"
echo ""
echo -e "${BLUE}Certificate location:${NC}"
echo -e " ${CERT_PATH}/"
[[ -f "$CERT_PATH/fullchain.pem" ]] && echo -e " ${GREEN}${NC} fullchain.pem exists" || echo -e " ${RED}${NC} fullchain.pem NOT FOUND"
[[ -f "$CERT_PATH/privkey.pem" ]] && echo -e " ${GREEN}${NC} privkey.pem exists" || echo -e " ${RED}${NC} privkey.pem NOT FOUND"
echo ""
mkdir -p certs
cp "$CERT_PATH/fullchain.pem" certs/local.crt
cp "$CERT_PATH/privkey.pem" certs/local.key
chmod 644 certs/local.crt certs/local.key
echo -e "${GREEN}${NC} Certificates copied to certs/ directory"
echo ""
echo -e "${BLUE}[5/7] Updating canonical remote settings and validating init-based config...${NC}"
dograh_load_env_file .env
if [[ -z "${SERVER_IP:-}" ]]; then
SERVER_IP="$(dograh_infer_server_ip "$(pwd)" || true)"
fi
[[ -n "${SERVER_IP:-}" ]] || dograh_fail "Could not determine SERVER_IP from the existing install"
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
# Switching domains is an explicit repoint of the whole deployment. Drop any
# legacy per-subsystem endpoint keys an older install pinned to the previous host
# so they re-derive from the new PUBLIC_BASE_URL / PUBLIC_HOST (see api/constants.py).
# No-op on current installs, which don't write these keys.
dograh_delete_env_key .env BACKEND_API_ENDPOINT
dograh_delete_env_key .env MINIO_PUBLIC_ENDPOINT
dograh_delete_env_key .env TURN_HOST
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)"
# Bring the stack up (recreating it) so dograh-init re-renders nginx with the
# domain server_name and the ACME challenge location, served with the existing
# certificate. certbot --webroot then validates against the running nginx:
# no downtime, and (unlike --standalone) renewal keeps working later while
# nginx holds port 80.
./remote_up.sh
cat > /etc/letsencrypt/renewal-hooks/deploy/dograh-reload.sh << HOOK_EOF
#!/bin/bash
cp /etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem $DOGRAH_PATH/certs/local.crt
cp /etc/letsencrypt/live/$DOMAIN_NAME/privkey.pem $DOGRAH_PATH/certs/local.key
chmod 644 $DOGRAH_PATH/certs/local.crt $DOGRAH_PATH/certs/local.key
echo -e "${BLUE}Waiting for nginx to answer on port 80...${NC}"
nginx_ready=0
for ((i=1; i<=60; i++)); do
if curl -s -o /dev/null --max-time 3 "http://127.0.0.1/"; then
nginx_ready=1
break
fi
sleep 2
done
[[ "$nginx_ready" == "1" ]] || dograh_fail "nginx did not come up on port 80; cannot run the ACME challenge."
echo -e "${GREEN}✓ Services running and serving the ACME challenge${NC}"
cd $DOGRAH_PATH
docker compose --profile remote restart nginx 2>/dev/null || true
HOOK_EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/dograh-reload.sh
echo -e "${BLUE}[4/6] Obtaining Let's Encrypt certificate for $DOMAIN_NAME...${NC}"
if ! dograh_issue_letsencrypt_webroot "$(pwd)" "$DOMAIN_NAME" "$EMAIL_ADDRESS"; then
echo -e "${RED}✗ Certificate issuance failed${NC}"
echo ""
echo -e "${YELLOW}Common causes:${NC}"
echo " - Port 80 not reachable from the internet (open it in your firewall)"
echo " - DNS A record for $DOMAIN_NAME does not point to this server yet"
echo " - Let's Encrypt rate limit reached (wait, then retry)"
echo " - Upgrading an older install: run ./update_remote.sh first to refresh the"
echo " nginx template so it serves the ACME challenge, then re-run this script"
echo ""
echo -e "The stack is still running with the previous certificate."
echo -e "After fixing the issue, re-run: ${BLUE}sudo ./setup_custom_domain.sh${NC}"
echo ""
exit 1
fi
echo -e "${GREEN}✓ Certificate issued and copied to certs/${NC}"
echo -e "${BLUE}[5/6] Loading the new certificate (restarting nginx)...${NC}"
docker compose --profile remote restart nginx >/dev/null 2>&1 || true
echo -e "${GREEN}✓ nginx restarted${NC}"
echo -e "${BLUE}[6/6] Configuring automatic certificate renewal...${NC}"
dograh_install_cert_renewal_hook "$(pwd)" "$DOMAIN_NAME"
if certbot renew --dry-run --quiet; then
echo -e "${GREEN}✓ Auto-renewal configured and tested${NC}"
else
echo -e "${YELLOW}⚠ Auto-renewal test had issues, but certificates are installed${NC}"
echo -e "${YELLOW}⚠ Auto-renewal dry-run had issues, but the certificate is installed${NC}"
fi
echo ""
echo -e "${BLUE}[7/7] Starting Dograh services through validated startup wrapper...${NC}"
./remote_up.sh
echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Custom Domain Setup Complete! ║${NC}"

View file

@ -307,13 +307,16 @@ Write-Host ''
if ($UseCoturn) {
Write-Warn 'To start Dograh with TURN, run:'
Write-Host ''
Write-Host ' docker compose --profile local-turn up --pull always' -ForegroundColor Blue
Write-Host ' docker compose --profile local-turn --profile tunnel up --pull always' -ForegroundColor Blue
} else {
Write-Warn 'To start Dograh, run:'
Write-Host ''
Write-Host ' docker compose up --pull always' -ForegroundColor Blue
Write-Host ' docker compose --profile tunnel up --pull always' -ForegroundColor Blue
}
Write-Host ''
Write-Host 'This starts a Cloudflare quick tunnel so inbound telephony webhooks can' -ForegroundColor Yellow
Write-Host 'reach your local API over a temporary public URL.' -ForegroundColor Yellow
Write-Host ''
Write-Warn 'Your application will be available at:'
Write-Host ''
Write-Host ' http://localhost:3010' -ForegroundColor Blue

View file

@ -211,13 +211,16 @@ echo ""
if [[ "${ENABLE_COTURN:-false}" == "true" ]]; then
echo -e "${YELLOW}To start Dograh with TURN, run:${NC}"
echo ""
echo -e " ${BLUE}docker compose --profile local-turn up --pull always${NC}"
echo -e " ${BLUE}docker compose --profile local-turn --profile tunnel up --pull always${NC}"
else
echo -e "${YELLOW}To start Dograh, run:${NC}"
echo ""
echo -e " ${BLUE}docker compose up --pull always${NC}"
echo -e " ${BLUE}docker compose --profile tunnel up --pull always${NC}"
fi
echo ""
echo -e "${YELLOW}This starts a Cloudflare quick tunnel so inbound telephony webhooks can${NC}"
echo -e "${YELLOW}reach your local API over a temporary public URL.${NC}"
echo ""
echo -e "${YELLOW}Your application will be available at:${NC}"
echo ""
echo -e " ${BLUE}http://localhost:3010${NC}"

View file

@ -35,9 +35,17 @@ echo "║ Automated HTTPS deployment with TURN server ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
# Get the public IP address (skip prompt if SERVER_IP is already set)
# This setup must run as root: it provisions Docker, binds privileged ports
# 80/443, and (for public IPs) installs a Let's Encrypt certificate plus a
# system renewal hook under /etc/letsencrypt — all of which require root. Stop
# early with clear guidance rather than getting halfway and degrading the install.
if [[ $EUID -ne 0 ]]; then
dograh_fail "setup_remote.sh must be run as root.\nRe-run with sudo:\n sudo ./setup_remote.sh"
fi
# Get the server IP address (skip prompt if SERVER_IP is already set)
if [[ -z "${SERVER_IP:-}" ]]; then
echo -e "${YELLOW}Enter your server's public IP address:${NC}"
echo -e "${YELLOW}Enter your server's IP address:${NC}"
read -p "> " SERVER_IP
fi
@ -49,6 +57,61 @@ if ! dograh_is_ipv4 "$SERVER_IP"; then
dograh_fail "Invalid IP address format"
fi
# Certificate strategy. CERT_MODE selects how HTTPS is secured:
# auto - public IP + root + docker -> sslip (trusted); otherwise self-signed
# sslip - free trusted Let's Encrypt cert via <ip>.sslip.io (public IP only)
# self-signed - generate a self-signed cert (browser shows a warning)
# Reserved for future private-network paths (not implemented yet):
# letsencrypt-dns, cloudflare-tunnel, external
CERT_MODE="${CERT_MODE:-auto}"
ACME_DOMAIN_SUFFIX="${ACME_DOMAIN_SUFFIX:-sslip.io}"
LETSENCRYPT_EMAIL="${LETSENCRYPT_EMAIL:-}"
if [[ "$CERT_MODE" == "auto" ]]; then
if dograh_is_local_ipv4 "$SERVER_IP"; then
CERT_MODE="self-signed"
dograh_warn "$SERVER_IP is a private IP — using a self-signed certificate."
dograh_warn "For a trusted cert, deploy on a public IP or a domain you own"
dograh_warn "(https://docs.dograh.com/deployment/custom-domain)."
elif ! command -v docker >/dev/null 2>&1; then
CERT_MODE="self-signed"
dograh_warn "Docker not found — skipping automatic Let's Encrypt setup and using a self-signed cert."
else
CERT_MODE="sslip"
fi
fi
case "$CERT_MODE" in
self-signed) ;;
sslip)
if dograh_is_local_ipv4 "$SERVER_IP"; then
dograh_fail "CERT_MODE=sslip needs a public IP; $SERVER_IP is private/reserved."
fi
command -v docker >/dev/null 2>&1 || dograh_fail "CERT_MODE=sslip needs Docker to serve the ACME challenge."
;;
letsencrypt-dns|cloudflare-tunnel|external)
dograh_fail "CERT_MODE=$CERT_MODE is reserved but not implemented yet. Use 'sslip' (public IP) or 'self-signed'."
;;
*)
dograh_fail "Unknown CERT_MODE '$CERT_MODE' (expected: auto, sslip, self-signed)."
;;
esac
if [[ "$CERT_MODE" == "sslip" ]]; then
PUBLIC_HOST_VALUE="$(dograh_sslip_host_from_ip "$SERVER_IP" "$ACME_DOMAIN_SUFFIX")"
CERT_DESC="Let's Encrypt via $ACME_DOMAIN_SUFFIX (trusted)"
else
PUBLIC_HOST_VALUE="$SERVER_IP"
CERT_DESC="self-signed (browser warning)"
fi
CERT_RESULT="$CERT_MODE"
if [[ "$CERT_MODE" == "sslip" && -z "$LETSENCRYPT_EMAIL" && -t 0 ]]; then
echo ""
echo -e "${YELLOW}Email for Let's Encrypt expiry notices (optional, press Enter to skip):${NC}"
read -p "> " LETSENCRYPT_EMAIL
fi
FORCE_TURN_RELAY="${FORCE_TURN_RELAY:-false}"
# Get the TURN secret (skip prompt if TURN_SECRET is already set)
@ -185,6 +248,8 @@ fi
echo ""
echo -e "${GREEN}Configuration:${NC}"
echo -e " Server IP: ${BLUE}$SERVER_IP${NC}"
echo -e " Public host: ${BLUE}$PUBLIC_HOST_VALUE${NC}"
echo -e " Certificate: ${BLUE}$CERT_DESC${NC}"
echo -e " TURN Secret: ${BLUE}********${NC}"
echo -e " Deploy mode: ${BLUE}$DEPLOY_MODE${NC}"
echo -e " Force TURN relay: ${BLUE}$FORCE_TURN_RELAY${NC}"
@ -240,7 +305,7 @@ openssl req -x509 -nodes -newkey rsa:2048 \\
-keyout certs/local.key \\
-out certs/local.crt \\
-days 365 \\
-subj "/CN=$SERVER_IP"
-subj "/CN=$PUBLIC_HOST_VALUE"
CERT_EOF
chmod +x generate_certificate.sh
echo -e "${GREEN}✓ generate_certificate.sh created${NC}"
@ -260,19 +325,16 @@ cat > .env << ENV_EOF
# Remote deployments run with production signaling and HTTPS defaults
ENVIRONMENT=production
# Canonical public host/base URL for this install.
# Canonical public host/base URL for this install. SERVER_IP stays the raw IP
# (coturn external-ip and validation need it); PUBLIC_HOST is the sslip.io
# hostname when using a trusted cert, otherwise the IP. BACKEND_API_ENDPOINT,
# MINIO_PUBLIC_ENDPOINT and TURN_HOST are derived from these by the API
# (see api/constants.py) — set them here only to override for a split deployment.
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=https://$SERVER_IP
# Public URL browsers use to fetch objects from MinIO (proxied by nginx)
MINIO_PUBLIC_ENDPOINT=https://$SERVER_IP
PUBLIC_HOST=$PUBLIC_HOST_VALUE
PUBLIC_BASE_URL=https://$PUBLIC_HOST_VALUE
# TURN Server Configuration (time-limited credentials via TURN REST API)
TURN_HOST=$SERVER_IP
TURN_SECRET=$TURN_SECRET
# Relay-only ICE candidates for explicit TURN diagnostics
FORCE_TURN_RELAY=$FORCE_TURN_RELAY
@ -332,6 +394,46 @@ OVERRIDE_EOF
echo -e "${GREEN}✓ docker-compose.override.yaml created${NC}"
fi
if [[ "$CERT_MODE" == "sslip" ]]; then
echo ""
echo -e "${BLUE}Starting Dograh and requesting a trusted certificate for ${PUBLIC_HOST_VALUE}...${NC}"
if [[ "$DEPLOY_MODE" == "build" ]]; then
./remote_up.sh --build
else
./remote_up.sh
fi
echo -e "${BLUE}Waiting for nginx to answer on port 80...${NC}"
nginx_ready=0
for ((i=1; i<=60; i++)); do
if curl -s -o /dev/null --max-time 3 "http://127.0.0.1/"; then
nginx_ready=1
break
fi
sleep 2
done
if [[ "$nginx_ready" != "1" ]]; then
CERT_RESULT="self-signed"
dograh_warn "nginx did not become reachable on port 80 — skipping Let's Encrypt for now."
dograh_warn "The stack is running with the bootstrap self-signed certificate."
elif dograh_install_certbot && dograh_issue_letsencrypt_webroot "$(pwd)" "$PUBLIC_HOST_VALUE" "$LETSENCRYPT_EMAIL"; then
docker compose --profile remote restart nginx >/dev/null 2>&1 || true
dograh_install_cert_renewal_hook "$(pwd)" "$PUBLIC_HOST_VALUE"
CERT_RESULT="sslip"
dograh_success "✓ Trusted Let's Encrypt certificate installed; auto-renewal configured"
else
CERT_RESULT="self-signed"
echo ""
dograh_warn "Let's Encrypt issuance failed — the stack is running with the self-signed certificate."
dograh_warn "Common causes and fixes:"
dograh_warn " - Port 80 not reachable from the internet: open it in your firewall/security group"
dograh_warn " - Rate limited on ${ACME_DOMAIN_SUFFIX}: re-run with ACME_DOMAIN_SUFFIX=nip.io"
dograh_warn " - Then retry: sudo certbot certonly --webroot -w \"$(pwd)/certs\" -d ${PUBLIC_HOST_VALUE}"
fi
fi
echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Setup Complete! ║${NC}"
@ -350,25 +452,42 @@ echo " - certs/local.crt"
echo " - certs/local.key"
echo " - .env"
echo ""
echo -e "${YELLOW}To start Dograh, run:${NC}"
echo ""
if [[ "$DEPLOY_MODE" != "build" || "${REPO_SOURCE:-}" != "existing" ]]; then
echo -e " ${BLUE}cd $(pwd)${NC}"
fi
if [[ "$DEPLOY_MODE" == "build" ]]; then
echo -e " ${BLUE}./remote_up.sh --build${NC}"
echo ""
echo -e "${YELLOW}A docker-compose.override.yaml has been created alongside${NC}"
echo -e "${YELLOW}docker-compose.yaml. Compose auto-loads it, so no -f flag is${NC}"
echo -e "${YELLOW}needed — it swaps the prebuilt images for local builds.${NC}"
if [[ "$CERT_MODE" == "sslip" ]]; then
if [[ "$CERT_RESULT" == "sslip" ]]; then
echo -e "${GREEN}Dograh is running with a trusted certificate at:${NC}"
echo ""
echo -e " ${BLUE}https://$PUBLIC_HOST_VALUE${NC}"
echo ""
echo -e "${GREEN}No browser warning — the certificate renews automatically before expiry.${NC}"
else
echo -e "${YELLOW}Dograh is running (with a temporary self-signed certificate) at:${NC}"
echo ""
echo -e " ${BLUE}https://$PUBLIC_HOST_VALUE${NC}"
echo ""
echo -e "${YELLOW}Let's Encrypt issuance did not complete (see the message above). Your${NC}"
echo -e "${YELLOW}browser will warn until a trusted certificate is issued.${NC}"
fi
else
echo -e " ${BLUE}./remote_up.sh${NC}"
echo -e "${YELLOW}To start Dograh, run:${NC}"
echo ""
if [[ "$DEPLOY_MODE" != "build" || "${REPO_SOURCE:-}" != "existing" ]]; then
echo -e " ${BLUE}cd $(pwd)${NC}"
fi
if [[ "$DEPLOY_MODE" == "build" ]]; then
echo -e " ${BLUE}./remote_up.sh --build${NC}"
echo ""
echo -e "${YELLOW}A docker-compose.override.yaml has been created alongside${NC}"
echo -e "${YELLOW}docker-compose.yaml. Compose auto-loads it, so no -f flag is${NC}"
echo -e "${YELLOW}needed — it swaps the prebuilt images for local builds.${NC}"
else
echo -e " ${BLUE}./remote_up.sh${NC}"
fi
echo ""
echo -e "${YELLOW}Your application will be available at:${NC}"
echo ""
echo -e " ${BLUE}https://$PUBLIC_HOST_VALUE${NC}"
echo ""
echo -e "${YELLOW}Note:${NC} Your browser will show a security warning for the self-signed"
echo "certificate. You can safely accept it to proceed."
fi
echo ""
echo -e "${YELLOW}Your application will be available at:${NC}"
echo ""
echo -e " ${BLUE}https://$SERVER_IP${NC}"
echo ""
echo -e "${YELLOW}Note:${NC} Your browser will show a security warning for the self-signed"
echo "certificate. You can safely accept it to proceed."
echo ""

View file

@ -207,10 +207,9 @@ if ([string]::IsNullOrEmpty($existingMinioRootPassword)) {
Write-Host ''
Write-Host "Docker registry: $Registry"
Write-Host "Telemetry enabled: $EnableTelemetry"
Write-Host ''
Write-Host 'This will run:'
Write-Host " `$env:REGISTRY = '$Registry'; `$env:ENABLE_TELEMETRY = '$EnableTelemetry'; docker compose up --pull always"
Write-Host " `$env:REGISTRY = '$Registry'; `$env:ENABLE_TELEMETRY = '$EnableTelemetry'; docker compose --profile tunnel up --pull always"
Write-Host ''
$answer = Read-Host 'Start Dograh now? [Y/n]'
@ -222,7 +221,7 @@ if ($answer -match '^[Nn]') {
$env:REGISTRY = $Registry
$env:ENABLE_TELEMETRY = $EnableTelemetry
Sync-PostgresPassword -Password (Get-DotEnvValue -Path $EnvFile -Key 'POSTGRES_PASSWORD')
docker compose up --pull always
docker compose --profile tunnel up --pull always
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}

View file

@ -200,10 +200,9 @@ fi
echo ""
echo "Docker registry: $REGISTRY"
echo "Telemetry enabled: $ENABLE_TELEMETRY"
echo ""
echo "This will run:"
echo " REGISTRY=$REGISTRY ENABLE_TELEMETRY=$ENABLE_TELEMETRY docker compose up --pull always"
echo " REGISTRY=$REGISTRY ENABLE_TELEMETRY=$ENABLE_TELEMETRY docker compose --profile tunnel up --pull always"
echo ""
if [[ ! -t 0 ]]; then
@ -222,4 +221,4 @@ esac
postgres_password="$(dotenv_value POSTGRES_PASSWORD || true)"
sync_postgres_password "$postgres_password"
REGISTRY="$REGISTRY" ENABLE_TELEMETRY="$ENABLE_TELEMETRY" docker compose up --pull always
REGISTRY="$REGISTRY" ENABLE_TELEMETRY="$ENABLE_TELEMETRY" docker compose --profile tunnel up --pull always