mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-25 12:01:04 +02:00
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>
This commit is contained in:
parent
40e34994fd
commit
d436fe3b62
6 changed files with 345 additions and 176 deletions
|
|
@ -4,8 +4,21 @@ server {
|
|||
listen 80;
|
||||
server_name __DOGRAH_PUBLIC_HOST__;
|
||||
|
||||
# Redirect all HTTP to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
# Serve Let's Encrypt HTTP-01 challenges out of the certs webroot that
|
||||
# certbot --webroot writes into (./certs is bind-mounted here read-only).
|
||||
# Only this path is exposed; local.crt/local.key are never served.
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /etc/nginx/certs;
|
||||
default_type "text/plain";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Redirect everything else to HTTPS. This must live in a location block,
|
||||
# not a server-level `return`, or it would fire before location matching
|
||||
# and hijack the ACME challenge above.
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ description: "Deploy Dograh AI with custom domain names and SSL certificates"
|
|||
|
||||
Deploy Dograh AI with your own custom domain name for a professional production setup. By now, you should be able to create and test a voice agent by following the previous guide to setup the platform on a remote server using [Docker](docker#option-2%3A-remote-server-deployment)
|
||||
|
||||
<Tip>
|
||||
**You don't need to own a domain just to remove the browser warning.** If your server has a public IP, the [remote setup](docker#option-2%3A-remote-server-deployment) already issues a free, auto-renewing Let's Encrypt certificate via [sslip.io](https://sslip.io) (e.g. `https://203-0-113-10.sslip.io`) — no DNS configuration required. Follow this Custom Domain guide only when you want your **own** domain name (e.g. `voice.yourcompany.com`).
|
||||
</Tip>
|
||||
|
||||
## What is Custom Domain Deployment?
|
||||
|
||||
Custom domain deployment allows you to run Dograh AI with a personalized domain name (like `voice.yourcompany.com`) instead of using IP addresses. This setup includes:
|
||||
|
|
@ -109,65 +113,50 @@ sudo apt install certbot -y
|
|||
sudo yum install certbot -y
|
||||
```
|
||||
|
||||
### Stop Dograh Services
|
||||
### Point `.env` at your domain
|
||||
|
||||
Before generating certificates, stop the running Dograh services to free up port 80:
|
||||
Update `.env` so the canonical remote settings use your domain — `dograh-init` reads these to render nginx's `server_name`. Replace `voice.yourcompany.com` with your actual domain throughout:
|
||||
|
||||
```bash
|
||||
cd dograh
|
||||
sudo docker compose --profile remote down
|
||||
sed -i "s/^PUBLIC_HOST=.*/PUBLIC_HOST=voice.yourcompany.com/" .env
|
||||
sed -i "s|^PUBLIC_BASE_URL=.*|PUBLIC_BASE_URL=https://voice.yourcompany.com|" .env
|
||||
```
|
||||
|
||||
### Generate SSL Certificates
|
||||
### Start services so nginx can answer the ACME challenge
|
||||
|
||||
Run Certbot to obtain SSL certificates for your domain:
|
||||
Bring the stack up (or recreate it) through the validated wrapper. nginx serves the Let's Encrypt HTTP-01 challenge from `certs/.well-known/acme-challenge/` on port 80, so the stack must be **running** during issuance — there's no need to stop it:
|
||||
|
||||
```bash
|
||||
sudo certbot certonly --standalone -d voice.yourcompany.com
|
||||
./remote_up.sh
|
||||
```
|
||||
|
||||
Replace `voice.yourcompany.com` with your actual domain name.
|
||||
### Generate the SSL certificate (webroot)
|
||||
|
||||
Issue the certificate using the webroot challenge served by the running nginx:
|
||||
|
||||
```bash
|
||||
sudo certbot certonly --webroot -w "$(pwd)/certs" -d voice.yourcompany.com
|
||||
```
|
||||
|
||||
Certbot will:
|
||||
1. Verify that you control the domain
|
||||
2. Generate SSL certificates
|
||||
3. Store them in `/etc/letsencrypt/live/voice.yourcompany.com/`
|
||||
1. Write a challenge file under `certs/.well-known/acme-challenge/`
|
||||
2. Have Let's Encrypt fetch it over HTTP (port 80) to verify you control the domain
|
||||
3. Store the certificate in `/etc/letsencrypt/live/voice.yourcompany.com/`
|
||||
|
||||
<Note>
|
||||
You'll be prompted to enter an email address for renewal notifications and agree to the terms of service.
|
||||
You'll be prompted for an email address (for renewal notices) and to agree to the terms of service. Because nginx keeps serving traffic throughout, issuance and renewal happen with **no downtime** — unlike the older `--standalone` flow, which had to stop the stack to free port 80.
|
||||
</Note>
|
||||
|
||||
### Copy Certificates to Dograh Directory
|
||||
### Copy the certificate and load it
|
||||
|
||||
Copy the generated certificates to the dograh certs directory:
|
||||
Copy the issued certificate into the `certs/` directory nginx reads, then restart nginx to load it:
|
||||
|
||||
```bash
|
||||
cd dograh
|
||||
sudo cp /etc/letsencrypt/live/voice.yourcompany.com/fullchain.pem certs/local.crt
|
||||
sudo cp /etc/letsencrypt/live/voice.yourcompany.com/privkey.pem certs/local.key
|
||||
sudo chmod 644 certs/local.crt certs/local.key
|
||||
```
|
||||
|
||||
### Update Canonical Public URL Settings
|
||||
|
||||
Update `.env` so the canonical remote settings point at your domain:
|
||||
|
||||
```bash
|
||||
nano dograh/.env
|
||||
```
|
||||
|
||||
```bash
|
||||
PUBLIC_HOST=voice.yourcompany.com
|
||||
PUBLIC_BASE_URL=https://voice.yourcompany.com
|
||||
```
|
||||
|
||||
### Start Dograh Services
|
||||
|
||||
Start Dograh through the validated startup wrapper so `dograh-init` regenerates nginx and coturn runtime config before Docker starts:
|
||||
|
||||
```bash
|
||||
cd dograh
|
||||
./remote_up.sh
|
||||
sudo docker compose --profile remote restart nginx
|
||||
```
|
||||
|
||||
### Access Your Application
|
||||
|
|
@ -222,7 +211,7 @@ sudo certbot renew --dry-run
|
|||
|
||||
If Certbot fails to generate certificates:
|
||||
|
||||
1. **Port 80 blocked**: Ensure port 80 is open in your firewall and no service is using it
|
||||
1. **Port 80 blocked**: Ensure port 80 is open in your firewall and reachable from the internet. With the webroot flow nginx must be **running** and serving the challenge on port 80 (don't stop the stack)
|
||||
2. **DNS not propagated**: Wait for DNS changes to propagate and verify with `nslookup`
|
||||
3. **Rate limits**: Let's Encrypt has rate limits. If you've exceeded them, wait before retrying
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ Watch the video tutorial below for a step-by-step walkthrough of deploying Dogra
|
|||
allowFullScreen
|
||||
></iframe>
|
||||
|
||||
Deploy Dograh AI on a remote server to make it accessible from anywhere using your server's IP address. This setup includes HTTPS support via nginx reverse proxy with self-signed certificates. **We need to serve the application over HTTPS, since modern browsers only allow microphone permissions for websites being served over HTTPS**.
|
||||
Deploy Dograh AI on a remote server to make it accessible from anywhere. If your server has a **public IP**, setup obtains a free, trusted HTTPS certificate automatically via [sslip.io](https://sslip.io) and Let's Encrypt — no domain name or DNS configuration required, and no browser warning. On a private/reserved IP (or when run without root) it falls back to a self-signed certificate. **We need to serve the application over HTTPS, since modern browsers only allow microphone permissions for websites being served over HTTPS**.
|
||||
|
||||
<Warning>We highly recommend you set up the platform on a fresh server, so that there are less chances of confliciting dependencies, and ports from other applications.</Warning>
|
||||
|
||||
|
|
@ -140,9 +140,13 @@ Deploy Dograh AI on a remote server to make it accessible from anywhere using yo
|
|||
Run the automated setup script that will configure everything for you:
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
<Note>
|
||||
Run with `sudo` so setup can obtain and auto-renew a Let's Encrypt certificate for a public IP. Without root (or on a private IP) it falls back to a self-signed certificate. Optional environment-variable overrides: `CERT_MODE=self-signed` forces a self-signed cert, `ACME_DOMAIN_SUFFIX=nip.io` switches to the nip.io pool if sslip.io is rate-limited, and `LETSENCRYPT_EMAIL=you@example.com` sets the address for expiry notices.
|
||||
</Note>
|
||||
|
||||
The script will prompt you for:
|
||||
- Your server's public IP address
|
||||
- A password for the TURN server (optional, press Enter for default)
|
||||
|
|
@ -152,7 +156,8 @@ The script will prompt you for:
|
|||
It will automatically:
|
||||
- Get the source — `docker-compose.yaml` only (prebuilt mode), or clone the full repo (build mode)
|
||||
- Download the validated remote deployment helper bundle
|
||||
- Generate SSL certificates
|
||||
- Obtain a free trusted Let's Encrypt certificate via sslip.io and configure auto-renewal (public IP), or generate a self-signed certificate (private IP / no root)
|
||||
- For a public IP, also start the stack and print your ready-to-use `https://…sslip.io` URL
|
||||
- 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)
|
||||
|
|
@ -163,7 +168,7 @@ It will automatically:
|
|||
Please ensure that Docker Compose is installed on your machine before proceeding further. You can check whether its installed by running `docker compose version` command. If its not installed, please install it by following your server provider documentation.
|
||||
</Note>
|
||||
|
||||
After the setup script completes, start Dograh. The script prints the exact command to run at the end — it differs slightly between modes:
|
||||
After the setup script completes, start Dograh. For a **public-IP install the trusted-certificate flow already started the stack** and printed your `https://…sslip.io` URL — you can skip to [Access Your Application](#access-your-application). For a self-signed install, start it yourself (the script prints the exact command at the end — it differs slightly between modes):
|
||||
|
||||
<CodeGroup>
|
||||
```bash Prebuilt mode
|
||||
|
|
@ -180,13 +185,13 @@ First boot in build mode takes several minutes — Docker has to build both the
|
|||
|
||||
### Access Your Application
|
||||
|
||||
Your application will be available at
|
||||
```
|
||||
https://YOUR_SERVER_IP
|
||||
```
|
||||
Your application will be available at the URL the setup script printed at the end:
|
||||
|
||||
- **Public IP (trusted certificate):** `https://<your-ip-with-dashes>.sslip.io` — for example `https://203-0-113-10.sslip.io`. No browser warning.
|
||||
- **Self-signed (private IP, or run without `sudo`):** `https://YOUR_SERVER_IP`
|
||||
|
||||
<Note>
|
||||
Since we are using a self-signed certificate, your browser will show a security warning. You can safely accept it to proceed.
|
||||
With a self-signed certificate your browser shows a security warning you can safely accept. With the sslip.io Let's Encrypt certificate there is no warning.
|
||||
</Note>
|
||||
|
||||
You should be able to create and test a voice agent now.
|
||||
|
|
@ -196,7 +201,7 @@ You should be able to create and test a voice agent now.
|
|||
- The remote deployment includes an nginx reverse proxy for HTTPS termination
|
||||
- File downloads (transcripts, recordings) are automatically routed through nginx
|
||||
- WebSocket connections for real-time features are properly proxied
|
||||
- The setup uses self-signed certificates - browsers will show a security warning that you can safely accept for testing
|
||||
- Public-IP installs get a trusted Let's Encrypt certificate (via sslip.io) that renews automatically; private-IP installs use a self-signed certificate (browser warning)
|
||||
- The TURN server (coturn) is configured for WebRTC NAT traversal
|
||||
- For production deployments with proper SSL and domain names, see the [Custom Domain](custom-domain) documentation
|
||||
|
||||
|
|
@ -213,7 +218,7 @@ The setup script creates the following files in the `dograh/` directory:
|
|||
| `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 |
|
||||
| `certs/local.crt` | Self-signed SSL certificate |
|
||||
| `certs/local.crt` | SSL certificate (Let's Encrypt via sslip.io, or self-signed) |
|
||||
| `certs/local.key` | SSL private key |
|
||||
| `.env` | Single source of truth for deployment settings (TURN secret, JWT secret, FastAPI worker count, public host/base URL) |
|
||||
|
||||
|
|
|
|||
|
|
@ -410,6 +410,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
|
||||
|
|
|
|||
|
|
@ -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,80 +99,10 @@ 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"
|
||||
|
|
@ -188,32 +110,56 @@ 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)"
|
||||
# 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}"
|
||||
|
|
|
|||
|
|
@ -49,6 +49,66 @@ 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 $SERVER_IP is a private/reserved address — Let's Encrypt cannot validate it."
|
||||
dograh_warn "Falling back to a self-signed certificate. For a trusted cert deploy on a public IP,"
|
||||
dograh_warn "or use a domain you own (https://docs.dograh.com/deployment/custom-domain)."
|
||||
elif [[ $EUID -ne 0 ]]; then
|
||||
CERT_MODE="self-signed"
|
||||
dograh_warn "Not running as root — skipping automatic Let's Encrypt setup."
|
||||
dograh_warn "Re-run with sudo (sudo ./setup_remote.sh) for a free trusted certificate via sslip.io."
|
||||
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
|
||||
[[ $EUID -eq 0 ]] || dograh_fail "CERT_MODE=sslip needs root for certbot. Re-run with sudo."
|
||||
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 +245,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 +302,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 +322,21 @@ 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.
|
||||
SERVER_IP=$SERVER_IP
|
||||
PUBLIC_HOST=$SERVER_IP
|
||||
PUBLIC_BASE_URL=https://$SERVER_IP
|
||||
PUBLIC_HOST=$PUBLIC_HOST_VALUE
|
||||
PUBLIC_BASE_URL=https://$PUBLIC_HOST_VALUE
|
||||
|
||||
# Backend API endpoint (public URL the backend uses to build webhook/embed links)
|
||||
BACKEND_API_ENDPOINT=https://$SERVER_IP
|
||||
BACKEND_API_ENDPOINT=https://$PUBLIC_HOST_VALUE
|
||||
|
||||
# Public URL browsers use to fetch objects from MinIO (proxied by nginx)
|
||||
MINIO_PUBLIC_ENDPOINT=https://$SERVER_IP
|
||||
MINIO_PUBLIC_ENDPOINT=https://$PUBLIC_HOST_VALUE
|
||||
|
||||
# TURN Server Configuration (time-limited credentials via TURN REST API)
|
||||
TURN_HOST=$SERVER_IP
|
||||
TURN_HOST=$PUBLIC_HOST_VALUE
|
||||
TURN_SECRET=$TURN_SECRET
|
||||
# Relay-only ICE candidates for explicit TURN diagnostics
|
||||
FORCE_TURN_RELAY=$FORCE_TURN_RELAY
|
||||
|
|
@ -332,6 +396,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 +454,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 ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue