#!/bin/env bash set -e ubuntu_version=$(lsb_release -a 2>/dev/null | grep -v "No LSB modules are available." | grep "Description:" | awk -F "Description:\t" '{print $2}') install_formbricks() { # Friendly welcome echo "๐Ÿงฑ Welcome to the Formbricks Setup Script" echo "" echo "๐Ÿ›ธ Fasten your seatbelts! We're setting up your Formbricks environment on your $ubuntu_version server." echo "" # Remove any old Docker installations, without stopping the script if they're not found echo "๐Ÿงน Time to sweep away any old Docker installations." sudo apt-get remove docker docker-engine docker.io containerd runc >/dev/null 2>&1 || true # Update package list echo "๐Ÿ”„ Updating your package list." sudo apt-get update >/dev/null 2>&1 # Install dependencies echo "๐Ÿ“ฆ Installing the necessary dependencies." sudo apt-get install -y \ ca-certificates \ curl \ gnupg \ lsb-release >/dev/null 2>&1 # Set up Docker's official GPG key & stable repository echo "๐Ÿ”‘ Adding Docker's official GPG key and setting up the stable repository." sudo mkdir -m 0755 -p /etc/apt/keyrings >/dev/null 2>&1 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg >/dev/null 2>&1 echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null 2>&1 # Update package list again echo "๐Ÿ”„ Updating your package list again." sudo apt-get update >/dev/null 2>&1 # Install Docker echo "๐Ÿณ Installing Docker." sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null 2>&1 # Test Docker installation echo "๐Ÿš€ Testing your Docker installation." if docker --version >/dev/null 2>&1; then echo "๐ŸŽ‰ Docker is installed!" else echo "โŒ Docker is not installed. Please install Docker before proceeding." exit 1 fi # Adding your user to the Docker group echo "๐Ÿณ Adding your user to the Docker group to avoid using sudo with docker commands." sudo groupadd docker >/dev/null 2>&1 || true sudo usermod -aG docker $USER >/dev/null 2>&1 echo "๐ŸŽ‰ Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!" mkdir -p formbricks && cd formbricks echo "๐Ÿ“ Created Formbricks Quickstart directory at ./formbricks." # Ask the user for their domain name (recommend surveys subdomain) echo "๐Ÿ”— Please enter your app domain (e.g., surveys.example.com). ๐Ÿšจ Do NOT enter the protocol (http/https):" read domain_name echo "๐Ÿ”— Do you want us to set up an HTTPS certificate for you? [Y/n]" read https_setup https_setup=$(echo "$https_setup" | tr '[:upper:]' '[:lower:]') # Set default value for HTTPS setup if [[ -z $https_setup ]]; then https_setup="y" fi if [[ $https_setup == "y" ]]; then echo "๐Ÿ”— Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]" read dns_setup dns_setup=$(echo "$dns_setup" | tr '[:upper:]' '[:lower:]') # Set default value for DNS setup if [[ -z $dns_setup ]]; then dns_setup="y" fi if [[ $dns_setup == "y" ]]; then echo "๐Ÿ’ก Please enter your email address for the SSL certificate:" read email_address echo "๐Ÿ”— Do you want to enforce HTTPS (HSTS)? [Y/n]" read hsts_enabled hsts_enabled=$(echo "$hsts_enabled" | tr '[:upper:]' '[:lower:]') # Set default value for HSTS if [[ -z $hsts_enabled ]]; then hsts_enabled="y" fi else echo "โŒ Ports 80 & 443 are not open. We can't help you in providing the SSL certificate." https_setup="n" hsts_enabled="n" fi else https_setup="n" hsts_enabled="n" fi # Ask for HSTS configuration for HTTPS redirection if custom certificate is used if [[ $https_setup == "n" ]]; then echo "You have chosen not to set up HTTPS certificate for your domain. Please make sure to set up HTTPS on your own. You can refer to the Formbricks documentation(https://formbricks.com/docs/self-hosting/custom-ssl) for more information." echo "๐Ÿ”— Do you want to enforce HTTPS (HSTS)? [Y/n]" read hsts_enabled hsts_enabled=$(echo "$hsts_enabled" | tr '[:upper:]' '[:lower:]') # Set default value for HSTS if [[ -z $hsts_enabled ]]; then hsts_enabled="y" fi fi # Installing Traefik echo "๐Ÿš— Configuring Traefik..." if [[ $hsts_enabled == "y" ]]; then hsts_middlewares="middlewares: - hstsHeader" http_redirection="http: redirections: entryPoint: to: websecure scheme: https permanent: true" else hsts_middlewares="" http_redirection="" fi if [[ $https_setup == "y" ]]; then certResolver="certResolver: default" certificates_resolvers="certificatesResolvers: default: acme: email: $email_address storage: acme.json caServer: "https://acme-v02.api.letsencrypt.org/directory" tlsChallenge: {}" else certResolver="" certificates_resolvers="" fi cat <traefik.yaml entryPoints: web: address: ":80" $http_redirection transport: respondingTimeouts: readTimeout: 60s websecure: address: ":443" transport: respondingTimeouts: readTimeout: 60s http: tls: $certResolver options: default $hsts_middlewares providers: docker: watch: true exposedByDefault: false file: directory: / $certificates_resolvers EOT cat <traefik-dynamic.yaml # configuring min TLS version tls: options: default: minVersion: VersionTLS12 cipherSuites: # TLS 1.2 strong ciphers - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 # TLS 1.3 ciphers are not configurable in Traefik; they are enabled by default curvePreferences: - CurveP521 - CurveP384 sniStrict: true alpnProtocols: - h2 - http/1.1 - acme-tls/1 EOT echo "๐Ÿ’ก Created traefik.yaml and traefik-dynamic.yaml file." if [[ $https_setup == "y" ]]; then touch acme.json chmod 600 acme.json echo "๐Ÿ’ก Created acme.json file with correct permissions." fi # Prompt for email service setup read -p "๐Ÿ“ง Do you want to set up the email service? You will need SMTP credentials for the same! [y/N]" email_service email_service=$(echo "$email_service" | tr '[:upper:]' '[:lower:]') # Set default value for email service setup if [[ -z $email_service ]]; then email_service="n" fi if [[ $email_service == "y" ]]; then echo "Please provide the following email service details: " echo -n "Enter your SMTP configured Email ID: " read mail_from echo -n "Enter your SMTP configured Email Name: " read mail_from_name echo -n "Enter your SMTP Host URL: " read smtp_host echo -n "Enter your SMTP Host Port: " read smtp_port echo -n "Enter your SMTP username: " read smtp_user read -s -p "Enter your SMTP password: " smtp_password; echo "***" echo -n "Enable Authenticated SMTP? Enter 1 for yes and 0 for no(default is 1): " read smtp_authenticated echo -n "Enable Secure SMTP (use SSL)? Enter 1 for yes and 0 for no: " read smtp_secure_enabled else mail_from="" mail_from_name="" smtp_host="" smtp_port="" smtp_user="" smtp_password="" smtp_authenticated=1 smtp_secure_enabled=0 fi # Prompt for file upload setup echo "" echo "๐Ÿ“ Do you want to configure file uploads?" echo " If you skip this, the following features will be disabled:" echo " - Adding images to surveys (e.g., in questions or as background)" echo " - 'File Upload' and 'Picture Selection' question types" echo " - Project logos" echo " - Custom organization logo in emails" read -p "Configure file uploads now? [Y/n] " configure_uploads configure_uploads=$(echo "$configure_uploads" | tr '[:upper:]' '[:lower:]') if [[ -z $configure_uploads ]]; then configure_uploads="y"; fi if [[ $configure_uploads == "y" ]]; then # Storage choice: External S3 vs bundled MinIO read -p "๐Ÿ—„๏ธ Do you want to use an external S3-compatible storage (AWS S3/DO Spaces/etc.)? [y/N] " use_external_s3 use_external_s3=$(echo "$use_external_s3" | tr '[:upper:]' '[:lower:]') if [[ -z $use_external_s3 ]]; then use_external_s3="n"; fi if [[ $use_external_s3 == "y" ]]; then echo "๐Ÿ”ง Enter S3 configuration (leave Endpoint empty for AWS S3):" read -p " S3 Access Key: " ext_s3_access_key read -s -p " S3 Secret Key: " ext_s3_secret_key; echo "***" read -p " S3 Region (e.g., us-east-1): " ext_s3_region read -p " S3 Bucket Name: " ext_s3_bucket read -p " S3 Endpoint URL (leave empty if you are using AWS S3, otherwise please enter the endpoint URL of the third party S3 compatible storage service): " ext_s3_endpoint minio_storage="n" else minio_storage="y" default_files_domain="files.$domain_name" read -p "๐Ÿ”— Enter the files subdomain for object storage (e.g., $default_files_domain): " files_domain if [[ -z $files_domain ]]; then files_domain="$default_files_domain"; fi echo "๐Ÿ”‘ Generating MinIO credentials..." minio_root_user="formbricks-$(openssl rand -hex 4)" minio_root_password=$(openssl rand -base64 20) minio_service_user="formbricks-service-$(openssl rand -hex 4)" minio_service_password=$(openssl rand -base64 20) minio_bucket_name="formbricks-uploads" minio_policy_name="formbricks-policy" echo "โœ… MinIO will be configured with:" echo " S3 Access Key (least privilege): $minio_service_user" echo " Bucket: $minio_bucket_name" fi else minio_storage="n" use_external_s3="n" echo "โš ๏ธ File uploads are disabled. Proceeding without S3/MinIO configuration." fi echo "๐Ÿ“ฅ Downloading docker-compose.yml from Formbricks GitHub repository..." curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/docker-compose.yml echo "๐Ÿš™ Updating docker-compose.yml with your custom inputs..." sed -i "/WEBAPP_URL:/s|WEBAPP_URL:.*|WEBAPP_URL: \"https://$domain_name\"|" docker-compose.yml sed -i "/NEXTAUTH_URL:/s|NEXTAUTH_URL:.*|NEXTAUTH_URL: \"https://$domain_name\"|" docker-compose.yml nextauth_secret=$(openssl rand -hex 32) && sed -i "/NEXTAUTH_SECRET:$/s/NEXTAUTH_SECRET:.*/NEXTAUTH_SECRET: $nextauth_secret/" docker-compose.yml echo "๐Ÿš— NEXTAUTH_SECRET updated successfully!" encryption_key=$(openssl rand -hex 32) && sed -i "/ENCRYPTION_KEY:$/s/ENCRYPTION_KEY:.*/ENCRYPTION_KEY: $encryption_key/" docker-compose.yml echo "๐Ÿš— ENCRYPTION_KEY updated successfully!" cron_secret=$(openssl rand -hex 32) && sed -i "/CRON_SECRET:$/s/CRON_SECRET:.*/CRON_SECRET: $cron_secret/" docker-compose.yml echo "๐Ÿš— CRON_SECRET updated successfully!" if [[ -n $mail_from ]]; then sed -i "s|# MAIL_FROM:|MAIL_FROM: \"$mail_from\"|" docker-compose.yml sed -i "s|# MAIL_FROM_NAME:|MAIL_FROM_NAME: \"$mail_from_name\"|" docker-compose.yml sed -i "s|# SMTP_HOST:|SMTP_HOST: \"$smtp_host\"|" docker-compose.yml sed -i "s|# SMTP_PORT:|SMTP_PORT: \"$smtp_port\"|" docker-compose.yml sed -i "s|# SMTP_SECURE_ENABLED:|SMTP_SECURE_ENABLED: $smtp_secure_enabled|" docker-compose.yml sed -i "s|# SMTP_USER:|SMTP_USER: \"$smtp_user\"|" docker-compose.yml sed -i "s|# SMTP_PASSWORD:|SMTP_PASSWORD: \"$smtp_password\"|" docker-compose.yml sed -i "s|# SMTP_AUTHENTICATED:|SMTP_AUTHENTICATED: $smtp_authenticated|" docker-compose.yml fi if [[ $use_external_s3 == "y" ]]; then echo "๐Ÿš— Configuring external S3..." sed -i "s|# S3_ACCESS_KEY:|S3_ACCESS_KEY: \"$ext_s3_access_key\"|" docker-compose.yml sed -i "s|# S3_SECRET_KEY:|S3_SECRET_KEY: \"$ext_s3_secret_key\"|" docker-compose.yml sed -i "s|# S3_REGION:|S3_REGION: \"$ext_s3_region\"|" docker-compose.yml sed -i "s|# S3_BUCKET_NAME:|S3_BUCKET_NAME: \"$ext_s3_bucket\"|" docker-compose.yml if [[ -n $ext_s3_endpoint ]]; then sed -i "s|# S3_ENDPOINT_URL:|S3_ENDPOINT_URL: \"$ext_s3_endpoint\"|" docker-compose.yml # Ensure S3_FORCE_PATH_STYLE is enabled for S3-compatible endpoints sed -E -i 's|^([[:space:]]*)#?[[:space:]]*S3_FORCE_PATH_STYLE:[[:space:]]*.*$|\1S3_FORCE_PATH_STYLE: 1|' docker-compose.yml else # Comment out S3_FORCE_PATH_STYLE for native AWS S3 sed -E -i 's|^([[:space:]]*)#?[[:space:]]*S3_FORCE_PATH_STYLE:[[:space:]]*.*$|\1# S3_FORCE_PATH_STYLE:|' docker-compose.yml fi echo "๐Ÿš— External S3 configuration updated successfully!" elif [[ $minio_storage == "y" ]]; then echo "๐Ÿš— Configuring bundled MinIO..." sed -i "s|# S3_ACCESS_KEY:|S3_ACCESS_KEY: \"$minio_service_user\"|" docker-compose.yml sed -i "s|# S3_SECRET_KEY:|S3_SECRET_KEY: \"$minio_service_password\"|" docker-compose.yml sed -i "s|# S3_REGION:|S3_REGION: \"us-east-1\"|" docker-compose.yml sed -i "s|# S3_BUCKET_NAME:|S3_BUCKET_NAME: \"$minio_bucket_name\"|" docker-compose.yml if [[ $https_setup == "y" ]]; then sed -i "s|# S3_ENDPOINT_URL:|S3_ENDPOINT_URL: \"https://$files_domain\"|" docker-compose.yml else sed -i "s|# S3_ENDPOINT_URL:|S3_ENDPOINT_URL: \"http://$files_domain\"|" docker-compose.yml fi # Ensure S3_FORCE_PATH_STYLE is enabled for MinIO sed -E -i 's|^([[:space:]]*)#?[[:space:]]*S3_FORCE_PATH_STYLE:[[:space:]]*.*$|\1S3_FORCE_PATH_STYLE: 1|' docker-compose.yml echo "๐Ÿš— MinIO S3 configuration updated successfully!" fi # SUPER SIMPLE: Use multiple simple operations instead of complex AWK # Step 1: Add Traefik labels to formbricks service awk -v domain_name="$domain_name" -v hsts_enabled="$hsts_enabled" ' /formbricks:/,/^ *$/ { if ($0 ~ /<<: \*environment$/) { print " labels:" print " - \"traefik.enable=true\"" print " - \"traefik.http.routers.formbricks.rule=Host(`" domain_name "`)\"" print " - \"traefik.http.routers.formbricks.entrypoints=websecure\"" print " - \"traefik.http.routers.formbricks.tls=true\"" print " - \"traefik.http.routers.formbricks.tls.certresolver=default\"" print " - \"traefik.http.services.formbricks.loadbalancer.server.port=3000\"" if (hsts_enabled == "y") { print " - \"traefik.http.middlewares.hstsHeader.headers.stsSeconds=31536000\"" print " - \"traefik.http.middlewares.hstsHeader.headers.forceSTSHeader=true\"" print " - \"traefik.http.middlewares.hstsHeader.headers.stsPreload=true\"" print " - \"traefik.http.middlewares.hstsHeader.headers.stsIncludeSubdomains=true\"" } else { print " - \"traefik.http.routers.formbricks_http.entrypoints=web\"" print " - \"traefik.http.routers.formbricks_http.rule=Host(`" domain_name "`)\"" } print $0 } else { print $0 } next } { print } ' docker-compose.yml >tmp.yml && mv tmp.yml docker-compose.yml # Step 2: Ensure formbricks waits for minio-init to complete successfully (mapping depends_on) if [[ $minio_storage == "y" ]]; then # Remove any existing simple depends_on list and replace with mapping awk ' BEGIN{in_fb=0; removing=0} /^ formbricks:/ {in_fb=1} in_fb && /^ depends_on:/ {removing=1; next} in_fb && removing && /^ [A-Za-z0-9_-]+:/ {removing=0} /^ [A-Za-z0-9_-]+:/ && !/^ formbricks:/ {in_fb=0} { if(!removing) print } ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml awk ' BEGIN{in_fb=0; inserted=0} /^ formbricks:/ {in_fb=1} /^ [A-Za-z0-9_-]+:/ && !/^ formbricks:/ {in_fb=0} { print if (in_fb && !inserted && $0 ~ /^ image:/) { print " depends_on:" print " postgres:" print " condition: service_started" print " minio-init:" print " condition: service_completed_successfully" inserted=1 } } ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml fi # Step 3: Build service snippets and inject them BEFORE the volumes section (non-destructive: skip if service exists) services_snippet_file="services_snippet.yml" : > "$services_snippet_file" insert_traefik="y" if grep -q "^ traefik:" docker-compose.yml; then insert_traefik="n"; fi if [[ $minio_storage == "y" ]]; then insert_minio="y"; insert_minio_init="y" if grep -q "^ minio:" docker-compose.yml; then insert_minio="n"; fi if grep -q "^ minio-init:" docker-compose.yml; then insert_minio_init="n"; fi if [[ $insert_minio == "y" ]]; then cat >> "$services_snippet_file" << EOF minio: restart: always image: minio/minio@sha256:13582eff79c6605a2d315bdd0e70164142ea7e98fc8411e9e10d089502a6d883 command: server /data environment: MINIO_ROOT_USER: "$minio_root_user" MINIO_ROOT_PASSWORD: "$minio_root_password" volumes: - minio-data:/data labels: - "traefik.enable=true" # S3 API on files subdomain - "traefik.http.routers.minio-s3.rule=Host(\`$files_domain\`)" - "traefik.http.routers.minio-s3.entrypoints=websecure" - "traefik.http.routers.minio-s3.tls=true" - "traefik.http.routers.minio-s3.tls.certresolver=default" - "traefik.http.routers.minio-s3.service=minio-s3" - "traefik.http.services.minio-s3.loadbalancer.server.port=9000" # CORS and rate limit (adjust origins if needed) - "traefik.http.routers.minio-s3.middlewares=minio-cors,minio-ratelimit" - "traefik.http.middlewares.minio-cors.headers.accesscontrolallowmethods=GET,PUT,POST,DELETE,HEAD,OPTIONS" - "traefik.http.middlewares.minio-cors.headers.accesscontrolallowheaders=*" - "traefik.http.middlewares.minio-cors.headers.accesscontrolalloworiginlist=https://$domain_name" - "traefik.http.middlewares.minio-cors.headers.accesscontrolmaxage=100" - "traefik.http.middlewares.minio-cors.headers.addvaryheader=true" - "traefik.http.middlewares.minio-ratelimit.ratelimit.average=100" - "traefik.http.middlewares.minio-ratelimit.ratelimit.burst=200" EOF fi if [[ $insert_minio_init == "y" ]]; then cat >> "$services_snippet_file" << EOF minio-init: image: minio/mc@sha256:95b5f3f7969a5c5a9f3a700ba72d5c84172819e13385aaf916e237cf111ab868 depends_on: - minio environment: MINIO_ROOT_USER: "$minio_root_user" MINIO_ROOT_PASSWORD: "$minio_root_password" MINIO_SERVICE_USER: "$minio_service_user" MINIO_SERVICE_PASSWORD: "$minio_service_password" MINIO_BUCKET_NAME: "$minio_bucket_name" entrypoint: ["/bin/sh", "/tmp/minio-init.sh"] volumes: - ./minio-init.sh:/tmp/minio-init.sh:ro EOF fi if [[ $insert_traefik == "y" ]]; then cat >> "$services_snippet_file" << EOF traefik: image: "traefik:v2.11.31" restart: always container_name: "traefik" depends_on: - formbricks - minio ports: - "80:80" - "443:443" volumes: - ./traefik.yaml:/traefik.yaml - ./traefik-dynamic.yaml:/traefik-dynamic.yaml - ./acme.json:/acme.json - /var/run/docker.sock:/var/run/docker.sock:ro EOF fi # Downgrade MinIO router to plain HTTP when HTTPS is not configured if [[ $https_setup != "y" ]]; then sed -i 's/traefik.http.routers.minio-s3.entrypoints=websecure/traefik.http.routers.minio-s3.entrypoints=web/' "$services_snippet_file" sed -i '/traefik.http.routers.minio-s3.tls=true/d' "$services_snippet_file" sed -i '/traefik.http.routers.minio-s3.tls.certresolver=default/d' "$services_snippet_file" sed -i "s|accesscontrolalloworiginlist=https://$domain_name|accesscontrolalloworiginlist=http://$domain_name|" "$services_snippet_file" fi else if [[ $insert_traefik == "y" ]]; then cat > "$services_snippet_file" << EOF traefik: image: "traefik:v2.11.31" restart: always container_name: "traefik" depends_on: - formbricks ports: - "80:80" - "443:443" volumes: - ./traefik.yaml:/traefik.yaml - ./traefik-dynamic.yaml:/traefik-dynamic.yaml - ./acme.json:/acme.json - /var/run/docker.sock:/var/run/docker.sock:ro EOF else : > "$services_snippet_file" fi fi awk ' { print if ($0 ~ /^services:$/ && !inserted) { while ((getline line < "services_snippet.yml") > 0) print line close("services_snippet.yml") inserted = 1 } } ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml rm -f "$services_snippet_file" # Ensure required volumes exist without removing user-defined volumes if grep -q '^volumes:' docker-compose.yml; then # Ensure postgres if ! awk '/^volumes:/{invol=1; next} invol && (/^[^[:space:]]/ || NF==0){invol=0} invol{ if($1=="postgres:") found=1 } END{ exit(found?0:1) }' docker-compose.yml; then awk ' /^volumes:/ { print; invol=1; next } invol && /^[^[:space:]]/ { if(!added){ print " postgres:"; print " driver: local"; added=1 } ; invol=0 } { print } END { if (invol && !added) { print " postgres:"; print " driver: local" } } ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml fi # Ensure redis if ! awk '/^volumes:/{invol=1; next} invol && (/^[^[:space:]]/ || NF==0){invol=0} invol{ if($1=="redis:") found=1 } END{ exit(found?0:1) }' docker-compose.yml; then awk ' /^volumes:/ { print; invol=1; next } invol && /^[^[:space:]]/ { if(!added){ print " redis:"; print " driver: local"; added=1 } ; invol=0 } { print } END { if (invol && !added) { print " redis:"; print " driver: local" } } ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml fi # Ensure minio-data if needed if [[ $minio_storage == "y" ]]; then if ! awk '/^volumes:/{invol=1; next} invol && (/^[^[:space:]]/ || NF==0){invol=0} invol{ if($1=="minio-data:") found=1 } END{ exit(found?0:1) }' docker-compose.yml; then awk ' /^volumes:/ { print; invol=1; next } invol && /^[^[:space:]]/ { if(!added){ print " minio-data:"; print " driver: local"; added=1 } ; invol=0 } { print } END { if (invol && !added) { print " minio-data:"; print " driver: local" } } ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml fi fi else { echo "" echo "volumes:" echo " postgres:" echo " driver: local" echo " redis:" echo " driver: local" if [[ $minio_storage == "y" ]]; then echo " minio-data:" echo " driver: local" fi } >> docker-compose.yml fi # Create minio-init script outside heredoc to avoid variable expansion issues if [[ $minio_storage == "y" ]]; then cat > minio-init.sh << 'MINIO_SCRIPT_EOF' #!/bin/sh echo 'โณ Waiting for MinIO to be ready...' attempts=0 max_attempts=30 until mc alias set minio http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null 2>&1 \ && mc ls minio >/dev/null 2>&1; do attempts=$((attempts + 1)) if [ $attempts -ge $max_attempts ]; then printf 'โŒ Failed to connect to MinIO after %s attempts\n' $max_attempts exit 1 fi printf '...still waiting attempt %s/%s\n' $attempts $max_attempts sleep 2 done echo '๐Ÿ”— MinIO reachable; alias configured.' echo '๐Ÿชฃ Creating bucket (idempotent)...'; mc mb minio/$MINIO_BUCKET_NAME --ignore-existing; echo '๐Ÿ“„ Creating JSON policy file...'; cat > /tmp/formbricks-policy.json << EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:DeleteObject", "s3:GetObject", "s3:PutObject"], "Resource": ["arn:aws:s3:::$MINIO_BUCKET_NAME/*"] }, { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": ["arn:aws:s3:::$MINIO_BUCKET_NAME"] } ] } EOF echo '๐Ÿ”’ Creating policy (idempotent)...'; if ! mc admin policy info minio formbricks-policy >/dev/null 2>&1; then mc admin policy create minio formbricks-policy /tmp/formbricks-policy.json || mc admin policy add minio formbricks-policy /tmp/formbricks-policy.json; echo 'Policy created successfully.'; else echo 'Policy already exists, skipping creation.'; fi echo '๐Ÿ‘ค Creating service user (idempotent)...'; if ! mc admin user info minio "$MINIO_SERVICE_USER" >/dev/null 2>&1; then mc admin user add minio "$MINIO_SERVICE_USER" "$MINIO_SERVICE_PASSWORD"; echo 'User created successfully.'; else echo 'User already exists, skipping creation.'; fi echo '๐Ÿ”— Attaching policy to user (idempotent)...'; mc admin policy attach minio formbricks-policy --user "$MINIO_SERVICE_USER" || echo 'Policy already attached or attachment failed (non-fatal).'; echo 'โœ… MinIO setup complete!'; exit 0; MINIO_SCRIPT_EOF chmod +x minio-init.sh fi newgrp docker < tmp.yml && mv tmp.yml docker-compose.yml # Remove list-style "- minio-init" lines under depends_on (if any) if sed --version >/dev/null 2>&1; then sed -E -i '/^[[:space:]]*-[[:space:]]*minio-init[[:space:]]*$/d' docker-compose.yml else sed -E -i '' '/^[[:space:]]*-[[:space:]]*minio-init[[:space:]]*$/d' docker-compose.yml fi # Remove the minio-init mapping and its condition line (mapping style depends_on) if sed --version >/dev/null 2>&1; then sed -i '/^[[:space:]]*minio-init:[[:space:]]*$/,/^[[:space:]]*condition:[[:space:]]*service_completed_successfully[[:space:]]*$/d' docker-compose.yml else sed -i '' '/^[[:space:]]*minio-init:[[:space:]]*$/,/^[[:space:]]*condition:[[:space:]]*service_completed_successfully[[:space:]]*$/d' docker-compose.yml fi # Remove any stopped minio-init container and restart without orphans docker compose rm -f -s minio-init >/dev/null 2>&1 || true docker compose up -d --remove-orphans echo "โœ… MinIO init cleanup complete." } case "$1" in install) install_formbricks ;; update) update_formbricks ;; stop) stop_formbricks ;; restart) restart_formbricks ;; logs) get_logs ;; cleanup-minio-init) cleanup_minio_init ;; uninstall) uninstall_formbricks ;; *) echo "๐Ÿš€ Executing default step of installing Formbricks" install_formbricks ;; esac