#!/usr/bin/env bash ###################################################################################### #### install-webtop.sh — Deploy LinuxServer.io Webtop behind nginx reverse proxy #### #### Installs Docker, runs Webtop container with persistent config, configures #### #### nginx with basic auth and WebSocket proxy for KasmVNC. #### #### Requires: Ubuntu 22.04+, root access #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version 1.00 #### #### #### #### Usage: #### #### sudo ./install-webtop.sh --domain webtop.example.com #### #### sudo ./install-webtop.sh --domain webtop.example.com --no-ssl #### #### sudo ./install-webtop.sh --domain webtop.example.com --allow-ip 1.2.3.4 #### #### #### #### See --help for all options. #### ###################################################################################### set -euo pipefail # ── Defaults ────────────────────────────────────────────────────────── DOMAIN="" DESKTOP="fedora-mate" WEBTOP_IMAGE="" CONTAINER_NAME="webtop" CONFIG_DIR="/opt/webtop" INTERNAL_PORT="3000" TIMEZONE="${TZ:-America/Chicago}" SHM_SIZE="1gb" ALLOW_IP="" NO_SSL=false AUTH_USER="admin" AUTH_PASS="" SKIP_DOCKER=false # ── Colors ──────────────────────────────────────────────────────────── if [[ -t 1 ]]; then GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m' BLUE='\033[0;34m' BOLD='\033[1m' RESET='\033[0m' else GREEN="" YELLOW="" RED="" BLUE="" BOLD="" RESET="" fi log() { echo -e "${GREEN}[OK]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } step() { echo -e "\n${BOLD}── $* ──${RESET}"; } # ── Help ────────────────────────────────────────────────────────────── show_help() { cat <<'EOF' Usage: install-webtop.sh [OPTIONS] Deploy LinuxServer.io Webtop (Fedora MATE) behind nginx reverse proxy with basic auth, WebSocket support, and optional Let's Encrypt SSL. Required: --domain DOMAIN Domain name for nginx vhost and SSL cert Options: --desktop DESKTOP Desktop environment (default: fedora-mate) Options: fedora-mate, fedora-xfce, fedora-kde, ubuntu-mate, ubuntu-xfce, ubuntu-kde, alpine-xfce, alpine-kde --image IMAGE Override with a custom image tag --name NAME Container name (default: webtop) --config-dir PATH Persistent config directory (default: /opt/webtop) --port PORT Internal KasmVNC port (default: 3000) --tz TIMEZONE Timezone (default: America/Chicago) --shm-size SIZE Shared memory size (default: 1gb) --allow-ip IP Restrict access to this IP in UFW (optional) --auth-user USER Basic auth username (default: admin) --auth-pass PASS Basic auth password (prompted if not set) --no-ssl Skip Let's Encrypt — use HTTP only --skip-docker Skip Docker installation (already installed) -h, --help Show this help Examples: sudo ./install-webtop.sh --domain webtop.example.com sudo ./install-webtop.sh --domain webtop.example.com --allow-ip 203.0.113.50 sudo ./install-webtop.sh --domain webtop.example.com --desktop ubuntu-xfce sudo ./install-webtop.sh --domain webtop.example.com --no-ssl --auth-pass s3cret EOF exit 0 } # ── Parse Arguments ─────────────────────────────────────────────────── parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --domain) DOMAIN="$2"; shift 2 ;; --desktop) DESKTOP="$2"; shift 2 ;; --image) WEBTOP_IMAGE="$2"; shift 2 ;; --name) CONTAINER_NAME="$2"; shift 2 ;; --config-dir) CONFIG_DIR="$2"; shift 2 ;; --port) INTERNAL_PORT="$2"; shift 2 ;; --tz) TIMEZONE="$2"; shift 2 ;; --shm-size) SHM_SIZE="$2"; shift 2 ;; --allow-ip) ALLOW_IP="$2"; shift 2 ;; --auth-user) AUTH_USER="$2"; shift 2 ;; --auth-pass) AUTH_PASS="$2"; shift 2 ;; --no-ssl) NO_SSL=true; shift ;; --skip-docker) SKIP_DOCKER=true; shift ;; -h|--help) show_help ;; *) err "Unknown option: $1"; echo "Run with --help for usage."; exit 1 ;; esac done if [[ -z "$DOMAIN" ]]; then err "--domain is required" exit 1 fi # resolve image from desktop choice (--image overrides) if [[ -z "$WEBTOP_IMAGE" ]]; then local valid_desktops="fedora-mate fedora-xfce fedora-kde ubuntu-mate ubuntu-xfce ubuntu-kde alpine-xfce alpine-kde" if ! echo "$valid_desktops" | grep -qw "$DESKTOP"; then err "Unknown desktop: ${DESKTOP}" echo "Valid options: ${valid_desktops}" exit 1 fi WEBTOP_IMAGE="lscr.io/linuxserver/webtop:${DESKTOP}" fi } # ── Root Check ──────────────────────────────────────────────────────── check_root() { if [[ $EUID -ne 0 ]]; then err "This script must be run as root." exit 1 fi } # ── Install Docker ──────────────────────────────────────────────────── install_docker() { step "Docker" if [[ "$SKIP_DOCKER" == "true" ]]; then log "Skipping Docker install (--skip-docker)" return fi if command -v docker &>/dev/null; then log "Docker already installed: $(docker --version)" return fi curl -fsSL https://get.docker.com | sh systemctl enable --now docker log "Docker installed: $(docker --version)" } # ── Create Config Directory ─────────────────────────────────────────── setup_config_dir() { step "Config directory" mkdir -p "${CONFIG_DIR}" log "Persistent config: ${CONFIG_DIR}" } # ── Run Webtop Container ───────────────────────────────────────────── run_webtop() { step "Webtop container" if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then warn "Container '${CONTAINER_NAME}' already exists — removing" docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 fi docker run -d \ --name "${CONTAINER_NAME}" \ --restart unless-stopped \ -p "127.0.0.1:${INTERNAL_PORT}:3000" \ -e PUID=1000 \ -e PGID=1000 \ -e "TZ=${TIMEZONE}" \ -v "${CONFIG_DIR}:/config" \ --shm-size="${SHM_SIZE}" \ "${WEBTOP_IMAGE}" log "Container '${CONTAINER_NAME}' running (${WEBTOP_IMAGE})" log "Bound to 127.0.0.1:${INTERNAL_PORT} (nginx will proxy)" } # ── Install nginx ───────────────────────────────────────────────────── install_nginx() { step "nginx" if ! command -v nginx &>/dev/null; then apt-get update -qq apt-get install -y -qq nginx apache2-utils >/dev/null log "nginx installed" else log "nginx already installed" if ! command -v htpasswd &>/dev/null; then apt-get install -y -qq apache2-utils >/dev/null fi fi } # ── Basic Auth ──────────────────────────────────────────────────────── setup_auth() { step "Basic auth" if [[ -z "$AUTH_PASS" ]]; then AUTH_PASS=$(openssl rand -base64 16) fi htpasswd -bc /etc/nginx/.htpasswd_webtop "${AUTH_USER}" "${AUTH_PASS}" 2>/dev/null chown root:www-data /etc/nginx/.htpasswd_webtop chmod 640 /etc/nginx/.htpasswd_webtop # save credentials to a root-only file local creds_file="${CONFIG_DIR}/.credentials" echo "username=${AUTH_USER}" > "${creds_file}" echo "password=${AUTH_PASS}" >> "${creds_file}" chmod 600 "${creds_file}" log "Basic auth configured (user: ${AUTH_USER})" log "Credentials saved to ${creds_file} (root-only)" } # ── nginx Vhost ─────────────────────────────────────────────────────── configure_nginx() { step "nginx vhost" cat > "/etc/nginx/sites-available/${DOMAIN}" <&1 systemctl reload nginx log "nginx vhost configured: ${DOMAIN}" } # ── Let's Encrypt SSL ──────────────────────────────────────────────── setup_ssl() { step "SSL (Let's Encrypt)" if [[ "$NO_SSL" == "true" ]]; then warn "Skipping SSL (--no-ssl) — access via http://${DOMAIN}" return fi if ! command -v certbot &>/dev/null; then apt-get install -y -qq certbot python3-certbot-nginx >/dev/null log "certbot installed" fi certbot --nginx -d "${DOMAIN}" --non-interactive --agree-tos \ --register-unsafely-without-email --redirect log "SSL configured: https://${DOMAIN}" } # ── UFW Rules ───────────────────────────────────────────────────────── setup_ufw() { step "Firewall" if ! command -v ufw &>/dev/null; then warn "UFW not found — skipping firewall config" return fi ufw allow OpenSSH >/dev/null 2>&1 || true ufw allow 'Nginx Full' >/dev/null 2>&1 || true if [[ -n "$ALLOW_IP" ]]; then ufw deny from any to any port 80 >/dev/null 2>&1 || true ufw deny from any to any port 443 >/dev/null 2>&1 || true ufw allow from "${ALLOW_IP}" to any port 80 >/dev/null 2>&1 || true ufw allow from "${ALLOW_IP}" to any port 443 >/dev/null 2>&1 || true log "Access restricted to ${ALLOW_IP}" else log "nginx ports open (no IP restriction)" warn "Consider using --allow-ip to restrict access" fi if ! ufw status | grep -q "^Status: active"; then ufw --force enable >/dev/null 2>&1 log "UFW enabled" fi } # ── Summary ─────────────────────────────────────────────────────────── print_summary() { local proto="http" if [[ "$NO_SSL" == "false" ]]; then proto="https" fi echo "" echo "────────────────────────────────────────" echo -e "${BOLD}Webtop Deployment Complete${RESET}" echo "────────────────────────────────────────" echo " URL: ${proto}://${DOMAIN}" echo " Username: ${AUTH_USER}" echo " Password: ${AUTH_PASS}" echo " Creds file: ${CONFIG_DIR}/.credentials" echo " Image: ${WEBTOP_IMAGE}" echo " Container: ${CONTAINER_NAME}" echo " Config: ${CONFIG_DIR}" echo " Timezone: ${TIMEZONE}" if [[ -n "$ALLOW_IP" ]]; then echo " Allowed IP: ${ALLOW_IP}" fi echo "────────────────────────────────────────" echo "" echo "Manage:" echo " docker logs ${CONTAINER_NAME} # view logs" echo " docker restart ${CONTAINER_NAME} # restart" echo " docker stop ${CONTAINER_NAME} # stop" echo " docker start ${CONTAINER_NAME} # start" echo " ls ${CONFIG_DIR}/ # persistent data" } # ── Main ────────────────────────────────────────────────────────────── main() { parse_args "$@" check_root echo -e "${BOLD}Webtop Installer${RESET}" echo "Domain: ${DOMAIN}" echo "Image: ${WEBTOP_IMAGE}" echo "Config: ${CONFIG_DIR}" install_docker setup_config_dir run_webtop install_nginx setup_auth configure_nginx setup_ssl setup_ufw print_summary } main "$@"