#!/bin/bash # # LetsBe Cloud Environment Setup Script # Non-interactive version for Orchestrator/SysAdmin Agent integration # # Usage: # ./env_setup.sh --customer "acme" --domain "acme.com" --company "Acme Corp" # ./env_setup.sh --json '{"customer":"acme","domain":"acme.com","company_name":"Acme Corp"}' # ./env_setup.sh --config /path/to/config.json # set -euo pipefail # ============================================================================ # CONFIGURATION # ============================================================================ LETSBE_BASE="/opt/letsbe" STACKS_DIR="${LETSBE_BASE}/stacks" NGINX_DIR="${LETSBE_BASE}/nginx" ENV_DIR="${LETSBE_BASE}/env" SCRIPTS_DIR="${LETSBE_BASE}/scripts" # ============================================================================ # HELPER FUNCTIONS # ============================================================================ usage() { cat <&2 } die() { log_error "$*" exit 1 } # Generate random string of specified length generate_random_string() { local length=$1 tr -dc A-Za-z0-9 /dev/null; then die "jq is required for JSON parsing. Install with: apt-get install jq" fi customer=$(echo "${json_input}" | jq -r '.customer // empty') domain=$(echo "${json_input}" | jq -r '.domain // empty') company_name=$(echo "${json_input}" | jq -r '.company_name // empty') } # ============================================================================ # ARGUMENT PARSING # ============================================================================ customer="" domain="" company_name="" docker_user="" while [[ $# -gt 0 ]]; do case $1 in --customer) customer="$2" shift 2 ;; --domain) domain="$2" shift 2 ;; --company) company_name="$2" shift 2 ;; --docker-user) docker_user="$2" shift 2 ;; --json) parse_json "$2" shift 2 ;; --config) if [[ ! -f "$2" ]]; then die "Config file not found: $2" fi parse_json "$(cat "$2")" shift 2 ;; --help|-h) usage ;; *) log_error "Unknown option: $1" usage ;; esac done # ============================================================================ # VALIDATION # ============================================================================ validate_required "customer" "${customer}" validate_required "domain" "${domain}" validate_required "company_name" "${company_name}" # Validate customer format (lowercase, no spaces/hyphens/numbers) if [[ ! "${customer}" =~ ^[a-z]+$ ]]; then die "Customer name must be lowercase letters only, no spaces/hyphens/numbers: ${customer}" fi # Validate domain format if [[ ! "${domain}" =~ ^[a-z0-9.-]+\.[a-z]{2,}$ ]]; then die "Invalid domain format: ${domain}" fi log_info "Configuration validated" log_info " Customer: ${customer}" log_info " Domain: ${domain}" log_info " Company: ${company_name}" # ============================================================================ # DERIVED VARIABLES # ============================================================================ # Email for Let's Encrypt letsencrypt_email="postmaster@${domain}" # Subdomains per tool domain_html="html.${domain}" domain_wordpress="${domain}" domain_squidex="contenthub.${domain}" domain_chatwoot="support.${domain}" domain_chatwoot_helpdesk="helpdesk.${domain}" domain_gitea="code.${domain}" domain_gitea_drone="ci.${domain}" domain_glitchtip="debug.${domain}" domain_listmonk="newsletters.${domain}" domain_n8n="n8n.${domain}" domain_nextcloud="cloud.${domain}" domain_penpot="design.${domain}" domain_poste="mail.${domain}" domain_umami="analytics.${domain}" domain_uptime_kuma="uptime.${domain}" domain_windmill="flows.${domain}" domain_calcom="bookings.${domain}" domain_odoo="crm.${domain}" domain_collabora="collabora.${domain}" domain_whiteboard="whiteboard.${domain}" domain_activepieces="automation.${domain}" domain_minio="minio.${domain}" domain_s3="s3.${domain}" domain_librechat="ai.${domain}" domain_bot_viewer="bots.${domain}" domain_botlab="botlab.${domain}" domain_nocodb="database.${domain}" domain_redash="data.${domain}" domain_documenso="signatures.${domain}" domain_keycloak="auth.${domain}" domain_pdf="pdf.${domain}" domain_ghost="${domain}" # ============================================================================ # GENERATED SECRETS # ============================================================================ log_info "Generating secrets and credentials..." # WordPress wordpresss_mariadb_root_password=$(generate_random_string 20) wordpress_db_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') wordpress_db_password=$(generate_random_string 20) # Squidex squidex_adminemail="postmaster@${domain}" squidex_adminpassword=$(generate_random_string 20) # Listmonk listmonk_admin_username=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') listmonk_admin_password=$(generate_random_string 20) listmonk_db_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') listmonk_db_password=$(generate_random_string 20) # Gitea gitea_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') gitea_postgres_password=$(generate_random_string 20) # Umami umami_app_secret=$(generate_random_string 32) umami_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') umami_postgres_password=$(generate_random_string 20) # Drone/Gitea drone_gitea_rpc_secret=$(generate_random_string 32) # Windmill windmill_database_password=$(generate_random_string 20) # Glitchtip glitchtip_database_password=$(generate_random_string 20) glitchtip_secret_key=$(generate_random_string 32) # Penpot penpot_secret_key=$(generate_random_string 32) penpot_db_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') penpot_db_password=$(generate_random_string 20) # Nextcloud nextcloud_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') nextcloud_postgres_password=$(generate_random_string 20) nextcloud_jwt_secret=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') nextcloud_admin_password=$(generate_random_string 20) # Collabora collabora_password=$(generate_random_string 20) collabora_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') # Chatwoot chatwoot_secret_key_base=$(generate_random_string 32) chatwoot_redis_password=$(generate_random_string 20) chatwoot_postgres_username=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') chatwoot_postgres_password=$(generate_random_string 20) chatwoot_rails_inbound_email_password=$(generate_random_string 20) # N8N n8n_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') n8n_postgres_password=$(generate_random_string 20) # Cal.com calcom_nextauth_secret=$(generate_random_string 32) calcom_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') calcom_postgres_password=$(generate_random_string 20) # Odoo odoo_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') odoo_postgres_password=$(generate_random_string 20) # Activepieces activepieces_api_key=$(generate_random_string 32) activepieces_encryption_key=$(generate_random_string 32 | tr '[:upper:]' '[:lower:]') activepieces_jwt_secret=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') activepieces_postgres_password=$(generate_random_string 32) # MinIO minio_root_user=$(generate_random_string 16) minio_root_password=$(generate_random_string 32) # Typebot typebot_encryption_secret=$(generate_random_string 32) typebot_postgres_password=$(generate_random_string 20) # NocoDB nocodb_postgres_password=$(generate_random_string 32) # LibreChat librechat_postgres_password=$(generate_random_string 20) librechat_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') librechat_jwt_secret=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') librechat_jwt_refresh_secret=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') # Redash redash_secret_key=$(generate_random_string 32) redash_cookie_secret=$(generate_random_string 32) redash_postgres_password=$(generate_random_string 20) redash_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') # Documenso documenso_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') documenso_postgres_password=$(generate_random_string 40) documenso_nextauth_secret=$(generate_random_string 32) documenso_encryption_key=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') documenso_encryption_secondary_key=$(generate_random_string 64 | tr '[:upper:]' '[:lower:]') # Ghost ghost_mysql_password=$(generate_random_string 40) ghost_s3_access_key=$(generate_random_string 20) ghost_s3_secret_key=$(generate_random_string 40) # Keycloak keycloak_postgres_password=$(generate_random_string 40) keycloak_admin_password=$(generate_random_string 40) keycloak_grafana_password=$(generate_random_string 40) # StirlingPDF stirlingpdf_postgres_user=$(generate_random_string 10 | tr '[:upper:]' '[:lower:]') stirlingpdf_postgres_password=$(generate_random_string 40) stirlingpdf_api_key=$(generate_random_string 40) # Sysadmin Agent # Registration token must be obtained from orchestrator API: # POST /api/v1/tenants/{tenant_id}/registration-tokens # The returned token is passed via SYSADMIN_REGISTRATION_TOKEN env var sysadmin_registration_token="${SYSADMIN_REGISTRATION_TOKEN:-}" if [[ -z "${sysadmin_registration_token}" ]]; then log_error "SYSADMIN_REGISTRATION_TOKEN environment variable is required" log_error "Obtain a registration token from the orchestrator:" log_error " curl -X POST https://orchestrator.letsbe.biz/api/v1/tenants/{tenant_id}/registration-tokens \\" log_error " -H 'X-Admin-Api-Key: YOUR_ADMIN_KEY' \\" log_error " -H 'Content-Type: application/json' \\" log_error " -d '{\"description\": \"Agent for ${customer}\"}'" die "Missing SYSADMIN_REGISTRATION_TOKEN" fi # Legacy token (deprecated, kept for backward compatibility) sysadmin_agent_token=$(generate_random_string 64) # ============================================================================ # TEMPLATE REPLACEMENT # ============================================================================ log_info "Replacing placeholders in template files..." # Process all template files for file in "${STACKS_DIR}"/*/* "${STACKS_DIR}"/*/.* "${NGINX_DIR}"/* "${SCRIPTS_DIR}"/backups.sh; do if [[ -f "${file}" ]]; then # Core variables sed -i "s/{{ customer }}/${customer}/g" "${file}" sed -i "s/{{ domain }}/${domain}/g" "${file}" sed -i "s/{{ company_name }}/${company_name}/g" "${file}" sed -i "s/{{ letsencrypt_email }}/${letsencrypt_email}/g" "${file}" # Domain variables sed -i "s/{{ domain_html }}/${domain_html}/g" "${file}" sed -i "s/{{ domain_wordpress }}/${domain_wordpress}/g" "${file}" sed -i "s/{{ domain_squidex }}/${domain_squidex}/g" "${file}" sed -i "s/{{ domain_chatwoot }}/${domain_chatwoot}/g" "${file}" sed -i "s/{{ domain_chatwoot_helpdesk }}/${domain_chatwoot_helpdesk}/g" "${file}" sed -i "s/{{ domain_gitea }}/${domain_gitea}/g" "${file}" sed -i "s/{{ domain_gitea_drone }}/${domain_gitea_drone}/g" "${file}" sed -i "s/{{ domain_glitchtip }}/${domain_glitchtip}/g" "${file}" sed -i "s/{{ domain_listmonk }}/${domain_listmonk}/g" "${file}" sed -i "s/{{ domain_librechat }}/${domain_librechat}/g" "${file}" sed -i "s/{{ domain_n8n }}/${domain_n8n}/g" "${file}" sed -i "s/{{ domain_nextcloud }}/${domain_nextcloud}/g" "${file}" sed -i "s/{{ domain_penpot }}/${domain_penpot}/g" "${file}" sed -i "s/{{ domain_poste }}/${domain_poste}/g" "${file}" sed -i "s/{{ domain_umami }}/${domain_umami}/g" "${file}" sed -i "s/{{ domain_uptime_kuma }}/${domain_uptime_kuma}/g" "${file}" sed -i "s/{{ domain_windmill }}/${domain_windmill}/g" "${file}" sed -i "s/{{ domain_calcom }}/${domain_calcom}/g" "${file}" sed -i "s/{{ domain_odoo }}/${domain_odoo}/g" "${file}" sed -i "s/{{ domain_collabora }}/${domain_collabora}/g" "${file}" sed -i "s/{{ domain_activepieces }}/${domain_activepieces}/g" "${file}" sed -i "s/{{ domain_bot_viewer }}/${domain_bot_viewer}/g" "${file}" sed -i "s/{{ domain_botlab }}/${domain_botlab}/g" "${file}" sed -i "s/{{ domain_minio }}/${domain_minio}/g" "${file}" sed -i "s/{{ domain_s3 }}/${domain_s3}/g" "${file}" sed -i "s/{{ domain_nocodb }}/${domain_nocodb}/g" "${file}" sed -i "s/{{ domain_whiteboard }}/${domain_whiteboard}/g" "${file}" sed -i "s/{{ domain_redash }}/${domain_redash}/g" "${file}" sed -i "s/{{ domain_documenso }}/${domain_documenso}/g" "${file}" sed -i "s/{{ domain_keycloak }}/${domain_keycloak}/g" "${file}" sed -i "s/{{ domain_pdf }}/${domain_pdf}/g" "${file}" sed -i "s/{{ domain_ghost }}/${domain_ghost}/g" "${file}" # Credential variables sed -i "s/{{ wordpresss_mariadb_root_password }}/${wordpresss_mariadb_root_password}/g" "${file}" sed -i "s/{{ wordpress_db_user }}/${wordpress_db_user}/g" "${file}" sed -i "s/{{ wordpress_db_password }}/${wordpress_db_password}/g" "${file}" sed -i "s/{{ squidex_adminemail }}/${squidex_adminemail}/g" "${file}" sed -i "s/{{ squidex_adminpassword }}/${squidex_adminpassword}/g" "${file}" sed -i "s/{{ listmonk_admin_username }}/${listmonk_admin_username}/g" "${file}" sed -i "s/{{ listmonk_admin_password }}/${listmonk_admin_password}/g" "${file}" sed -i "s/{{ listmonk_db_user }}/${listmonk_db_user}/g" "${file}" sed -i "s/{{ listmonk_db_password }}/${listmonk_db_password}/g" "${file}" sed -i "s/{{ gitea_postgres_user }}/${gitea_postgres_user}/g" "${file}" sed -i "s/{{ gitea_postgres_password }}/${gitea_postgres_password}/g" "${file}" sed -i "s/{{ umami_app_secret }}/${umami_app_secret}/g" "${file}" sed -i "s/{{ umami_postgres_user }}/${umami_postgres_user}/g" "${file}" sed -i "s/{{ umami_postgres_password }}/${umami_postgres_password}/g" "${file}" sed -i "s/{{ drone_gitea_rpc_secret }}/${drone_gitea_rpc_secret}/g" "${file}" sed -i "s/{{ windmill_database_password }}/${windmill_database_password}/g" "${file}" sed -i "s/{{ glitchtip_database_password }}/${glitchtip_database_password}/g" "${file}" sed -i "s/{{ glitchtip_secret_key }}/${glitchtip_secret_key}/g" "${file}" sed -i "s/{{ penpot_secret_key }}/${penpot_secret_key}/g" "${file}" sed -i "s/{{ penpot_db_user }}/${penpot_db_user}/g" "${file}" sed -i "s/{{ penpot_db_password }}/${penpot_db_password}/g" "${file}" sed -i "s/{{ nextcloud_postgres_user }}/${nextcloud_postgres_user}/g" "${file}" sed -i "s/{{ nextcloud_postgres_password }}/${nextcloud_postgres_password}/g" "${file}" sed -i "s/{{ nextcloud_admin_password }}/${nextcloud_admin_password}/g" "${file}" sed -i "s/{{ nextcloud_jwt_secret }}/${nextcloud_jwt_secret}/g" "${file}" sed -i "s/{{ collabora_password }}/${collabora_password}/g" "${file}" sed -i "s/{{ collabora_user }}/${collabora_user}/g" "${file}" sed -i "s/{{ chatwoot_secret_key_base }}/${chatwoot_secret_key_base}/g" "${file}" sed -i "s/{{ chatwoot_redis_password }}/${chatwoot_redis_password}/g" "${file}" sed -i "s/{{ chatwoot_postgres_username }}/${chatwoot_postgres_username}/g" "${file}" sed -i "s/{{ chatwoot_postgres_password }}/${chatwoot_postgres_password}/g" "${file}" sed -i "s/{{ chatwoot_rails_inbound_email_password }}/${chatwoot_rails_inbound_email_password}/g" "${file}" sed -i "s/{{ n8n_postgres_user }}/${n8n_postgres_user}/g" "${file}" sed -i "s/{{ n8n_postgres_password }}/${n8n_postgres_password}/g" "${file}" sed -i "s/{{ calcom_nextauth_secret }}/${calcom_nextauth_secret}/g" "${file}" sed -i "s/{{ calcom_postgres_user }}/${calcom_postgres_user}/g" "${file}" sed -i "s/{{ calcom_postgres_password }}/${calcom_postgres_password}/g" "${file}" sed -i "s/{{ odoo_postgres_user }}/${odoo_postgres_user}/g" "${file}" sed -i "s/{{ odoo_postgres_password }}/${odoo_postgres_password}/g" "${file}" sed -i "s/{{ activepieces_api_key }}/${activepieces_api_key}/g" "${file}" sed -i "s/{{ activepieces_encryption_key }}/${activepieces_encryption_key}/g" "${file}" sed -i "s/{{ activepieces_jwt_secret }}/${activepieces_jwt_secret}/g" "${file}" sed -i "s/{{ activepieces_postgres_password }}/${activepieces_postgres_password}/g" "${file}" sed -i "s/{{ minio_root_user }}/${minio_root_user}/g" "${file}" sed -i "s/{{ minio_root_password }}/${minio_root_password}/g" "${file}" sed -i "s/{{ typebot_encryption_secret }}/${typebot_encryption_secret}/g" "${file}" sed -i "s/{{ nocodb_postgres_password }}/${nocodb_postgres_password}/g" "${file}" sed -i "s/{{ typebot_postgres_password }}/${typebot_postgres_password}/g" "${file}" sed -i "s/{{ redash_secret_key }}/${redash_secret_key}/g" "${file}" sed -i "s/{{ redash_cookie_secret }}/${redash_cookie_secret}/g" "${file}" sed -i "s/{{ redash_postgres_user }}/${redash_postgres_user}/g" "${file}" sed -i "s/{{ redash_postgres_password }}/${redash_postgres_password}/g" "${file}" sed -i "s/{{ librechat_postgres_password }}/${librechat_postgres_password}/g" "${file}" sed -i "s/{{ librechat_postgres_user }}/${librechat_postgres_user}/g" "${file}" sed -i "s/{{ librechat_jwt_secret }}/${librechat_jwt_secret}/g" "${file}" sed -i "s/{{ librechat_jwt_refresh_secret }}/${librechat_jwt_refresh_secret}/g" "${file}" sed -i "s/{{ documenso_postgres_user }}/${documenso_postgres_user}/g" "${file}" sed -i "s/{{ documenso_postgres_password }}/${documenso_postgres_password}/g" "${file}" sed -i "s/{{ documenso_nextauth_secret }}/${documenso_nextauth_secret}/g" "${file}" sed -i "s/{{ documenso_encryption_key }}/${documenso_encryption_key}/g" "${file}" sed -i "s/{{ documenso_encryption_secondary_key }}/${documenso_encryption_secondary_key}/g" "${file}" sed -i "s/{{ ghost_mysql_password }}/${ghost_mysql_password}/g" "${file}" sed -i "s/{{ ghost_s3_access_key }}/${ghost_s3_access_key}/g" "${file}" sed -i "s/{{ ghost_s3_secret_key }}/${ghost_s3_secret_key}/g" "${file}" sed -i "s/{{ keycloak_postgres_password }}/${keycloak_postgres_password}/g" "${file}" sed -i "s/{{ keycloak_admin_password }}/${keycloak_admin_password}/g" "${file}" sed -i "s/{{ keycloak_grafana_password }}/${keycloak_grafana_password}/g" "${file}" sed -i "s/{{ stirlingpdf_postgres_user }}/${stirlingpdf_postgres_user}/g" "${file}" sed -i "s/{{ stirlingpdf_postgres_password }}/${stirlingpdf_postgres_password}/g" "${file}" sed -i "s/{{ stirlingpdf_api_key }}/${stirlingpdf_api_key}/g" "${file}" sed -i "s/{{ sysadmin_agent_token }}/${sysadmin_agent_token}/g" "${file}" sed -i "s/{{ sysadmin_registration_token }}/${sysadmin_registration_token}/g" "${file}" fi done log_info "All placeholders replaced successfully." # ============================================================================ # GENERATE ENV FILES # ============================================================================ log_info "Generating centralized environment files..." mkdir -p "${ENV_DIR}" # Write master credentials file for reference cat > "${ENV_DIR}/credentials.env" <> "${ENV_DIR}/credentials.env" <