Merge pull request #180 from outis1one/claude/asterisk-digital-ocean-w22kk8
Claude/asterisk digital ocean w22kk8
This commit is contained in:
@@ -125,9 +125,12 @@ log_error "message" # red [ERROR]
|
||||
```bash
|
||||
prompt_yn "Question? (y/n):" "default_y_or_n" VARNAME
|
||||
prompt_text "Question? [default]:" "default" VARNAME
|
||||
prompt_reinstall_mode VARNAME # sets VARNAME to: update | fresh | cancel
|
||||
```
|
||||
|
||||
When `UNATTENDED=true` both functions skip the prompt and use the default.
|
||||
When `UNATTENDED=true` all three skip the prompt; `prompt_yn`/`prompt_text` use
|
||||
their given default, `prompt_reinstall_mode` always resolves to `cancel`. See
|
||||
**Update vs. fresh reinstall on rerun** below for how to use the latter.
|
||||
|
||||
### Pre-flight
|
||||
|
||||
@@ -262,6 +265,51 @@ fi
|
||||
Put the check early — after any pure-display output (banners, info text)
|
||||
but before the first write.
|
||||
|
||||
## Update vs. fresh reinstall on rerun
|
||||
|
||||
Every service should detect an existing install at the top of its
|
||||
`install_<name>()` — after the `DRY_RUN` check, before any prompts — and
|
||||
offer `prompt_reinstall_mode` instead of silently re-running every prompt
|
||||
(domain, secrets, firewall, Authelia, extras...) from scratch. What counts
|
||||
as "already installed" is service-specific: usually `docker-compose.yml` and
|
||||
`.env` both existing in the service's `$DOCKER_DIR/<name>` directory.
|
||||
|
||||
```bash
|
||||
if [[ -f "$DIR/docker-compose.yml" && -f "$DIR/.env" ]]; then
|
||||
local MODE=""
|
||||
prompt_reinstall_mode MODE
|
||||
case "$MODE" in
|
||||
update)
|
||||
# Refresh vendor files / config templates, rebuild, done.
|
||||
# Do NOT touch .env, firewall rules, or Caddy/Authelia config.
|
||||
...
|
||||
return 0
|
||||
;;
|
||||
cancel)
|
||||
log_info "Leaving the existing install as-is."
|
||||
return 0
|
||||
;;
|
||||
fresh) ;; # fall through to the full install flow below
|
||||
esac
|
||||
fi
|
||||
```
|
||||
|
||||
`update` should be genuinely non-destructive: refresh whatever the service
|
||||
vendors or templates (Docker image sources, config templates,
|
||||
`docker-compose.yml`) and rebuild/restart, but never touch `.env`, firewall
|
||||
rules, or reverse-proxy/SSO config that's already in place. If the
|
||||
vendor-copy or `docker-compose.yml`-generation logic is more than a few
|
||||
lines, factor it into a helper function so the fresh-install path and the
|
||||
update path share one copy instead of drifting apart — see
|
||||
`_asterisk_do_refresh_vendor_files`/`_asterisk_do_write_compose` in
|
||||
`services/asterisk-do.sh` (and their `_asterisk_*` counterparts in
|
||||
`services/asterisk.sh`) for the reference pattern.
|
||||
|
||||
`cancel` must leave the install completely untouched — it's the default for
|
||||
a reason (a stray Enter on a service you're just checking on shouldn't
|
||||
trigger anything). `fresh` runs the exact same flow a first-time install
|
||||
would, prompts included.
|
||||
|
||||
## .env files and secrets
|
||||
|
||||
Generate passwords with `generate_password` (never hardcode them).
|
||||
|
||||
+38
-2
@@ -362,6 +362,32 @@ prompt_text() {
|
||||
eval "$varname='${response:-$default}'"
|
||||
}
|
||||
|
||||
# Prompt for how to handle a service that's already installed, honoring
|
||||
# unattended. prompt_reinstall_mode VARNAME
|
||||
# Sets VARNAME to one of: update | fresh | cancel
|
||||
# Enter (no input) and any unrecognized input both resolve to "cancel" — this
|
||||
# guards a destructive full reinstall behind a deliberate keypress instead of
|
||||
# a stray Enter. Unattended mode always resolves to "cancel" too: never
|
||||
# silently touch an existing install when nobody's watching the prompt.
|
||||
prompt_reinstall_mode() {
|
||||
local varname="$1" response
|
||||
if [ "$UNATTENDED" = true ]; then
|
||||
eval "$varname='cancel'"
|
||||
echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]"
|
||||
return
|
||||
fi
|
||||
echo " Existing install detected. Choose:"
|
||||
echo " r) Reinstall in place — refresh vendor files/config, keep existing settings"
|
||||
echo " f) Full install — re-run every prompt from scratch"
|
||||
echo " c) Cancel — leave everything as-is [default]"
|
||||
read -p " Choice [r/f/c, Enter=cancel]: " response
|
||||
case "${response,,}" in
|
||||
r) eval "$varname='update'" ;;
|
||||
f) eval "$varname='fresh'" ;;
|
||||
*) eval "$varname='cancel'" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Per-service README generation ────────────────────────────────────────────
|
||||
# Write <dir>/README.md from stdin (markdown). Every module is encouraged to
|
||||
# call this so each ~/docker/<service>/ folder is self-documenting.
|
||||
@@ -512,10 +538,20 @@ CADDY_BLOCK
|
||||
echo " ✓ Configuration added to Caddyfile"
|
||||
echo " Reloading Caddy configuration..."
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
# The template Caddyfile ships with "admin off" (security hardening —
|
||||
# no local API attack surface), so `caddy reload` never works here;
|
||||
# it depends on that same admin endpoint. Try it anyway in case a
|
||||
# box has admin enabled, but fall back to a full container restart
|
||||
# (brief availability gap for everything Caddy fronts, but reliable
|
||||
# regardless of the admin setting) rather than leaving the change
|
||||
# sitting unapplied on disk.
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
echo " ✓ $SERVICE_NAME is now accessible at: https://$SERVICE_DOMAIN"
|
||||
elif docker restart caddy &>/dev/null; then
|
||||
echo " ✓ Caddy restarted to apply changes (reload API is disabled by default)"
|
||||
echo " ✓ $SERVICE_NAME should be accessible at: https://$SERVICE_DOMAIN"
|
||||
else
|
||||
echo " ⚠ Failed to reload Caddy. Check: docker logs caddy"
|
||||
echo " ⚠ Failed to reload or restart Caddy. Check: docker logs caddy"
|
||||
echo " You can restore from backup: $BACKUP_FILE"
|
||||
fi
|
||||
|
||||
@@ -533,7 +569,7 @@ CADDY_BLOCK
|
||||
echo " scp $SNIPPET_FILE caddy-host:~/caddy-snippets/"
|
||||
echo " # then on the Caddy machine:"
|
||||
echo " cat ~/caddy-snippets/${DEFAULT_SUBDOMAIN}.caddy >> /path/to/Caddyfile"
|
||||
echo " docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
echo " docker restart caddy # reload API is disabled by default; a restart is what applies it"
|
||||
echo ""
|
||||
echo " Or rsync all snippets at once:"
|
||||
echo " rsync -av $SNIPPET_DIR/ caddy-host:~/caddy-snippets/"
|
||||
|
||||
+241
-125
@@ -61,6 +61,25 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_reinstall_mode() {
|
||||
local _var="$1" _r
|
||||
if [[ "${UNATTENDED:-false}" == "true" ]]; then
|
||||
eval "$_var='cancel'"
|
||||
echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]"
|
||||
return
|
||||
fi
|
||||
echo " Existing install detected. Choose:"
|
||||
echo " r) Reinstall in place — refresh vendor files/config, keep existing settings"
|
||||
echo " f) Full install — re-run every prompt from scratch"
|
||||
echo " c) Cancel — leave everything as-is [default]"
|
||||
read -r -p " Choice [r/f/c, Enter=cancel]: " _r
|
||||
case "${_r,,}" in
|
||||
r) eval "$_var='update'" ;;
|
||||
f) eval "$_var='fresh'" ;;
|
||||
*) eval "$_var='cancel'" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
@@ -145,11 +164,17 @@ CBLOCK
|
||||
printf '%s\n' "$_site_block" >> "$_caddyfile"
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
# The template Caddyfile ships with "admin off", so `caddy
|
||||
# reload` (which needs that same admin API) never actually
|
||||
# works here. Try it anyway, fall back to a restart.
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
elif docker restart caddy &>/dev/null; then
|
||||
log_success "Caddy restarted to apply changes (reload API is disabled by default)"
|
||||
log_success "$_name should be accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
log_warning "Reload/restart failed — check: docker logs caddy"
|
||||
log_info "Manual fix: docker restart caddy"
|
||||
fi
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
@@ -197,6 +222,123 @@ fi
|
||||
|
||||
register_service asterisk-do homelab "Easy Asterisk PBX + coturn, tuned for a public DigitalOcean droplet" 5061
|
||||
|
||||
# ── Shared: vendor file refresh ────────────────────────────────────────────
|
||||
# Called from both a fresh install and an "update in place" run, so a single
|
||||
# copy of this logic stays current for both instead of drifting apart. Must
|
||||
# be called with $PWD already at $EA_DIR.
|
||||
_asterisk_do_refresh_vendor_files() {
|
||||
mkdir -p docker scripts
|
||||
|
||||
local _SELF_DIR_LOCAL
|
||||
_SELF_DIR_LOCAL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local VENDOR_DIR="$_SELF_DIR_LOCAL/../vendor/easy-asterisk"
|
||||
|
||||
if [[ -d "$VENDOR_DIR" ]]; then
|
||||
log_info "Copying vendor files from $VENDOR_DIR ..."
|
||||
cp "$VENDOR_DIR/Dockerfile" ./Dockerfile
|
||||
cp "$VENDOR_DIR/docker/entrypoint.sh" ./docker/entrypoint.sh
|
||||
cp "$VENDOR_DIR/docker/coturn-entrypoint.sh" ./docker/coturn-entrypoint.sh
|
||||
cp "$VENDOR_DIR/easy-asterisk-v0.10.0.sh" ./easy-asterisk.sh
|
||||
cp "$VENDOR_DIR/easy-asterisk-v0.10.0.sh" ./easy-asterisk-v0.10.0.sh
|
||||
cp "$VENDOR_DIR/scripts/vpn-diagnostics.sh" ./scripts/vpn-diagnostics.sh
|
||||
cp "$VENDOR_DIR/scripts/dns-whitelist.sh" ./scripts/dns-whitelist.sh
|
||||
else
|
||||
log_info "Vendor directory not found — downloading from GitHub ..."
|
||||
local GH_RAW="https://raw.githubusercontent.com/DeadDork/easy-asterisk/main"
|
||||
curl -fsSL "$GH_RAW/Dockerfile" -o ./Dockerfile
|
||||
curl -fsSL "$GH_RAW/docker/entrypoint.sh" -o ./docker/entrypoint.sh
|
||||
curl -fsSL "$GH_RAW/docker/coturn-entrypoint.sh" -o ./docker/coturn-entrypoint.sh
|
||||
curl -fsSL "$GH_RAW/easy-asterisk-v0.10.0.sh" -o ./easy-asterisk.sh
|
||||
curl -fsSL "$GH_RAW/scripts/vpn-diagnostics.sh" -o ./scripts/vpn-diagnostics.sh
|
||||
curl -fsSL "$GH_RAW/scripts/dns-whitelist.sh" -o ./scripts/dns-whitelist.sh
|
||||
cp ./easy-asterisk.sh ./easy-asterisk-v0.10.0.sh
|
||||
fi
|
||||
|
||||
chmod 755 ./easy-asterisk.sh ./easy-asterisk-v0.10.0.sh \
|
||||
./docker/entrypoint.sh ./docker/coturn-entrypoint.sh \
|
||||
./scripts/vpn-diagnostics.sh ./scripts/dns-whitelist.sh
|
||||
|
||||
# Persist security-level logging to a file — vendor's logger.conf only
|
||||
# sends the "security" level (auth failures, SIP brute-force attempts) to
|
||||
# the console (Docker stdout), not a file CrowdSec/fail2ban can tail.
|
||||
if grep -q '^console => notice,warning,error,security$' ./docker/entrypoint.sh; then
|
||||
sed -i '/^console => notice,warning,error,security$/a full => notice,warning,error,security' \
|
||||
./docker/entrypoint.sh
|
||||
else
|
||||
log_warning "entrypoint.sh logger.conf template changed upstream — security events won't be logged to a file. Update the sed patch in this installer."
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Shared: docker-compose.yml ─────────────────────────────────────────────
|
||||
# Same reasoning as above — one copy of the template used by both fresh
|
||||
# installs and updates. Must be called with $PWD already at $EA_DIR.
|
||||
_asterisk_do_write_compose() {
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
name: asterisk-do
|
||||
|
||||
services:
|
||||
asterisk:
|
||||
build: .
|
||||
container_name: easy-asterisk-do
|
||||
network_mode: host
|
||||
depends_on:
|
||||
coturn:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- ./config/asterisk:/etc/asterisk
|
||||
- ./config/easy-asterisk:/etc/easy-asterisk
|
||||
- ./logs:/var/log/asterisk
|
||||
- ./spool:/var/spool/asterisk
|
||||
- ./lib:/var/lib/asterisk
|
||||
- ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro
|
||||
- ./exports:/root
|
||||
CADDY_VOLUME_PLACEHOLDER
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "asterisk", "-rx", "core show version"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
coturn:
|
||||
image: coturn/coturn:latest
|
||||
container_name: easy-asterisk-do-coturn
|
||||
network_mode: host
|
||||
user: root
|
||||
entrypoint: ["/coturn-entrypoint.sh"]
|
||||
volumes:
|
||||
- ./docker/coturn-entrypoint.sh:/coturn-entrypoint.sh:ro
|
||||
env_file: .env
|
||||
command:
|
||||
- -n
|
||||
- --listening-port=${TURN_PORT:-3478}
|
||||
- --listening-ip=0.0.0.0
|
||||
- --fingerprint
|
||||
- --lt-cred-mech
|
||||
- --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD}
|
||||
- --realm=${DOMAIN_NAME:-localhost}
|
||||
- --min-port=49152
|
||||
- --max-port=49252
|
||||
- --no-tls
|
||||
- --no-dtls
|
||||
- --no-cli
|
||||
- --no-multicast-peers
|
||||
- --log-file=stdout
|
||||
restart: unless-stopped
|
||||
|
||||
EOF
|
||||
|
||||
# Share Caddy's cert store (read-only) so the entrypoint can auto-sync a
|
||||
# real Let's Encrypt cert for DOMAIN_NAME instead of falling back to
|
||||
# self-signed. No-op if Caddy isn't installed on this box.
|
||||
if [[ -d "$DOCKER_DIR/caddy/data" ]]; then
|
||||
sed -i "s#CADDY_VOLUME_PLACEHOLDER# - ${DOCKER_DIR}/caddy/data:/caddy-data:ro#" docker-compose.yml
|
||||
else
|
||||
sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml
|
||||
fi
|
||||
}
|
||||
|
||||
install_asterisk-do() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Easy Asterisk PBX + coturn (DigitalOcean droplet edition)..."
|
||||
@@ -210,16 +352,69 @@ install_asterisk-do() {
|
||||
echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env"
|
||||
echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk"
|
||||
echo "[DRY-RUN] Would detect droplet public IP via DO metadata service"
|
||||
echo "[DRY-RUN] Would open UFW ports: 5060, 5061, 8080, 8088, 8089, 3478, 10000-20000, 49152-49252"
|
||||
echo "[DRY-RUN] Would scan for a free web admin port starting at 8081 (avoids e.g. CrowdSec's 8080)"
|
||||
echo "[DRY-RUN] Would open UFW ports: 5060, 5061, <web admin port>, 8088, 8089, 3478, 10000-20000, 49152-49252"
|
||||
echo "[DRY-RUN] Would open 51820/udp (not 51821) if wg-easy was selected"
|
||||
echo "[DRY-RUN] Would offer to create a DigitalOcean Cloud Firewall via doctl"
|
||||
echo "[DRY-RUN] Would reverse-proxy the web admin on the SAME FQDN used for SIP (needed for cert sync)"
|
||||
echo "[DRY-RUN] Would offer local OR remote Authelia to protect the web admin"
|
||||
echo "[DRY-RUN] Would offer to install CrowdSec if not already present (full repo only)"
|
||||
echo "[DRY-RUN] Would offer to run base setup first if not already done (full repo only)"
|
||||
echo "[DRY-RUN] Would offer 'update in place' instead of a fresh install if $EA_DIR already exists"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Existing install? Offer update-in-place instead of a full reinstall ───
|
||||
# A fresh install re-runs every prompt (domain, extras, DO firewall,
|
||||
# Authelia). An update only refreshes vendor files + docker-compose.yml —
|
||||
# picking up fixes like this one — and rebuilds, without touching .env,
|
||||
# UFW, the Cloud Firewall, or the Caddy/Authelia config already in place.
|
||||
if [[ -f "$EA_DIR/docker-compose.yml" && -f "$EA_DIR/.env" ]]; then
|
||||
echo ""
|
||||
log_info "Existing install found at $EA_DIR."
|
||||
local REINSTALL_MODE=""
|
||||
prompt_reinstall_mode REINSTALL_MODE
|
||||
case "$REINSTALL_MODE" in
|
||||
update)
|
||||
mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \
|
||||
"$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports"
|
||||
ensure_docker_dir_ownership "$EA_DIR"
|
||||
cd "$EA_DIR" || return 1
|
||||
|
||||
_asterisk_do_refresh_vendor_files
|
||||
_asterisk_do_write_compose
|
||||
|
||||
log_info "Rebuilding and restarting containers..."
|
||||
if docker compose up -d --build --force-recreate; then
|
||||
log_success "Update complete — vendor files and docker-compose.yml refreshed."
|
||||
else
|
||||
log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs"
|
||||
fi
|
||||
|
||||
local _EXISTING_DOMAIN _EXISTING_PORT
|
||||
_EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)"
|
||||
_EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)"
|
||||
echo ""
|
||||
log_success "Existing .env, UFW rules, Cloud Firewall, and Caddy/Authelia config were left untouched."
|
||||
if [[ -n "$_EXISTING_DOMAIN" ]]; then
|
||||
echo " Web admin: https://${_EXISTING_DOMAIN}/"
|
||||
else
|
||||
echo " Web admin: http://<droplet-ip>:${_EXISTING_PORT:-8081}"
|
||||
fi
|
||||
echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f"
|
||||
echo ""
|
||||
return 0
|
||||
;;
|
||||
cancel)
|
||||
log_info "Leaving the existing install as-is — nothing changed."
|
||||
return 0
|
||||
;;
|
||||
fresh)
|
||||
log_info "Proceeding with a full fresh reinstall — every prompt below runs from scratch."
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ── Bring in base first, if this is a genuinely fresh box ─────────────────
|
||||
# Naming a service directly (sudo ./setup.sh asterisk-do) skips setup.sh's
|
||||
# own first-run base step — essential packages, SSH key import, disabling
|
||||
@@ -330,57 +525,11 @@ install_asterisk-do() {
|
||||
|
||||
mkdir -p "$EA_DIR"
|
||||
mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \
|
||||
"$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib"
|
||||
"$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports"
|
||||
ensure_docker_dir_ownership "$EA_DIR"
|
||||
cd "$EA_DIR" || return 1
|
||||
|
||||
mkdir -p docker
|
||||
|
||||
# ── Vendor files (shared with services/asterisk.sh — no duplication) ──────
|
||||
local _SELF_DIR_LOCAL
|
||||
_SELF_DIR_LOCAL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local VENDOR_DIR="$_SELF_DIR_LOCAL/../vendor/easy-asterisk"
|
||||
|
||||
mkdir -p scripts
|
||||
|
||||
if [[ -d "$VENDOR_DIR" ]]; then
|
||||
log_info "Copying vendor files from $VENDOR_DIR ..."
|
||||
cp "$VENDOR_DIR/Dockerfile" ./Dockerfile
|
||||
cp "$VENDOR_DIR/docker/entrypoint.sh" ./docker/entrypoint.sh
|
||||
cp "$VENDOR_DIR/docker/coturn-entrypoint.sh" ./docker/coturn-entrypoint.sh
|
||||
cp "$VENDOR_DIR/easy-asterisk-v0.10.0.sh" ./easy-asterisk.sh
|
||||
cp "$VENDOR_DIR/easy-asterisk-v0.10.0.sh" ./easy-asterisk-v0.10.0.sh
|
||||
cp "$VENDOR_DIR/scripts/vpn-diagnostics.sh" ./scripts/vpn-diagnostics.sh
|
||||
cp "$VENDOR_DIR/scripts/dns-whitelist.sh" ./scripts/dns-whitelist.sh
|
||||
else
|
||||
log_info "Vendor directory not found — downloading from GitHub ..."
|
||||
local GH_RAW="https://raw.githubusercontent.com/DeadDork/easy-asterisk/main"
|
||||
curl -fsSL "$GH_RAW/Dockerfile" -o ./Dockerfile
|
||||
curl -fsSL "$GH_RAW/docker/entrypoint.sh" -o ./docker/entrypoint.sh
|
||||
curl -fsSL "$GH_RAW/docker/coturn-entrypoint.sh" -o ./docker/coturn-entrypoint.sh
|
||||
curl -fsSL "$GH_RAW/easy-asterisk-v0.10.0.sh" -o ./easy-asterisk.sh
|
||||
curl -fsSL "$GH_RAW/scripts/vpn-diagnostics.sh" -o ./scripts/vpn-diagnostics.sh
|
||||
curl -fsSL "$GH_RAW/scripts/dns-whitelist.sh" -o ./scripts/dns-whitelist.sh
|
||||
cp ./easy-asterisk.sh ./easy-asterisk-v0.10.0.sh
|
||||
fi
|
||||
|
||||
chmod 755 ./easy-asterisk.sh ./easy-asterisk-v0.10.0.sh \
|
||||
./docker/entrypoint.sh ./docker/coturn-entrypoint.sh \
|
||||
./scripts/vpn-diagnostics.sh ./scripts/dns-whitelist.sh
|
||||
|
||||
# ── Persist security-level logging to a file ──────────────────────────────
|
||||
# Vendor's logger.conf only sends the "security" level (auth failures, SIP
|
||||
# brute-force attempts) to the console — that's Docker's stdout, not a file
|
||||
# CrowdSec/fail2ban can tail. Patch our copy of entrypoint.sh (not the
|
||||
# shared vendor/ source) so it also writes those events to
|
||||
# /var/log/asterisk/full, which is bind-mounted to $EA_DIR/logs/full — a
|
||||
# host path services/crowdsec.sh can point its Asterisk acquisition at.
|
||||
if grep -q '^console => notice,warning,error,security$' ./docker/entrypoint.sh; then
|
||||
sed -i '/^console => notice,warning,error,security$/a full => notice,warning,error,security' \
|
||||
./docker/entrypoint.sh
|
||||
else
|
||||
log_warning "entrypoint.sh logger.conf template changed upstream — security events won't be logged to a file. Update the sed patch in this installer."
|
||||
fi
|
||||
_asterisk_do_refresh_vendor_files
|
||||
|
||||
# ── DigitalOcean droplet detection ────────────────────────────────────────
|
||||
# A droplet's own public IP/ID are readable, unauthenticated, from the
|
||||
@@ -422,69 +571,25 @@ install_asterisk-do() {
|
||||
# a usable address (the FQDN if set, otherwise the droplet's public IP).
|
||||
local TURN_SERVER_VAL="${DOMAIN_NAME:-$PUBLIC_IP}:3478"
|
||||
|
||||
# ── docker-compose.yml ────────────────────────────────────────────────────
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
name: asterisk-do
|
||||
_asterisk_do_write_compose
|
||||
|
||||
services:
|
||||
asterisk:
|
||||
build: .
|
||||
container_name: easy-asterisk-do
|
||||
network_mode: host
|
||||
depends_on:
|
||||
coturn:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- ./config/asterisk:/etc/asterisk
|
||||
- ./config/easy-asterisk:/etc/easy-asterisk
|
||||
- ./logs:/var/log/asterisk
|
||||
- ./spool:/var/spool/asterisk
|
||||
- ./lib:/var/lib/asterisk
|
||||
- ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro
|
||||
CADDY_VOLUME_PLACEHOLDER
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "asterisk", "-rx", "core show version"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
coturn:
|
||||
image: coturn/coturn:latest
|
||||
container_name: easy-asterisk-do-coturn
|
||||
network_mode: host
|
||||
user: root
|
||||
entrypoint: ["/coturn-entrypoint.sh"]
|
||||
volumes:
|
||||
- ./docker/coturn-entrypoint.sh:/coturn-entrypoint.sh:ro
|
||||
env_file: .env
|
||||
command:
|
||||
- -n
|
||||
- --listening-port=${TURN_PORT:-3478}
|
||||
- --listening-ip=0.0.0.0
|
||||
- --fingerprint
|
||||
- --lt-cred-mech
|
||||
- --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD}
|
||||
- --realm=${DOMAIN_NAME:-localhost}
|
||||
- --min-port=49152
|
||||
- --max-port=49252
|
||||
- --no-tls
|
||||
- --no-dtls
|
||||
- --no-cli
|
||||
- --no-multicast-peers
|
||||
- --log-file=stdout
|
||||
restart: unless-stopped
|
||||
|
||||
EOF
|
||||
|
||||
# Share Caddy's cert store (read-only) so the entrypoint can auto-sync a
|
||||
# real Let's Encrypt cert for DOMAIN_NAME instead of falling back to
|
||||
# self-signed. No-op if Caddy isn't installed on this box.
|
||||
if [[ -d "$DOCKER_DIR/caddy/data" ]]; then
|
||||
sed -i "s#CADDY_VOLUME_PLACEHOLDER# - ${DOCKER_DIR}/caddy/data:/caddy-data:ro#" docker-compose.yml
|
||||
else
|
||||
sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml
|
||||
# ── Pick a free port for the web admin ─────────────────────────────────────
|
||||
# Hardcoding a single number gets fragile fast once several services share
|
||||
# a host — CrowdSec's own LAPI already collides with 8080 by default (its
|
||||
# own upstream default, confirmed against its real config.yaml). Scan
|
||||
# instead: start at 8081 and take the first port nothing is listening on,
|
||||
# capped so a pathological box can't spin this forever.
|
||||
local WEB_ADMIN_PORT_VAL=8081
|
||||
local _port_scan_limit=$((WEB_ADMIN_PORT_VAL + 100))
|
||||
while ss -tlnH "sport = :${WEB_ADMIN_PORT_VAL}" 2>/dev/null | grep -q . \
|
||||
&& [[ "$WEB_ADMIN_PORT_VAL" -lt "$_port_scan_limit" ]]; do
|
||||
WEB_ADMIN_PORT_VAL=$((WEB_ADMIN_PORT_VAL + 1))
|
||||
done
|
||||
if [[ "$WEB_ADMIN_PORT_VAL" -ge "$_port_scan_limit" ]]; then
|
||||
log_warning "No free port found in 8081-${_port_scan_limit} — falling back to 8081 anyway."
|
||||
WEB_ADMIN_PORT_VAL=8081
|
||||
elif [[ "$WEB_ADMIN_PORT_VAL" != 8081 ]]; then
|
||||
log_info "Port 8081 was already taken — web admin will use ${WEB_ADMIN_PORT_VAL} instead."
|
||||
fi
|
||||
|
||||
# ── .env ──────────────────────────────────────────────────────────────────
|
||||
@@ -512,7 +617,11 @@ HAS_VLANS=n
|
||||
VLAN_SUBNETS=
|
||||
|
||||
# ── Web admin ─────────────────────────────────────────────────
|
||||
WEB_ADMIN_PORT=8080
|
||||
# Picked automatically at install time (first free port starting at 8081) —
|
||||
# see WEB_ADMIN_PORT_VAL in services/asterisk-do.sh if this ever needs to
|
||||
# change again; don't hand-edit without also updating Caddy's Caddyfile and
|
||||
# both firewall layers to match.
|
||||
WEB_ADMIN_PORT=${WEB_ADMIN_PORT_VAL}
|
||||
WEB_ADMIN_AUTH_DISABLED=false
|
||||
ENV
|
||||
chmod 600 .env
|
||||
@@ -523,7 +632,7 @@ ENV
|
||||
ufw allow 5060/udp
|
||||
ufw allow 5060/tcp
|
||||
ufw allow 5061/tcp
|
||||
ufw allow 8080/tcp
|
||||
ufw allow "${WEB_ADMIN_PORT_VAL}/tcp"
|
||||
ufw allow 8088/tcp
|
||||
ufw allow 8089/tcp
|
||||
ufw allow 3478/udp
|
||||
@@ -555,7 +664,7 @@ ENV
|
||||
"protocol:tcp,ports:5060,address:0.0.0.0/0,address:::/0"
|
||||
"protocol:udp,ports:5060,address:0.0.0.0/0,address:::/0"
|
||||
"protocol:tcp,ports:5061,address:0.0.0.0/0,address:::/0"
|
||||
"protocol:tcp,ports:8080,address:0.0.0.0/0,address:::/0"
|
||||
"protocol:tcp,ports:${WEB_ADMIN_PORT_VAL},address:0.0.0.0/0,address:::/0"
|
||||
"protocol:tcp,ports:8088-8089,address:0.0.0.0/0,address:::/0"
|
||||
"protocol:tcp,ports:3478,address:0.0.0.0/0,address:::/0"
|
||||
"protocol:udp,ports:3478,address:0.0.0.0/0,address:::/0"
|
||||
@@ -604,9 +713,9 @@ ENV
|
||||
# matching $DOMAIN_NAME, and SIP TLS would silently stay self-signed. So
|
||||
# there's no separate domain prompt: this always targets $DOMAIN_NAME.
|
||||
if [[ -z "$DOMAIN_NAME" ]]; then
|
||||
log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:8080 (nothing for Caddy to do)."
|
||||
log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL} (nothing for Caddy to do)."
|
||||
elif [[ ! -d "$DOCKER_DIR/caddy" ]] && [[ -z "${CADDY_REMOTE_HOST:-}" ]]; then
|
||||
log_info "Caddy not installed — web admin stays on http://${PUBLIC_IP:-localhost}:8080, SIP TLS stays self-signed."
|
||||
log_info "Caddy not installed — web admin stays on http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL}, SIP TLS stays self-signed."
|
||||
else
|
||||
local EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
@@ -665,7 +774,7 @@ ENV
|
||||
|
||||
# Asterisk Web Admin
|
||||
${DOMAIN_NAME} {
|
||||
reverse_proxy localhost:8080
|
||||
reverse_proxy localhost:${WEB_ADMIN_PORT_VAL}
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
@@ -697,11 +806,18 @@ CADDY_BLOCK
|
||||
printf '%s\n' "$_SITE_BLOCK" >> "$_CADDYFILE"
|
||||
log_success "Added ${DOMAIN_NAME} to Caddyfile (backup: $(basename "$_CADDY_BACKUP"))"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
# The template Caddyfile ships with "admin off", so
|
||||
# `caddy reload` (which needs that same admin API) never
|
||||
# actually works here. Try it anyway, fall back to a
|
||||
# restart — confirmed necessary on a real deployment.
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "Web admin accessible at: https://${DOMAIN_NAME}"
|
||||
elif docker restart caddy &>/dev/null; then
|
||||
log_success "Caddy restarted to apply changes (reload API is disabled by default)"
|
||||
log_success "Web admin should be accessible at: https://${DOMAIN_NAME}"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
log_warning "Reload/restart failed — check: docker logs caddy"
|
||||
log_info "Manual fix: docker restart caddy"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
@@ -847,7 +963,7 @@ plan for the admin panel.
|
||||
| 22 | TCP | SSH (keep this open or you're locked out) |
|
||||
| 5060 | UDP/TCP | SIP signalling (unencrypted) |
|
||||
| 5061 | TCP | SIP over TLS |
|
||||
| 8080 | TCP | Easy Asterisk web admin |
|
||||
| ${WEB_ADMIN_PORT_VAL} | TCP | Easy Asterisk web admin (auto-picked — see \`.env\`) |
|
||||
| 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) |
|
||||
| 3478 | UDP/TCP | TURN/STUN (coturn) |
|
||||
| 10000–20000 | UDP | RTP media streams |
|
||||
@@ -881,7 +997,7 @@ be added later by running \`sudo ./setup.sh <name>\` from the repo.
|
||||
the public firewall; the web UI (51821) is deliberately **not** exposed —
|
||||
reach it via SSH tunnel: \`ssh -L 51821:localhost:51821 user@<droplet-ip>\`,
|
||||
then browse \`http://localhost:51821\`. A natural next step once it's
|
||||
installed: restrict the web admin (8080) to the VPN subnet only, on both
|
||||
installed: restrict the web admin (${WEB_ADMIN_PORT_VAL}) to the VPN subnet only, on both
|
||||
firewall layers, so reconfiguring the PBX requires being on the VPN —
|
||||
done manually, not automatically, since a firewall mistake there can lock
|
||||
you out.
|
||||
@@ -965,7 +1081,7 @@ accept it manually).
|
||||
|
||||
## Web admin
|
||||
|
||||
Access the Easy Asterisk web interface at http://<droplet-ip>:8080
|
||||
Access the Easy Asterisk web interface at http://<droplet-ip>:${WEB_ADMIN_PORT_VAL}
|
||||
or via your configured reverse-proxy domain.
|
||||
|
||||
## Data directories (all inside ~/docker/asterisk-do/, included in backup)
|
||||
@@ -1001,7 +1117,7 @@ MD
|
||||
fi
|
||||
echo " Public IP: ${PUBLIC_IP:-unknown}"
|
||||
echo " SIP port: 5061 (TLS) / 5060 (UDP)"
|
||||
echo " Web admin: http://${PUBLIC_IP:-localhost}:8080"
|
||||
echo " Web admin: http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL}"
|
||||
echo " Manage: docker compose -f $EA_DIR/docker-compose.yml <up|down|logs>"
|
||||
echo " Script: docker exec -it easy-asterisk-do easy-asterisk --help"
|
||||
if [[ -n "$DOMAIN_NAME" ]] && [[ -d "$DOCKER_DIR/caddy" ]]; then
|
||||
|
||||
+212
-94
@@ -59,6 +59,25 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_reinstall_mode() {
|
||||
local _var="$1" _r
|
||||
if [[ "${UNATTENDED:-false}" == "true" ]]; then
|
||||
eval "$_var='cancel'"
|
||||
echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]"
|
||||
return
|
||||
fi
|
||||
echo " Existing install detected. Choose:"
|
||||
echo " r) Reinstall in place — refresh vendor files/config, keep existing settings"
|
||||
echo " f) Full install — re-run every prompt from scratch"
|
||||
echo " c) Cancel — leave everything as-is [default]"
|
||||
read -r -p " Choice [r/f/c, Enter=cancel]: " _r
|
||||
case "${_r,,}" in
|
||||
r) eval "$_var='update'" ;;
|
||||
f) eval "$_var='fresh'" ;;
|
||||
*) eval "$_var='cancel'" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
@@ -143,11 +162,17 @@ CBLOCK
|
||||
printf '%s\n' "$_site_block" >> "$_caddyfile"
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
# The template Caddyfile ships with "admin off", so `caddy
|
||||
# reload` (which needs that same admin API) never actually
|
||||
# works here. Try it anyway, fall back to a restart.
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
elif docker restart caddy &>/dev/null; then
|
||||
log_success "Caddy restarted to apply changes (reload API is disabled by default)"
|
||||
log_success "$_name should be accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
log_warning "Reload/restart failed — check: docker logs caddy"
|
||||
log_info "Manual fix: docker restart caddy"
|
||||
fi
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
@@ -195,34 +220,17 @@ fi
|
||||
|
||||
register_service asterisk homelab "Easy Asterisk PBX + coturn TURN server (home intercom/VoIP)" 5061
|
||||
|
||||
install_asterisk() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Easy Asterisk PBX + coturn..."
|
||||
# ── Shared: vendor file refresh ────────────────────────────────────────────
|
||||
# Called from both a fresh install and an "update in place" run, so a single
|
||||
# copy of this logic stays current for both instead of drifting apart. Must
|
||||
# be called with $PWD already at $EA_DIR.
|
||||
_asterisk_refresh_vendor_files() {
|
||||
mkdir -p docker scripts
|
||||
|
||||
local EA_DIR="$DOCKER_DIR/asterisk"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env"
|
||||
echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk"
|
||||
echo "[DRY-RUN] Would open UFW ports: 5060, 5061, 8080, 8088, 8089, 3478, 10000-20000, 49152-49252"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$EA_DIR"
|
||||
mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \
|
||||
"$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib"
|
||||
ensure_docker_dir_ownership "$EA_DIR"
|
||||
cd "$EA_DIR" || return 1
|
||||
|
||||
mkdir -p docker
|
||||
|
||||
# ── Vendor files ──────────────────────────────────────────────────────────
|
||||
local _SELF_DIR_LOCAL
|
||||
_SELF_DIR_LOCAL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local VENDOR_DIR="$_SELF_DIR_LOCAL/../vendor/easy-asterisk"
|
||||
|
||||
mkdir -p scripts
|
||||
|
||||
if [[ -d "$VENDOR_DIR" ]]; then
|
||||
log_info "Copying vendor files from $VENDOR_DIR ..."
|
||||
cp "$VENDOR_DIR/Dockerfile" ./Dockerfile
|
||||
@@ -247,6 +255,153 @@ install_asterisk() {
|
||||
chmod 755 ./easy-asterisk.sh ./easy-asterisk-v0.10.0.sh \
|
||||
./docker/entrypoint.sh ./docker/coturn-entrypoint.sh \
|
||||
./scripts/vpn-diagnostics.sh ./scripts/dns-whitelist.sh
|
||||
}
|
||||
|
||||
# ── Shared: docker-compose.yml ─────────────────────────────────────────────
|
||||
# Same reasoning as above — one copy of the template used by both fresh
|
||||
# installs and updates. Must be called with $PWD already at $EA_DIR.
|
||||
# HAS_VLANS_VAL/VLAN_SUBNETS_VAL aren't referenced here — they live only in
|
||||
# .env, which the entrypoint reads at container start.
|
||||
_asterisk_write_compose() {
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
name: asterisk
|
||||
|
||||
services:
|
||||
asterisk:
|
||||
build: .
|
||||
container_name: easy-asterisk
|
||||
network_mode: host
|
||||
depends_on:
|
||||
coturn:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- ./config/asterisk:/etc/asterisk
|
||||
- ./config/easy-asterisk:/etc/easy-asterisk
|
||||
- ./logs:/var/log/asterisk
|
||||
- ./spool:/var/spool/asterisk
|
||||
- ./lib:/var/lib/asterisk
|
||||
- ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro
|
||||
- ./exports:/root
|
||||
CADDY_VOLUME_PLACEHOLDER
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "asterisk", "-rx", "core show version"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
coturn:
|
||||
image: coturn/coturn:latest
|
||||
container_name: easy-asterisk-coturn
|
||||
network_mode: host
|
||||
user: root
|
||||
entrypoint: ["/coturn-entrypoint.sh"]
|
||||
volumes:
|
||||
- ./docker/coturn-entrypoint.sh:/coturn-entrypoint.sh:ro
|
||||
env_file: .env
|
||||
command:
|
||||
- -n
|
||||
- --listening-port=${TURN_PORT:-3478}
|
||||
- --listening-ip=0.0.0.0
|
||||
- --fingerprint
|
||||
- --lt-cred-mech
|
||||
- --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD}
|
||||
- --realm=${DOMAIN_NAME:-localhost}
|
||||
- --min-port=49152
|
||||
- --max-port=49252
|
||||
- --no-tls
|
||||
- --no-dtls
|
||||
- --no-cli
|
||||
- --no-multicast-peers
|
||||
- --log-file=stdout
|
||||
restart: unless-stopped
|
||||
|
||||
EOF
|
||||
|
||||
# Share Caddy's cert store (read-only) so the entrypoint can auto-sync a
|
||||
# real Let's Encrypt cert for DOMAIN_NAME instead of falling back to
|
||||
# self-signed. No-op if Caddy isn't installed on this box.
|
||||
if [[ -d "$DOCKER_DIR/caddy/data" ]]; then
|
||||
sed -i "s#CADDY_VOLUME_PLACEHOLDER# - ${DOCKER_DIR}/caddy/data:/caddy-data:ro#" docker-compose.yml
|
||||
else
|
||||
sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml
|
||||
fi
|
||||
}
|
||||
|
||||
install_asterisk() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Easy Asterisk PBX + coturn..."
|
||||
|
||||
local EA_DIR="$DOCKER_DIR/asterisk"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env"
|
||||
echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk"
|
||||
echo "[DRY-RUN] Would scan for a free web admin port starting at 8081 (avoids e.g. CrowdSec's 8080)"
|
||||
echo "[DRY-RUN] Would open UFW ports: 5060, 5061, <web admin port>, 8088, 8089, 3478, 10000-20000, 49152-49252"
|
||||
echo "[DRY-RUN] Would offer 'update in place' instead of a fresh install if $EA_DIR already exists"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Existing install? Offer update-in-place instead of a full reinstall ───
|
||||
# A fresh install re-runs every prompt (networking mode, domain, VLANs,
|
||||
# Authelia). An update only refreshes vendor files + docker-compose.yml —
|
||||
# picking up fixes like this one — and rebuilds, without touching .env,
|
||||
# UFW, or the Caddy/Authelia config already in place.
|
||||
if [[ -f "$EA_DIR/docker-compose.yml" && -f "$EA_DIR/.env" ]]; then
|
||||
echo ""
|
||||
log_info "Existing install found at $EA_DIR."
|
||||
local REINSTALL_MODE=""
|
||||
prompt_reinstall_mode REINSTALL_MODE
|
||||
case "$REINSTALL_MODE" in
|
||||
update)
|
||||
mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \
|
||||
"$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports"
|
||||
ensure_docker_dir_ownership "$EA_DIR"
|
||||
cd "$EA_DIR" || return 1
|
||||
|
||||
_asterisk_refresh_vendor_files
|
||||
_asterisk_write_compose
|
||||
|
||||
log_info "Rebuilding and restarting containers..."
|
||||
if docker compose up -d --build --force-recreate; then
|
||||
log_success "Update complete — vendor files and docker-compose.yml refreshed."
|
||||
else
|
||||
log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs"
|
||||
fi
|
||||
|
||||
local _EXISTING_DOMAIN _EXISTING_PORT
|
||||
_EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)"
|
||||
_EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)"
|
||||
echo ""
|
||||
log_success "Existing .env, UFW rules, and Caddy/Authelia config were left untouched."
|
||||
if [[ -n "$_EXISTING_DOMAIN" ]]; then
|
||||
echo " Web admin: https://${_EXISTING_DOMAIN}/"
|
||||
else
|
||||
echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${_EXISTING_PORT:-8081}"
|
||||
fi
|
||||
echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f"
|
||||
echo ""
|
||||
return 0
|
||||
;;
|
||||
cancel)
|
||||
log_info "Leaving the existing install as-is — nothing changed."
|
||||
return 0
|
||||
;;
|
||||
fresh)
|
||||
log_info "Proceeding with a full fresh reinstall — every prompt below runs from scratch."
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
mkdir -p "$EA_DIR"
|
||||
mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \
|
||||
"$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports"
|
||||
ensure_docker_dir_ownership "$EA_DIR"
|
||||
cd "$EA_DIR" || return 1
|
||||
|
||||
_asterisk_refresh_vendor_files
|
||||
|
||||
# ── Networking mode ───────────────────────────────────────────────────────
|
||||
echo ""
|
||||
@@ -294,69 +449,25 @@ install_asterisk() {
|
||||
local TURN_SERVER_VAL=""
|
||||
[[ -n "$DOMAIN_NAME" ]] && TURN_SERVER_VAL="${DOMAIN_NAME}:3478"
|
||||
|
||||
# ── docker-compose.yml ────────────────────────────────────────────────────
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
name: asterisk
|
||||
_asterisk_write_compose
|
||||
|
||||
services:
|
||||
asterisk:
|
||||
build: .
|
||||
container_name: easy-asterisk
|
||||
network_mode: host
|
||||
depends_on:
|
||||
coturn:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- ./config/asterisk:/etc/asterisk
|
||||
- ./config/easy-asterisk:/etc/easy-asterisk
|
||||
- ./logs:/var/log/asterisk
|
||||
- ./spool:/var/spool/asterisk
|
||||
- ./lib:/var/lib/asterisk
|
||||
- ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro
|
||||
CADDY_VOLUME_PLACEHOLDER
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "asterisk", "-rx", "core show version"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
coturn:
|
||||
image: coturn/coturn:latest
|
||||
container_name: easy-asterisk-coturn
|
||||
network_mode: host
|
||||
user: root
|
||||
entrypoint: ["/coturn-entrypoint.sh"]
|
||||
volumes:
|
||||
- ./docker/coturn-entrypoint.sh:/coturn-entrypoint.sh:ro
|
||||
env_file: .env
|
||||
command:
|
||||
- -n
|
||||
- --listening-port=${TURN_PORT:-3478}
|
||||
- --listening-ip=0.0.0.0
|
||||
- --fingerprint
|
||||
- --lt-cred-mech
|
||||
- --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD}
|
||||
- --realm=${DOMAIN_NAME:-localhost}
|
||||
- --min-port=49152
|
||||
- --max-port=49252
|
||||
- --no-tls
|
||||
- --no-dtls
|
||||
- --no-cli
|
||||
- --no-multicast-peers
|
||||
- --log-file=stdout
|
||||
restart: unless-stopped
|
||||
|
||||
EOF
|
||||
|
||||
# Share Caddy's cert store (read-only) so the entrypoint can auto-sync a
|
||||
# real Let's Encrypt cert for DOMAIN_NAME instead of falling back to
|
||||
# self-signed. No-op if Caddy isn't installed on this box.
|
||||
if [[ -d "$DOCKER_DIR/caddy/data" ]]; then
|
||||
sed -i "s#CADDY_VOLUME_PLACEHOLDER# - ${DOCKER_DIR}/caddy/data:/caddy-data:ro#" docker-compose.yml
|
||||
else
|
||||
sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml
|
||||
# ── Pick a free port for the web admin ─────────────────────────────────────
|
||||
# Hardcoding a single number gets fragile fast once several services share
|
||||
# a host — CrowdSec's own LAPI already collides with 8080 by default (its
|
||||
# own upstream default, confirmed against its real config.yaml). Scan
|
||||
# instead: start at 8081 and take the first port nothing is listening on,
|
||||
# capped so a pathological box can't spin this forever.
|
||||
local WEB_ADMIN_PORT_VAL=8081
|
||||
local _port_scan_limit=$((WEB_ADMIN_PORT_VAL + 100))
|
||||
while ss -tlnH "sport = :${WEB_ADMIN_PORT_VAL}" 2>/dev/null | grep -q . \
|
||||
&& [[ "$WEB_ADMIN_PORT_VAL" -lt "$_port_scan_limit" ]]; do
|
||||
WEB_ADMIN_PORT_VAL=$((WEB_ADMIN_PORT_VAL + 1))
|
||||
done
|
||||
if [[ "$WEB_ADMIN_PORT_VAL" -ge "$_port_scan_limit" ]]; then
|
||||
log_warning "No free port found in 8081-${_port_scan_limit} — falling back to 8081 anyway."
|
||||
WEB_ADMIN_PORT_VAL=8081
|
||||
elif [[ "$WEB_ADMIN_PORT_VAL" != 8081 ]]; then
|
||||
log_info "Port 8081 was already taken — web admin will use ${WEB_ADMIN_PORT_VAL} instead."
|
||||
fi
|
||||
|
||||
# ── .env ──────────────────────────────────────────────────────────────────
|
||||
@@ -383,7 +494,11 @@ HAS_VLANS=${HAS_VLANS_VAL}
|
||||
VLAN_SUBNETS=${VLAN_SUBNETS_VAL}
|
||||
|
||||
# ── Web admin ─────────────────────────────────────────────────
|
||||
WEB_ADMIN_PORT=8080
|
||||
# Picked automatically at install time (first free port starting at 8081) —
|
||||
# see WEB_ADMIN_PORT_VAL in services/asterisk.sh if this ever needs to
|
||||
# change again; don't hand-edit without also updating Caddy's Caddyfile and
|
||||
# any firewall rules to match.
|
||||
WEB_ADMIN_PORT=${WEB_ADMIN_PORT_VAL}
|
||||
WEB_ADMIN_AUTH_DISABLED=false
|
||||
ENV
|
||||
chmod 600 .env
|
||||
@@ -394,7 +509,7 @@ ENV
|
||||
ufw allow 5060/udp
|
||||
ufw allow 5060/tcp
|
||||
ufw allow 5061/tcp
|
||||
ufw allow 8080/tcp
|
||||
ufw allow "${WEB_ADMIN_PORT_VAL}/tcp"
|
||||
ufw allow 8088/tcp
|
||||
ufw allow 8089/tcp
|
||||
ufw allow 3478/udp
|
||||
@@ -415,7 +530,7 @@ ENV
|
||||
sed -i "s/^WEB_ADMIN_AUTH_DISABLED=.*/WEB_ADMIN_AUTH_DISABLED=true/" .env
|
||||
fi
|
||||
fi
|
||||
configure_caddy_for_service "Asterisk Web Admin" "8080" "asterisk" "$EXTRA_BLOCK"
|
||||
configure_caddy_for_service "Asterisk Web Admin" "${WEB_ADMIN_PORT_VAL}" "asterisk" "$EXTRA_BLOCK"
|
||||
|
||||
# ── README ────────────────────────────────────────────────────────────────
|
||||
write_readme "$EA_DIR" << 'MD'
|
||||
@@ -483,8 +598,11 @@ to accept it).
|
||||
|
||||
## Web admin
|
||||
|
||||
Access the Easy Asterisk web interface at http://<host-ip>:8080
|
||||
or via your configured reverse-proxy domain.
|
||||
Access the Easy Asterisk web interface at http://<host-ip>:8081
|
||||
or via your configured reverse-proxy domain. (8081 is the default; if that
|
||||
port was already taken by something else on this box, the installer picked
|
||||
the next free one instead — check WEB_ADMIN_PORT in .env for the actual
|
||||
value.)
|
||||
|
||||
## Data directories (all inside ~/docker/asterisk/, included in backup)
|
||||
|
||||
@@ -502,7 +620,7 @@ or via your configured reverse-proxy domain.
|
||||
|---------------|----------|----------------------------------|
|
||||
| 5060 | UDP/TCP | SIP signalling (unencrypted) |
|
||||
| 5061 | TCP | SIP over TLS |
|
||||
| 8080 | TCP | Easy Asterisk web admin |
|
||||
| 8081 | TCP | Easy Asterisk web admin (default — see .env) |
|
||||
| 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) |
|
||||
| 3478 | UDP/TCP | TURN/STUN (coturn) |
|
||||
| 10000–20000 | UDP | RTP media streams |
|
||||
@@ -530,7 +648,7 @@ MD
|
||||
echo " TURN server: (none — LAN/VPN only)"
|
||||
fi
|
||||
echo " SIP port: 5061 (TLS) / 5060 (UDP)"
|
||||
echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):8080"
|
||||
echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${WEB_ADMIN_PORT_VAL}"
|
||||
echo " Manage: docker compose -f $EA_DIR/docker-compose.yml <up|down|logs>"
|
||||
echo " Script: docker exec -it easy-asterisk easy-asterisk --help"
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user