From 8e6f0996de18e9e00e847a27a50f488e8e3972c8 Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Sun, 22 Mar 2026 20:34:49 -0600 Subject: [PATCH] Add pre-built image installer and release tarball system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New install method: curl one-liner downloads a lightweight release tarball (~9 MB) and runs the config wizard. No git clone needed, no TypeScript compilation — pulls pre-built images from Gitea registry. - docker-compose.prod.yml: production compose without build blocks or source code volume mounts; IMAGE_TAG defaults to latest - scripts/install.sh: curl-friendly installer (downloads tarball, extracts, runs config.sh) - scripts/build-release.sh: creates release tarball from dev repo with only runtime files (configs, scripts, docs, empty data dirs) - config.sh: release-mode detection (VERSION file + no .git dir), auto-sets IMAGE_TAG=latest and NODE_ENV=production - upgrade.sh: release-mode upgrade path (downloads new tarball from Gitea Releases API instead of git pull, always uses registry mode) - upgrade-check.sh: release-mode version check via Gitea API - .gitignore: exclude releases/ and api/dist/ - Docs: updated getting-started with pre-built install instructions Bunker Admin --- .gitignore | 9 +- config.sh | 94 +- docker-compose.prod.yml | 1264 +++++++++++++++++ mkdocs/docs/docs/getting-started/index.md | 14 +- .../docs/docs/getting-started/installation.md | 53 +- mkdocs/docs/docs/getting-started/upgrades.md | 44 +- scripts/build-release.sh | 235 +++ scripts/install.sh | 177 +++ scripts/upgrade-check.sh | 64 + scripts/upgrade.sh | 74 +- 10 files changed, 1996 insertions(+), 32 deletions(-) create mode 100644 docker-compose.prod.yml create mode 100755 scripts/build-release.sh create mode 100755 scripts/install.sh diff --git a/.gitignore b/.gitignore index f9ed10ba..c1a70f17 100644 --- a/.gitignore +++ b/.gitignore @@ -51,14 +51,21 @@ docker-compose.override.yml core.* */core.* -# MkDocs core binary +# MkDocs core binary and container-generated assets (owned by root, not stashable) /mkdocs/core +/mkdocs/assets/ # Upgrade artifacts /logs/ /backups/ .upgrade.lock +# Release tarballs (generated by build-release.sh) +/releases/ + +# API compiled output (generated by tsc, baked into Docker images) +/api/dist/ + # Control Panel runtime data (managed deployments + backups) /changemaker-control-panel/instances/ /changemaker-control-panel/backups/ diff --git a/config.sh b/config.sh index 6d7318a9..5475bb06 100755 --- a/config.sh +++ b/config.sh @@ -12,6 +12,17 @@ ENV_EXAMPLE="$SCRIPT_DIR/.env.example" MKDOCS_YML="$SCRIPT_DIR/mkdocs/mkdocs.yml" SERVICES_YAML="$SCRIPT_DIR/configs/homepage/services.yaml" +# --- Detect install mode --- +# Release mode: installed from tarball (has VERSION file, no .git directory) +# Source mode: cloned from git repository +if [[ -f "$SCRIPT_DIR/VERSION" ]] && [[ ! -d "$SCRIPT_DIR/.git" ]]; then + INSTALL_MODE="release" + RELEASE_VERSION=$(head -1 "$SCRIPT_DIR/VERSION") +else + INSTALL_MODE="source" + RELEASE_VERSION="" +fi + # --- Colors (respects NO_COLOR convention) --- if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' @@ -275,17 +286,19 @@ configure_admin() { generate_all_secrets() { header "Generating Secrets" - info "Auto-generating 21 unique secrets and passwords..." + info "Auto-generating 22 unique secrets and passwords..." echo "" # JWT & Encryption (64-char hex) - local jwt_access jwt_refresh enc_key + local jwt_access jwt_refresh jwt_invite enc_key jwt_access=$(generate_secret) jwt_refresh=$(generate_secret) + jwt_invite=$(generate_secret) enc_key=$(generate_secret) update_env_var "JWT_ACCESS_SECRET" "$jwt_access" update_env_var "JWT_REFRESH_SECRET" "$jwt_refresh" + update_env_var "JWT_INVITE_SECRET" "$jwt_invite" update_env_var "ENCRYPTION_KEY" "$enc_key" success "JWT secrets + encryption key" @@ -1082,7 +1095,7 @@ print_summary() { echo -e " ${BOLD}Bunker Ops:${NC} ${BUNKER_OPS_ENABLED:-no}" echo -e " ${BOLD}Pangolin:${NC} ${PANGOLIN_CONFIGURED:-no}" echo -e " ${BOLD}Upgrade watcher:${NC} ${UPGRADE_WATCHER:-skipped}" - echo -e " ${BOLD}Secrets:${NC} 21 auto-generated" + echo -e " ${BOLD}Secrets:${NC} 22 auto-generated" echo "" echo -e " ${DIM}Config file: $ENV_FILE${NC}" } @@ -1093,29 +1106,50 @@ print_next_steps() { echo -e "${BOLD}${BLUE} Next Steps${NC}" echo -e "${BOLD}${BLUE}══════════════════════════════════════${NC}" echo "" - echo -e " ${BOLD}1.${NC} Start core services:" - echo -e " ${CYAN}docker compose up -d v2-postgres redis api admin${NC}" - echo "" - echo -e " ${BOLD}2.${NC} Run database setup:" - echo -e " ${CYAN}docker compose exec api npx prisma migrate deploy${NC}" - echo -e " ${CYAN}docker compose exec api npx prisma db seed${NC}" - echo "" - echo -e " ${BOLD}3.${NC} Access the application:" - echo -e " Admin GUI: ${CYAN}http://localhost:3000${NC}" - echo -e " API: ${CYAN}http://localhost:4000${NC}" - echo "" - echo -e " ${BOLD}4.${NC} Optional — start additional services:" - echo -e " ${CYAN}docker compose up -d nginx${NC} # Reverse proxy" - echo -e " ${CYAN}docker compose up -d media-api${NC} # Video library" - echo -e " ${CYAN}docker compose up -d listmonk-app${NC} # Newsletters" - echo -e " ${CYAN}docker compose up -d rocketchat${NC} # Team chat" - echo -e " ${CYAN}docker compose up -d jitsi-web jitsi-prosody jitsi-jicofo jitsi-jvb${NC} # Video calls" - echo -e " ${CYAN}docker compose up -d homepage${NC} # Service dashboard" - echo -e " ${CYAN}docker compose --profile monitoring up -d${NC} # Monitoring" - echo "" - echo -e " ${BOLD}5.${NC} Or start everything at once:" - echo -e " ${CYAN}docker compose up -d${NC}" - echo "" + + if [[ "$INSTALL_MODE" == "release" ]]; then + # Release mode: simpler instructions (production images, auto-migration) + echo -e " ${BOLD}1.${NC} Start all services:" + echo -e " ${CYAN}docker compose up -d${NC}" + echo "" + echo -e " Pre-built images will be pulled from the registry (~2 min first time)." + echo -e " Database migrations and seeding run automatically on startup." + echo "" + echo -e " ${BOLD}2.${NC} Access the application:" + echo -e " Admin GUI: ${CYAN}http://localhost:3000${NC}" + echo -e " API: ${CYAN}http://localhost:4000${NC}" + echo "" + echo -e " ${BOLD}3.${NC} Check status:" + echo -e " ${CYAN}docker compose ps${NC}" + echo -e " ${CYAN}docker compose logs -f api --tail 20${NC}" + echo "" + else + # Source mode: existing instructions + echo -e " ${BOLD}1.${NC} Start core services:" + echo -e " ${CYAN}docker compose up -d v2-postgres redis api admin${NC}" + echo "" + echo -e " ${BOLD}2.${NC} Run database setup:" + echo -e " ${CYAN}docker compose exec api npx prisma migrate deploy${NC}" + echo -e " ${CYAN}docker compose exec api npx prisma db seed${NC}" + echo "" + echo -e " ${BOLD}3.${NC} Access the application:" + echo -e " Admin GUI: ${CYAN}http://localhost:3000${NC}" + echo -e " API: ${CYAN}http://localhost:4000${NC}" + echo "" + echo -e " ${BOLD}4.${NC} Optional — start additional services:" + echo -e " ${CYAN}docker compose up -d nginx${NC} # Reverse proxy" + echo -e " ${CYAN}docker compose up -d media-api${NC} # Video library" + echo -e " ${CYAN}docker compose up -d listmonk-app${NC} # Newsletters" + echo -e " ${CYAN}docker compose up -d rocketchat${NC} # Team chat" + echo -e " ${CYAN}docker compose up -d jitsi-web jitsi-prosody jitsi-jicofo jitsi-jvb${NC} # Video calls" + echo -e " ${CYAN}docker compose up -d homepage${NC} # Service dashboard" + echo -e " ${CYAN}docker compose --profile monitoring up -d${NC} # Monitoring" + echo "" + echo -e " ${BOLD}5.${NC} Or start everything at once:" + echo -e " ${CYAN}docker compose up -d${NC}" + echo "" + fi + echo -e " ${YELLOW}IMPORTANT: Change your admin password after first login!${NC}" echo -e " ${YELLOW}JITSI: Ensure UDP port 10000 is open in your firewall for video/audio.${NC}" echo "" @@ -1142,6 +1176,14 @@ main() { fix_container_permissions install_upgrade_watcher + # Release mode: auto-set production defaults + if [[ "$INSTALL_MODE" == "release" ]]; then + header "Release Mode Settings" + update_env_var "IMAGE_TAG" "latest" + update_env_var "NODE_ENV" "production" + success "Set IMAGE_TAG=latest, NODE_ENV=production (pre-built images)" + fi + print_summary print_next_steps } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..8f4dc98d --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,1264 @@ +############################################################################### +############################################################################### +# Changemaker Lite v2 — Production Docker Compose +# Pre-built images only. No source code mounts, no build blocks. +# Generated from docker-compose.yml by build-release.sh +############################################################################### +############################################################################### + +services: + # ========================================================================= + # V2 CORE SERVICES + # ========================================================================= + + # Unified Express.js API + api: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-api:${IMAGE_TAG:-latest} + container_name: changemaker-v2-api + restart: unless-stopped + ports: + - "127.0.0.1:${API_PORT:-4000}:4000" + - "127.0.0.1:${LISTMONK_PROXY_PORT:-9002}:9002" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/api/health"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 30s + environment: + - NODE_ENV=${NODE_ENV:-development} + - PORT=4000 + - DATABASE_URL=postgresql://${V2_POSTGRES_USER:-changemaker}:${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}@changemaker-v2-postgres:5432/${V2_POSTGRES_DB:-changemaker_v2} + - 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} + - JWT_ACCESS_EXPIRY=${JWT_ACCESS_EXPIRY:-15m} + - JWT_REFRESH_EXPIRY=${JWT_REFRESH_EXPIRY:-7d} + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - INITIAL_ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-admin@cmlite.org} + - INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD:?INITIAL_ADMIN_PASSWORD must be set in .env} + - SMTP_HOST=${SMTP_HOST:-mailhog-changemaker} + - SMTP_PORT=${SMTP_PORT:-1025} + - SMTP_USER=${SMTP_USER:-} + - SMTP_PASS=${SMTP_PASS:-} + - SMTP_FROM=${SMTP_FROM:-noreply@cmlite.org} + - EMAIL_TEST_MODE=${EMAIL_TEST_MODE:-true} + - LISTMONK_URL=http://listmonk-app:9000 + - LISTMONK_ADMIN_USER=${LISTMONK_ADMIN_USER:-admin} + - LISTMONK_ADMIN_PASSWORD=${LISTMONK_ADMIN_PASSWORD:-} + - LISTMONK_SYNC_ENABLED=${LISTMONK_SYNC_ENABLED:-false} + - LISTMONK_PROXY_PORT=${LISTMONK_PROXY_PORT:-9002} + - REPRESENT_API_URL=${REPRESENT_API_URL:-https://represent.opennorth.ca} + - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000,http://localhost} + - ADMIN_URL=${ADMIN_URL:-http://localhost:3000} + - API_URL=${API_URL:-http://localhost:4000} + - DOMAIN=${DOMAIN:-cmlite.org} + - NAR_DATA_DIR=/data + - PANGOLIN_API_URL=${PANGOLIN_API_URL:-} + - PANGOLIN_API_KEY=${PANGOLIN_API_KEY:-} + - PANGOLIN_ORG_ID=${PANGOLIN_ORG_ID:-} + - PANGOLIN_SITE_ID=${PANGOLIN_SITE_ID:-} + - PANGOLIN_ENDPOINT=${PANGOLIN_ENDPOINT:-} + - PANGOLIN_NEWT_ID=${PANGOLIN_NEWT_ID:-} + - PANGOLIN_NEWT_SECRET=${PANGOLIN_NEWT_SECRET:-} + # NODE_TLS_REJECT_UNAUTHORIZED removed — never disable TLS validation globally + - EXCALIDRAW_URL=${EXCALIDRAW_URL:-http://excalidraw-changemaker:80} + - EXCALIDRAW_PORT=${EXCALIDRAW_PORT:-8090} + - EXCALIDRAW_EMBED_PORT=${EXCALIDRAW_EMBED_PORT:-8886} + - HOMEPAGE_URL=${HOMEPAGE_URL:-http://homepage-changemaker:3000} + - HOMEPAGE_EMBED_PORT=${HOMEPAGE_EMBED_PORT:-8887} + - VAULTWARDEN_URL=${VAULTWARDEN_URL:-http://vaultwarden-changemaker:80} + - VAULTWARDEN_EMBED_PORT=${VAULTWARDEN_EMBED_PORT:-8890} + - ROCKETCHAT_URL=${ROCKETCHAT_URL:-http://rocketchat-changemaker:3000} + - ROCKETCHAT_ADMIN_USER=${ROCKETCHAT_ADMIN_USER:-rcadmin} + - ROCKETCHAT_ADMIN_PASSWORD=${ROCKETCHAT_ADMIN_PASSWORD:?ROCKETCHAT_ADMIN_PASSWORD must be set in .env} + - ROCKETCHAT_EMBED_PORT=${ROCKETCHAT_EMBED_PORT:-8891} + - ENABLE_CHAT=${ENABLE_CHAT:-false} + - GANCIO_URL=${GANCIO_URL:-http://gancio-changemaker:13120} + - GANCIO_EMBED_PORT=${GANCIO_EMBED_PORT:-8892} + - GANCIO_ADMIN_USER=${GANCIO_ADMIN_USER:-admin} + - GANCIO_ADMIN_PASSWORD=${GANCIO_ADMIN_PASSWORD:-} + - GANCIO_SYNC_ENABLED=${GANCIO_SYNC_ENABLED:-false} + # Jitsi Meet (video conferencing) + - ENABLE_MEET=${ENABLE_MEET:-false} + - JITSI_APP_ID=${JITSI_APP_ID:-changemaker} + - JITSI_APP_SECRET=${JITSI_APP_SECRET:-} + - JITSI_URL=${JITSI_URL:-http://jitsi-web-changemaker:80} + - JITSI_EMBED_PORT=${JITSI_EMBED_PORT:-8893} + # Monitoring embed ports (for iframe embedding without DNS/subdomain) + - GRAFANA_EMBED_PORT=${GRAFANA_EMBED_PORT:-8894} + - ALERTMANAGER_EMBED_PORT=${ALERTMANAGER_EMBED_PORT:-8895} + # SMS Campaigns (Termux Android Bridge) + - ENABLE_SMS=${ENABLE_SMS:-false} + - TERMUX_API_URL=${TERMUX_API_URL:-http://10.0.0.193:5001} + - TERMUX_API_KEY=${TERMUX_API_KEY:-} + - SMS_DELAY_BETWEEN_MS=${SMS_DELAY_BETWEEN_MS:-3000} + - SMS_MAX_RETRIES=${SMS_MAX_RETRIES:-3} + - SMS_RESPONSE_SYNC_INTERVAL_MS=${SMS_RESPONSE_SYNC_INTERVAL_MS:-30000} + - 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: + - ./assets/uploads:/app/uploads + - ./mkdocs:/mkdocs:rw + - ./data:/data:ro + - ./data/upgrade:/app/upgrade:rw + - ./configs:/app/configs:ro + deploy: + resources: + limits: + cpus: '2' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + depends_on: + v2-postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - changemaker-lite + + # Fastify Media API (Microservice for Media Management) + media-api: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-media-api:${IMAGE_TAG:-latest} + container_name: changemaker-media-api + restart: unless-stopped + ports: + - "127.0.0.1:${MEDIA_API_PORT:-4100}:4100" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:4100/health"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 30s + environment: + - NODE_ENV=${NODE_ENV:-development} + - MEDIA_API_PORT=${MEDIA_API_PORT:-4100} + - DATABASE_URL=postgresql://${V2_POSTGRES_USER:-changemaker}:${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}@changemaker-v2-postgres:5432/${V2_POSTGRES_DB:-changemaker_v2} + - 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 + - MEDIA_UPLOADS=/media/uploads + - MAX_UPLOAD_SIZE_GB=${MAX_UPLOAD_SIZE_GB:-10} + - INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD} + volumes: + - ${MEDIA_ROOT:-./media}:/media:ro + - ${MEDIA_ROOT:-./media}/local/inbox:/media/local/inbox:rw + - ${MEDIA_ROOT:-./media}/local/thumbnails:/media/local/thumbnails:rw + - ${MEDIA_ROOT:-./media}/local/photos:/media/local/photos:rw + - ${MEDIA_ROOT:-./media}/public:/media/public:rw + deploy: + resources: + limits: + cpus: '2' + memory: 1G + reservations: + cpus: '0.25' + memory: 256M + depends_on: + v2-postgres: + condition: service_healthy + networks: + - changemaker-lite + + # React Admin GUI (Vite dev server) + admin: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-admin:${IMAGE_TAG:-latest} + container_name: changemaker-v2-admin + restart: unless-stopped + ports: + - "127.0.0.1:${ADMIN_PORT:-3000}:3000" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + environment: + - DOMAIN=${DOMAIN:-cmlite.org} + - NODE_ENV=${NODE_ENV:-development} + - VITE_API_URL=http://changemaker-v2-api:4000 + - VITE_MEDIA_API_URL=${VITE_MEDIA_API_URL:-http://changemaker-media-api:4100} + - VITE_MKDOCS_URL=http://mkdocs-changemaker:8000 + - VITE_DOMAIN=${DOMAIN:-cmlite.org} + - VITE_MKDOCS_SITE_PORT=${MKDOCS_SITE_SERVER_PORT:-4004} + depends_on: + - api + networks: + - changemaker-lite + + # PostgreSQL 16 (v2 database) + v2-postgres: + image: postgres:16-alpine + container_name: changemaker-v2-postgres + restart: unless-stopped + ports: + - "127.0.0.1:${V2_POSTGRES_PORT:-5433}:5432" + environment: + POSTGRES_USER: ${V2_POSTGRES_USER:-changemaker} + POSTGRES_PASSWORD: ${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} + POSTGRES_DB: ${V2_POSTGRES_DB:-changemaker_v2} + volumes: + - v2-postgres-data:/var/lib/postgresql/data + - ./scripts/init-nocodb-db.sh:/docker-entrypoint-initdb.d/init-nocodb-db.sh:ro + - ./scripts/init-gancio-db.sh:/docker-entrypoint-initdb.d/init-gancio-db.sh:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${V2_POSTGRES_USER:-changemaker}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - changemaker-lite + + # Nginx reverse proxy + nginx: + image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/changemaker-nginx:${IMAGE_TAG:-latest} + container_name: changemaker-v2-nginx + restart: unless-stopped + ports: + - "${NGINX_HTTP_PORT:-80}:80" + - "${NGINX_HTTPS_PORT:-443}:443" + - "127.0.0.1:8881:8881" # NocoDB embed proxy (strips X-Frame-Options) + - "127.0.0.1:8882:8882" # n8n embed proxy + - "127.0.0.1:8883:8883" # Gitea embed proxy + - "127.0.0.1:8884:8884" # MailHog embed proxy + - "127.0.0.1:8885:8885" # Mini QR embed proxy + - "127.0.0.1:8886:8886" # Excalidraw embed proxy + - "127.0.0.1:8887:8887" # Homepage embed proxy + - "127.0.0.1:8890:8890" # Vaultwarden embed proxy + - "127.0.0.1:8891:8891" # Rocket.Chat embed proxy + - "127.0.0.1:8892:8892" # Gancio embed proxy + - "127.0.0.1:8893:8893" # Jitsi Meet embed proxy + - "127.0.0.1:8894:8894" # Grafana embed proxy + - "127.0.0.1:8895:8895" # Alertmanager embed proxy + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/"] + interval: 30s + timeout: 5s + retries: 3 + environment: + - DOMAIN=${DOMAIN:-cmlite.org} + - PANGOLIN_SITE_ID=${PANGOLIN_SITE_ID:-} + volumes: + # Note: conf.d is NOT mounted (configs are generated at startup from templates) + - ./public-web:/usr/share/nginx/public-web:ro + - ./configs/pangolin:/etc/pangolin:ro + depends_on: + - api + - admin + networks: + - changemaker-lite + + # NocoDB v2 — pointed at v2 PostgreSQL as read-only data browser + nocodb-v2: + image: nocodb/nocodb:0.301.3 + container_name: changemaker-v2-nocodb + restart: unless-stopped + ports: + - "127.0.0.1:${NOCODB_V2_PORT:-8091}:8080" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/api/v1/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + environment: + NC_DB: "pg://changemaker-v2-postgres:5432?u=${V2_POSTGRES_USER:-changemaker}&p=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}&d=nocodb_meta" + NC_ADMIN_EMAIL: ${NC_ADMIN_EMAIL:-admin@cmlite.org} + NC_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD:?NC_ADMIN_PASSWORD must be set in .env} + NC_PUBLIC_URL: ${NC_PUBLIC_URL:-http://localhost:8091} + volumes: + - nocodb-v2-data:/usr/app/data + depends_on: + v2-postgres: + condition: service_healthy + networks: + - changemaker-lite + + # NocoDB Init — auto-registers changemaker_v2 as a browsable data source + nocodb-init: + image: alpine:3 + container_name: nocodb-init + depends_on: + nocodb-v2: + condition: service_healthy + v2-postgres: + condition: service_healthy + restart: "no" + environment: + NOCODB_URL: http://changemaker-v2-nocodb:8080 + NC_ADMIN_EMAIL: ${NC_ADMIN_EMAIL:-admin@cmlite.org} + NC_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD:?NC_ADMIN_PASSWORD must be set in .env} + DB_HOST: changemaker-v2-postgres + DB_PORT: "5432" + DB_USER: ${V2_POSTGRES_USER:-changemaker} + DB_PASSWORD: ${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} + DB_NAME: ${V2_POSTGRES_DB:-changemaker_v2} + volumes: + - ./scripts/nocodb-init.sh:/init.sh:ro + entrypoint: ["/bin/sh", "/init.sh"] + networks: + - changemaker-lite + + # ========================================================================= + # SHARED INFRASTRUCTURE (kept from v1) + # ========================================================================= + + # Shared Redis — sessions, BullMQ queues, cache + redis: + image: redis:7-alpine + container_name: redis-changemaker + command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru --requirepass "${REDIS_PASSWORD}" + ports: + - "127.0.0.1:6379:6379" + volumes: + - redis-data:/data + restart: always + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 5s + retries: 5 + deploy: + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M + networks: + - changemaker-lite + logging: + driver: "json-file" + options: + max-size: "5m" + max-file: "2" + + # Listmonk — Email marketing (kept as Docker image, controlled via REST API) + listmonk-app: + image: listmonk/listmonk:v6.0.0 + container_name: listmonk-app + restart: unless-stopped + ports: + - "127.0.0.1:${LISTMONK_PORT:-9001}:9000" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + depends_on: + - listmonk-db + command: [sh, -c, "./listmonk --install --idempotent --yes --config '' && ./listmonk --upgrade --yes --config '' && ./listmonk --config ''"] + environment: + LISTMONK_app__address: 0.0.0.0:9000 + LISTMONK_db__user: ${LISTMONK_DB_USER:-listmonk} + LISTMONK_db__password: ${LISTMONK_DB_PASSWORD:-listmonk} + LISTMONK_db__database: ${LISTMONK_DB_NAME:-listmonk} + LISTMONK_db__host: listmonk-db + LISTMONK_db__port: 5432 + LISTMONK_db__ssl_mode: disable + TZ: Etc/UTC + LISTMONK_ADMIN_USER: ${LISTMONK_WEB_ADMIN_USER:-admin} + LISTMONK_ADMIN_PASSWORD: ${LISTMONK_WEB_ADMIN_PASSWORD:-} + volumes: + - ./assets/uploads:/listmonk/uploads:rw + networks: + - changemaker-lite + + listmonk-db: + image: postgres:17-alpine + container_name: listmonk-db + restart: unless-stopped + ports: + - "127.0.0.1:${LISTMONK_DB_PORT:-5434}:5432" + environment: + POSTGRES_USER: ${LISTMONK_DB_USER:-listmonk} + POSTGRES_PASSWORD: ${LISTMONK_DB_PASSWORD:-listmonk} + POSTGRES_DB: ${LISTMONK_DB_NAME:-listmonk} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${LISTMONK_DB_USER:-listmonk}"] + interval: 10s + timeout: 5s + retries: 6 + volumes: + - listmonk-data:/var/lib/postgresql/data + networks: + - changemaker-lite + + # One-shot: creates the Listmonk API user for V2 integration after tables exist. + # Safe to re-run (upserts). Exits 0 on success. Set LISTMONK_API_TOKEN in .env. + listmonk-init: + image: postgres:17-alpine + container_name: listmonk-init + depends_on: + listmonk-app: + condition: service_started + restart: "no" + environment: + PGPASSWORD: ${LISTMONK_DB_PASSWORD:-listmonk} + LISTMONK_API_USER: ${LISTMONK_API_USER:-v2-api} + LISTMONK_API_TOKEN: ${LISTMONK_API_TOKEN:-} + LISTMONK_SMTP_HOST: ${LISTMONK_SMTP_HOST:-mailhog-changemaker} + LISTMONK_SMTP_PORT: ${LISTMONK_SMTP_PORT:-1025} + LISTMONK_SMTP_USER: ${LISTMONK_SMTP_USER:-} + LISTMONK_SMTP_PASSWORD: ${LISTMONK_SMTP_PASSWORD:-} + LISTMONK_SMTP_TLS_TYPE: ${LISTMONK_SMTP_TLS_TYPE:-none} + LISTMONK_SMTP_FROM: ${LISTMONK_SMTP_FROM:-Changemaker Lite } + entrypoint: ["/bin/sh", "-c"] + command: + - | + echo "[listmonk-init] Waiting for Listmonk tables..." + for i in $$(seq 1 30); do + if psql -h listmonk-db -U ${LISTMONK_DB_USER:-listmonk} -d ${LISTMONK_DB_NAME:-listmonk} -c "SELECT 1 FROM users LIMIT 1" >/dev/null 2>&1; then + break + fi + sleep 2 + done + + if [ -n "$$LISTMONK_API_TOKEN" ]; then + echo "[listmonk-init] Upserting API user '$$LISTMONK_API_USER'..." + psql -h listmonk-db -U ${LISTMONK_DB_USER:-listmonk} -d ${LISTMONK_DB_NAME:-listmonk} -q </dev/null 2>&1; then + break + fi + sleep 2 + done + + if [ -z "$$VAULTWARDEN_ADMIN_TOKEN" ]; then + echo "[vaultwarden-init] VAULTWARDEN_ADMIN_TOKEN not set, skipping invite" + exit 0 + fi + + echo "[vaultwarden-init] Authenticating with admin panel..." + SESSION_COOKIE=$(mktemp) + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -c "$$SESSION_COOKIE" \ + -X POST "$$VAULTWARDEN_URL/admin" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "token=$$VAULTWARDEN_ADMIN_TOKEN") + + if [ "$$HTTP_CODE" != "200" ] && [ "$$HTTP_CODE" != "302" ]; then + echo "[vaultwarden-init] Admin auth failed (HTTP $$HTTP_CODE)" + rm -f "$$SESSION_COOKIE" + exit 1 + fi + echo "[vaultwarden-init] Authenticated" + + echo "[vaultwarden-init] Inviting $$INVITE_EMAIL..." + INVITE_CODE=$(curl -s -w "\n%{http_code}" \ + -b "$$SESSION_COOKIE" \ + -X POST "$$VAULTWARDEN_URL/admin/invite" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"$$INVITE_EMAIL\"}") + + INVITE_HTTP=$(echo "$$INVITE_CODE" | tail -1) + INVITE_BODY=$(echo "$$INVITE_CODE" | head -n -1) + + if [ "$$INVITE_HTTP" = "200" ] || [ "$$INVITE_HTTP" = "422" ]; then + echo "[vaultwarden-init] Invite sent (or user already exists)" + else + echo "[vaultwarden-init] Invite failed (HTTP $$INVITE_HTTP): $$INVITE_BODY" + fi + + rm -f "$$SESSION_COOKIE" + echo "[vaultwarden-init] Done" + networks: + - changemaker-lite + + # Rocket.Chat — Team coordination chat + rocketchat: + image: rocketchat/rocket.chat:7.9.7 + container_name: rocketchat-changemaker + restart: unless-stopped + depends_on: + mongodb-rocketchat: + condition: service_healthy + nats-rocketchat: + condition: service_started + environment: + - ROOT_URL=http://chat.${DOMAIN:-cmlite.org} + - MONGO_URL=mongodb://mongodb-rocketchat:27017/rocketchat?replicaSet=rs0 + - MONGO_OPLOG_URL=mongodb://mongodb-rocketchat:27017/local?replicaSet=rs0 + - TRANSPORTER=monolith+nats://nats-rocketchat:4222 + - PORT=3000 + - ADMIN_USERNAME=${ROCKETCHAT_ADMIN_USER:-rcadmin} + - ADMIN_NAME=Admin + - ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-admin@cmlite.org} + - ADMIN_PASS=${ROCKETCHAT_ADMIN_PASSWORD:?ROCKETCHAT_ADMIN_PASSWORD must be set in .env} + - CREATE_TOKENS_FOR_USERS=true + - OVERWRITE_SETTING_Iframe_Integration_send_enable=true + - OVERWRITE_SETTING_Iframe_Integration_receive_enable=true + - OVERWRITE_SETTING_Iframe_Integration_receive_origin=http://app.${DOMAIN:-cmlite.org},https://app.${DOMAIN:-cmlite.org},http://localhost:${ADMIN_PORT:-3000} + # Jitsi integration (JWT-authenticated video calls) + - OVERWRITE_SETTING_Jitsi_Enabled=${ENABLE_MEET:-false} + - OVERWRITE_SETTING_Jitsi_Domain=meet.${DOMAIN:-cmlite.org} + - OVERWRITE_SETTING_Jitsi_URL_Room_Prefix=RocketChat + - OVERWRITE_SETTING_Jitsi_Enable_Channels=true + - OVERWRITE_SETTING_Jitsi_Open_New_Window=false + - OVERWRITE_SETTING_Jitsi_Enabled_TokenAuth=true + - OVERWRITE_SETTING_Jitsi_Application_ID=${JITSI_APP_ID:-changemaker} + - OVERWRITE_SETTING_Jitsi_Application_Secret=${JITSI_APP_SECRET:-} + # Conference Call provider (marketplace app-based system in RC 7.x) + - OVERWRITE_SETTING_VideoConf_Default_Provider=jitsi + volumes: + - rocketchat-uploads:/app/uploads + networks: + - changemaker-lite + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/info"] + interval: 30s + timeout: 10s + retries: 10 + start_period: 90s + + # NATS (required by Rocket.Chat 7.x+ for microservice messaging) + nats-rocketchat: + image: nats:2.11-alpine + container_name: nats-rocketchat + restart: unless-stopped + command: --http_port 8222 + networks: + - changemaker-lite + + # MongoDB (required by Rocket.Chat — replica set for oplog tailing) + mongodb-rocketchat: + image: mongo:6.0 + container_name: mongodb-rocketchat + restart: unless-stopped + command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] + volumes: + - mongodb-rocketchat-data:/data/db + networks: + - changemaker-lite + healthcheck: + test: ["CMD", "mongosh", "--quiet", "--eval", "try { rs.status().ok } catch(e) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb-rocketchat:27017'}]}).ok }"] + interval: 10s + timeout: 10s + retries: 10 + start_period: 30s + + # Gancio — Event management platform (uses shared PostgreSQL) + gancio: + image: cisti/gancio:1.28.2 + container_name: gancio-changemaker + restart: unless-stopped + depends_on: + v2-postgres: + condition: service_healthy + ports: + - "127.0.0.1:${GANCIO_PORT:-8092}:13120" + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:13120/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + environment: + - GANCIO_DATA=/home/node/data + - NODE_ENV=production + - GANCIO_DB_DIALECT=postgres + - GANCIO_DB_HOST=changemaker-v2-postgres + - GANCIO_DB_PORT=5432 + - GANCIO_DB_DATABASE=gancio + - GANCIO_DB_USERNAME=${V2_POSTGRES_USER:-changemaker} + - GANCIO_DB_PASSWORD=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} + - server__baseurl=${GANCIO_BASE_URL:-https://events.cmlite.org} + volumes: + - gancio-data:/home/node/data + networks: + - changemaker-lite + + # Gancio Init — Creates admin user + seeds default theme settings after Gancio creates its tables + # Runs once after Gancio is healthy, then exits. Idempotent (ON CONFLICT DO NOTHING). + gancio-init: + image: postgres:16-alpine + container_name: gancio-init + depends_on: + gancio: + condition: service_healthy + environment: + - PGHOST=changemaker-v2-postgres + - PGUSER=${V2_POSTGRES_USER:-changemaker} + - PGPASSWORD=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} + - PGDATABASE=gancio + - GANCIO_ADMIN_USER=${GANCIO_ADMIN_USER:-admin} + - GANCIO_ADMIN_PASSWORD=${GANCIO_ADMIN_PASSWORD} + entrypoint: ["sh", "-c"] + command: + - | + echo "Ensuring pgcrypto extension exists..." + psql -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" + + echo "Creating Gancio admin user (if not exists)..." + if [ -n "$$GANCIO_ADMIN_PASSWORD" ]; then + psql -c "INSERT INTO users (email, password, display_name, role, is_admin, is_active, \"createdAt\", \"updatedAt\") + VALUES ('$$GANCIO_ADMIN_USER', crypt('$$GANCIO_ADMIN_PASSWORD', gen_salt('bf', 10)), 'Admin', 'admin', true, true, NOW(), NOW()) + ON CONFLICT (email) DO NOTHING;" + echo "Gancio admin user ensured." + else + echo "WARNING: GANCIO_ADMIN_PASSWORD not set, skipping admin user creation." + fi + + echo "Seeding Gancio default theme settings..." + psql -c "INSERT INTO settings (key, value, is_secret, \"createdAt\", \"updatedAt\") VALUES + ('dark_colors', '{\"primary\": \"#FF6E40\", \"error\": \"#FF5252\", \"info\": \"#2196F3\", \"success\": \"#4CAF50\", \"warning\": \"#FB8C00\"}', false, NOW(), NOW()), + ('light_colors', '{\"primary\": \"#FF4500\", \"error\": \"#FF5252\", \"info\": \"#2196F3\", \"success\": \"#4CAF50\", \"warning\": \"#FB8C00\"}', false, NOW(), NOW()) + ON CONFLICT (key) DO NOTHING;" + echo "Gancio theme settings seeded." + restart: "no" + networks: + - changemaker-lite + + # ========================================================================= + # JITSI MEET (Self-hosted video conferencing with JWT auth) + # ========================================================================= + + # Jitsi Web — Frontend UI (served via nginx at meet.${DOMAIN}) + jitsi-web: + image: jitsi/web:stable-9823 + container_name: jitsi-web-changemaker + restart: unless-stopped + depends_on: + jitsi-prosody: + condition: service_healthy + environment: + - ENABLE_AUTH=1 + - ENABLE_GUESTS=1 + - AUTH_TYPE=jwt + - JWT_APP_ID=${JITSI_APP_ID:-changemaker} + - JWT_APP_SECRET=${JITSI_APP_SECRET:-} + - XMPP_DOMAIN=meet.jitsi + - XMPP_AUTH_DOMAIN=auth.meet.jitsi + - XMPP_MUC_DOMAIN=muc.meet.jitsi + - XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi + - XMPP_BOSH_URL_BASE=http://jitsi-prosody-changemaker:5280 + - XMPP_SERVER=jitsi-prosody-changemaker + - PUBLIC_URL=https://meet.${DOMAIN:-cmlite.org} + - TOKEN_AUTH_URL=https://app.${DOMAIN:-cmlite.org}/jitsi-auth/{room} + - TZ=UTC + volumes: + - jitsi-web-config:/config + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - changemaker-lite + + # Jitsi Prosody — XMPP server + JWT validation + jitsi-prosody: + image: jitsi/prosody:stable-9823 + container_name: jitsi-prosody-changemaker + restart: unless-stopped + environment: + - ENABLE_AUTH=1 + - ENABLE_GUESTS=1 + - AUTH_TYPE=jwt + - JWT_APP_ID=${JITSI_APP_ID:-changemaker} + - JWT_APP_SECRET=${JITSI_APP_SECRET:-} + - JWT_ACCEPTED_ISSUERS=${JITSI_APP_ID:-changemaker} + - JWT_ACCEPTED_AUDIENCES=${JITSI_APP_ID:-changemaker} + - XMPP_DOMAIN=meet.jitsi + - XMPP_AUTH_DOMAIN=auth.meet.jitsi + - XMPP_MUC_DOMAIN=muc.meet.jitsi + - XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi + - XMPP_MUC_MODULES=token_verification,token_affiliation + - JICOFO_AUTH_PASSWORD=${JITSI_JICOFO_AUTH_PASSWORD:-} + - JVB_AUTH_PASSWORD=${JITSI_JVB_AUTH_PASSWORD:-} + - TZ=UTC + volumes: + - jitsi-prosody-config:/config + - jitsi-prosody-plugins:/prosody-plugins-custom + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:5280/health"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - changemaker-lite + + # Jitsi Jicofo — Conference focus / room management + jitsi-jicofo: + image: jitsi/jicofo:stable-9823 + container_name: jitsi-jicofo-changemaker + restart: unless-stopped + depends_on: + jitsi-prosody: + condition: service_healthy + environment: + - ENABLE_AUTH=1 + - AUTH_TYPE=jwt + - XMPP_DOMAIN=meet.jitsi + - XMPP_AUTH_DOMAIN=auth.meet.jitsi + - XMPP_MUC_DOMAIN=muc.meet.jitsi + - XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi + - XMPP_SERVER=jitsi-prosody-changemaker + - JICOFO_AUTH_PASSWORD=${JITSI_JICOFO_AUTH_PASSWORD:-} + - TZ=UTC + volumes: + - jitsi-jicofo-config:/config + networks: + - changemaker-lite + + # Jitsi JVB — Video bridge (media relay for audio/video) + jitsi-jvb: + image: jitsi/jvb:stable-9823 + container_name: jitsi-jvb-changemaker + restart: unless-stopped + depends_on: + jitsi-prosody: + condition: service_healthy + ports: + - "127.0.0.1:${JVB_PORT:-10000}:10000/udp" + environment: + - XMPP_DOMAIN=meet.jitsi + - XMPP_AUTH_DOMAIN=auth.meet.jitsi + - XMPP_MUC_DOMAIN=muc.meet.jitsi + - XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi + - XMPP_SERVER=jitsi-prosody-changemaker + - JVB_AUTH_PASSWORD=${JITSI_JVB_AUTH_PASSWORD:-} + - JVB_ADVERTISE_IPS=${JVB_ADVERTISE_IP:-} + - JVB_PORT=${JVB_PORT:-10000} + - TZ=UTC + volumes: + - jitsi-jvb-config:/config + networks: + - changemaker-lite + + # MailHog — Email testing (dev) + mailhog: + image: mailhog/mailhog:v1.0.1 + container_name: mailhog-changemaker + ports: + - "127.0.0.1:${MAILHOG_WEB_PORT:-8025}:8025" + # SMTP port 1025 is only exposed on the Docker network (containers connect via mailhog-changemaker:1025) + restart: unless-stopped + networks: + - changemaker-lite + logging: + driver: "json-file" + options: + max-size: "5m" + max-file: "2" + + # ========================================================================= + # TUNNEL (Pangolin Newt connector) + # ========================================================================= + + # Newt — Pangolin tunnel connector (connects to Pangolin server) + newt: + image: fosrl/newt + container_name: newt-changemaker + restart: unless-stopped + environment: + - PANGOLIN_ENDPOINT=${PANGOLIN_ENDPOINT} + - NEWT_ID=${PANGOLIN_NEWT_ID} + - NEWT_SECRET=${PANGOLIN_NEWT_SECRET} + depends_on: + - nginx + networks: + - changemaker-lite + + # Docker socket proxy — read-only access for container status monitoring + docker-socket-proxy: + image: tecnativa/docker-socket-proxy:v0.4.2 + container_name: docker-socket-proxy + restart: unless-stopped + environment: + - CONTAINERS=1 # Allow container inspection + - POST=0 # Block all write operations + - IMAGES=0 + - NETWORKS=0 + - VOLUMES=0 + - EXEC=0 + - SWARM=0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - changemaker-lite + + # ========================================================================= + # MONITORING (behind profile flag) + # ========================================================================= + + prometheus: + image: prom/prometheus:v3.10.0 + container_name: prometheus-changemaker + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--storage.tsdb.retention.time=30d' + ports: + - "127.0.0.1:${PROMETHEUS_PORT:-9090}:9090" + volumes: + - ./configs/prometheus:/etc/prometheus + - prometheus-data:/prometheus + restart: always + networks: + - changemaker-lite + profiles: + - monitoring + + grafana: + image: grafana/grafana:12.3.0 + container_name: grafana-changemaker + ports: + - "127.0.0.1:${GRAFANA_PORT:-3001}:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD must be set in .env} + - GF_USERS_ALLOW_SIGN_UP=false + - GF_SERVER_ROOT_URL=${GRAFANA_ROOT_URL:-http://localhost:3001} + - GF_SECURITY_ALLOW_EMBEDDING=true + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer + volumes: + - grafana-data:/var/lib/grafana + - ./configs/grafana:/etc/grafana/provisioning + restart: always + depends_on: + - prometheus + networks: + - changemaker-lite + profiles: + - monitoring + + cadvisor: + image: gcr.io/cadvisor/cadvisor:v0.55.1 + container_name: cadvisor-changemaker + ports: + - "127.0.0.1:${CADVISOR_PORT:-8080}:8080" + volumes: + - /:/rootfs:ro + - /var/run:/var/run:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + - /dev/disk/:/dev/disk:ro + privileged: true + read_only: true + devices: + - /dev/kmsg + restart: always + networks: + - changemaker-lite + profiles: + - monitoring + + node-exporter: + image: prom/node-exporter:v1.10.2 + container_name: node-exporter-changemaker + ports: + - "127.0.0.1:${NODE_EXPORTER_PORT:-9100}:9100" + command: + - '--path.rootfs=/host' + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + restart: always + networks: + - changemaker-lite + profiles: + - monitoring + + redis-exporter: + image: oliver006/redis_exporter:v1.81.0 + container_name: redis-exporter-changemaker + ports: + - "127.0.0.1:${REDIS_EXPORTER_PORT:-9121}:9121" + environment: + - REDIS_ADDR=redis://redis-changemaker:6379 + - REDIS_PASSWORD=${REDIS_PASSWORD} + restart: always + depends_on: + - redis + networks: + - changemaker-lite + profiles: + - monitoring + + alertmanager: + image: prom/alertmanager:v0.31.1 + container_name: alertmanager-changemaker + ports: + - "127.0.0.1:${ALERTMANAGER_PORT:-9093}:9093" + volumes: + - ./configs/alertmanager:/etc/alertmanager + - alertmanager-data:/alertmanager + command: + - '--config.file=/etc/alertmanager/alertmanager.yml' + - '--storage.path=/alertmanager' + restart: always + networks: + - changemaker-lite + profiles: + - monitoring + + gotify: + image: gotify/server:v2.9.0 + container_name: gotify-changemaker + ports: + - "127.0.0.1:${GOTIFY_PORT:-8889}:80" + environment: + - GOTIFY_DEFAULTUSER_NAME=${GOTIFY_ADMIN_USER:-admin} + - GOTIFY_DEFAULTUSER_PASS=${GOTIFY_ADMIN_PASSWORD:?GOTIFY_ADMIN_PASSWORD must be set in .env} + - TZ=Etc/UTC + volumes: + - gotify-data:/app/data + restart: always + networks: + - changemaker-lite + profiles: + - monitoring + +# ============================================================================= +# NETWORKS & VOLUMES +# ============================================================================= + +networks: + changemaker-lite: + driver: bridge + +volumes: + # v2 + v2-postgres-data: + nocodb-v2-data: + # Shared + redis-data: + listmonk-data: + # Platform + n8n-data: + gitea-data: + mysql-data: + vaultwarden-data: + # Rocket.Chat + rocketchat-uploads: + mongodb-rocketchat-data: + # Gancio + gancio-data: + # Jitsi Meet + jitsi-web-config: + jitsi-prosody-config: + jitsi-prosody-plugins: + jitsi-jicofo-config: + jitsi-jvb-config: + # Monitoring + prometheus-data: + grafana-data: + alertmanager-data: + gotify-data: diff --git a/mkdocs/docs/docs/getting-started/index.md b/mkdocs/docs/docs/getting-started/index.md index 57e75e18..3cb7279f 100644 --- a/mkdocs/docs/docs/getting-started/index.md +++ b/mkdocs/docs/docs/getting-started/index.md @@ -19,7 +19,19 @@ This guide walks you through installing Changemaker Lite, running your first dep - At least 2 GB RAM and 10 GB disk space - A domain name (optional, but recommended for production) -## Quick Start +## Quick Install (Pre-built Images) + +The fastest way to deploy — no source code, no compilation: + +```bash +curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash +``` + +This downloads a lightweight release package (~2 MB), runs the configuration wizard, and pulls pre-built Docker images. First startup takes ~2 minutes. See [Installation](installation.md#pre-built-image-installation) for details. + +## Quick Start (From Source) + +For development or customization, clone the full repository: ```bash git clone https://gitea.bnkops.com/admin/changemaker.lite diff --git a/mkdocs/docs/docs/getting-started/installation.md b/mkdocs/docs/docs/getting-started/installation.md index 9810550b..5b3459d6 100644 --- a/mkdocs/docs/docs/getting-started/installation.md +++ b/mkdocs/docs/docs/getting-started/installation.md @@ -49,6 +49,54 @@ Open **http://localhost:3000** and sign in with the admin credentials you config --- +## Pre-built Image Installation + +For production deployments, you can skip cloning the source repository entirely. Pre-built Docker images are pulled from the Gitea container registry. + +### One-Line Install + +```bash +curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash +``` + +This script: + +1. Checks prerequisites (Docker, Docker Compose, OpenSSL) +2. Downloads the latest release package from Gitea +3. Extracts to `~/changemaker.lite/` +4. Launches the configuration wizard (`config.sh`) + +After the wizard completes, start everything with `docker compose up -d`. + +### Manual Download + +If you prefer not to pipe to bash: + +```bash +# Download latest release +curl -LO https://gitea.bnkops.com/admin/changemaker.lite/releases/latest/download/changemaker-lite-latest.tar.gz +tar xzf changemaker-lite-latest.tar.gz +cd changemaker-lite +bash config.sh +docker compose up -d +``` + +### What's Different from Source Install + +| | Source Install | Pre-built Install | +|---|---|---| +| **Download size** | ~200 MB (full repo) | ~2 MB (config + scripts) | +| **First startup** | 10+ min (TypeScript compile + Docker build) | ~2 min (image pull only) | +| **Requires** | Git, full repo | Docker only | +| **Upgrades** | `git pull` + rebuild | Download new release tarball | +| **Development** | Edit source, hot-reload | Not for development | + +!!! tip "When to use which" + Use **pre-built install** for production deployments and quick evaluation. + Use **source install** when you want to modify the platform code or contribute to development. + +--- + ## Configuration Wizard (`config.sh`) The wizard performs **14 steps** to produce a fully configured `.env` file and prepare the system for startup. Each step is interactive with sensible defaults. @@ -101,7 +149,7 @@ Auto-generates **21 unique secrets** — no placeholder passwords remain after t | Category | Count | Secrets | |----------|-------|---------| -| JWT & Encryption | 3 | `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`, `ENCRYPTION_KEY` (64-char hex) | +| JWT & Encryption | 4 | `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`, `JWT_INVITE_SECRET` (each 64-char hex), `ENCRYPTION_KEY` (64-char hex, must differ from JWT secrets) | | Database | 2 | `V2_POSTGRES_PASSWORD`, `REDIS_PASSWORD` (24-char alphanumeric) | | Listmonk | 3 | `LISTMONK_DB_PASSWORD`, `LISTMONK_WEB_ADMIN_PASSWORD`, `LISTMONK_API_TOKEN` | | NocoDB | 1 | `NC_ADMIN_PASSWORD` | @@ -227,7 +275,8 @@ V2_POSTGRES_PASSWORD=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 24 REDIS_PASSWORD=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 24) JWT_ACCESS_SECRET=$(openssl rand -hex 32) JWT_REFRESH_SECRET=$(openssl rand -hex 32) -ENCRYPTION_KEY=$(openssl rand -hex 32) +JWT_INVITE_SECRET=$(openssl rand -hex 32) +ENCRYPTION_KEY=$(openssl rand -hex 32) # must differ from all JWT secrets ``` Set your admin credentials (password must meet the 12+ char complexity requirement): diff --git a/mkdocs/docs/docs/getting-started/upgrades.md b/mkdocs/docs/docs/getting-started/upgrades.md index cac0e60f..aba670fd 100644 --- a/mkdocs/docs/docs/getting-started/upgrades.md +++ b/mkdocs/docs/docs/getting-started/upgrades.md @@ -71,6 +71,7 @@ The system fetches from the git remote and shows: 3. Optionally configure: - **Skip backup** — skip the database backup phase (not recommended) - **Pull images** — also update third-party Docker images (PostgreSQL, Redis, etc.) + - **Use registry images** — pull pre-built images from Gitea instead of compiling from source (faster — requires `scripts/build-and-push.sh` to have been run first) - **Dry run** — preview what would happen without making changes 4. Monitor the 6-phase progress indicator @@ -87,7 +88,7 @@ Both the GUI and CLI methods execute the same 6-phase process: | **1** | 5% | Pre-flight Checks | Verifies Docker, git, disk space (2 GB minimum), remote reachability, and clean working directory | | **2** | 15% | Backup | Runs `scripts/backup.sh` (pg_dump + archive), backs up user-modifiable content, saves pre-upgrade commit hash | | **3** | 30% | Code Update | Saves user paths, stashes local changes, `git pull`, pops stash with auto-conflict resolution, detects new `.env` variables | -| **4** | 50% | Container Rebuild | Rebuilds `api`, `admin`, `media-api`; conditionally rebuilds `nginx` and `code-server` if their configs changed; optionally pulls third-party images | +| **4** | 50% | Container Rebuild | Rebuilds `api`, `admin`, `media-api` from source (default) **or** pulls pre-built images from the Gitea registry (`--use-registry`); conditionally rebuilds `nginx` and `code-server` if their configs changed; optionally pulls third-party images | | **5** | 70% | Service Restart | Stops app containers, force-recreates LSIO containers, verifies Gancio config, starts infrastructure, waits for PostgreSQL, starts API (runs migrations), starts everything else, restarts Newt tunnel and monitoring if they were running | | **6** | 90% | Verification | Health checks for API, Admin, Media API, Gancio, MkDocs; detects containers in restart loops | @@ -126,6 +127,7 @@ Run the upgrade script directly: |------|-------------| | `--skip-backup` | Skip the backup phase (requires `--force`) | | `--pull-services` | Also pull new third-party Docker images | +| `--use-registry` | Pull pre-built images from Gitea instead of compiling from source | | `--dry-run` | Show what would happen without executing | | `--force` | Continue past non-critical warnings | | `--branch BRANCH` | Git branch to pull (default: current branch) | @@ -144,12 +146,52 @@ Run the upgrade script directly: # Full upgrade including third-party image updates ./scripts/upgrade.sh --pull-services +# Upgrade using pre-built images from Gitea registry (faster, no TypeScript compile) +./scripts/upgrade.sh --use-registry --force --skip-backup + # Rollback to the last pre-upgrade state ./scripts/upgrade.sh --rollback ``` --- +## Registry Mode (Fast Upgrades) + +By default, the upgrade script compiles TypeScript from source (`npm run build`) and rebuilds Docker images on the deployment server. **Registry mode** skips this by pulling pre-built production images from the Gitea container registry — faster and requires no build tooling on the server. + +### How It Works + +1. Run `scripts/build-and-push.sh` on a machine with Docker (usually your dev machine) to build and push production images tagged with the current commit SHA +2. During the next upgrade, pass `--use-registry` (CLI) or enable the checkbox (GUI) +3. The upgrade script pulls `gitea.bnkops.com/admin/changemaker-{service}:{sha}` instead of rebuilding from source +4. If a registry image is unavailable (e.g., the SHA wasn't pushed), it automatically falls back to a source build + +### Building and Pushing Images + +```bash +# Build and push all core services (api, admin, media-api, nginx) +./scripts/build-and-push.sh + +# Skip code-server (9 GB — push only when Dockerfile changes) +./scripts/build-and-push.sh --services api,admin,media-api,nginx + +# Build only, no push (verify locally first) +./scripts/build-and-push.sh --no-push + +# Also mirror third-party images (postgres, redis, etc.) to Gitea +./scripts/mirror-images.sh +``` + +!!! note "Registry prerequisites" + - Run `docker login gitea.bnkops.com` once per machine before pushing + - Set `GITEA_REGISTRY_USER` and `GITEA_REGISTRY_PASS` in `.env` for the admin GUI's Registry status endpoint + - gitea.bnkops.com must be reachable without proxies that limit upload size (Cloudflare free plan blocks blobs >100 MB) + +!!! info "Release installs upgrade automatically via registry" + If you installed from a release tarball (not git clone), the upgrade script automatically uses registry mode. It downloads the latest release package from Gitea instead of running `git pull`. No additional configuration needed. + +--- + ## Rollback ### Automatic Rollback diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100755 index 00000000..b3e4cf20 --- /dev/null +++ b/scripts/build-release.sh @@ -0,0 +1,235 @@ +#!/usr/bin/env bash +# ============================================================================= +# Changemaker Lite V2 — Build Release Tarball +# +# Creates a lightweight release tarball (~1-2 MB) containing only runtime files +# needed to deploy with pre-built Docker images. No source code included. +# +# Usage: +# ./scripts/build-release.sh [OPTIONS] +# +# Options: +# --tag TAG Version tag (default: git describe or commit SHA) +# --output DIR Output directory (default: ./releases/) +# --upload Upload to Gitea Releases API after building +# --dry-run Show what would be included without creating tarball +# --help Show this help +# +# Prerequisites: +# Run ./scripts/build-and-push.sh first to push Docker images to registry. +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# --- Defaults --- +TAG="" +OUTPUT_DIR="${PROJECT_DIR}/releases" +UPLOAD=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; } + +# --- Arg parser --- +while [[ $# -gt 0 ]]; do + case "$1" in + --tag) TAG="$2"; shift 2 ;; + --output) OUTPUT_DIR="$2"; shift 2 ;; + --upload) UPLOAD=true; shift ;; + --dry-run) DRY_RUN=true; shift ;; + --help|-h) + sed -n '2,20p' "$0" | grep '^#' | sed 's/^# \?//' + exit 0 ;; + *) error "Unknown option: $1"; exit 1 ;; + esac +done + +# --- Determine version --- +cd "$PROJECT_DIR" +if [[ -z "$TAG" ]]; then + TAG="$(git describe --tags --always 2>/dev/null || git rev-parse --short HEAD)" +fi +COMMIT_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")" +BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + +echo -e "${BOLD}Changemaker Lite — Build Release${NC}" +echo " Tag: $TAG" +echo " Commit: $COMMIT_SHA" +echo " Date: $BUILD_DATE" +echo "" + +# --- Create staging directory --- +STAGE_DIR="$(mktemp -d)/changemaker-lite" +mkdir -p "$STAGE_DIR" + +# --- Write VERSION file --- +cat > "$STAGE_DIR/VERSION" << EOF +$TAG +$COMMIT_SHA +$BUILD_DATE +EOF +info "VERSION: $TAG ($COMMIT_SHA)" + +# --- Copy production docker-compose --- +if [[ ! -f "$PROJECT_DIR/docker-compose.prod.yml" ]]; then + error "docker-compose.prod.yml not found. Generate it first." + exit 1 +fi +cp "$PROJECT_DIR/docker-compose.prod.yml" "$STAGE_DIR/docker-compose.yml" +info "docker-compose.yml (production)" + +# --- Copy config files --- +cp "$PROJECT_DIR/.env.example" "$STAGE_DIR/" +cp "$PROJECT_DIR/config.sh" "$STAGE_DIR/" +info "Config files (.env.example, config.sh)" + +# --- Copy scripts --- +mkdir -p "$STAGE_DIR/scripts" + +# Init scripts (from api/prisma/ to scripts/) +cp "$PROJECT_DIR/api/prisma/init-nocodb-db.sh" "$STAGE_DIR/scripts/" +cp "$PROJECT_DIR/api/prisma/init-gancio-db.sh" "$STAGE_DIR/scripts/" + +# Runtime scripts +for script in nocodb-init.sh mkdocs-entrypoint.sh backup.sh \ + upgrade.sh upgrade-check.sh upgrade-watcher.sh; do + if [[ -f "$PROJECT_DIR/scripts/$script" ]]; then + cp "$PROJECT_DIR/scripts/$script" "$STAGE_DIR/scripts/" + fi +done + +# MkDocs build trigger +if [[ -f "$PROJECT_DIR/scripts/mkdocs-build-trigger.py" ]]; then + cp "$PROJECT_DIR/scripts/mkdocs-build-trigger.py" "$STAGE_DIR/scripts/" +fi + +# Systemd units +if [[ -d "$PROJECT_DIR/scripts/systemd" ]]; then + cp -r "$PROJECT_DIR/scripts/systemd" "$STAGE_DIR/scripts/" +fi + +# Install script (for reference) +cp "$PROJECT_DIR/scripts/install.sh" "$STAGE_DIR/scripts/" + +chmod +x "$STAGE_DIR/scripts/"*.sh 2>/dev/null || true +info "Scripts ($(ls "$STAGE_DIR/scripts/" | wc -l) files)" + +# --- Copy configs --- +if [[ -d "$PROJECT_DIR/configs" ]]; then + cp -r "$PROJECT_DIR/configs" "$STAGE_DIR/" + # Ensure code-server skeleton dirs exist + mkdir -p "$STAGE_DIR/configs/code-server/.config" + mkdir -p "$STAGE_DIR/configs/code-server/.local" + info "Configs (homepage, prometheus, grafana, alertmanager, pangolin)" +fi + +# --- Copy nginx templates (for reference) --- +mkdir -p "$STAGE_DIR/nginx/conf.d" +cp "$PROJECT_DIR"/nginx/conf.d/*.template "$STAGE_DIR/nginx/conf.d/" 2>/dev/null || true +info "Nginx templates (for reference)" + +# --- Copy MkDocs starter docs --- +if [[ -d "$PROJECT_DIR/mkdocs" ]]; then + mkdir -p "$STAGE_DIR/mkdocs" + cp "$PROJECT_DIR/mkdocs/mkdocs.yml" "$STAGE_DIR/mkdocs/" 2>/dev/null || true + cp -r "$PROJECT_DIR/mkdocs/docs" "$STAGE_DIR/mkdocs/" 2>/dev/null || true + cp -r "$PROJECT_DIR/mkdocs/overrides" "$STAGE_DIR/mkdocs/" 2>/dev/null || true + mkdir -p "$STAGE_DIR/mkdocs/.cache" + mkdir -p "$STAGE_DIR/mkdocs/site" + info "MkDocs (starter documentation)" +fi + +# --- Copy public-web --- +if [[ -d "$PROJECT_DIR/public-web" ]]; then + cp -r "$PROJECT_DIR/public-web" "$STAGE_DIR/" +else + mkdir -p "$STAGE_DIR/public-web" +fi +info "Public web assets" + +# --- Create empty data directories --- +mkdir -p "$STAGE_DIR/assets/uploads" +mkdir -p "$STAGE_DIR/assets/icons" +mkdir -p "$STAGE_DIR/assets/images" +mkdir -p "$STAGE_DIR/data/upgrade" +mkdir -p "$STAGE_DIR/media/local/inbox" +mkdir -p "$STAGE_DIR/media/local/thumbnails" +mkdir -p "$STAGE_DIR/media/local/photos" +mkdir -p "$STAGE_DIR/media/public" +mkdir -p "$STAGE_DIR/local-files" +info "Data directories (empty)" + +# --- Summary --- +echo "" +STAGE_SIZE=$(du -sh "$STAGE_DIR" | cut -f1) +FILE_COUNT=$(find "$STAGE_DIR" -type f | wc -l) +info "Staging: ${FILE_COUNT} files, ${STAGE_SIZE}" + +if [[ "$DRY_RUN" == "true" ]]; then + echo "" + info "[DRY RUN] Would create: changemaker-lite-${TAG}.tar.gz" + find "$STAGE_DIR" -type f | sed "s|$STAGE_DIR/||" | sort + rm -rf "$(dirname "$STAGE_DIR")" + exit 0 +fi + +# --- Create tarball --- +mkdir -p "$OUTPUT_DIR" +TARBALL="${OUTPUT_DIR}/changemaker-lite-${TAG}.tar.gz" + +tar czf "$TARBALL" -C "$(dirname "$STAGE_DIR")" "changemaker-lite" +rm -rf "$(dirname "$STAGE_DIR")" + +TARBALL_SIZE=$(du -h "$TARBALL" | cut -f1) +success "Created: $TARBALL (${TARBALL_SIZE})" + +# --- Upload to Gitea (optional) --- +if [[ "$UPLOAD" == "true" ]]; then + source "$PROJECT_DIR/.env" 2>/dev/null || true + GITEA_TOKEN="${GITEA_API_TOKEN:-}" + GITEA_HOST="${GITEA_URL:-https://gitea.bnkops.com}" + + if [[ -z "$GITEA_TOKEN" ]]; then + warn "GITEA_API_TOKEN not set — skipping upload" + warn "Set GITEA_API_TOKEN in .env and re-run with --upload" + else + info "Creating Gitea release ${TAG}..." + RELEASE_RESPONSE=$(curl -sf -X POST \ + "${GITEA_HOST}/api/v1/repos/admin/changemaker.lite/releases" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"${TAG}\",\"name\":\"Changemaker Lite ${TAG}\",\"body\":\"Release ${TAG} (${COMMIT_SHA})\"}" \ + 2>/dev/null || true) + + RELEASE_ID=$(echo "$RELEASE_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [[ -n "$RELEASE_ID" ]]; then + info "Uploading tarball to release ${RELEASE_ID}..." + curl -sf -X POST \ + "${GITEA_HOST}/api/v1/repos/admin/changemaker.lite/releases/${RELEASE_ID}/assets" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -F "attachment=@${TARBALL}" \ + >/dev/null 2>&1 + success "Uploaded to Gitea release ${TAG}" + else + warn "Failed to create release — upload manually at ${GITEA_HOST}/admin/changemaker.lite/releases" + fi + fi +fi + +echo "" +success "Release ${TAG} ready." +echo " Tarball: $TARBALL" +echo " Install: tar xzf $(basename "$TARBALL") && cd changemaker-lite && bash config.sh" diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 00000000..0f6a19d1 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,177 @@ +#!/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="" + +# --- 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; } + +# --- 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)" + +# --- 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 --- +TARBALL_PATH="" +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 registry requires authentication:" + 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" + rm -rf "$EXTRACT_DIR" + exit 1 +fi + +mv "$EXTRACTED" "$INSTALL_DIR" +rm -rf "$EXTRACT_DIR" + +# 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" +bash config.sh + +# --- Done --- +echo "" +echo -e "${BOLD}${GREEN}Installation complete!${NC}" +echo "" +echo " Start all services:" +echo " cd ${INSTALL_DIR} && docker compose up -d" +echo "" +echo " Check status:" +echo " docker compose ps" +echo "" +echo " View API logs:" +echo " docker compose logs -f api --tail 20" +echo "" diff --git a/scripts/upgrade-check.sh b/scripts/upgrade-check.sh index 2fa021cd..587a85f7 100755 --- a/scripts/upgrade-check.sh +++ b/scripts/upgrade-check.sh @@ -24,6 +24,70 @@ done cd "$PROJECT_DIR" mkdir -p "$UPGRADE_DIR" +# --- Detect install mode --- +if [[ -f "$PROJECT_DIR/VERSION" ]] && [[ ! -d "$PROJECT_DIR/.git" ]]; then + INSTALL_MODE="release" +else + INSTALL_MODE="source" +fi + +# --- Release mode: check Gitea Releases API --- +if [[ "$INSTALL_MODE" == "release" ]]; then + GITEA_API="https://gitea.bnkops.com/api/v1" + CURRENT_VERSION=$(head -1 "$PROJECT_DIR/VERSION" 2>/dev/null || echo "unknown") + CURRENT_SHA=$(sed -n '2p' "$PROJECT_DIR/VERSION" 2>/dev/null || echo "unknown") + CURRENT_DATE=$(sed -n '3p' "$PROJECT_DIR/VERSION" 2>/dev/null || echo "") + + RELEASE_JSON=$(curl -sf "${GITEA_API}/repos/admin/changemaker.lite/releases/latest" 2>/dev/null || true) + if [[ -z "$RELEASE_JSON" ]]; then + cat > "$STATUS_FILE" </dev/null) + LATEST_DATE=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('created_at',''))" 2>/dev/null) + LATEST_BODY=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('body','').replace('\"','\\\\\"')[:200])" 2>/dev/null) + + if [[ "$CURRENT_VERSION" == "$LATEST_TAG" ]]; then + COMMITS_BEHIND=0 + else + COMMITS_BEHIND=1 + fi + + cat > "$STATUS_FILE" </dev/null || echo "unknown") + + info "Release mode — checking for updates (current: ${CURRENT_VERSION})..." + RELEASE_JSON=$(curl -sf "${GITEA_API}/repos/admin/changemaker.lite/releases/latest" 2>/dev/null || true) + + if [[ -z "$RELEASE_JSON" ]]; then + error "Could not reach Gitea API. Check network or GITEA_REGISTRY_URL." + exit 1 + fi + + LATEST_TAG=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tag_name',''))" 2>/dev/null) + TARBALL_URL=$(echo "$RELEASE_JSON" | python3 -c " +import sys, json +for a in json.load(sys.stdin).get('assets', []): + if a['name'].endswith('.tar.gz'): + print(a['browser_download_url']); break +" 2>/dev/null || true) + + if [[ "$CURRENT_VERSION" == "$LATEST_TAG" ]] && [[ "$FORCE" != "true" ]]; then + info "Already at latest version: ${CURRENT_VERSION}" + write_progress 3 "Code Update" 45 "Already up to date" + elif [[ -z "$TARBALL_URL" ]]; then + error "No tarball found in release ${LATEST_TAG}" + exit 1 + else + info "Updating ${CURRENT_VERSION} → ${LATEST_TAG}..." + write_progress 3 "Code Update" 35 "Downloading ${LATEST_TAG}..." + + # Download + DOWNLOAD_DIR=$(mktemp -d) + curl -fSL "$TARBALL_URL" -o "${DOWNLOAD_DIR}/update.tar.gz" + tar xzf "${DOWNLOAD_DIR}/update.tar.gz" -C "$DOWNLOAD_DIR" + UPDATE_SRC=$(find "$DOWNLOAD_DIR" -maxdepth 1 -mindepth 1 -type d | head -1) + + # Save user paths + save_user_paths + + # Sync new files, preserving .env + write_progress 3 "Code Update" 40 "Applying update..." + rsync -a --exclude='.env' "$UPDATE_SRC/" "$PROJECT_DIR/" + + # Restore user paths + restore_user_paths + + # Restore tracked files that may have been overwritten + DELETED_TRACKED="$(git ls-files --deleted 2>/dev/null || true)" + if [[ -n "$DELETED_TRACKED" ]]; then + echo "$DELETED_TRACKED" | xargs git checkout HEAD -- 2>/dev/null || true + fi + + rm -rf "$DOWNLOAD_DIR" + success "Updated to ${LATEST_TAG}" + fi + + # Skip the git-based update flow below + POST_PULL_COMMIT="$(head -2 "$PROJECT_DIR/VERSION" | tail -1 2>/dev/null || echo "release")" + +elif [[ "$DRY_RUN" == "true" ]]; then info "[DRY RUN] Would fetch and show incoming changes:" git fetch origin "$BRANCH" 2>/dev/null || true INCOMING="$(git log --oneline HEAD..origin/"$BRANCH" 2>/dev/null || echo "(unable to preview)")"