#!/usr/bin/env bash # # Bivious one-command installer and updater. # # Usage: # curl -fsSL https://install.bivious.net | sudo bash # # Or locally: # sudo ./scripts/install.sh # # Supported hosts: # Ubuntu 26.04.x LTS (amd64, arm64) — requires Docker + Compose v2 (>= 2.33.1) # Debian 13.x (amd64, arm64) — requires Docker + Compose v2 (>= 2.33.1) # # Missing prerequisites are provisioned automatically: Docker and the # Docker Compose v2 CLI plugin are installed or upgraded as needed. # # Environment overrides: # BIVIOUS_MANIFEST_URL — override manifest URL # BIVIOUS_RELEASE_BASE — override release download base URL # BIVIOUS_VERSION — force a specific version instead of latest # set -euo pipefail # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_NAME readonly MANIFEST_URL="${BIVIOUS_MANIFEST_URL:-https://install.bivious.net/latest.json}" readonly RELEASE_BASE_URL="${BIVIOUS_RELEASE_BASE:-https://install.bivious.net/releases}" readonly BIVIOUS_PKG="bivious" readonly BIVIOUS_SERVICE="bivious-manager.service" readonly SUPPORTED_UBUNTU_VERSIONS=("26.04") readonly SUPPORTED_DEBIAN_VERSIONS=("13") readonly SUPPORTED_PYTHON_MAJOR=3 readonly SUPPORTED_PYTHON_MIN=9 readonly SUPPORTED_PYTHON_MAX=14 readonly SERVICE_WAIT_TIMEOUT=30 readonly DOCKER_SERVICE="docker.service" readonly DOCKER_WAIT_TIMEOUT=45 readonly COMPOSE_MIN_VERSION="2.33.1" readonly DOCKER_APT_KEYRING="/etc/apt/keyrings/docker.asc" readonly DOCKER_APT_LIST="/etc/apt/sources.list.d/docker.list" readonly CADDY_SERVICE="caddy.service" readonly CADDY_SETUP_SERVICE="caddy-setup.service" readonly CADDY_WAIT_TIMEOUT=60 # --------------------------------------------------------------------------- # Bridge netfilter isolation # --------------------------------------------------------------------------- # When the br_netfilter module is loaded with bridge-nf-call-iptables=1, # same-bridge container traffic traverses the host firewall, where Docker's # default DROP policy silently discards Bivious device forwarding between the # ingress and Connector namespaces. Keep bridged traffic out of iptables. ensure_bridge_netfilter_off() { if [ -d /proc/sys/net/bridge ]; then sysctl -w net.bridge.bridge-nf-call-iptables=0 >/dev/null || true fi # Persist across reboots; the '-' prefix tolerates an unloaded module. printf '%s\n' '-net.bridge.bridge-nf-call-iptables = 0' \ > /etc/sysctl.d/90-bivious-bridge.conf # Prevent silent autoload by other software; explicit loads still work. printf '%s\n' 'blacklist br_netfilter' \ > /etc/modprobe.d/bivious-no-br-netfilter.conf # Docker's daemon explicitly modprobes br_netfilter on every start — after # systemd-sysctl has run at boot — so re-apply the correction from inside the # unit itself. Guarded so a missing module never fails docker.service. install -d -m 0755 /etc/systemd/system/docker.service.d cat > /etc/systemd/system/docker.service.d/bivious-bridge.conf <<'EOF' [Service] ExecStartPost=-/bin/sh -c '[ -d /proc/sys/net/bridge ] && sysctl -w net.bridge.bridge-nf-call-iptables=0 || true' EOF systemctl daemon-reload 2>/dev/null || true } # Populated by preflight checks — used in install_or_update. DEB_ARCH="" OS_ID="" OS_VERSION="" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- die() { printf 'error: %s\n' "$1" >&2 exit 1 } info() { printf ':: %s\n' "$1" } # --------------------------------------------------------------------------- # Cleanup # --------------------------------------------------------------------------- WORK_DIR="" cleanup() { if [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ]; then rm -rf "$WORK_DIR" fi } trap cleanup EXIT # --------------------------------------------------------------------------- # Preflight: root # --------------------------------------------------------------------------- check_root() { if [ "$(id -u)" -ne 0 ]; then printf 'error: this script must be run as root (use: sudo %s)\n' "$SCRIPT_NAME" >&2 return 1 fi } # --------------------------------------------------------------------------- # Preflight: operating system # --------------------------------------------------------------------------- check_os() { if [ ! -f /etc/os-release ]; then printf 'error: cannot determine OS — /etc/os-release not found\n' >&2 return 1 fi # shellcheck disable=SC1091 . /etc/os-release local id="${ID:-}" local version_id="${VERSION_ID:-}" case "$id" in ubuntu) local match=0 for v in "${SUPPORTED_UBUNTU_VERSIONS[@]}"; do if [ "$version_id" = "$v" ] || [[ "$version_id" == "$v".* ]]; then match=1 break fi done if [ "$match" -eq 0 ]; then printf 'error: unsupported Ubuntu version: %s (supported: %s.x)\n' \ "$version_id" "${SUPPORTED_UBUNTU_VERSIONS[*]}" >&2 return 1 fi OS_ID="ubuntu" OS_VERSION="$version_id" ;; debian) local match=0 for v in "${SUPPORTED_DEBIAN_VERSIONS[@]}"; do if [ "$version_id" = "$v" ] || [[ "$version_id" == "$v".* ]]; then match=1 break fi done if [ "$match" -eq 0 ]; then printf 'error: unsupported Debian version: %s (supported: %s.x)\n' \ "$version_id" "${SUPPORTED_DEBIAN_VERSIONS[*]}" >&2 return 1 fi OS_ID="debian" OS_VERSION="$version_id" ;; *) printf 'error: unsupported OS: %s (supported: Ubuntu %s.x, Debian %s.x)\n' \ "${id:-unknown}" "${SUPPORTED_UBUNTU_VERSIONS[*]}" "${SUPPORTED_DEBIAN_VERSIONS[*]}" >&2 return 1 ;; esac info "detected ${OS_ID} ${OS_VERSION}" } # --------------------------------------------------------------------------- # Preflight: architecture # --------------------------------------------------------------------------- check_arch() { local raw_arch="" if command -v dpkg >/dev/null 2>&1; then raw_arch="$(dpkg --print-architecture)" elif command -v uname >/dev/null 2>&1; then raw_arch="$(uname -m)" else printf 'error: cannot determine architecture — neither dpkg nor uname available\n' >&2 return 1 fi case "$raw_arch" in amd64|x86_64) DEB_ARCH="amd64" ;; arm64|aarch64) DEB_ARCH="arm64" ;; *) printf 'error: unsupported architecture: %s (supported: amd64/x86_64, arm64/aarch64)\n' \ "$raw_arch" >&2 return 1 ;; esac info "detected architecture: ${DEB_ARCH}" } # --------------------------------------------------------------------------- # Preflight: /dev/net/tun # --------------------------------------------------------------------------- check_tun() { if [ ! -c /dev/net/tun ]; then printf 'error: /dev/net/tun is not a character device — TUN support is required\n' >&2 return 1 fi } # --------------------------------------------------------------------------- # Preflight: openssl # --------------------------------------------------------------------------- check_openssl() { if [ ! -x /usr/bin/openssl ]; then printf 'error: /usr/bin/openssl not found or not executable — install openssl\n' >&2 return 1 fi } # --------------------------------------------------------------------------- # Preflight: gpg # --------------------------------------------------------------------------- check_gpg() { if ! command -v gpg >/dev/null 2>&1; then printf 'gnupg not found — will install\n' return 0 fi } # --------------------------------------------------------------------------- # Preflight: python3 # --------------------------------------------------------------------------- check_python() { if [ ! -x /usr/bin/python3 ]; then printf 'error: /usr/bin/python3 not found or not executable — install python3\n' >&2 return 1 fi local py_version py_version="$(/usr/bin/python3 -c " import sys print(f'{sys.version_info.major}.{sys.version_info.minor}') ")" local py_major py_minor py_major="${py_version%%.*}" py_minor="${py_version##*.}" if [ "$py_major" -ne "$SUPPORTED_PYTHON_MAJOR" ]; then printf 'error: Python %s is not supported (requires %d.%d–%d.%d)\n' \ "$py_version" "$SUPPORTED_PYTHON_MAJOR" "$SUPPORTED_PYTHON_MIN" \ "$SUPPORTED_PYTHON_MAJOR" "$SUPPORTED_PYTHON_MAX" >&2 return 1 fi if [ "$py_minor" -lt "$SUPPORTED_PYTHON_MIN" ] || [ "$py_minor" -gt "$SUPPORTED_PYTHON_MAX" ]; then printf 'error: Python %s is not supported (requires %d.%d–%d.%d)\n' \ "$py_version" "$SUPPORTED_PYTHON_MAJOR" "$SUPPORTED_PYTHON_MIN" \ "$SUPPORTED_PYTHON_MAJOR" "$SUPPORTED_PYTHON_MAX" >&2 return 1 fi info "detected Python ${py_version}" } # --------------------------------------------------------------------------- # Preflight: systemd # --------------------------------------------------------------------------- check_systemd() { if [ ! -d /run/systemd/system ]; then printf 'error: systemd not detected (/run/systemd/system missing)\n' >&2 return 1 fi } # --------------------------------------------------------------------------- # Preflight: package management tools # --------------------------------------------------------------------------- check_package_manager() { local missing="" if ! command -v dpkg >/dev/null 2>&1; then missing="${missing} dpkg" fi if ! command -v apt-get >/dev/null 2>&1; then missing="${missing} apt-get" fi if [ -n "$missing" ]; then printf 'error: missing required tools:%s\n' "$missing" >&2 return 1 fi } # --------------------------------------------------------------------------- # Docker Compose plugin detection # --------------------------------------------------------------------------- # Print the installed Compose plugin version as x.y.z, or nothing when the # plugin is missing or reports an unparseable version. compose_plugin_version() { local raw raw="$(docker compose version --short 2>/dev/null)" || return 0 printf '%s\n' "$raw" | \ sed -n 's/^[^0-9]*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' } # --------------------------------------------------------------------------- # Preflight: docker # --------------------------------------------------------------------------- check_docker() { if command -v docker >/dev/null 2>&1; then local docker_version docker_version="$(docker --version 2>/dev/null)" || docker_version="Docker (version unknown)" info "detected Docker: ${docker_version}" return 0 fi info "Docker not found — will install" return 0 } # --------------------------------------------------------------------------- # Preflight: docker compose plugin # --------------------------------------------------------------------------- check_docker_compose() { if ! command -v docker >/dev/null 2>&1; then info "Docker not found — Compose plugin will be handled after Docker installation" return 0 fi local compose_version compose_version="$(compose_plugin_version)" if [ -z "$compose_version" ]; then info "Docker Compose plugin not found — will install" return 0 fi if ! version_ge "$compose_version" "$COMPOSE_MIN_VERSION"; then info "detected Docker Compose ${compose_version} — older than required ${COMPOSE_MIN_VERSION}, will upgrade" return 0 fi info "detected Docker Compose: ${compose_version}" } # --------------------------------------------------------------------------- # Preflight: run all checks # --------------------------------------------------------------------------- run_preflight() { info "running preflight checks..." local failures=0 for check in check_root check_os check_arch check_tun check_openssl check_gpg \ check_python check_systemd check_package_manager check_docker \ check_docker_compose; do if ! "$check"; then failures=$((failures + 1)) fi done if [ "$failures" -gt 0 ]; then die "${failures} preflight check(s) failed — fix the issues above and retry" fi info "all preflight checks passed" } # --------------------------------------------------------------------------- # Version management # --------------------------------------------------------------------------- get_installed_version() { dpkg-query -W -f '${Version}' "$BIVIOUS_PKG" 2>/dev/null || echo "" } # Compare two Debian version strings. # Returns 0 if $1 > $2, 1 otherwise. version_gt() { dpkg --compare-versions "$1" gt "$2" } # Compare two dotted version strings (e.g. Compose x.y.z). # Returns 0 if $1 >= $2, 1 otherwise. version_ge() { dpkg --compare-versions "$1" ge "$2" } fetch_latest_version() { local manifest_content="" if command -v curl >/dev/null 2>&1; then manifest_content="$(curl -fsSL --connect-timeout 10 --max-time 30 "$MANIFEST_URL" 2>&1)" || \ die "failed to fetch version manifest from ${MANIFEST_URL} — check network connectivity" elif command -v wget >/dev/null 2>&1; then manifest_content="$(wget -qO- --timeout=30 "$MANIFEST_URL" 2>&1)" || \ die "failed to fetch version manifest from ${MANIFEST_URL} — check network connectivity" else die "neither curl nor wget available — cannot fetch version manifest" fi if [ -z "$manifest_content" ]; then die "failed to fetch version manifest from ${MANIFEST_URL}" fi # Parse "latest" field from JSON without jq. # Anchored to line start; version value restricted to valid Debian version chars. local version version="$(printf '%s' "$manifest_content" | \ sed -n 's/^[[:space:]]*"latest"[[:space:]]*:[[:space:]]*"\([0-9][0-9a-zA-Z.+~-]*\)".*/\1/p' | \ head -n1)" if [ -z "$version" ]; then die "could not parse latest version from manifest" fi echo "$version" } # --------------------------------------------------------------------------- # Download and checksum verification # --------------------------------------------------------------------------- download_file() { local url="$1" local dest="$2" if command -v curl >/dev/null 2>&1; then curl -fsSL --connect-timeout 10 --max-time 120 -o "$dest" "$url" || \ die "failed to download ${url}" elif command -v wget >/dev/null 2>&1; then wget -q --timeout=120 -O "$dest" "$url" || \ die "failed to download ${url}" else die "neither curl nor wget available" fi } verify_checksum() { local file_path="$1" local checksum_url="$2" local checksum_content="" download_file "$checksum_url" "${file_path}.sha256" checksum_content="$(cat "${file_path}.sha256")" # The .sha256 file contains the hex hash, possibly followed by whitespace and # a filename reference. Extract the first 64-hex-char token. local expected_hash expected_hash="$(printf '%s' "$checksum_content" | tr -d '[:space:]' | cut -c1-64)" if [ "${#expected_hash}" -ne 64 ]; then die "invalid SHA-256 checksum format in ${checksum_url}" fi local actual_hash actual_hash="$(openssl dgst -sha256 "$file_path" | awk '{print $NF}')" if [ "$expected_hash" != "$actual_hash" ]; then die "SHA-256 checksum mismatch for $(basename "$file_path"): expected: ${expected_hash} actual: ${actual_hash}" fi info "checksum verified: $(basename "$file_path")" rm -f "${file_path}.sha256" } # --------------------------------------------------------------------------- # Install or update # --------------------------------------------------------------------------- install_or_update() { local installed_version installed_version="$(get_installed_version)" local target_version="${BIVIOUS_VERSION:-}" if [ -z "$target_version" ]; then info "resolving latest version..." target_version="$(fetch_latest_version)" fi info "target version: ${target_version}" # Determine action local action="install" if [ -n "$installed_version" ]; then if [ "$installed_version" = "$target_version" ]; then info "Bivious ${installed_version} is already installed and up to date — nothing to do" return 0 fi if version_gt "$installed_version" "$target_version"; then die "installed version ${installed_version} is newer than available ${target_version} — refusing to downgrade" fi action="update" info "upgrading from ${installed_version} to ${target_version}" else info "no existing Bivious installation found — installing ${target_version}" fi # Prepare download directory WORK_DIR="$(mktemp -d)" local deb_filename="${BIVIOUS_PKG}_${target_version}_${DEB_ARCH}.deb" local deb_url="${RELEASE_BASE_URL}/${target_version}/${deb_filename}" local sha256_url="${deb_url}.sha256" local deb_path="${WORK_DIR}/${deb_filename}" # Download info "downloading ${deb_filename}..." download_file "$deb_url" "$deb_path" # Verify checksum info "verifying package integrity..." verify_checksum "$deb_path" "$sha256_url" # Install if [ "$action" = "update" ]; then info "stopping Bivious service..." systemctl stop "$BIVIOUS_SERVICE" 2>/dev/null || true fi local apt_deb="/var/cache/apt/archives/${deb_filename}" cp "$deb_path" "$apt_deb" || die "failed to copy .deb to apt cache" info "installing package..." apt-get install -y --no-install-recommends --allow-downgrades "$apt_deb" || \ die "package installation failed" rm -f "$apt_deb" # Post-install service management info "reloading systemd daemon..." systemctl daemon-reload if [ "$action" = "install" ]; then info "enabling and starting Bivious service..." systemctl enable "$BIVIOUS_SERVICE" systemctl start "$BIVIOUS_SERVICE" else info "restarting Bivious service..." systemctl restart "$BIVIOUS_SERVICE" fi # Wait for service to become active info "waiting for Bivious service to become active (timeout: ${SERVICE_WAIT_TIMEOUT}s)..." local elapsed=0 while [ "$elapsed" -lt "$SERVICE_WAIT_TIMEOUT" ]; do if systemctl is-active --quiet "$BIVIOUS_SERVICE"; then info "Bivious ${target_version} ${action} successful — service is active" return 0 fi sleep 1 elapsed=$((elapsed + 1)) done die "Bivious service did not reach active state within ${SERVICE_WAIT_TIMEOUT}s — check: journalctl -u ${BIVIOUS_SERVICE}" } # --------------------------------------------------------------------------- # Docker installation # --------------------------------------------------------------------------- install_gnupg() { info "installing gnupg..." apt-get update || \ die "apt-get update failed — gnupg installation aborted" apt-get install -y gnupg || \ die "gnupg installation failed — required for apt repository keys" } install_docker() { info "installing Docker..." # Verify network is available before attempting package installation if command -v curl >/dev/null 2>&1; then curl -fsSL --connect-timeout 5 --max-time 5 "$MANIFEST_URL" >/dev/null 2>&1 || \ die "no network connectivity — cannot install Docker" elif command -v wget >/dev/null 2>&1; then wget -q --spider --timeout=5 "$MANIFEST_URL" 2>/dev/null || \ die "no network connectivity — cannot install Docker" fi info "refreshing package lists..." apt-get update || \ die "apt-get update failed — Docker installation aborted" apt-get install -y docker.io || \ die "Docker installation failed — Docker is mandatory for Bivious" # Debian splits the Docker client out of docker.io (the daemon-only package); # without the CLI neither this installer nor Bivious can talk to the daemon. if ! command -v docker >/dev/null 2>&1; then info "installing Docker CLI (separate package)..." apt-get install -y docker-cli || \ die "Docker CLI installation failed — required to manage the Bivious data plane" fi info "enabling and starting Docker service..." if ! systemctl enable "$DOCKER_SERVICE" || ! systemctl start "$DOCKER_SERVICE"; then die "failed to start Docker service — check: systemctl status ${DOCKER_SERVICE}" fi # Brief pause — allows systemd to propagate service state before verification sleep 2 info "waiting for Docker service to become active (timeout: ${DOCKER_WAIT_TIMEOUT}s)..." local elapsed=0 while [ "$elapsed" -lt "$DOCKER_WAIT_TIMEOUT" ]; do if systemctl is-active --quiet "$DOCKER_SERVICE" && docker info >/dev/null 2>&1; then local docker_version docker_version="$(docker info --format '{{.ServerVersion}}' 2>/dev/null)" || \ docker_version="$(docker --version 2>/dev/null)" || \ docker_version="unknown" info "Docker installed and started successfully — version ${docker_version}" return 0 fi sleep 1 elapsed=$((elapsed + 1)) done die "Docker service did not reach active state within ${DOCKER_WAIT_TIMEOUT}s — check: journalctl -u ${DOCKER_SERVICE}" } # --------------------------------------------------------------------------- # Docker Compose plugin installation # --------------------------------------------------------------------------- install_docker_compose_plugin() { info "installing Docker Compose plugin..." # Verify network is available before attempting package installation if command -v curl >/dev/null 2>&1; then curl -fsSL --connect-timeout 5 --max-time 5 "$MANIFEST_URL" >/dev/null 2>&1 || \ die "no network connectivity — cannot install Docker Compose plugin" elif command -v wget >/dev/null 2>&1; then wget -q --spider --timeout=5 "$MANIFEST_URL" 2>/dev/null || \ die "no network connectivity — cannot install Docker Compose plugin" fi info "adding Docker apt repository..." install -m 0755 -d /etc/apt/keyrings download_file "https://download.docker.com/linux/${OS_ID}/gpg" "$DOCKER_APT_KEYRING" chmod a+r "$DOCKER_APT_KEYRING" # shellcheck disable=SC1091 . /etc/os-release echo "deb [arch=${DEB_ARCH} signed-by=${DOCKER_APT_KEYRING}] https://download.docker.com/linux/${OS_ID} ${VERSION_CODENAME} stable" > "$DOCKER_APT_LIST" info "refreshing package lists..." apt-get update || \ die "apt-get update failed — Docker Compose plugin installation aborted" apt-get install -y --no-install-recommends docker-compose-plugin || \ die "Docker Compose plugin installation failed" info "waiting for Docker Compose >= ${COMPOSE_MIN_VERSION} (timeout: ${DOCKER_WAIT_TIMEOUT}s)..." local elapsed=0 while [ "$elapsed" -lt "$DOCKER_WAIT_TIMEOUT" ]; do local compose_version compose_version="$(compose_plugin_version)" if [ -n "$compose_version" ] && version_ge "$compose_version" "$COMPOSE_MIN_VERSION"; then info "Docker Compose installed successfully — version ${compose_version}" return 0 fi sleep 1 elapsed=$((elapsed + 1)) done die "Docker Compose plugin did not become usable within ${DOCKER_WAIT_TIMEOUT}s — see https://docs.docker.com/compose/install/linux/" } # --------------------------------------------------------------------------- # Caddy installation # --------------------------------------------------------------------------- install_caddy() { if command -v caddy >/dev/null 2>&1; then local caddy_version caddy_version="$(caddy version 2>/dev/null)" || caddy_version="Caddy (version unknown)" info "detected Caddy: ${caddy_version}" return 0 fi info "Caddy not found — will install" # Verify network is available if command -v curl >/dev/null 2>&1; then curl -fsSL --connect-timeout 5 --max-time 5 "$MANIFEST_URL" >/dev/null 2>&1 || \ die "no network connectivity — cannot install Caddy" elif command -v wget >/dev/null 2>&1; then wget -q --spider --timeout=5 "$MANIFEST_URL" 2>/dev/null || \ die "no network connectivity — cannot install Caddy" fi info "adding Caddy apt repository..." install -m 0755 -d /usr/share/keyrings # --retry tolerates transient upstream errors; --yes keeps gpg non-interactive # when a previous run left the keyring in place (re-runs must stay unattended). curl -1sLf --retry 3 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \ gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 2>/dev/null || \ die "failed to fetch Caddy GPG key" curl -1sLf --retry 3 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ -o /etc/apt/sources.list.d/caddy-stable.list || \ die "failed to fetch Caddy repository list" chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg chmod o+r /etc/apt/sources.list.d/caddy-stable.list # Mandatory: apt must fetch metadata from the freshly-added Caddy # repository or the caddy package cannot be resolved. info "refreshing package lists..." apt-get update || \ die "apt-get update failed — Caddy installation aborted" apt-get install -y --no-install-recommends caddy || \ die "Caddy installation failed" info "Caddy installed successfully" } # --------------------------------------------------------------------------- # Caddy setup # --------------------------------------------------------------------------- setup_caddy() { local caddy_setup="/opt/bivious/appliance/caddy-setup.sh" if [ ! -x "$caddy_setup" ]; then die "caddy-setup.sh not found at ${caddy_setup}" fi # The caddy package auto-starts caddy.service on install, occupying # ports 80/443 and tripping caddy-setup.sh's port check. info "stopping Caddy before configuration..." systemctl stop "$CADDY_SERVICE" 2>/dev/null || true info "running caddy-setup.sh..." "$caddy_setup" || die "caddy-setup.sh failed" info "enabling and starting Caddy service..." systemctl enable "$CADDY_SERVICE" || \ die "failed to enable Caddy — check: systemctl status ${CADDY_SERVICE}" # The Bivious package enables caddy-setup.service only when Caddy is already # present at package-configure time. A fresh install adds Caddy afterwards, # so enable the bootstrap unit here (idempotent on re-runs) to keep boot-time # HTTPS configuration intact. systemctl enable "$CADDY_SETUP_SERVICE" || \ die "failed to enable ${CADDY_SETUP_SERVICE} — check: systemctl status ${CADDY_SETUP_SERVICE}" systemctl start "$CADDY_SERVICE" || \ die "failed to start Caddy — check: systemctl status ${CADDY_SERVICE}" info "waiting for Caddy service to become active (timeout: ${CADDY_WAIT_TIMEOUT}s)..." local elapsed=0 while [ "$elapsed" -lt "$CADDY_WAIT_TIMEOUT" ]; do if systemctl is-active --quiet "$CADDY_SERVICE"; then info "Caddy is active" return 0 fi sleep 1 elapsed=$((elapsed + 1)) done die "Caddy service did not reach active state within ${CADDY_WAIT_TIMEOUT}s — check: journalctl -u ${CADDY_SERVICE}" } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { info "Bivious installer" run_preflight # Install gnupg if it wasn't found during preflight (needed for apt keyrings) if ! command -v gpg >/dev/null 2>&1; then install_gnupg fi # Install Docker if it wasn't found during preflight if ! command -v docker >/dev/null 2>&1; then install_docker fi # Install or upgrade the Compose plugin when missing or older than required compose_version="$(compose_plugin_version)" if [ -z "$compose_version" ] || ! version_ge "$compose_version" "$COMPOSE_MIN_VERSION"; then install_docker_compose_plugin fi install_or_update ensure_bridge_netfilter_off install_caddy setup_caddy info "done" } main "$@"