bunker-admin 0c634e100f Replace custom code-server (9GB) with upstream LinuxServer image (~1GB)
Drop the custom Dockerfile.code-server that bundled Claude Code CLI,
Python/MkDocs tooling, and build-essential on top of codercom base.
Switch to the already-mirrored linuxserver/code-server image instead.

- Both compose files: use code-server:latest, LinuxServer env vars
  (PUID/PGID/DEFAULT_WORKSPACE), port 8443, /config mount layout
- Nginx configs + templates: proxy to :8443 instead of :8080
- API env default: CODE_SERVER_URL updated to :8443
- build-and-push.sh: remove --include-code-server flag
- upgrade.sh: remove code-server conditional rebuild + registry fallback
- install.sh: add --ignore-pull-failures for optional missing images
- .env.example, CCP templates, bunker-ops template: updated

Bunker Admin
2026-03-25 20:10:36 -06:00

375 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# Changemaker Lite — One-Line Installer
#
# Downloads the latest release tarball and runs the configuration wizard.
#
# Usage:
# curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
# bash install.sh [OPTIONS]
#
# Options:
# --dir DIR Install directory (default: ~/changemaker.lite)
# --version TAG Specific version tag (default: latest release)
# --tarball FILE Use a local tarball instead of downloading
# --help Show this help
# =============================================================================
set -euo pipefail
GITEA_URL="https://gitea.bnkops.com"
REPO="admin/changemaker.lite"
INSTALL_DIR="${HOME}/changemaker.lite"
VERSION=""
LOCAL_TARBALL=""
MIN_DISK_MB=10000
HEALTH_TIMEOUT=180
HEALTH_INTERVAL=5
# --- State flags for cleanup ---
EXTRACT_DIR=""
TARBALL_PATH=""
CONFIG_COMPLETE=false
# --- Colors ---
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m'
BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC=''
fi
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
# --- Cleanup on failure ---
cleanup() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
# Clean up temp extraction directory
if [[ -n "$EXTRACT_DIR" ]] && [[ -d "$EXTRACT_DIR" ]]; then
rm -rf "$EXTRACT_DIR"
fi
# Clean up downloaded tarball (but not user-provided ones)
if [[ -z "$LOCAL_TARBALL" ]] && [[ -n "$TARBALL_PATH" ]] && [[ -f "$TARBALL_PATH" ]]; then
rm -f "$TARBALL_PATH"
fi
# Remove install dir only if config wizard never ran (no user data to lose)
if [[ "$CONFIG_COMPLETE" == "false" ]] && [[ -d "$INSTALL_DIR" ]] && [[ ! -f "$INSTALL_DIR/.env" ]]; then
rm -rf "$INSTALL_DIR"
fi
echo ""
error "Installation failed. See errors above."
fi
}
trap cleanup EXIT
# --- Arg parser ---
while [[ $# -gt 0 ]]; do
case "$1" in
--dir) INSTALL_DIR="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--tarball) LOCAL_TARBALL="$2"; shift 2 ;;
--help|-h)
sed -n '2,20p' "$0" | grep '^#' | sed 's/^# \?//'
exit 0 ;;
*) shift ;;
esac
done
echo -e "${BOLD}Changemaker Lite — Installer${NC}"
echo ""
# =============================================================================
# Step 1: Check prerequisites
# =============================================================================
info "Checking prerequisites..."
MISSING=()
command -v docker >/dev/null 2>&1 || MISSING+=("docker")
docker compose version >/dev/null 2>&1 || MISSING+=("docker-compose-v2")
command -v openssl >/dev/null 2>&1 || MISSING+=("openssl")
command -v curl >/dev/null 2>&1 || MISSING+=("curl")
if [[ ${#MISSING[@]} -gt 0 ]]; then
error "Missing required tools: ${MISSING[*]}"
echo ""
echo "Install Docker: https://docs.docker.com/engine/install/"
echo "Install OpenSSL: apt install openssl (or equivalent)"
exit 1
fi
success "Prerequisites OK (Docker $(docker --version | grep -oP '\d+\.\d+\.\d+'), OpenSSL available)"
# Docker daemon must be running (not just installed)
if ! docker info >/dev/null 2>&1; then
error "Docker daemon is not running."
echo ""
echo " Start it with: sudo systemctl start docker"
echo " Or: sudo service docker start"
exit 1
fi
success "Docker daemon is running"
# Disk space check
AVAILABLE_MB=$(df -m "$(dirname "$INSTALL_DIR")" | awk 'NR==2 {print $4}')
if [[ "$AVAILABLE_MB" -lt "$MIN_DISK_MB" ]]; then
error "Insufficient disk space: ${AVAILABLE_MB}MB available, ${MIN_DISK_MB}MB required."
echo ""
echo " The full stack (images + volumes) needs ~10GB of free space."
exit 1
fi
success "Disk space: ${AVAILABLE_MB}MB available (${MIN_DISK_MB}MB required)"
# =============================================================================
# Step 2: Check install directory
# =============================================================================
if [[ -d "$INSTALL_DIR" ]]; then
if [[ -f "$INSTALL_DIR/docker-compose.yml" ]]; then
error "Changemaker Lite is already installed at $INSTALL_DIR"
echo " To upgrade: cd $INSTALL_DIR && ./scripts/upgrade.sh"
echo " To reinstall: rm -rf $INSTALL_DIR && re-run this script"
exit 1
fi
fi
# =============================================================================
# Step 3: Get tarball
# =============================================================================
if [[ -n "$LOCAL_TARBALL" ]]; then
if [[ ! -f "$LOCAL_TARBALL" ]]; then
error "Tarball not found: $LOCAL_TARBALL"
exit 1
fi
TARBALL_PATH="$LOCAL_TARBALL"
info "Using local tarball: $LOCAL_TARBALL"
else
# Determine download URL
if [[ -n "$VERSION" ]]; then
RELEASE_URL="${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${VERSION}"
else
RELEASE_URL="${GITEA_URL}/api/v1/repos/${REPO}/releases/latest"
fi
info "Fetching release info from Gitea..."
RELEASE_JSON=$(curl -sf "$RELEASE_URL" 2>/dev/null || true)
if [[ -z "$RELEASE_JSON" ]]; then
error "Could not fetch release info from ${GITEA_URL}"
echo ""
echo "If the server is unreachable:"
echo " 1. Download the tarball manually from ${GITEA_URL}/${REPO}/releases"
echo " 2. Run: bash install.sh --tarball /path/to/changemaker-lite-*.tar.gz"
exit 1
fi
TARBALL_URL=$(echo "$RELEASE_JSON" | python3 -c "
import sys, json
data = json.load(sys.stdin)
assets = data.get('assets', [])
for a in assets:
if a['name'].endswith('.tar.gz'):
print(a['browser_download_url'])
break
" 2>/dev/null || true)
if [[ -z "$TARBALL_URL" ]]; then
error "No tarball found in the release. Check ${GITEA_URL}/${REPO}/releases"
exit 1
fi
RELEASE_TAG=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tag_name','unknown'))" 2>/dev/null)
info "Downloading Changemaker Lite ${RELEASE_TAG}..."
TARBALL_PATH="/tmp/changemaker-lite-install.tar.gz"
curl -fSL "$TARBALL_URL" -o "$TARBALL_PATH"
success "Downloaded $(du -h "$TARBALL_PATH" | cut -f1)"
fi
# =============================================================================
# Step 4: Extract
# =============================================================================
info "Extracting to ${INSTALL_DIR}..."
mkdir -p "$(dirname "$INSTALL_DIR")"
# Extract to temp, then move (handles tarball root directory naming)
EXTRACT_DIR=$(mktemp -d)
tar xzf "$TARBALL_PATH" -C "$EXTRACT_DIR"
# Find the extracted directory (tarball might have any root name)
EXTRACTED=$(find "$EXTRACT_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)
if [[ -z "$EXTRACTED" ]]; then
error "Tarball extraction failed — no directory found"
exit 1
fi
mv "$EXTRACTED" "$INSTALL_DIR"
rm -rf "$EXTRACT_DIR"
EXTRACT_DIR="" # Clear so cleanup doesn't try to remove it
# Clean up downloaded tarball
if [[ -z "$LOCAL_TARBALL" ]] && [[ -f "$TARBALL_PATH" ]]; then
rm -f "$TARBALL_PATH"
fi
success "Extracted to ${INSTALL_DIR}"
# =============================================================================
# Step 5: Run config wizard
# =============================================================================
echo ""
echo -e "${BOLD}Starting configuration wizard...${NC}"
echo ""
cd "$INSTALL_DIR"
# Redirect stdin from terminal — when piped (curl | bash), stdin is the pipe,
# so interactive prompts in config.sh would read garbage instead of user input.
if [[ -e /dev/tty ]]; then
bash config.sh </dev/tty
else
bash config.sh
fi
CONFIG_COMPLETE=true
# =============================================================================
# Step 6: Start services
# =============================================================================
echo ""
echo -e "${BOLD}Configuration complete!${NC}"
echo ""
START_SERVICES="y"
if [[ -t 0 ]]; then
read -rp "Start all services now? [Y/n]: " START_SERVICES
START_SERVICES=${START_SERVICES:-y}
elif [[ -e /dev/tty ]]; then
read -rp "Start all services now? [Y/n]: " START_SERVICES </dev/tty
START_SERVICES=${START_SERVICES:-y}
fi
if [[ "$START_SERVICES" =~ ^[Yy]$ ]]; then
echo ""
info "Pulling images from registry (this may take a few minutes on first run)..."
echo ""
cd "$INSTALL_DIR"
# --ignore-pull-failures: optional services may not have images in the
# registry yet — don't abort the whole install for those.
if ! docker compose pull --ignore-pull-failures 2>&1; then
echo ""
error "Failed to pull images from the registry."
echo ""
echo " Check that the registry is reachable:"
echo " curl -sf https://gitea.bnkops.com/api/v1/repos/admin/changemaker.lite/releases/latest"
echo ""
echo " If pulling from a private registry, log in first:"
echo " docker login gitea.bnkops.com"
echo ""
echo " Then retry:"
echo " cd ${INSTALL_DIR} && docker compose pull && docker compose up -d"
exit 1
fi
success "All images pulled"
echo ""
info "Starting services..."
if ! docker compose up -d 2>&1; then
echo ""
error "Failed to start services."
echo " Check logs: docker compose logs --tail 30"
exit 1
fi
# --- Post-startup health verification ---
echo ""
info "Waiting for services to become healthy (up to ${HEALTH_TIMEOUT}s)..."
info " Database migrations and seeding run automatically on first boot."
echo ""
CORE_SERVICES=("v2-postgres" "redis" "api" "admin")
ELAPSED=0
ALL_HEALTHY=false
while [[ $ELAPSED -lt $HEALTH_TIMEOUT ]]; do
ALL_HEALTHY=true
for svc in "${CORE_SERVICES[@]}"; do
# Detect crashed containers early
state=$(docker compose ps "$svc" --format '{{.State}}' 2>/dev/null || echo "missing")
if [[ "$state" == "exited" || "$state" == "dead" ]]; then
echo ""
error "Service '${svc}' exited unexpectedly. Last logs:"
docker compose logs "$svc" --tail 20 2>/dev/null || true
echo ""
error "Fix the issue and retry: cd ${INSTALL_DIR} && docker compose up -d"
exit 1
fi
# Check health status
health=$(docker compose ps "$svc" --format '{{.Health}}' 2>/dev/null || echo "")
if [[ "$health" != "healthy" ]]; then
ALL_HEALTHY=false
fi
done
if [[ "$ALL_HEALTHY" == "true" ]]; then
break
fi
sleep "$HEALTH_INTERVAL"
ELAPSED=$((ELAPSED + HEALTH_INTERVAL))
done
# Print status table
echo ""
echo -e "${BOLD} Service Status:${NC}"
for svc in "${CORE_SERVICES[@]}"; do
health=$(docker compose ps "$svc" --format '{{.Health}}' 2>/dev/null || echo "unknown")
state=$(docker compose ps "$svc" --format '{{.State}}' 2>/dev/null || echo "unknown")
if [[ "$health" == "healthy" ]]; then
echo -e " ${GREEN}[healthy]${NC} $svc"
elif [[ "$state" == "running" ]]; then
echo -e " ${YELLOW}[starting]${NC} $svc"
else
echo -e " ${RED}[${state}]${NC} $svc"
fi
done
echo ""
if [[ "$ALL_HEALTHY" == "true" ]]; then
success "All core services are healthy! (${ELAPSED}s)"
echo ""
echo " Admin GUI: http://localhost:3000"
echo " API: http://localhost:4000"
echo ""
echo " Check full stack: docker compose ps"
echo " View API logs: docker compose logs -f api --tail 20"
else
warn "Some services are still starting after ${HEALTH_TIMEOUT}s."
echo ""
echo " This may be normal on first boot (migrations + seeding can be slow)."
echo " Monitor progress with:"
echo " docker compose logs -f api --tail 30"
echo ""
echo " Check status with:"
echo " docker compose ps"
fi
else
echo ""
info "Skipped. Start services manually when ready:"
echo ""
echo " cd ${INSTALL_DIR} && docker compose up -d"
echo ""
echo " Pre-built images will be pulled from the registry (~2 min first time)."
echo " Database migrations and seeding run automatically on startup."
fi
echo ""
echo -e "${BOLD}${GREEN}Installation complete!${NC}"
echo ""
echo -e " \033[0;33mIMPORTANT: Change your admin password after first login!\033[0m"
echo ""