From be2fa5d80bc0b368434bf600c4f0399de2c72734 Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Sun, 22 Mar 2026 19:17:10 -0600 Subject: [PATCH] Fix media-api restart loop and add registry build scripts - Fix @/utils/logger path alias (tsc doesn't transform @/ in output) - Add JWT_INVITE_SECRET to media-api compose environment block - Fix redis-exporter depends_on to use service name not container name - Fix upgrade.sh: restore tracked files deleted by restore_user_paths - Add scripts/build-and-push.sh for building + pushing production images - Add scripts/mirror-images.sh for mirroring third-party images Bunker Admin --- .../media/services/thumbnail.service.ts | 2 +- docker-compose.yml | 12 +- scripts/build-and-push.sh | 171 ++++++++++++++++++ scripts/mirror-images.sh | 126 +++++++++++++ scripts/upgrade.sh | 134 ++++++++++---- 5 files changed, 411 insertions(+), 34 deletions(-) create mode 100755 scripts/build-and-push.sh create mode 100755 scripts/mirror-images.sh diff --git a/api/src/modules/media/services/thumbnail.service.ts b/api/src/modules/media/services/thumbnail.service.ts index 3fb699cc..40f40914 100644 --- a/api/src/modules/media/services/thumbnail.service.ts +++ b/api/src/modules/media/services/thumbnail.service.ts @@ -1,7 +1,7 @@ import { spawn } from 'child_process'; import fs from 'fs/promises'; import path from 'path'; -import { logger } from '@/utils/logger'; +import { logger } from '../../../utils/logger'; export interface ThumbnailOptions { videoPath: string; diff --git a/docker-compose.yml b/docker-compose.yml index 67a23536..b6174aa7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: # Unified Express.js API api: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-api:${IMAGE_TAG:-local} build: context: ./api target: ${BUILD_TARGET:-development} @@ -97,6 +98,10 @@ services: - SMS_DEVICE_MONITOR_INTERVAL_MS=${SMS_DEVICE_MONITOR_INTERVAL_MS:-30000} # Docker container status via socket proxy (read-only, containers endpoint only) - DOCKER_PROXY_URL=http://docker-socket-proxy:2375 + # Container Registry + - GITEA_REGISTRY=${GITEA_REGISTRY:-gitea.bnkops.com/admin} + - GITEA_REGISTRY_USER=${GITEA_REGISTRY_USER:-} + - GITEA_REGISTRY_PASS=${GITEA_REGISTRY_PASS:-} volumes: - ./api:/app - /app/node_modules @@ -123,6 +128,7 @@ services: # Fastify Media API (Microservice for Media Management) media-api: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-media-api:${IMAGE_TAG:-local} build: context: ./api dockerfile: Dockerfile.media @@ -144,6 +150,7 @@ services: - REDIS_URL=redis://:${REDIS_PASSWORD}@redis-changemaker:6379 - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET} - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} + - JWT_INVITE_SECRET=${JWT_INVITE_SECRET} - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000,http://localhost:3100} - ENABLE_MEDIA_FEATURES=${ENABLE_MEDIA_FEATURES:-true} - MEDIA_ROOT=/media/local @@ -174,6 +181,7 @@ services: # React Admin GUI (Vite dev server) admin: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-admin:${IMAGE_TAG:-local} build: context: ./admin target: ${BUILD_TARGET:-development} @@ -228,6 +236,7 @@ services: # Nginx reverse proxy nginx: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-nginx:${IMAGE_TAG:-local} build: context: ./nginx container_name: changemaker-v2-nginx @@ -478,6 +487,7 @@ services: # Code Server — Browser IDE code-server: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-code-server:${IMAGE_TAG:-local} build: context: . dockerfile: Dockerfile.code-server @@ -1196,7 +1206,7 @@ services: - REDIS_PASSWORD=${REDIS_PASSWORD} restart: always depends_on: - - redis-changemaker + - redis networks: - changemaker-lite profiles: diff --git a/scripts/build-and-push.sh b/scripts/build-and-push.sh new file mode 100755 index 00000000..7d391061 --- /dev/null +++ b/scripts/build-and-push.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +# ============================================================================= +# Changemaker Lite V2 — Build & Push to Gitea Registry +# Builds production Docker images, tags with commit SHA + latest, and pushes +# to the Gitea container registry at gitea.bnkops.com/admin. +# +# Usage: +# ./scripts/build-and-push.sh [OPTIONS] +# +# Options: +# --services a,b,c Comma-separated list of services to build +# (default: api admin media-api nginx) +# --include-code-server Also build and push code-server (~9GB, slow) +# --no-push Build only, skip push (verify builds work) +# --tag TAG Override commit SHA tag (default: git rev-parse --short HEAD) +# --registry URL Override registry (default: gitea.bnkops.com/admin) +# --dry-run Print commands without executing +# --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}" +COMMIT_SHA="$(git -C "$PROJECT_DIR" rev-parse --short HEAD 2>/dev/null || echo "local")" +TAG="${COMMIT_SHA}" +SERVICES="api admin media-api nginx" +INCLUDE_CODE_SERVER=false +NO_PUSH=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 + --services) SERVICES="${2//,/ }"; shift 2 ;; + --include-code-server) INCLUDE_CODE_SERVER=true; shift ;; + --no-push) NO_PUSH=true; shift ;; + --tag) TAG="$2"; shift 2 ;; + --registry) REGISTRY="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + --help|-h) + sed -n '2,30p' "$0" | grep '^#' | sed 's/^# \?//' + exit 0 ;; + *) error "Unknown option: $1"; exit 1 ;; + esac +done + +# Add code-server if requested +if [[ "$INCLUDE_CODE_SERVER" == "true" ]]; then + SERVICES="$SERVICES code-server" +fi + +echo -e "${BOLD}Changemaker Lite — Build & Push${NC}" +echo " Registry: $REGISTRY" +echo " Tag: $TAG" +echo " Services: $SERVICES" +echo " Push: $([[ "$NO_PUSH" == "true" ]] && echo "no" || echo "yes")" +echo "" + +# --- Build mapping: service → dockerfile + context --- +build_service() { + local svc="$1" + local image="$REGISTRY/changemaker-${svc}:${TAG}" + local image_latest="$REGISTRY/changemaker-${svc}:latest" + + case "$svc" in + api) + info "Building api (Express + Prisma)..." + run docker buildx build \ + --target production \ + --tag "${image}" \ + --tag "${image_latest}" \ + --load \ + "${PROJECT_DIR}/api" + ;; + admin) + info "Building admin (React + Vite)..." + run docker buildx build \ + --target production \ + --tag "${image}" \ + --tag "${image_latest}" \ + --load \ + "${PROJECT_DIR}/admin" + ;; + media-api) + info "Building media-api (Fastify + FFmpeg)..." + run docker buildx build \ + --target production \ + --file "${PROJECT_DIR}/api/Dockerfile.media" \ + --tag "${image}" \ + --tag "${image_latest}" \ + --load \ + "${PROJECT_DIR}/api" + ;; + nginx) + info "Building nginx (reverse proxy)..." + run docker buildx build \ + --tag "${image}" \ + --tag "${image_latest}" \ + --load \ + "${PROJECT_DIR}/nginx" + ;; + code-server) + warn "Building code-server (~9GB) — this will take a while..." + run docker buildx build \ + --file "${PROJECT_DIR}/Dockerfile.code-server" \ + --tag "${image}" \ + --tag "${image_latest}" \ + --load \ + "${PROJECT_DIR}" + ;; + *) + error "Unknown service: $svc" + return 1 + ;; + esac + + success "Built ${svc} → ${image}" + + if [[ "$NO_PUSH" == "false" ]]; then + info "Pushing ${svc}:${TAG}..." + run docker push "${image}" + run docker push "${image_latest}" + success "Pushed ${svc}:${TAG} + :latest" + fi +} + +# --- Main --- +FAILED=() +for svc in $SERVICES; do + if ! build_service "$svc"; then + FAILED+=("$svc") + fi +done + +echo "" +if [[ ${#FAILED[@]} -eq 0 ]]; then + success "All services built${NO_PUSH:+ (not pushed)}." + echo "" + if [[ "$NO_PUSH" == "false" ]]; then + info "Images available in registry:" + for svc in $SERVICES; do + echo " ${REGISTRY}/changemaker-${svc}:${TAG}" + done + echo "" + info "To use these images: IMAGE_TAG=${TAG} docker compose pull ${SERVICES}" + info "Or set IMAGE_TAG=${TAG} in .env" + fi +else + error "Failed services: ${FAILED[*]}" + exit 1 +fi diff --git a/scripts/mirror-images.sh b/scripts/mirror-images.sh new file mode 100755 index 00000000..b6d54483 --- /dev/null +++ b/scripts/mirror-images.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# ============================================================================= +# Changemaker Lite V2 — Mirror Third-Party Images to Gitea Registry +# Pulls infrastructure images from Docker Hub and re-pushes to the Gitea +# container registry, protecting against Docker Hub rate limits. +# +# Usage: +# ./scripts/mirror-images.sh [OPTIONS] +# +# Options: +# --all Also mirror heavy optional images (Rocket.Chat, MongoDB, n8n, +# Gitea, Jitsi x4) in addition to core images +# --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}" +INCLUDE_ALL=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 + --all) INCLUDE_ALL=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 ;; + *) error "Unknown option: $1"; exit 1 ;; + esac +done + +# --- Core images (always mirrored) --- +CORE_IMAGES=( + "postgres:16-alpine" + "postgres:17-alpine" + "redis:7-alpine" + "nocodb/nocodb:0.301.3" + "listmonk/listmonk:v6.0.0" + "mailhog/mailhog:v1.0.1" + "prom/prometheus:v3.10.0" + "grafana/grafana:12.3.0" + "prom/alertmanager:v0.31.1" + "oliver006/redis_exporter:v1.81.0" + "gcr.io/cadvisor/cadvisor:v0.52.1" + "prom/node-exporter:v1.9.1" + "alpine:3" +) + +# --- Heavy optional images (--all flag) --- +OPTIONAL_IMAGES=( + "gitea/gitea:1.23" + "n8nio/n8n:1.83.2" + "rocket.chat:7.4.0" + "mongo:6.0" + "jitsi/web:stable-10168" + "jitsi/prosody:stable-10168" + "jitsi/jicofo:stable-10168" + "jitsi/jvb:stable-10168" + "gotify/server:2.6.1" +) + +mirror_image() { + local source="$1" + # Get short name: last path segment + tag + local name_tag="${source##*/}" + local dest="${REGISTRY}/${name_tag}" + + info "Mirroring ${source} → ${dest}..." + run docker pull "${source}" + run docker tag "${source}" "${dest}" + run docker push "${dest}" + success "Mirrored ${name_tag}" +} + +echo -e "${BOLD}Changemaker Lite — Mirror Images${NC}" +echo " Registry: $REGISTRY" +echo " All: $INCLUDE_ALL" +echo "" + +IMAGES=("${CORE_IMAGES[@]}") +if [[ "$INCLUDE_ALL" == "true" ]]; then + IMAGES+=("${OPTIONAL_IMAGES[@]}") + warn "Including heavy optional images — this will take a while" +fi + +FAILED=() +for img in "${IMAGES[@]}"; do + if ! mirror_image "$img"; then + FAILED+=("$img") + warn "Failed to mirror: $img (continuing...)" + fi +done + +echo "" +if [[ ${#FAILED[@]} -eq 0 ]]; then + success "All ${#IMAGES[@]} images mirrored to ${REGISTRY}" +else + warn "Mirrored $((${#IMAGES[@]} - ${#FAILED[@]}))/${#IMAGES[@]} images" + error "Failed: ${FAILED[*]}" + exit 1 +fi diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index aa9c0846..d1d1de21 100755 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -47,6 +47,7 @@ FORCE=false BRANCH="" ROLLBACK=false API_MODE=false +USE_REGISTRY=false # --- Colors (respects NO_COLOR convention) --- if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then @@ -145,7 +146,9 @@ restore_user_paths() { if [[ -e "$USER_SAVE_DIR/$p" ]]; then # Ensure parent directory exists (in case pull deleted it) mkdir -p "$PROJECT_DIR/$(dirname "$p")" - rm -rf "$PROJECT_DIR/$p" + # Use docker alpine to remove if regular rm fails (root-owned files from containers) + rm -rf "$PROJECT_DIR/$p" 2>/dev/null || \ + docker run --rm -v "$PROJECT_DIR:/project" alpine rm -rf "/project/$p" 2>/dev/null || true cp -a "$USER_SAVE_DIR/$p" "$PROJECT_DIR/$p" restored=$((restored + 1)) fi @@ -279,6 +282,7 @@ Usage: ./scripts/upgrade.sh [OPTIONS] Options: --skip-backup Skip backup phase (requires --force) --pull-services Also pull new third-party Docker images + --use-registry Pull pre-built images from Gitea registry instead of rebuilding --dry-run Show what would happen without executing --force Continue past non-critical warnings --branch BRANCH Git branch to pull (default: current branch) @@ -287,7 +291,8 @@ Options: --help Show this help message Examples: - ./scripts/upgrade.sh # Standard upgrade + ./scripts/upgrade.sh # Standard upgrade (build from source) + ./scripts/upgrade.sh --use-registry # Fast upgrade using pre-built Gitea images ./scripts/upgrade.sh --dry-run # Preview changes ./scripts/upgrade.sh --pull-services # Also update PostgreSQL, Redis, etc. ./scripts/upgrade.sh --rollback # Revert last upgrade @@ -304,6 +309,7 @@ while [[ $# -gt 0 ]]; do --branch) BRANCH="$2"; shift 2 ;; --rollback) ROLLBACK=true; shift ;; --api-mode) API_MODE=true; shift ;; + --use-registry) USE_REGISTRY=true; shift ;; --help|-h) show_help ;; *) error "Unknown option: $1"; echo "Run with --help for usage."; exit 1 ;; esac @@ -677,6 +683,15 @@ fi # Step 4b: Restore user-modifiable paths (unconditionally overwrites with saved copies) restore_user_paths +# Step 4c: Restore any tracked files accidentally deleted by restore_user_paths +# (can happen when save_user_paths can't read root-owned files in user paths) +DELETED_TRACKED="$(git ls-files --deleted 2>/dev/null || true)" +if [[ -n "$DELETED_TRACKED" ]]; then + info "Restoring $(echo "$DELETED_TRACKED" | wc -l | xargs) tracked file(s) deleted during restore..." + echo "$DELETED_TRACKED" | xargs git checkout HEAD -- 2>/dev/null || true + success "Tracked files restored from HEAD" +fi + # Step 5: Detect new env vars info "Checking for new environment variables..." if [[ -f "$PROJECT_DIR/.env.example" ]] && [[ -f "$PROJECT_DIR/.env" ]]; then @@ -729,37 +744,89 @@ fi # ============================================================================= phase "4" "Container Rebuild" -write_progress 4 "Container Rebuild" 50 "Rebuilding containers..." +write_progress 4 "Container Rebuild" 50 "Preparing containers..." -# Always rebuild source-built containers -info "Rebuilding source containers: $SOURCE_CONTAINERS" -docker compose build $SOURCE_CONTAINERS -success "Source containers rebuilt" - -# Conditionally rebuild containers whose Dockerfiles changed CHANGED_FILES="$(git diff --name-only "$PRE_UPGRADE_COMMIT..HEAD" 2>/dev/null || true)" -for svc in $CONDITIONAL_CONTAINERS; do - case "$svc" in - nginx) - if echo "$CHANGED_FILES" | grep -q "^nginx/"; then - info "Rebuilding nginx (config changed)..." - docker compose build nginx - success "nginx rebuilt" - else - info "nginx unchanged, skipping rebuild" + +if [[ "$USE_REGISTRY" == "true" ]]; then + # --- Registry pull path: pull pre-built production images from Gitea --- + REGISTRY="${GITEA_REGISTRY:-gitea.bnkops.com/admin}" + REGISTRY_TAG="$(git rev-parse --short HEAD 2>/dev/null || echo "latest")" + export GITEA_REGISTRY="$REGISTRY" + export IMAGE_TAG="$REGISTRY_TAG" + export BUILD_TARGET=production + + info "Registry mode: ${REGISTRY} (tag: ${REGISTRY_TAG})" + write_progress 4 "Container Rebuild" 55 "Pulling images from registry..." + + # Pull core app containers; fall back to source build if registry unavailable + if docker compose pull api admin media-api 2>/dev/null; then + success "Core images pulled from registry" + else + warn "Registry pull failed — falling back to source build" + docker compose build $SOURCE_CONTAINERS + success "Source containers rebuilt (registry fallback)" + fi + + # nginx: pull if available, else rebuild only if config changed + if ! docker compose pull nginx 2>/dev/null; then + if echo "$CHANGED_FILES" | grep -q "^nginx/"; then + info "Rebuilding nginx (config changed, not in registry)..." + docker compose build nginx + success "nginx rebuilt" + else + info "nginx unchanged, skipping rebuild" + fi + fi + + # code-server: pull from registry if Dockerfile changed; never build during upgrade + # (code-server is 9GB+ and takes 30+ min to build — run build-and-push.sh separately) + CS_IMAGE="${REGISTRY}/changemaker-code-server" + if docker image inspect "${CS_IMAGE}:${REGISTRY_TAG}" &>/dev/null 2>&1; then + info "code-server:${REGISTRY_TAG} already present, skipping" + elif docker compose pull code-server 2>/dev/null; then + success "code-server pulled from registry" + else + # Retag any existing local code-server image so compose up doesn't try to build it + for fallback_tag in local latest; do + if docker image inspect "${CS_IMAGE}:${fallback_tag}" &>/dev/null 2>&1; then + docker tag "${CS_IMAGE}:${fallback_tag}" "${CS_IMAGE}:${REGISTRY_TAG}" 2>/dev/null || true + info "Tagged code-server:${fallback_tag} → :${REGISTRY_TAG} (registry push pending)" + break fi - ;; - code-server) - if echo "$CHANGED_FILES" | grep -q "^Dockerfile.code-server"; then - info "Rebuilding code-server (Dockerfile changed)..." - docker compose build code-server - success "code-server rebuilt" - else - info "code-server unchanged, skipping rebuild" - fi - ;; - esac -done + done + fi + +else + # --- Source build path (original behaviour) --- + info "Rebuilding source containers: $SOURCE_CONTAINERS" + docker compose build $SOURCE_CONTAINERS + success "Source containers rebuilt" + + # Conditionally rebuild containers whose Dockerfiles changed + for svc in $CONDITIONAL_CONTAINERS; do + case "$svc" in + nginx) + if echo "$CHANGED_FILES" | grep -q "^nginx/"; then + info "Rebuilding nginx (config changed)..." + docker compose build nginx + success "nginx rebuilt" + else + info "nginx unchanged, skipping rebuild" + fi + ;; + code-server) + if echo "$CHANGED_FILES" | grep -q "^Dockerfile.code-server"; then + info "Rebuilding code-server (Dockerfile changed)..." + docker compose build code-server + success "code-server rebuilt" + else + info "code-server unchanged, skipping rebuild" + fi + ;; + esac + done +fi # Optionally pull third-party images if [[ "$PULL_SERVICES" == "true" ]]; then @@ -960,8 +1027,11 @@ fi # Restart monitoring if it was running before if [[ "$MONITORING_WAS_RUNNING" == "true" ]]; then info "Restarting monitoring stack..." - docker compose --profile monitoring up -d - success "Monitoring stack restarted" + if docker compose --profile monitoring up -d 2>&1; then + success "Monitoring stack restarted" + else + warn "Monitoring stack restart had errors (non-fatal, services may already be running)" + fi fi # =============================================================================