#!/usr/bin/env bash # ============================================================================= # Changemaker Lite V2 — Mirror ALL Images to Gitea Registry # # Pulls every Docker image used in docker-compose.prod.yml and re-pushes it # to the Gitea container registry. After mirroring, release installs can # pull the entire platform from a single registry — no Docker Hub, GHCR, # LSCR, GCR, or other external registries needed. # # Usage: # ./scripts/mirror-images.sh [OPTIONS] # # Options: # --core-only Mirror only the essential infrastructure images (postgres, # redis, alpine) — skip platform and communication services # --dry-run Print commands without executing # --registry Override registry (default: gitea.bnkops.com/admin) # --help Show this help # # Prerequisites: # docker login gitea.bnkops.com # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # --- Defaults --- REGISTRY="${GITEA_REGISTRY:-gitea.bnkops.com/admin}" CORE_ONLY=false DRY_RUN=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' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' CYAN='' 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; } run() { if [[ "$DRY_RUN" == "true" ]]; then echo -e "${CYAN}[DRY-RUN]${NC} $*"; else eval "$@"; fi; } # --- Arg parser --- while [[ $# -gt 0 ]]; do case "$1" in --core-only) CORE_ONLY=true; shift ;; --dry-run) DRY_RUN=true; shift ;; --registry) REGISTRY="$2"; shift 2 ;; --help|-h) sed -n '2,30p' "$0" | grep '^#' | sed 's/^# \?//' exit 0 ;; # Legacy flag — now default behavior --all) shift ;; *) error "Unknown option: $1"; exit 1 ;; esac done # ============================================================================= # IMAGE MAP — source → destination name in Gitea registry # # Naming conventions: # - Simple names kept as-is: postgres, redis, alpine, mongo, mysql, nats # - Namespaced → short name: nocodb/nocodb → nocodb, listmonk/listmonk → listmonk # - Conflicts resolved explicitly: # gotify/server → gotify (not "server") # vaultwarden/server → vaultwarden (not "server") # jitsi/web → jitsi-web (not "web") # lscr.io/.../nginx → ls-nginx (not "nginx", avoids changemaker-nginx conflict) # alpine/curl → alpine-curl (not "curl") # - Untagged images get :latest appended # ============================================================================= # --- Core infrastructure (always mirrored) --- declare -A CORE_IMAGES=( # PostgreSQL ["postgres:16-alpine"]="postgres:16-alpine" ["postgres:17-alpine"]="postgres:17-alpine" # Redis ["redis:7-alpine"]="redis:7-alpine" # Alpine (init containers) ["alpine:3"]="alpine:3" ["alpine/curl:8.11.1"]="alpine-curl:8.11.1" ) # --- Platform services --- declare -A PLATFORM_IMAGES=( # NocoDB ["nocodb/nocodb:0.301.3"]="nocodb:0.301.3" # Listmonk ["listmonk/listmonk:v6.0.0"]="listmonk:v6.0.0" # MailHog ["mailhog/mailhog:v1.0.1"]="mailhog:v1.0.1" # MkDocs ["squidfunk/mkdocs-material:latest"]="mkdocs-material:latest" # MkDocs static site server (LinuxServer nginx — NOT our custom nginx) ["lscr.io/linuxserver/nginx:1.28.2"]="ls-nginx:1.28.2" # n8n workflow automation ["docker.n8n.io/n8nio/n8n:latest"]="n8n:latest" # Homepage dashboard ["ghcr.io/gethomepage/homepage:v0.7.2"]="homepage:v0.7.2" # Gitea + MySQL ["gitea/gitea:1.23.7"]="gitea:1.23.7" ["mysql:8"]="mysql:8" # Mini QR ["ghcr.io/lyqht/mini-qr:v0.26.0"]="mini-qr:v0.26.0" # Excalidraw ["excalidraw/excalidraw:latest"]="excalidraw:latest" # Vaultwarden ["vaultwarden/server:1.35.4"]="vaultwarden:1.35.4" # Docker socket proxy ["tecnativa/docker-socket-proxy:v0.4.2"]="docker-socket-proxy:v0.4.2" # Pangolin tunnel ["fosrl/newt:latest"]="newt:latest" # Plain nginx (CCP instances: reverse proxy + mkdocs-site-server) ["nginx:alpine"]="nginx:alpine" # Code Server (LinuxServer) ["lscr.io/linuxserver/code-server:latest"]="code-server:latest" ) # --- Communication services (Rocket.Chat, Gancio, Jitsi) --- declare -A COMMS_IMAGES=( # Rocket.Chat + dependencies ["rocketchat/rocket.chat:7.9.7"]="rocketchat:7.9.7" ["mongo:6.0"]="mongo:6.0" ["nats:2.11-alpine"]="nats:2.11-alpine" # Gancio events ["cisti/gancio:1.28.2"]="gancio:1.28.2" # Jitsi Meet (4 containers) ["jitsi/web:stable-9823"]="jitsi-web:stable-9823" ["jitsi/prosody:stable-9823"]="jitsi-prosody:stable-9823" ["jitsi/jicofo:stable-9823"]="jitsi-jicofo:stable-9823" ["jitsi/jvb:stable-9823"]="jitsi-jvb:stable-9823" ) # --- Monitoring stack --- declare -A MONITORING_IMAGES=( ["prom/prometheus:v3.10.0"]="prometheus:v3.10.0" ["grafana/grafana:12.3.0"]="grafana:12.3.0" ["prom/alertmanager:v0.31.1"]="alertmanager:v0.31.1" ["oliver006/redis_exporter:v1.81.0"]="redis_exporter:v1.81.0" ["gcr.io/cadvisor/cadvisor:v0.55.1"]="cadvisor:v0.55.1" ["prom/node-exporter:v1.10.2"]="node-exporter:v1.10.2" ["gotify/server:2.9.0"]="gotify:v2.9.0" ) # ============================================================================= mirror_image() { local source="$1" local dest_name="$2" local dest="${REGISTRY}/${dest_name}" info "Mirroring ${source} → ${dest}..." run docker pull "${source}" run docker tag "${source}" "${dest}" run docker push "${dest}" success "Mirrored ${dest_name}" } mirror_map() { local -n map=$1 local label="$2" echo "" echo -e "${BOLD}── ${label} (${#map[@]} images) ──${NC}" for source in "${!map[@]}"; do if ! mirror_image "$source" "${map[$source]}"; then FAILED+=("$source") warn "Failed to mirror: $source (continuing...)" else ((MIRRORED++)) || true fi done } echo -e "${BOLD}Changemaker Lite — Mirror ALL Images to Gitea${NC}" echo " Registry: $REGISTRY" echo " Core only: $CORE_ONLY" echo "" FAILED=() MIRRORED=0 TOTAL=0 # Always mirror core for _ in "${!CORE_IMAGES[@]}"; do ((TOTAL++)) || true; done mirror_map CORE_IMAGES "Core Infrastructure" if [[ "$CORE_ONLY" != "true" ]]; then for _ in "${!PLATFORM_IMAGES[@]}"; do ((TOTAL++)) || true; done for _ in "${!COMMS_IMAGES[@]}"; do ((TOTAL++)) || true; done for _ in "${!MONITORING_IMAGES[@]}"; do ((TOTAL++)) || true; done mirror_map PLATFORM_IMAGES "Platform Services" mirror_map COMMS_IMAGES "Communication Services" mirror_map MONITORING_IMAGES "Monitoring Stack" else warn "Skipping platform, communication, and monitoring images (--core-only)" fi echo "" echo -e "${BOLD}── Summary ──${NC}" if [[ ${#FAILED[@]} -eq 0 ]]; then success "All ${MIRRORED} images mirrored to ${REGISTRY}" else warn "Mirrored ${MIRRORED}/${TOTAL} images" error "Failed: ${FAILED[*]}" exit 1 fi