Add asterisk, nextcloud, onlyoffice, mattermost services + vendor/easy-asterisk
asterisk.sh (homelab): - Easy Asterisk PBX with self-hosted coturn TURN server - Vendored from outis1one/easy-asterisk v0.10.0 for offline install - LAN-only or FQDN mode (TLS + TURN relay for remote access) - Auto-answer SIP headers for intercom use case - Authelia SSO for web admin; WEB_ADMIN_AUTH_DISABLED=true when chosen - UFW rules: 5060-5061, 8080, 8088-8089, 3478, 10000-20000/udp, 49152-49252/udp - Builds custom Docker image from vendor/easy-asterisk/ nextcloud.sh (utilities): - Custom Dockerfile: nextcloud:apache + smbclient (SMB external storage) - MariaDB 10.11 sidecar with matching env vars - OVERWRITEPROTOCOL/OVERWRITECLIURL/TRUSTED_PROXIES set for Caddy - Enables files_external app after first-run init (waits up to 90s) onlyoffice.sh (utilities): - JWT generated once, preserved across re-runs - _ensure_yq: auto-installs yq v4 for FileBrowser config patching - _wire_nextcloud: idempotent occ wiring (DocumentServerUrl, jwt_secret) - _wire_filebrowser: patches config.yaml + restarts container - Caddy block overrides X-Frame-Options to allow iframe embedding mattermost.sh (utilities): - PostgreSQL 15-alpine + Mattermost Team Edition + coturn (port 3479) - 8443/udp for Calls plugin RTC server - coturn uses --use-auth-secret HMAC mode (required by Calls plugin) - SITE_URL computed from SITE_DOMAIN, promptable - UFW: 8443/udp, 3479, 49153-49352/udp vendor/easy-asterisk/: - All upstream source files vendored for offline/self-contained installs - Dockerfile, docker/entrypoint.sh, docker/coturn-entrypoint.sh - easy-asterisk-v0.10.0.sh (6929-line management script) - scripts/vpn-diagnostics.sh, scripts/dns-whitelist.sh - .env.example https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
This commit is contained in:
@@ -0,0 +1,482 @@
|
||||
#!/bin/bash
|
||||
# services/asterisk.sh — Easy Asterisk PBX + coturn TURN server (home intercom/VoIP).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash asterisk.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
# Detected when the script is executed directly rather than sourced by setup.sh.
|
||||
# Sets up helpers and globals, then defers execution until after the function
|
||||
# definition at the bottom of this file.
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
# Full repo present — use the real helpers (picks up ~/docker/.config too)
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$_COMMON"
|
||||
else
|
||||
# One-off copy — inline minimal stubs so the script works without the repo
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Match common.sh's eval-based pattern so local vars in install_* are set correctly
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
|
||||
# Remote Caddy support: if CADDY_REMOTE_HOST is set, operate on the
|
||||
# remote machine via SSH instead of the local filesystem.
|
||||
if [[ -n "${CADDY_REMOTE_HOST:-}" ]]; then
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
read -r -p " Configure Caddy reverse proxy for $_name on $CADDY_REMOTE_HOST? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://$(hostname -I | awk '{print $1}'):${_upstream##*:}"
|
||||
return 0
|
||||
}
|
||||
|
||||
local _domain=""
|
||||
read -r -p " Domain (e.g. ${_subdomain}.${SITE_DOMAIN:-example.com}): " _domain
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
local _block
|
||||
_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
$_domain {
|
||||
reverse_proxy $_upstream
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
echo "$_block" | ssh "$CADDY_REMOTE_HOST" "cat >> $_caddyfile"
|
||||
ssh "$CADDY_REMOTE_HOST" "docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true"
|
||||
if ssh "$CADDY_REMOTE_HOST" "docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null"; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: ssh $CADDY_REMOTE_HOST docker logs caddy"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -d "$_caddy_dir" ]]; then
|
||||
log_info "Access $_name directly on port ${_upstream##*:}."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:${_upstream##*:}"
|
||||
return 0
|
||||
}
|
||||
|
||||
local _domain=""
|
||||
read -r -p " Domain (e.g. ${_subdomain}.${SITE_DOMAIN:-example.com}): " _domain
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Back up before touching
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
# Remove existing block for this domain if present
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
cat >> "$_caddyfile" << CBLOCK
|
||||
|
||||
# $_name
|
||||
$_domain {
|
||||
reverse_proxy $_upstream
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name 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"
|
||||
fi
|
||||
}
|
||||
|
||||
write_readme() {
|
||||
local _dir="$1"
|
||||
mkdir -p "$_dir"
|
||||
[[ "${DRY_RUN:-false}" == "true" ]] && return 0
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
|
||||
generate_password() {
|
||||
local _len="${1:-32}"
|
||||
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$_len"
|
||||
echo
|
||||
}
|
||||
fi
|
||||
|
||||
# Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
|
||||
# ($HOME under sudo is /root, not the real user's home)
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
||||
|
||||
register_service() { :; } # no-op — no wizard to register into
|
||||
_RUN_STANDALONE=1
|
||||
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..."
|
||||
|
||||
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"
|
||||
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"
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
# ── Networking mode ───────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo " Networking mode:"
|
||||
echo " 1) LAN-only — no domain, self-signed cert, works on local network/VPN only"
|
||||
echo " 2) FQDN — TLS + TURN relay, works from anywhere (requires public domain)"
|
||||
local HA_NETMODE=""
|
||||
prompt_text "Choose [1]:" "1" HA_NETMODE
|
||||
|
||||
local DOMAIN_NAME=""
|
||||
if [[ "$HA_NETMODE" == "2" ]]; then
|
||||
prompt_text "FQDN (e.g. asterisk.${SITE_DOMAIN:-example.com}) [blank=skip]:" "" DOMAIN_NAME
|
||||
fi
|
||||
|
||||
# ── Secrets ───────────────────────────────────────────────────────────────
|
||||
local TURN_PASSWORD
|
||||
TURN_PASSWORD="$(generate_password 24)"
|
||||
|
||||
local TURN_SERVER_VAL=""
|
||||
[[ -n "$DOMAIN_NAME" ]] && TURN_SERVER_VAL="${DOMAIN_NAME}:3478"
|
||||
|
||||
# ── docker-compose.yml ────────────────────────────────────────────────────
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
name: asterisk
|
||||
|
||||
services:
|
||||
asterisk:
|
||||
build: .
|
||||
container_name: easy-asterisk
|
||||
network_mode: host
|
||||
depends_on:
|
||||
coturn:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- asterisk-config:/etc/asterisk
|
||||
- easy-asterisk-config:/etc/easy-asterisk
|
||||
- asterisk-logs:/var/log/asterisk
|
||||
- asterisk-spool:/var/spool/asterisk
|
||||
- asterisk-lib:/var/lib/asterisk
|
||||
- ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro
|
||||
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
|
||||
|
||||
volumes:
|
||||
asterisk-config:
|
||||
easy-asterisk-config:
|
||||
asterisk-logs:
|
||||
asterisk-spool:
|
||||
asterisk-lib:
|
||||
EOF
|
||||
|
||||
# ── .env ──────────────────────────────────────────────────────────────────
|
||||
cat > .env << ENV
|
||||
# ── Domain ────────────────────────────────────────────────────
|
||||
# Set to your FQDN for remote access. Leave empty for LAN-only.
|
||||
DOMAIN_NAME=${DOMAIN_NAME}
|
||||
|
||||
# ── TURN/STUN ─────────────────────────────────────────────────
|
||||
TURN_USERNAME=easyasterisk
|
||||
TURN_PASSWORD=${TURN_PASSWORD}
|
||||
TURN_PORT=3478
|
||||
# For LAN-only: TURN_SERVER is empty. For FQDN: set to domain:3478
|
||||
TURN_SERVER=${TURN_SERVER_VAL}
|
||||
|
||||
# ── RTP port range ────────────────────────────────────────────
|
||||
RTP_START=10000
|
||||
RTP_END=20000
|
||||
|
||||
# ── Web admin ─────────────────────────────────────────────────
|
||||
WEB_ADMIN_PORT=8080
|
||||
WEB_ADMIN_AUTH_DISABLED=false
|
||||
ENV
|
||||
chmod 600 .env
|
||||
|
||||
# ── UFW firewall rules ────────────────────────────────────────────────────
|
||||
if command -v ufw &>/dev/null; then
|
||||
log_info "Opening UFW ports for Asterisk + coturn..."
|
||||
ufw allow 5060/udp
|
||||
ufw allow 5060/tcp
|
||||
ufw allow 5061/tcp
|
||||
ufw allow 8080/tcp
|
||||
ufw allow 8088/tcp
|
||||
ufw allow 8089/tcp
|
||||
ufw allow 3478/udp
|
||||
ufw allow 3478/tcp
|
||||
ufw allow 10000:20000/udp
|
||||
ufw allow 49152:49252/udp
|
||||
log_success "UFW rules added."
|
||||
fi
|
||||
|
||||
# ── Caddy reverse proxy for web admin ─────────────────────────────────────
|
||||
local EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
local _use_auth=""
|
||||
prompt_yn "Protect Asterisk web admin with Authelia SSO? (y/n):" "y" _use_auth
|
||||
if [[ "$_use_auth" =~ ^[Yy]$ ]]; then
|
||||
EXTRA_BLOCK=" import authelia"
|
||||
# Disable built-in auth since Authelia handles it
|
||||
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"
|
||||
|
||||
# ── README ────────────────────────────────────────────────────────────────
|
||||
write_readme "$EA_DIR" << 'MD'
|
||||
# Easy Asterisk PBX + coturn
|
||||
|
||||
Self-hosted SIP PBX using Easy Asterisk with a coturn TURN/STUN server for
|
||||
NAT traversal. Suitable for home intercom, VoIP handsets, and softphones.
|
||||
|
||||
## Manage
|
||||
|
||||
```bash
|
||||
docker compose up -d --build # build image and start
|
||||
docker compose up -d # start (after initial build)
|
||||
docker compose down # stop
|
||||
docker compose logs -f # follow logs
|
||||
docker compose pull # update coturn image
|
||||
docker compose up -d --build # rebuild asterisk image
|
||||
```
|
||||
|
||||
## Management script
|
||||
|
||||
```bash
|
||||
docker exec -it easy-asterisk easy-asterisk --help
|
||||
```
|
||||
|
||||
## SIP client setup
|
||||
|
||||
| Setting | Value |
|
||||
|-----------------|--------------------------------------|
|
||||
| SIP server | <host-ip> (LAN) or your FQDN (FQDN) |
|
||||
| SIP port | 5061 (TLS) / 5060 (UDP) |
|
||||
| TURN server | <DOMAIN_NAME>:3478 (FQDN mode only) |
|
||||
| TURN username | easyasterisk |
|
||||
| TURN password | see .env → TURN_PASSWORD |
|
||||
|
||||
Recommended softphones: Linphone, Zoiper, Bria, Grandstream Wave.
|
||||
|
||||
## Web admin
|
||||
|
||||
Access the Easy Asterisk web interface at http://<host-ip>:8080
|
||||
or via your configured reverse-proxy domain.
|
||||
|
||||
## Volumes
|
||||
|
||||
| Volume | Contents |
|
||||
|----------------------|-------------------------------|
|
||||
| asterisk-config | /etc/asterisk — dialplan, SIP |
|
||||
| easy-asterisk-config | /etc/easy-asterisk — web config|
|
||||
| asterisk-logs | /var/log/asterisk |
|
||||
| asterisk-spool | /var/spool/asterisk |
|
||||
| asterisk-lib | /var/lib/asterisk |
|
||||
|
||||
## Ports
|
||||
|
||||
| Port | Protocol | Purpose |
|
||||
|---------------|----------|----------------------------------|
|
||||
| 5060 | UDP/TCP | SIP signalling (unencrypted) |
|
||||
| 5061 | TCP | SIP over TLS |
|
||||
| 8080 | TCP | Easy Asterisk web admin |
|
||||
| 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) |
|
||||
| 3478 | UDP/TCP | TURN/STUN (coturn) |
|
||||
| 10000–20000 | UDP | RTP media streams |
|
||||
| 49152–49252 | UDP | TURN relay media ports |
|
||||
MD
|
||||
|
||||
# ── Start ─────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
local START_NOW=""
|
||||
prompt_yn "Build and start Asterisk now? (y/n):" "y" START_NOW
|
||||
if [ "$START_NOW" = "y" ] || [ "$START_NOW" = "Y" ]; then
|
||||
docker compose up -d --build \
|
||||
&& log_success "Easy Asterisk started" \
|
||||
|| log_warning "Start failed — check: docker compose logs"
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
log_success "Easy Asterisk installed at $EA_DIR"
|
||||
if [[ -n "$DOMAIN_NAME" ]]; then
|
||||
echo " Mode: FQDN ($DOMAIN_NAME)"
|
||||
echo " TURN server: ${DOMAIN_NAME}:3478"
|
||||
else
|
||||
echo " Mode: LAN-only"
|
||||
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 " Manage: docker compose -f $EA_DIR/docker-compose.yml <up|down|logs>"
|
||||
echo " Script: docker exec -it easy-asterisk easy-asterisk --help"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_asterisk
|
||||
@@ -0,0 +1,408 @@
|
||||
#!/bin/bash
|
||||
# services/mattermost.sh — Team messaging with voice/video calls (Mattermost + coturn).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash mattermost.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
# Detected when the script is executed directly rather than sourced by setup.sh.
|
||||
# Sets up helpers and globals, then defers execution until after the function
|
||||
# definition at the bottom of this file.
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
# Full repo present — use the real helpers (picks up ~/docker/.config too)
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$_COMMON"
|
||||
else
|
||||
# One-off copy — inline minimal stubs so the script works without the repo
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Match common.sh's eval-based pattern so local vars in install_* are set correctly
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
|
||||
# Remote Caddy support: if CADDY_REMOTE_HOST is set, operate on the
|
||||
# remote machine via SSH instead of the local filesystem.
|
||||
if [[ -n "${CADDY_REMOTE_HOST:-}" ]]; then
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
read -r -p " Configure Caddy reverse proxy for $_name on $CADDY_REMOTE_HOST? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://$(hostname -I | awk '{print $1}'):${_upstream##*:}"
|
||||
return 0
|
||||
}
|
||||
|
||||
local _domain=""
|
||||
read -r -p " Domain (e.g. ${_subdomain}.${SITE_DOMAIN:-example.com}): " _domain
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
local _block
|
||||
_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
$_domain {
|
||||
reverse_proxy $_upstream
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
echo "$_block" | ssh "$CADDY_REMOTE_HOST" "cat >> $_caddyfile"
|
||||
ssh "$CADDY_REMOTE_HOST" "docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true"
|
||||
if ssh "$CADDY_REMOTE_HOST" "docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null"; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: ssh $CADDY_REMOTE_HOST docker logs caddy"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -d "$_caddy_dir" ]]; then
|
||||
log_info "Access $_name directly on port ${_upstream##*:}."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:${_upstream##*:}"
|
||||
return 0
|
||||
}
|
||||
|
||||
local _domain=""
|
||||
read -r -p " Domain (e.g. ${_subdomain}.${SITE_DOMAIN:-example.com}): " _domain
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Back up before touching
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
# Remove existing block for this domain if present
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
cat >> "$_caddyfile" << CBLOCK
|
||||
|
||||
# $_name
|
||||
$_domain {
|
||||
reverse_proxy $_upstream
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name 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"
|
||||
fi
|
||||
}
|
||||
|
||||
write_readme() {
|
||||
local _dir="$1"
|
||||
mkdir -p "$_dir"
|
||||
[[ "${DRY_RUN:-false}" == "true" ]] && return 0
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
|
||||
generate_password() {
|
||||
local _len="${1:-32}"
|
||||
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$_len"
|
||||
echo
|
||||
}
|
||||
fi
|
||||
|
||||
# Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
|
||||
# ($HOME under sudo is /root, not the real user's home)
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
||||
|
||||
register_service() { :; } # no-op — no wizard to register into
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service mattermost utilities "Team messaging with voice/video calls (Mattermost + coturn)" 8065
|
||||
|
||||
install_mattermost() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Mattermost + coturn..."
|
||||
|
||||
local DIR="$DOCKER_DIR/mattermost"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $DIR with docker-compose.yml"
|
||||
echo "[DRY-RUN] Would write .env with DB and Mattermost secrets"
|
||||
echo "[DRY-RUN] Would create data/ logs/ config/ plugins/ db/ subdirectories"
|
||||
echo "[DRY-RUN] Would open UFW ports 8443/udp, 3479, 49153:49352/udp"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$DIR"
|
||||
ensure_docker_dir_ownership "$DIR"
|
||||
cd "$DIR" || return 1
|
||||
|
||||
local DB_PASS
|
||||
local MM_SECRET
|
||||
DB_PASS=$(generate_password 32)
|
||||
MM_SECRET=$(generate_password 48)
|
||||
|
||||
local TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
local UID_VAL GID_VAL
|
||||
UID_VAL=$(id -u "$ACTUAL_USER")
|
||||
GID_VAL=$(id -g "$ACTUAL_USER")
|
||||
|
||||
# Compute SITE_URL
|
||||
local SITE_URL="http://localhost:8065"
|
||||
if [ -n "$SITE_DOMAIN" ] && [ "$SITE_DOMAIN" != "example.com" ]; then
|
||||
SITE_URL="https://mattermost.${SITE_DOMAIN}"
|
||||
fi
|
||||
local CONFIGURED_SITEURL=""
|
||||
prompt_text "Mattermost site URL [$SITE_URL]:" "$SITE_URL" CONFIGURED_SITEURL
|
||||
[[ -n "$CONFIGURED_SITEURL" ]] && SITE_URL="$CONFIGURED_SITEURL"
|
||||
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
name: mattermost
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: mattermost-db
|
||||
hostname: mattermost-db
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- caddy_net
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
mattermost:
|
||||
image: mattermost/mattermost-team-edition:latest
|
||||
container_name: mattermost
|
||||
hostname: mattermost
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./data:/mattermost/data
|
||||
- ./logs:/mattermost/logs
|
||||
- ./config:/mattermost/config
|
||||
- ./plugins:/mattermost/plugins
|
||||
ports:
|
||||
- "8065:8065"
|
||||
- "8443:8443/udp"
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
coturn:
|
||||
image: coturn/coturn:latest
|
||||
container_name: mattermost-coturn
|
||||
network_mode: host
|
||||
user: root
|
||||
command:
|
||||
- -n
|
||||
- --listening-port=3479
|
||||
- --listening-ip=0.0.0.0
|
||||
- --fingerprint
|
||||
- --use-auth-secret
|
||||
- --static-auth-secret=${COTURN_SECRET}
|
||||
- --realm=${MM_REALM:-localhost}
|
||||
- --min-port=49153
|
||||
- --max-port=49352
|
||||
- --no-tls
|
||||
- --no-dtls
|
||||
- --no-cli
|
||||
- --no-multicast-peers
|
||||
- --log-file=stdout
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: ${CADDY_NET:-caddy_net}
|
||||
EOF
|
||||
|
||||
cat > .env << EOF
|
||||
TZ=$TZ_VAL
|
||||
CADDY_NET=$SITE_CADDY_NET
|
||||
|
||||
# PostgreSQL
|
||||
POSTGRES_DB=mattermost
|
||||
POSTGRES_USER=mattermost
|
||||
POSTGRES_PASSWORD=$DB_PASS
|
||||
|
||||
# Mattermost
|
||||
MM_SQLSETTINGS_DRIVERNAME=postgres
|
||||
MM_SQLSETTINGS_DATASOURCE=postgres://mattermost:${DB_PASS}@mattermost-db:5432/mattermost?sslmode=disable&connect_timeout=10
|
||||
MM_SERVICESETTINGS_SITEURL=$SITE_URL
|
||||
MM_SERVICESETTINGS_ENABLELOCALMODE=true
|
||||
MM_FILESETTINGS_DRIVERNAME=local
|
||||
MM_PLUGINSETTINGS_ENABLE=true
|
||||
|
||||
# coturn HMAC secret for Mattermost Calls plugin
|
||||
COTURN_SECRET=$MM_SECRET
|
||||
MM_REALM=${SITE_DOMAIN:-localhost}
|
||||
|
||||
# PUID/PGID for file ownership
|
||||
PUID=$UID_VAL
|
||||
PGID=$GID_VAL
|
||||
EOF
|
||||
chmod 600 .env
|
||||
|
||||
mkdir -p data logs config plugins db
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$DIR"
|
||||
|
||||
# Open required firewall ports
|
||||
if command -v ufw &>/dev/null; then
|
||||
ufw allow 8443/udp comment "Mattermost Calls RTC"
|
||||
ufw allow 3479/udp; ufw allow 3479/tcp
|
||||
ufw allow 49153:49352/udp comment "Mattermost coturn relay"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_success "Mattermost configured at $DIR"
|
||||
|
||||
configure_caddy_for_service "Mattermost" "mattermost:8065" "mattermost"
|
||||
|
||||
write_readme "$DIR" << MD
|
||||
# Mattermost
|
||||
|
||||
Team messaging with voice/video calls. PostgreSQL backend + coturn TURN relay.
|
||||
|
||||
## Access
|
||||
- URL: $SITE_URL (or http://localhost:8065)
|
||||
- First run: create admin account at the URL above
|
||||
|
||||
## Voice/Video Calls (Calls plugin)
|
||||
Port 8443/udp must be open on your router/firewall.
|
||||
coturn relay runs on port 3479 (HMAC secret in .env).
|
||||
|
||||
Configure in Mattermost: System Console → Plugins → Calls:
|
||||
- TURN Server URI: turn:YOUR_DOMAIN_OR_IP:3479?transport=udp
|
||||
- TURN Credentials: use static-auth-secret (see .env COTURN_SECRET)
|
||||
|
||||
## Manage
|
||||
\`\`\`bash
|
||||
docker compose up -d
|
||||
docker compose down
|
||||
docker compose logs -f
|
||||
docker compose pull && docker compose up -d
|
||||
\`\`\`
|
||||
MD
|
||||
|
||||
if [[ "$SITE_URL" == http://* ]]; then
|
||||
log_warning "WebRTC (voice/video calls) requires HTTPS. Configure Caddy and update SITE_URL."
|
||||
fi
|
||||
|
||||
local START=""
|
||||
prompt_yn "Start Mattermost now? (y/n):" "y" START
|
||||
if [ "$START" = "y" ] || [ "$START" = "Y" ]; then
|
||||
docker compose up -d \
|
||||
&& log_success "Mattermost started" \
|
||||
|| log_warning "Start failed — check: docker compose logs"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Access at: $SITE_URL"
|
||||
echo " First run: open the URL above and create your admin account."
|
||||
echo " Calls plugin: System Console → Plugins → Calls to configure coturn."
|
||||
echo " TURN URI: turn:${SITE_DOMAIN:-YOUR_IP}:3479?transport=udp"
|
||||
echo " Auth secret: see COTURN_SECRET in $DIR/.env"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_mattermost
|
||||
@@ -0,0 +1,356 @@
|
||||
#!/bin/bash
|
||||
# services/nextcloud.sh — Self-hosted cloud storage with SMB/local file access (Nextcloud).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash nextcloud.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
# Detected when the script is executed directly rather than sourced by setup.sh.
|
||||
# Sets up helpers and globals, then defers execution until after the function
|
||||
# definition at the bottom of this file.
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
# Full repo present — use the real helpers (picks up ~/docker/.config too)
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$_COMMON"
|
||||
else
|
||||
# One-off copy — inline minimal stubs so the script works without the repo
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
generate_password() {
|
||||
local _len="${1:-32}"
|
||||
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$_len"
|
||||
}
|
||||
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
[[ "${DRY_RUN:-false}" == "true" ]] && return 0
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
|
||||
# Match common.sh's eval-based pattern so local vars in install_* are set correctly
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
|
||||
# Support remote Caddy host via CADDY_REMOTE_HOST
|
||||
if [[ -n "${CADDY_REMOTE_HOST:-}" ]]; then
|
||||
log_info "Remote Caddy detected at $CADDY_REMOTE_HOST — printing block to add manually."
|
||||
echo ""
|
||||
echo " Add the following to your Caddyfile on $CADDY_REMOTE_HOST:"
|
||||
echo " ──────────────────────────────────────────────────────────"
|
||||
echo " # $_name"
|
||||
echo " ${_subdomain}.${SITE_DOMAIN:-example.com} {"
|
||||
echo " reverse_proxy $_upstream"
|
||||
[[ -n "$_extra" ]] && echo "$_extra"
|
||||
echo " }"
|
||||
echo " ──────────────────────────────────────────────────────────"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -d "$_caddy_dir" ]]; then
|
||||
log_info "Access $_name directly on port ${_upstream##*:}."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:${_upstream##*:}"
|
||||
return 0
|
||||
}
|
||||
|
||||
local _domain=""
|
||||
read -r -p " Domain (e.g. ${_subdomain}.${SITE_DOMAIN:-example.com}): " _domain
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Back up before touching
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
# Remove existing block for this domain if present
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
cat >> "$_caddyfile" << CBLOCK
|
||||
|
||||
# $_name
|
||||
$_domain {
|
||||
reverse_proxy $_upstream
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name 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"
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
# Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
|
||||
# ($HOME under sudo is /root, not the real user's home)
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
|
||||
register_service() { :; } # no-op — no wizard to register into
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service nextcloud utilities "Self-hosted cloud storage with SMB/local file access (Nextcloud)" 8080
|
||||
|
||||
install_nextcloud() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Nextcloud..."
|
||||
local DIR="$DOCKER_DIR/nextcloud"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $DIR with Dockerfile, docker-compose.yml, .env"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$DIR"
|
||||
ensure_docker_dir_ownership "$DIR"
|
||||
cd "$DIR" || return 1
|
||||
|
||||
local DB_PASS
|
||||
DB_PASS=$(generate_password 32)
|
||||
local NC_ADMIN_PASS
|
||||
NC_ADMIN_PASS=$(generate_password 16)
|
||||
local TZ_VAL="${SITE_TZ:-UTC}"
|
||||
|
||||
# ── Dockerfile ──────────────────────────────────────────────────────────
|
||||
cat > Dockerfile << 'NCDF'
|
||||
FROM nextcloud:apache
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends procps smbclient \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
NCDF
|
||||
|
||||
# ── docker-compose.yml ──────────────────────────────────────────────────
|
||||
cat > docker-compose.yml << 'NCCOMPOSE'
|
||||
name: nextcloud
|
||||
services:
|
||||
nextcloud:
|
||||
build: .
|
||||
container_name: nextcloud
|
||||
hostname: nextcloud
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
depends_on:
|
||||
- db
|
||||
volumes:
|
||||
- ./html:/var/www/html
|
||||
- ./config:/var/www/html/config
|
||||
- ./custom_apps:/var/www/html/custom_apps
|
||||
ports:
|
||||
- "8080:80"
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
db:
|
||||
image: mariadb:10.11
|
||||
container_name: nextcloud-db
|
||||
hostname: nextcloud-db
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: ${CADDY_NET:-caddy_net}
|
||||
NCCOMPOSE
|
||||
|
||||
# ── .env ────────────────────────────────────────────────────────────────
|
||||
cat > .env << NCENV
|
||||
TZ=$TZ_VAL
|
||||
CADDY_NET=$SITE_CADDY_NET
|
||||
|
||||
# MariaDB
|
||||
MYSQL_ROOT_PASSWORD=$DB_PASS
|
||||
MYSQL_DATABASE=nextcloud
|
||||
MYSQL_USER=nextcloud
|
||||
MYSQL_PASSWORD=$DB_PASS
|
||||
MARIADB_AUTO_UPGRADE=1
|
||||
|
||||
# Nextcloud bootstrap (first run only)
|
||||
NEXTCLOUD_ADMIN_USER=admin
|
||||
NEXTCLOUD_ADMIN_PASSWORD=$NC_ADMIN_PASS
|
||||
NEXTCLOUD_DB_TYPE=mysql
|
||||
MYSQL_HOST=db
|
||||
|
||||
# Reverse proxy (required for correct share links and redirects behind Caddy)
|
||||
OVERWRITEPROTOCOL=https
|
||||
OVERWRITECLIURL=https://cloud.${SITE_DOMAIN:-example.com}
|
||||
TRUSTED_PROXIES=172.16.0.0/12
|
||||
NCENV
|
||||
chmod 600 .env
|
||||
|
||||
# ── Subdirectories ──────────────────────────────────────────────────────
|
||||
mkdir -p html config custom_apps db
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$DIR"
|
||||
|
||||
echo ""
|
||||
log_success "Nextcloud configured at $DIR"
|
||||
|
||||
configure_caddy_for_service "Nextcloud" "nextcloud:80" "cloud"
|
||||
|
||||
# ── Prompt to start ─────────────────────────────────────────────────────
|
||||
local START_NC=""
|
||||
prompt_yn "Start Nextcloud now? (y/n):" "y" START_NC
|
||||
if [ "$START_NC" = "y" ] || [ "$START_NC" = "Y" ]; then
|
||||
docker compose up -d --build \
|
||||
&& log_success "Nextcloud started" \
|
||||
|| { log_warning "Start failed — check: docker compose logs"; return 1; }
|
||||
|
||||
# ── Wait for occ and enable files_external ──────────────────────────
|
||||
log_info "Waiting for Nextcloud to initialize (up to 90s)..."
|
||||
local _wait=0
|
||||
until docker exec nextcloud php occ status 2>/dev/null | grep -q "installed: true"; do
|
||||
sleep 5; _wait=$((_wait+5))
|
||||
[ $_wait -ge 90 ] && { log_warning "Nextcloud not ready after 90s — enable files_external manually"; break; }
|
||||
done
|
||||
if docker exec nextcloud php occ app:enable files_external 2>/dev/null; then
|
||||
log_success "files_external app enabled (SMB/local external storage)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── README ───────────────────────────────────────────────────────────────
|
||||
write_readme "$DIR" << NCREADME
|
||||
# Nextcloud
|
||||
|
||||
Self-hosted cloud storage with SMB/local file access.
|
||||
|
||||
## Access
|
||||
|
||||
- URL: https://cloud.${SITE_DOMAIN:-example.com} (or http://localhost:8080)
|
||||
- Admin: admin
|
||||
- Password: see \`NEXTCLOUD_ADMIN_PASSWORD\` in \`$DIR/.env\`
|
||||
|
||||
## Manage
|
||||
|
||||
\`\`\`bash
|
||||
docker compose up -d --build # start / rebuild
|
||||
docker compose down # stop
|
||||
docker compose logs -f # follow logs
|
||||
docker compose pull && docker compose up -d --build # update
|
||||
\`\`\`
|
||||
|
||||
## Run occ commands
|
||||
|
||||
\`\`\`bash
|
||||
docker exec -u www-data nextcloud php occ <command>
|
||||
\`\`\`
|
||||
|
||||
## Enable external storage (SMB / local)
|
||||
|
||||
\`\`\`bash
|
||||
docker exec -u www-data nextcloud php occ app:enable files_external
|
||||
\`\`\`
|
||||
|
||||
Then configure mounts in Nextcloud → Settings → External Storages.
|
||||
|
||||
## Backup
|
||||
|
||||
Back up these directories:
|
||||
- \`$DIR/html\` — Nextcloud application files
|
||||
- \`$DIR/config\` — configuration
|
||||
- \`$DIR/custom_apps\` — third-party apps
|
||||
- \`$DIR/db\` — MariaDB data
|
||||
- \`$DIR/.env\` — credentials (permissions 600)
|
||||
NCREADME
|
||||
|
||||
echo ""
|
||||
echo " Access URL: http://localhost:8080"
|
||||
echo " Admin user: admin"
|
||||
echo " Admin pass: $NC_ADMIN_PASS"
|
||||
echo " Config dir: $DIR"
|
||||
echo ""
|
||||
echo " Note: First startup may take 1-2 minutes while Nextcloud initialises."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_nextcloud
|
||||
@@ -0,0 +1,382 @@
|
||||
#!/bin/bash
|
||||
# services/onlyoffice.sh — Self-hosted OnlyOffice Document Server (Nextcloud/FileBrowser).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash onlyoffice.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
# Detected when the script is executed directly rather than sourced by setup.sh.
|
||||
# Sets up helpers and globals, then defers execution until after the function
|
||||
# definition at the bottom of this file.
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
# Full repo present — use the real helpers (picks up ~/docker/.config too)
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$_COMMON"
|
||||
else
|
||||
# One-off copy — inline minimal stubs so the script works without the repo
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
generate_password() {
|
||||
local _len="${1:-32}"
|
||||
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$_len"
|
||||
}
|
||||
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
[[ "${DRY_RUN:-false}" == "true" ]] && return 0
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
|
||||
# Match common.sh's eval-based pattern so local vars in install_* are set correctly
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
|
||||
# Support remote Caddy host via CADDY_REMOTE_HOST
|
||||
if [[ -n "${CADDY_REMOTE_HOST:-}" ]]; then
|
||||
log_info "Remote Caddy detected at $CADDY_REMOTE_HOST — printing block to add manually."
|
||||
echo ""
|
||||
echo " Add the following to your Caddyfile on $CADDY_REMOTE_HOST:"
|
||||
echo " ──────────────────────────────────────────────────────────"
|
||||
echo " # $_name"
|
||||
echo " ${_subdomain}.${SITE_DOMAIN:-example.com} {"
|
||||
echo " reverse_proxy $_upstream"
|
||||
[[ -n "$_extra" ]] && echo "$_extra"
|
||||
echo " }"
|
||||
echo " ──────────────────────────────────────────────────────────"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -d "$_caddy_dir" ]]; then
|
||||
log_info "Access $_name directly on port ${_upstream##*:}."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:${_upstream##*:}"
|
||||
return 0
|
||||
}
|
||||
|
||||
local _domain=""
|
||||
read -r -p " Domain (e.g. ${_subdomain}.${SITE_DOMAIN:-example.com}): " _domain
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Back up before touching
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
# Remove existing block for this domain if present
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
cat >> "$_caddyfile" << CBLOCK
|
||||
|
||||
# $_name
|
||||
$_domain {
|
||||
reverse_proxy $_upstream
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name 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"
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
# Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
|
||||
# ($HOME under sudo is /root, not the real user's home)
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
|
||||
register_service() { :; } # no-op — no wizard to register into
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service onlyoffice utilities "Self-hosted OnlyOffice Document Server (Nextcloud/FileBrowser)" 8082
|
||||
|
||||
# ── Helper: install yq v4 if absent ──────────────────────────────────────────
|
||||
_ensure_yq() {
|
||||
command -v yq &>/dev/null && return 0
|
||||
log_info "Installing yq (required for FileBrowser config patching)..."
|
||||
local _arch; _arch=$(uname -m)
|
||||
local _binary="yq_linux_amd64"
|
||||
[[ "$_arch" == "aarch64" || "$_arch" == "arm64" ]] && _binary="yq_linux_arm64"
|
||||
curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/${_binary}" \
|
||||
-o /usr/local/bin/yq && chmod +x /usr/local/bin/yq \
|
||||
&& log_success "yq installed" || log_warning "yq install failed — FileBrowser wiring skipped"
|
||||
}
|
||||
|
||||
# ── Helper: wire OnlyOffice into Nextcloud (idempotent) ───────────────────────
|
||||
_wire_nextcloud() {
|
||||
local _jwt="$1"
|
||||
local _nc_container="nextcloud"
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${_nc_container}$" || {
|
||||
log_info "Nextcloud container not running — skipping Nextcloud wiring"
|
||||
return 0
|
||||
}
|
||||
log_info "Wiring OnlyOffice into Nextcloud..."
|
||||
docker exec "$_nc_container" php occ app:enable onlyoffice 2>/dev/null || true
|
||||
docker exec "$_nc_container" php occ config:system:set onlyoffice DocumentServerUrl \
|
||||
--value="https://office.${SITE_DOMAIN:-example.com}/" 2>/dev/null \
|
||||
&& log_success "DocumentServerUrl set" || log_warning "Could not set DocumentServerUrl"
|
||||
docker exec "$_nc_container" php occ config:system:set onlyoffice jwt_secret \
|
||||
--value="$_jwt" 2>/dev/null \
|
||||
&& log_success "jwt_secret set" || log_warning "Could not set jwt_secret"
|
||||
docker exec "$_nc_container" php occ config:system:set onlyoffice jwt_header \
|
||||
--value="AuthorizationJwt" 2>/dev/null \
|
||||
&& log_success "jwt_header set" || log_warning "Could not set jwt_header"
|
||||
}
|
||||
|
||||
# ── Helper: patch FileBrowser Quantum config.yaml with OnlyOffice endpoint ───
|
||||
_wire_filebrowser() {
|
||||
local _fbq_config="$DOCKER_DIR/filebrowser/config.yaml"
|
||||
[[ -f "$_fbq_config" ]] || { log_info "FileBrowser config not found — skipping"; return 0; }
|
||||
command -v yq &>/dev/null || { log_info "yq not found — skipping FileBrowser wiring"; return 0; }
|
||||
log_info "Wiring OnlyOffice into FileBrowser Quantum..."
|
||||
yq e '.officeServer = "http://onlyoffice:80/"' -i "$_fbq_config" \
|
||||
&& log_success "FileBrowser officeServer set" || log_warning "Could not patch FileBrowser config"
|
||||
docker restart filebrowser 2>/dev/null && log_success "FileBrowser restarted" || true
|
||||
}
|
||||
|
||||
install_onlyoffice() {
|
||||
require_docker || return 1
|
||||
_ensure_yq
|
||||
|
||||
log_info "Installing OnlyOffice Document Server..."
|
||||
local DIR="$DOCKER_DIR/onlyoffice"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $DIR with docker-compose.yml, .env"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$DIR"
|
||||
ensure_docker_dir_ownership "$DIR"
|
||||
cd "$DIR" || return 1
|
||||
|
||||
# ── Preserve JWT secret across re-runs ──────────────────────────────────
|
||||
local JWT_SECRET=""
|
||||
if [[ -f "$DIR/.env" ]]; then
|
||||
JWT_SECRET=$(grep "^JWT_SECRET=" "$DIR/.env" 2>/dev/null | cut -d= -f2-)
|
||||
fi
|
||||
[[ -z "$JWT_SECRET" ]] && JWT_SECRET="$(generate_password 32)"
|
||||
|
||||
# ── docker-compose.yml ──────────────────────────────────────────────────
|
||||
cat > docker-compose.yml << 'OOCOMPOSE'
|
||||
name: onlyoffice
|
||||
services:
|
||||
onlyoffice:
|
||||
image: onlyoffice/documentserver:latest
|
||||
container_name: onlyoffice
|
||||
hostname: onlyoffice
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./logs:/var/log/onlyoffice
|
||||
- ./data:/var/www/onlyoffice/Data
|
||||
- ./fonts:/usr/share/fonts/truetype/custom
|
||||
ports:
|
||||
- "8082:80"
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: ${CADDY_NET:-caddy_net}
|
||||
OOCOMPOSE
|
||||
|
||||
# ── .env ────────────────────────────────────────────────────────────────
|
||||
cat > .env << OOENV
|
||||
CADDY_NET=$SITE_CADDY_NET
|
||||
# JWT authentication — keep JWT_SECRET private
|
||||
JWT_ENABLED=true
|
||||
JWT_SECRET=$JWT_SECRET
|
||||
JWT_HEADER=AuthorizationJwt
|
||||
OOENV
|
||||
chmod 600 .env
|
||||
|
||||
# ── Subdirectories ──────────────────────────────────────────────────────
|
||||
mkdir -p logs data fonts
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$DIR"
|
||||
|
||||
echo ""
|
||||
log_success "OnlyOffice configured at $DIR"
|
||||
|
||||
# OnlyOffice must be embeddable as an iframe (Nextcloud / FileBrowser open
|
||||
# documents in a frame). Override the default X-Frame-Options header that
|
||||
# Caddy would otherwise set to SAMEORIGIN.
|
||||
local OO_EXTRA_BLOCK=' header {
|
||||
-X-Frame-Options
|
||||
Content-Security-Policy "frame-ancestors '\''self'\'' *"
|
||||
}'
|
||||
configure_caddy_for_service "OnlyOffice" "onlyoffice:80" "office" "$OO_EXTRA_BLOCK"
|
||||
|
||||
# ── Prompt to start ─────────────────────────────────────────────────────
|
||||
local START_OO=""
|
||||
prompt_yn "Start OnlyOffice now? (y/n):" "y" START_OO
|
||||
if [ "$START_OO" = "y" ] || [ "$START_OO" = "Y" ]; then
|
||||
docker compose up -d \
|
||||
&& log_success "OnlyOffice started" \
|
||||
|| log_warning "Start failed — check: docker compose logs"
|
||||
fi
|
||||
|
||||
# ── Wire integrations (runs every install/re-install) ───────────────────
|
||||
echo ""
|
||||
_wire_nextcloud "$JWT_SECRET"
|
||||
_wire_filebrowser
|
||||
|
||||
# ── README ───────────────────────────────────────────────────────────────
|
||||
write_readme "$DIR" << OOREAD
|
||||
# OnlyOffice Document Server
|
||||
|
||||
Self-hosted document editing server, integrated with Nextcloud and FileBrowser Quantum.
|
||||
|
||||
## Access
|
||||
|
||||
- URL: https://office.${SITE_DOMAIN:-example.com} (or http://localhost:8082)
|
||||
- The document server itself has no user-facing login page — it is accessed
|
||||
through Nextcloud or FileBrowser Quantum.
|
||||
|
||||
## Manage
|
||||
|
||||
\`\`\`bash
|
||||
docker compose up -d # start
|
||||
docker compose down # stop
|
||||
docker compose logs -f # follow logs
|
||||
docker compose pull && docker compose up -d # update
|
||||
\`\`\`
|
||||
|
||||
## JWT secret rotation
|
||||
|
||||
1. Generate a new secret:
|
||||
\`\`\`bash
|
||||
openssl rand -hex 24
|
||||
\`\`\`
|
||||
2. Update \`JWT_SECRET\` in \`$DIR/.env\`
|
||||
3. Restart OnlyOffice:
|
||||
\`\`\`bash
|
||||
docker compose restart
|
||||
\`\`\`
|
||||
4. Update Nextcloud's stored secret:
|
||||
\`\`\`bash
|
||||
docker exec nextcloud php occ config:system:set onlyoffice jwt_secret --value="<new-secret>"
|
||||
\`\`\`
|
||||
|
||||
## Verify Nextcloud integration
|
||||
|
||||
\`\`\`bash
|
||||
docker exec nextcloud php occ config:system:get onlyoffice
|
||||
\`\`\`
|
||||
|
||||
## Verify FileBrowser integration
|
||||
|
||||
\`\`\`bash
|
||||
grep officeServer $DOCKER_DIR/filebrowser/config.yaml
|
||||
\`\`\`
|
||||
|
||||
## Add custom fonts
|
||||
|
||||
Copy \`.ttf\` / \`.otf\` font files into \`$DIR/fonts/\`, then restart the container.
|
||||
OOREAD
|
||||
|
||||
echo ""
|
||||
echo " OnlyOffice Document Server"
|
||||
echo " Access URL: http://localhost:8082"
|
||||
echo " JWT secret: $JWT_SECRET"
|
||||
echo " Config dir: $DIR"
|
||||
echo ""
|
||||
echo " Integration status:"
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^nextcloud$" \
|
||||
&& echo " Nextcloud: wired (onlyoffice app + JWT configured)" \
|
||||
|| echo " Nextcloud: not running — wire manually after starting Nextcloud"
|
||||
[[ -f "$DOCKER_DIR/filebrowser/config.yaml" ]] \
|
||||
&& echo " FileBrowser: config.yaml patched" \
|
||||
|| echo " FileBrowser: config not found — will wire on next onlyoffice install"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_onlyoffice
|
||||
Reference in New Issue
Block a user