Files
mc-webui/scripts/updater/install.sh
T
MarekWoandClaude Opus 5 60faa44d37 feat(updater): one-click updates for Docker Hub installations
The version check already worked for an installation running the published
image - CI freezes the version before building, so version_frozen.py ships
inside it - but "Update now" never appeared, because the webhook behind it
was installed from scripts/, a folder such an installation does not have.
And update.sh could not have served it anyway: it does git pull + rebuild.

- both installers accept being piped through bash (curl | sudo bash). They
  then download what they need into /opt/mc-webui-{updater,watchdog} and
  find the instance by looking for docker-compose.yml. A checkout install
  keeps running its scripts from the checkout, unchanged
- updater.py picks the update path per request: git checkout -> update.sh,
  bare docker-compose.yml -> the new update-image.sh (pull + recreate).
  Resolved per request, so cloning the repo later flips it without a restart
- both update scripts end with MC_UPDATE_RESULT=updated|no_change, which
  the webhook parses and /api/updater/status passes on. The UI used to
  watch only for the version to change and then claim it timed out after
  two minutes - the normal outcome right after a push, while the image for
  that commit is still building. It now says so as soon as the run ends,
  in a neutral state rather than a red cross
- the marker prints only when stdout is not a terminal, so mcupdate output
  stays as it was

Also corrects the claim, in the README and in updater.py's own docstring,
that the webhook listens on localhost. It binds 0.0.0.0 - the container
reaches it over the Docker bridge - and sends no token, so port 5050 is
worth keeping off untrusted networks.

Verified: update-image.sh reports updated then no_change against a real
docker compose project; the standalone installer installs, runs and
uninstalls cleanly under systemd (WSL), with the app reaching it through
host.docker.internal and a triggered run coming back changed=false; the
resulting modal checked in a browser in both en and pl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 18:53:35 +02:00

255 lines
8.1 KiB
Bash
Executable File

#!/bin/bash
#
# mc-webui Update Webhook Installer
#
# This script installs the update webhook service that allows
# remote updates from the mc-webui GUI.
#
# Two ways to run it:
#
# From a git checkout:
# sudo ./install.sh
#
# Without the repository (Docker Hub installations) - run this from the
# folder holding your docker-compose.yml, it downloads what it needs:
# curl -fsSL https://raw.githubusercontent.com/MarekWo/mc-webui/main/scripts/updater/install.sh | sudo bash
#
# Uninstall:
# sudo ./install.sh --uninstall
# curl -fsSL <same url> | sudo bash -s -- --uninstall
#
# Environment:
# MCWEBUI_DIR - your mc-webui folder, if it cannot be found automatically
# MC_BRANCH - branch to download from in standalone mode (default: main)
# MC_RAW_BASE - download from somewhere else entirely (fork or mirror)
#
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
# Check if running as root
if [ "$EUID" -ne 0 ]; then
error "Please run as root: sudo $0"
fi
SERVICE_NAME="mc-webui-updater"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
STANDALONE_DIR="/opt/mc-webui-updater"
MC_BRANCH="${MC_BRANCH:-main}"
# MC_RAW_BASE lets a fork, a mirror, or a test run point the download
# somewhere other than this repository.
RAW_BASE="${MC_RAW_BASE:-https://raw.githubusercontent.com/MarekWo/mc-webui/${MC_BRANCH}/scripts/updater}"
# Uninstall
if [ "$1" == "--uninstall" ]; then
info "Uninstalling ${SERVICE_NAME}..."
if systemctl is-active --quiet "$SERVICE_NAME"; then
systemctl stop "$SERVICE_NAME"
info "Service stopped"
fi
if systemctl is-enabled --quiet "$SERVICE_NAME" 2>/dev/null; then
systemctl disable "$SERVICE_NAME"
info "Service disabled"
fi
if [ -f "$SERVICE_FILE" ]; then
rm "$SERVICE_FILE"
systemctl daemon-reload
info "Service file removed"
fi
# Only ever created by a standalone install, so it is safe to remove.
# A repository install keeps its scripts where the checkout has them.
if [ -d "$STANDALONE_DIR" ]; then
rm -rf "$STANDALONE_DIR"
info "Removed $STANDALONE_DIR"
fi
echo -e "${GREEN}Uninstallation complete!${NC}"
exit 0
fi
# ---------------------------------------------------------------------------
# Where are we running from, and what are we updating?
# ---------------------------------------------------------------------------
# Piped through bash (curl | sudo bash) leaves BASH_SOURCE pointing at stdin,
# so the presence of updater.py next to us is what tells the two apart.
SELF_DIR=""
if [ -n "${BASH_SOURCE[0]:-}" ] && [ -f "${BASH_SOURCE[0]}" ]; then
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
fi
# Find the folder holding docker-compose.yml - that is the mc-webui instance
find_instance_dir() {
local candidates=("$@")
local d
for d in "${candidates[@]}"; do
[ -n "$d" ] || continue
if [ -f "$d/docker-compose.yml" ] || [ -f "$d/docker-compose.yaml" ]; then
(cd "$d" && pwd)
return 0
fi
done
return 1
}
fetch() { # fetch <url> <destination>
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$1" -o "$2"
elif command -v wget >/dev/null 2>&1; then
wget -qO "$2" "$1"
else
error "Neither curl nor wget is available - cannot download $1"
fi
}
if [ -n "$SELF_DIR" ] && [ -f "$SELF_DIR/updater.py" ]; then
# Repository install: run the scripts straight out of the checkout, so
# they stay in step with the rest of the code on every git pull.
INSTALL_MODE="repository"
SCRIPT_DIR="$SELF_DIR"
MCWEBUI_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
else
INSTALL_MODE="standalone"
SCRIPT_DIR="$STANDALONE_DIR"
# $HOME is root's under sudo, so the invoking user's home is the one worth
# looking in - that is where the README tells people to put mc-webui.
SUDO_HOME=""
if [ -n "${SUDO_USER:-}" ]; then
SUDO_HOME="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
fi
MCWEBUI_DIR="$(find_instance_dir \
"${MCWEBUI_DIR:-}" \
"$(pwd)" \
"${SUDO_HOME:+$SUDO_HOME/mc-webui}" \
"$HOME/mc-webui" \
"/opt/mc-webui")" || error \
"Cannot find your mc-webui folder (no docker-compose.yml). Run this from that folder, or set MCWEBUI_DIR=/path/to/mc-webui."
fi
# Install
info "Installing ${SERVICE_NAME}..."
info " Install mode: $INSTALL_MODE"
info " mc-webui directory: $MCWEBUI_DIR"
if [ "$INSTALL_MODE" == "standalone" ]; then
info " Downloading from branch: $MC_BRANCH"
mkdir -p "$SCRIPT_DIR"
# Into a temp file first, so a failed download cannot truncate a working
# install of the service that is running right now.
for f in updater.py update-image.sh; do
tmp="$(mktemp)"
fetch "$RAW_BASE/$f" "$tmp" || error "Failed to download $f from $RAW_BASE"
[ -s "$tmp" ] || error "Downloaded $f is empty"
mv "$tmp" "$SCRIPT_DIR/$f"
chmod 644 "$SCRIPT_DIR/$f"
info " Downloaded $f"
done
chmod +x "$SCRIPT_DIR/update-image.sh"
fi
# Check if updater.py exists
if [ ! -f "$SCRIPT_DIR/updater.py" ]; then
error "updater.py not found in $SCRIPT_DIR"
fi
# Sanity-check that there is something here to update at all
if [ ! -d "$MCWEBUI_DIR/.git" ] \
&& [ ! -f "$MCWEBUI_DIR/docker-compose.yml" ] \
&& [ ! -f "$MCWEBUI_DIR/docker-compose.yaml" ]; then
error "$MCWEBUI_DIR is neither a git checkout nor a docker-compose folder"
fi
# Configure git safe.directory for root (required since service runs as root).
# Only relevant to a checkout - a Docker Hub install has no repository.
if [ -d "$MCWEBUI_DIR/.git" ]; then
info "Configuring git safe.directory..."
git config --global --add safe.directory "$MCWEBUI_DIR" 2>/dev/null || true
fi
# Create service file with correct paths
info "Creating systemd service file..."
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=mc-webui Update Webhook Server
Documentation=https://github.com/MarekWo/mc-webui
After=network.target docker.service
[Service]
Type=simple
User=root
Environment=MCWEBUI_DIR=${MCWEBUI_DIR}
Environment=UPDATER_TOKEN=
ExecStart=/usr/bin/python3 -u ${SCRIPT_DIR}/updater.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
info "Reloading systemd..."
systemctl daemon-reload
info "Enabling service..."
systemctl enable "$SERVICE_NAME"
# restart, not start: re-running the installer is how you upgrade a
# standalone install, and a running service would otherwise keep the old code
info "Starting service..."
systemctl restart "$SERVICE_NAME"
# Wait a moment for service to start
sleep 2
# Check if service is running
if systemctl is-active --quiet "$SERVICE_NAME"; then
info "Service is running!"
# Test health endpoint
if command -v curl &> /dev/null; then
HEALTH=$(curl -s http://127.0.0.1:5050/health 2>/dev/null || echo "")
if echo "$HEALTH" | grep -q '"status": *"ok"'; then
info "Health check passed!"
# Which update path did it settle on? Worth showing - it is the
# difference between "git pull + rebuild" and "docker compose pull".
DETECTED=$(echo "$HEALTH" | grep -o '"mode": *"[^"]*"' | cut -d'"' -f4)
[ -n "$DETECTED" ] && info "Detected update mode: $DETECTED"
else
warn "Health check failed - service may still be starting"
fi
fi
else
error "Service failed to start. Check: journalctl -u $SERVICE_NAME"
fi
echo ""
echo -e "${GREEN}Installation complete!${NC}"
echo ""
echo "The update webhook is now running on port 5050."
echo "You can now use the 'Update' button in mc-webui GUI."
echo ""
echo "Useful commands:"
echo " systemctl status $SERVICE_NAME # Check status"
echo " sudo journalctl -u $SERVICE_NAME -f # View logs"
if [ "$INSTALL_MODE" == "standalone" ]; then
echo " curl -fsSL $RAW_BASE/install.sh | sudo bash -s -- --uninstall"
else
echo " sudo $0 --uninstall # Uninstall"
fi