#!/usr/bin/env bash # Download and install the official Docker Engine package via get.docker.com script. install_docker() { colorized_echo blue "Installing Docker (offline mirror)" local arch tarball tmp case "$(uname -m)" in x86_64|amd64) arch=x86_64 ;; aarch64|arm64) arch=aarch64 ;; *) die "Unsupported architecture for the offline Docker bundle: $(uname -m)" ;; esac tarball="https://git.nimbaha.org/pgnode/bin/docker-static/docker-29.9.3-${arch}.tgz" tmp=$(mktemp -d) || die "mktemp failed" colorized_echo cyan " Downloading $tarball" if ! curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 "$tarball" -o "$tmp/docker.tgz"; then rm -rf "$tmp" die "Failed to download the Docker bundle from the mirror" fi tar -xzf "$tmp/docker.tgz" -C "$tmp" || { rm -rf "$tmp"; die "Failed to extract the Docker bundle"; } install -m 755 "$tmp"/docker/* /usr/local/bin/ || { rm -rf "$tmp"; die "Failed to install the Docker binaries"; } rm -rf "$tmp" _install_compose_plugin "$arch" || die "Failed to install the docker compose v2 plugin" # Static builds ship no unit file; dockerd starts containerd itself. cat > /etc/systemd/system/docker.service <<'DOCKERUNIT' [Unit] Description=Docker Application Container Engine (offline static build) After=network-online.target Wants=network-online.target [Service] Type=notify ExecStart=/usr/local/bin/dockerd -H fd:// ExecReload=/bin/kill -s HUP $MAINPID LimitNOFILE=infinity LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity TimeoutStartSec=0 Delegate=yes KillMode=process Restart=on-failure StartLimitBurst=3 StartLimitInterval=60s [Install] WantedBy=multi-user.target DOCKERUNIT systemctl daemon-reload >/dev/null 2>&1 || true ensure_docker_service_running # Systemd may be absent (containers) or the unit may refuse to start; the # daemon still has to exist before anything can be loaded into it. ensure_docker_daemon >/dev/null 2>&1 || true colorized_echo green "Docker installed successfully from the mirror" } # Docker Compose v2 is not part of the static Docker tarball, so it comes from # the mirror instead of the distro package manager. _install_compose_plugin() { local arch="$1" local dest=/usr/local/lib/docker/cli-plugins/docker-compose mkdir -p /usr/local/lib/docker/cli-plugins || return 1 if [ -x "$dest" ]; then return 0 fi colorized_echo blue "Installing Docker Compose v2 plugin (offline mirror)" if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 "https://git.nimbaha.org/pgnode/bin/compose/docker-compose-linux-${arch}" -o "$dest.tmp"; then chmod 755 "$dest.tmp" && mv -f "$dest.tmp" "$dest" return $? fi rm -f "$dest.tmp" return 1 } # Load the node image from the bundled docker-archive tarball. The image is # saved by crane under the source registry's name, so it is re-tagged to the # reference the compose file expects. # Start the daemon if it is not answering. ensure_docker_service_running() returns # silently when there is no systemd (containers) or no unit file, which leaves no # socket at all and makes every docker call fail with a confusing message. ensure_docker_daemon() { docker info >/dev/null 2>&1 && return 0 if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then systemctl enable --now docker >/dev/null 2>&1 || systemctl start docker >/dev/null 2>&1 || true fi local i for i in $(seq 1 15); do docker info >/dev/null 2>&1 && return 0 sleep 1 done if command -v dockerd >/dev/null 2>&1 && { ! command -v pgrep >/dev/null 2>&1 || ! pgrep -x dockerd >/dev/null 2>&1; }; then colorized_echo yellow " The Docker service did not come up; starting dockerd directly" nohup dockerd -H unix:///var/run/docker.sock >/var/log/dockerd.log 2>&1 & fi for i in $(seq 1 30); do docker info >/dev/null 2>&1 && return 0 sleep 1 done return 1 } _load_node_image() { local ver="${1:-latest}" local arch tag tmp src case "$(uname -m)" in x86_64|amd64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; *) return 1 ;; esac tag="pasarguard/node:${ver}" if docker image inspect "$tag" >/dev/null 2>&1; then return 0 fi colorized_echo blue "Loading the node image from the mirror (${arch})" if ! ensure_docker_daemon; then colorized_echo red "✗ The Docker daemon is not running and could not be started" (systemctl status docker --no-pager -l 2>/dev/null || true) | tail -8 | sed 's/^/ /' if [ -s /var/log/dockerd.log ]; then tail -8 /var/log/dockerd.log | sed 's/^/ /' fi return 1 fi tmp=$(mktemp) || return 1 if ! curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 "https://git.nimbaha.org/pgnode/images/node-${arch}.tar" -o "$tmp"; then rm -f "$tmp" colorized_echo red "✗ Could not download the node image from the mirror" return 1 fi if ! load_out=$(docker load -i "$tmp" 2>&1); then colorized_echo red "✗ Could not load the node image tarball" printf '%s ' "$load_out" | tail -6 | sed 's/^/ /' root=$(docker info -f '{{.DockerRootDir}}' 2>/dev/null) if [ -n "$root" ] && [ -d "$root" ]; then df -h "$root" | tail -2 | sed 's/^/ /' fi rm -f "$tmp" return 1 fi rm -f "$tmp" for src in "hub.rat.dev/pasarguard/node:latest" "dockerproxy.net/pasarguard/node:latest" \ "docker.1ms.run/pasarguard/node:latest" "pasarguard/node:latest"; do if docker tag "$src" "$tag" 2>/dev/null; then break fi done if ! docker image inspect "$tag" >/dev/null 2>&1; then return 1 fi # The bundle carries one snapshot; never let a different --version pass # silently as if that exact release had been installed. case "$ver" in latest|pre-release|dev) return 0 ;; esac mirrored=$(curl -fsSL --retry 2 --retry-delay 1 --connect-timeout 15 "https://git.nimbaha.org/pgnode/versions.json" 2>/dev/null | jq -r '.node_image_tag // empty' 2>/dev/null) if [ -n "$mirrored" ] && [ "${mirrored#v}" != "${ver#v}" ]; then colorized_echo yellow "⚠ The mirror ships image $mirrored, but $ver was requested." colorized_echo yellow " Installing the mirrored image; re-run the sync to publish $ver." fi return 0 } # Verify that the systemd docker service unit is active and started. ensure_docker_service_running() { if ! command -v systemctl >/dev/null 2>&1; then return fi if [ ! -d /run/systemd/system ]; then return fi if ! systemctl list-unit-files docker.service >/dev/null 2>&1; then return fi if systemctl is-active --quiet docker; then return fi colorized_echo blue "Starting Docker service" if ! systemctl enable --now docker >/dev/null 2>&1; then systemctl start docker >/dev/null 2>&1 || die "Failed to start Docker service" fi } # Detect whether Docker Compose v2 is functional and assign the COMPOSE variable. detect_compose() { if docker compose version >/dev/null 2>&1; then COMPOSE='docker compose' else die "docker compose v2 not found. Install the Docker Compose v2 plugin (e.g. 'apt-get install docker-compose-plugin' or 'docker-compose-v2'), then re-run." fi } # Ensure the Docker Compose v2 plugin is available. A bare 'command -v docker' # check is not enough: a machine can have the docker engine (e.g. the distro # docker.io package) without the compose v2 plugin. Installs the plugin via the # package manager when missing -- the package is named docker-compose-plugin in # Docker's official repo and docker-compose-v2 in Debian/Ubuntu's -- and aborts # with actionable guidance if it still cannot be provided. ensure_docker_compose() { if docker compose version >/dev/null 2>&1; then return 0 fi colorized_echo blue "Docker Compose v2 plugin not found; installing it" # Offline mirror: the distro repos are unreachable, the plugin is bundled. local _arch case "$(uname -m)" in x86_64|amd64) _arch=x86_64 ;; aarch64|arm64) _arch=aarch64 ;; *) _arch=x86_64 ;; esac _install_compose_plugin "$_arch" || true if ! docker compose version >/dev/null 2>&1; then die "docker compose v2 is required but could not be installed automatically. Install the Docker Compose v2 plugin (e.g. 'apt-get install docker-compose-plugin' or 'docker-compose-v2'), then re-run." fi colorized_echo green "Docker Compose v2 plugin installed" } # Start all containers defined in COMPOSE_FILE in detached mode with orphan removal. compose_up() { ensure_docker_service_running $COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" up -d --remove-orphans } # Stop and remove containers and networks defined in COMPOSE_FILE. compose_down() { $COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" down } # View recent aggregate logs from services defined in COMPOSE_FILE. compose_logs() { $COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" logs } # Stream and follow live log output from services defined in COMPOSE_FILE. compose_logs_follow() { $COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" logs -f }