diff --git a/docker/migrate-to-minio.sh b/docker/migrate-to-v4.sh
similarity index 76%
rename from docker/migrate-to-minio.sh
rename to docker/migrate-to-v4.sh
index 71be9098c7..5c6161c578 100755
--- a/docker/migrate-to-minio.sh
+++ b/docker/migrate-to-v4.sh
@@ -45,7 +45,7 @@ external_s3_guard() {
if ! has_service minio; then
print_warning "Detected existing S3 credentials in docker-compose.yml and no bundled MinIO service."
print_error "This migration is intended only for setups using local uploads."
- print_info "No changes were made. If you already use external S3 (incl. AWS), you don't need this migration."
+ print_info "No files were migrated. If you already use external S3 (incl. AWS), you don't need this migration."
exit 0
fi
fi
@@ -287,7 +287,8 @@ has_service() {
}
add_or_replace_env_var() {
- local key="$1"; local value="$2"
+ local key="$1"; local value="$2"; local section="${3:-STORAGE}"
+
if grep -q "^[[:space:]]*$key:" docker-compose.yml; then
# Replace existing uncommented key
if sed --version >/dev/null 2>&1; then sed -i "s|^\([[:space:]]*$key:\).*|\1 \"$value\"|" docker-compose.yml; else sed -i '' "s|^\([[:space:]]*$key:\).*|\1 \"$value\"|" docker-compose.yml; fi
@@ -295,29 +296,77 @@ add_or_replace_env_var() {
# Uncomment placeholder and set
if sed --version >/dev/null 2>&1; then sed -i "s|^[[:space:]]*#[[:space:]]*$key:.*| $key: \"$value\"|" docker-compose.yml; else sed -i '' "s|^[[:space:]]*#[[:space:]]*$key:.*| $key: \"$value\"|" docker-compose.yml; fi
else
- # Append into STORAGE section before OAUTH header if present
- awk -v insert_key="$key" -v insert_val="$value" '
- BEGIN{printed=0}
- /################################################### OPTIONAL \(STORAGE\) ###################################################/ {print; in_storage=1; next}
- in_storage && /############################################# OPTIONAL \(OAUTH CONFIGURATION\) #############################################/ && !printed { print " " insert_key ": \"" insert_val "\""; printed=1; print; in_storage=0; next }
- { print }
- END { if(in_storage && !printed) print " " insert_key ": \"" insert_val "\"" }
- ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
+ # Add to specified section with fallback
+ local section_found=false
+
+ if [[ "$section" == "REQUIRED" ]] && grep -q -E "^[[:space:]]*#+[[:space:]]*REQUIRED[[:space:]]*#+[[:space:]]*$" docker-compose.yml; then
+ # Add to REQUIRED section
+ awk -v insert_key="$key" -v insert_val="$value" '
+ /^[[:space:]]*#+[[:space:]]*REQUIRED[[:space:]]*#+[[:space:]]*$/ {print; in_required=1; next}
+ in_required && /^[[:space:]]*#+.*OPTIONAL/ && !printed {
+ print ""; print " # " insert_key " (required for Formbricks 4.0)";
+ print " " insert_key ": \"" insert_val "\""; printed=1
+ }
+ { print }
+ END { if(in_required && !printed) { print ""; print " # " insert_key " (required for Formbricks 4.0)"; print " " insert_key ": \"" insert_val "\"" } }
+ ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
+ section_found=true
+ elif [[ "$section" == "STORAGE" ]] && grep -q -E "^[[:space:]]*#+[[:space:]]*OPTIONAL[[:space:]]*\\([[:space:]]*STORAGE[[:space:]]*\\)[[:space:]]*#+[[:space:]]*$" docker-compose.yml; then
+ # Add to STORAGE section (original behavior)
+ awk -v insert_key="$key" -v insert_val="$value" '
+ BEGIN{printed=0}
+ /^[[:space:]]*#+[[:space:]]*OPTIONAL[[:space:]]*\([[:space:]]*STORAGE[[:space:]]*\)[[:space:]]*#+[[:space:]]*$/ {print; in_storage=1; next}
+ in_storage && /^[[:space:]]*#+.*OPTIONAL.*OAUTH/ && !printed {
+ print " " insert_key ": \"" insert_val "\""; printed=1; print; in_storage=0; next
+ }
+ { print }
+ END { if(in_storage && !printed) print " " insert_key ": \"" insert_val "\"" }
+ ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
+ section_found=true
+ fi
+
+ # Fallback: add at the end of environment block if section not found
+ if [[ "$section_found" == false ]]; then
+ awk -v insert_key="$key" -v insert_val="$value" '
+ /^ environment:/ {print; in_env=1; next}
+ in_env && /^[^[:space:]]/ {
+ if (!printed) { print " " insert_key ": \"" insert_val "\""; printed=1 }
+ in_env=0
+ }
+ { print }
+ END { if(in_env && !printed) print " " insert_key ": \"" insert_val "\"" }
+ ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
+ fi
fi
}
# Function to check if we're in the correct directory
check_formbricks_directory() {
- if [[ ! -f "docker-compose.yml" ]]; then
- print_error "docker-compose.yml not found in current directory!"
- print_info "Please run this script from your Formbricks installation directory (usually ./formbricks/)"
- exit 1
+ # Case 1: docker-compose.yml in current directory
+ if [[ -f "docker-compose.yml" ]]; then
+ if grep -q "formbricks" docker-compose.yml; then
+ return 0
+ else
+ print_error "This doesn't appear to be a Formbricks docker-compose.yml file!"
+ exit 1
+ fi
fi
-
- if ! grep -q "formbricks" docker-compose.yml; then
- print_error "This doesn't appear to be a Formbricks docker-compose.yml file!"
- exit 1
+
+ # Case 2: one-click setup parent directory containing ./formbricks/docker-compose.yml
+ if [[ -f "formbricks/docker-compose.yml" ]]; then
+ cd formbricks
+ print_status "Detected one-click setup layout. Switched to ./formbricks directory."
+ if ! grep -q "formbricks" docker-compose.yml; then
+ print_error "This doesn't appear to be a Formbricks docker-compose.yml file!"
+ exit 1
+ fi
+ return 0
fi
+
+ # Neither current directory nor ./formbricks contains a compose file
+ print_error "docker-compose.yml not found in current directory or in ./formbricks/"
+ print_info "Run this script from the parent directory created by the one-click setup (containing ./formbricks/), or from the directory containing docker-compose.yml."
+ exit 1
}
# Function to backup existing docker-compose.yml
@@ -635,35 +684,202 @@ ${tls_block}
print_status "Added MinIO service to docker-compose.yml"
}
-# Function to add minio-init dependency to formbricks service
-add_minio_dependency() {
- # Only add if not already present
- if ! awk '/formbricks:/,/depends_on:/{ if($0 ~ /minio-init/) found=1 } END{ exit(found) }' docker-compose.yml; then
- if sed --version >/dev/null 2>&1; then sed -i '/formbricks:/,/depends_on:/{/- postgres/a\
- - minio-init
-}' docker-compose.yml; else sed -i '' '/formbricks:/,/depends_on:/{/- postgres/a\
- - minio-init
-}' docker-compose.yml; fi
- print_status "Added minio-init dependency to formbricks service"
+# Generic function to add service dependency
+add_service_dependency() {
+ local service="$1" # Target service (e.g., "formbricks", "traefik")
+ local dependency="$2" # Dependency to add (e.g., "redis", "minio-init", "minio")
+ local optional="${3:-false}" # Optional parameter - if true, don't exit if service not found
+
+ # Check if service exists
+ if ! grep -q "^ $service:" docker-compose.yml; then
+ if [[ "$optional" == "true" ]]; then
+ print_info "$service service not found - skipping dependency addition."
+ return 0
+ else
+ print_error "$service service not found in docker-compose.yml!"
+ print_info "Please ensure the $service service is properly configured before running this migration."
+ exit 1
+ fi
+ fi
+
+ # Check if dependency is already present in the service depends_on section
+ if awk -v srv="$service" -v dep="$dependency" 'BEGIN{found=0} /^ / && $0 ~ "^ " srv ":" {in_svc=1} in_svc && /^ [a-zA-Z]/ && $0 !~ "^ " srv ":" {in_svc=0} in_svc && $0 ~ "^[[:space:]]*-[[:space:]]*" dep "[[:space:]]*$" {found=1} END{exit(!found)}' docker-compose.yml; then
+ # Dependency already present, skip addition
+ print_info "$dependency dependency already present in $service service."
else
- print_info "minio-init dependency already present."
+ # Write awk script to temporary file to avoid shell escaping issues
+ local awk_script_tmp
+ awk_script_tmp=$(mktemp)
+ cat > "$awk_script_tmp" << 'AWK_EOF'
+# Store all lines to be able to look ahead
+{
+ lines[NR] = $0
+}
+END {
+ # First pass: check if target service has depends_on
+ for (i = 1; i <= NR; i++) {
+ if (lines[i] ~ "^ " srv ":") {
+ in_target = 1
+ start_line = i
+ } else if (in_target && lines[i] ~ /^ [a-zA-Z]/ && lines[i] !~ "^ " srv ":") {
+ in_target = 0
+ end_line = i - 1
+ break
+ }
+ if (in_target && lines[i] ~ /^[[:space:]]*depends_on:[[:space:]]*$/) {
+ has_depends_on = 1
+ }
+ }
+ if (in_target && !end_line) end_line = NR
+
+ # Second pass: output with modifications
+ in_svc = 0; in_depends = 0; added = 0
+ for (i = 1; i <= NR; i++) {
+ line = lines[i]
+
+ if (line ~ "^ " srv ":") {
+ in_svc = 1
+ print line
+ continue
+ }
+
+ if (in_svc && line ~ /^ [a-zA-Z]/ && line !~ "^ " srv ":") {
+ # Exiting service
+ if (!has_depends_on && !added) {
+ print " depends_on:"
+ print " - " new_dep
+ added = 1
+ }
+ in_svc = 0; in_depends = 0
+ print line
+ continue
+ }
+
+ if (in_svc && line ~ /^[[:space:]]*depends_on:[[:space:]]*$/) {
+ in_depends = 1
+ print line
+ continue
+ }
+
+ if (in_svc && in_depends && line ~ /^[[:space:]]*-[[:space:]]*/) {
+ print line
+ continue
+ }
+
+ if (in_svc && in_depends && line !~ /^[[:space:]]*-[[:space:]]*/) {
+ # End of depends_on list
+ if (!added) {
+ print " - " new_dep
+ added = 1
+ }
+ in_depends = 0
+ print line
+ continue
+ }
+
+ if (in_svc && line ~ /^[[:space:]]+[a-zA-Z_][^:]*:[[:space:]]*/) {
+ # First property in service without depends_on
+ if (!has_depends_on && !added) {
+ print " depends_on:"
+ print " - " new_dep
+ added = 1
+ has_depends_on = 1
+ }
+ print line
+ continue
+ }
+
+ print line
+ }
+
+ # Handle case where service ends at EOF
+ if (in_depends && !added) {
+ print " - " new_dep
+ } else if (in_svc && !has_depends_on && !added) {
+ print " depends_on:"
+ print " - " new_dep
+ }
+}
+AWK_EOF
+
+ # Use the awk script with variables
+ awk -v srv="$service" -v new_dep="$dependency" -f "$awk_script_tmp" docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
+ rm -f "$awk_script_tmp"
+ print_status "Added $dependency dependency to $service service"
fi
}
-# Function to update Traefik configuration to include MinIO dependency
-update_traefik_config() {
- # Check if traefik service exists and add minio dependency
- if grep -q "traefik:" docker-compose.yml; then
- if ! awk '/traefik:/,/depends_on:/{ if($0 ~ /- minio$/) found=1 } END{ exit(found) }' docker-compose.yml; then
- if sed --version >/dev/null 2>&1; then sed -i '/traefik:/,/depends_on:/{/- formbricks/a\
- - minio
-}' docker-compose.yml; else sed -i '' '/traefik:/,/depends_on:/{/- formbricks/a\
- - minio
-}' docker-compose.yml; fi
- print_status "Updated Traefik configuration to include MinIO dependency"
- else
- print_info "Traefik already depends_on minio."
- fi
+# Function to add Redis environment variable
+add_redis_environment_variables() {
+ # Use the enhanced helper function with REQUIRED section
+ add_or_replace_env_var "REDIS_URL" "redis://redis:6379" "REQUIRED"
+ print_status "Redis environment variables ensured in docker-compose.yml"
+}
+
+# Function to add Redis service to docker-compose.yml
+add_redis_service() {
+ # Skip injecting if service already exists
+ if has_service redis; then
+ print_info "Redis service already present. Skipping service injection."
+ return 0
+ fi
+
+ # Create Redis service configuration
+ local redis_service_config="
+ redis:
+ restart: always
+ image: valkey/valkey@sha256:12ba4f45a7c3e1d0f076acd616cb230834e75a77e8516dde382720af32832d6d
+ command: valkey-server --appendonly yes
+ volumes:
+ - redis:/data
+"
+
+ # Write Redis service to temporary file
+ echo "$redis_service_config" > redis_service.tmp
+
+ # Add Redis service before the volumes section
+ awk '
+ {
+ print
+ if ($0 ~ /^services:$/ && !inserted) {
+ while ((getline line < "redis_service.tmp") > 0) print line
+ close("redis_service.tmp")
+ inserted = 1
+ }
+ }
+ ' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
+
+ # Clean up temporary file
+ rm -f redis_service.tmp
+
+ print_status "Added Redis service to docker-compose.yml"
+}
+
+# Function to add redis volume
+add_redis_volume() {
+ # Ensure redis volume exists once
+ if grep -q '^volumes:' docker-compose.yml; then
+ # volumes block exists; check for redis inside it
+ if awk '/^volumes:/{invol=1; next} invol && NF==0{invol=0} invol{ if($1=="redis:") found=1 } END{ exit(!found) }' docker-compose.yml; then
+ print_info "Redis volume already present."
+ else
+ 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
+ print_status "Redis volume ensured"
+ fi
+ else
+ # no volumes block; append one with redis only (non-destructive to services)
+ {
+ echo ""
+ echo "volumes:"
+ echo " redis:"
+ echo " driver: local"
+ } >> docker-compose.yml
+ print_status "Added volumes section with Redis"
fi
}
@@ -885,19 +1101,21 @@ migrate_files_to_minio() {
# Function to restart Docker Compose
restart_docker_compose() {
- echo -n "Restart Docker Compose now to start MinIO and apply changes? [Y/n]: "
+ echo -n "Restart now to apply changes and continue the migration? (recommended) [Y/n]: "
local restart_confirm
read -r restart_confirm
restart_confirm=$(echo "$restart_confirm" | tr '[:upper:]' '[:lower:]')
if [[ -z "$restart_confirm" || "$restart_confirm" == "y" ]]; then
- print_info "Stopping current services..."
+ print_info "We need to briefly restart Formbricks to apply the changes."
+ print_info "Stopping services..."
docker compose down
print_info "Starting services with MinIO..."
docker compose up -d
print_status "Docker Compose restarted successfully!"
return 0
else
- print_warning "Skipping restart. You can run 'docker compose down && docker compose up -d' later to start MinIO."
+ print_warning "Skipping restart for now."
+ print_info "When you're ready, run: docker compose down && docker compose up -d"
return 1
fi
}
@@ -924,16 +1142,34 @@ wait_for_service_up() {
}
# Main migration function
-migrate_to_minio() {
- echo "đź§± Formbricks MinIO Migration Script for v4.0"
- echo "=============================================="
+migrate_to_v4() {
+ echo "đź§± Formbricks v4.0 Migration"
+ echo "============================"
+ echo ""
+ print_info "We'll prepare your Formbricks instance for v4.0 by:"
+ print_info "- Adding Redis (for caching)"
+ print_info "- Adding MinIO (for file storage)"
+ print_info "- Moving your existing files into MinIO"
+ print_info "You'll be asked to restart briefly so changes take effect."
echo ""
# Check if we're in the right directory
check_formbricks_directory
+
+ # Backup docker-compose.yml before making any changes
+ backup_docker_compose
+
+ # Add Redis configuration first (prerequisite for Formbricks 4.0)
+ print_status "Setting up Redis..."
+ add_redis_environment_variables
+ add_redis_service
+ add_redis_volume
+ add_service_dependency "formbricks" "redis"
+ echo ""
+
# Abort early if external S3 already configured and no bundled MinIO
external_s3_guard
-
+
# Check if MinIO is already configured unless migrating only
if [[ "$MIGRATE_ONLY" != true ]]; then
if [[ "$FORCE_RECONFIGURE" == true ]]; then
@@ -954,8 +1190,8 @@ migrate_to_minio() {
fi
print_info "Detected configuration:"
- print_info " Main domain: $main_domain"
- print_info " HTTPS setup: $https_setup"
+ print_info "Main domain: $main_domain"
+ print_info "HTTPS setup: $https_setup"
echo ""
local files_domain
@@ -984,7 +1220,7 @@ migrate_to_minio() {
exit 1
fi
local default_files_domain="files.$main_domain"
- echo -n "Enter the files subdomain for MinIO (e.g., $default_files_domain): "
+ echo -n "Enter the files subdomain to use for MinIO (e.g., $default_files_domain): "
read files_domain
if [[ -z "$files_domain" ]]; then
files_domain="$default_files_domain"
@@ -998,9 +1234,6 @@ migrate_to_minio() {
generate_minio_credentials
echo ""
- # Backup docker-compose.yml
- backup_docker_compose
-
# If reconfiguring/rotating creds, remove existing service blocks so reinjection is clean
if [[ "$REGENERATE_CREDS" == true || "$FORCE_RECONFIGURE" == true ]]; then
remove_minio_services || true
@@ -1013,10 +1246,10 @@ migrate_to_minio() {
add_minio_service "$files_domain" "$main_domain" "$https_setup"
# Add MinIO dependency to formbricks
- add_minio_dependency
+ add_service_dependency "formbricks" "minio-init"
- # Update Traefik configuration
- update_traefik_config
+ # Update Traefik configuration (optional - skip if traefik not present)
+ add_service_dependency "traefik" "minio" "true"
# Add MinIO volume
add_minio_volume
@@ -1034,6 +1267,26 @@ migrate_to_minio() {
if restart_docker_compose; then
restart_success=true
proceed_migration=true
+ else
+ # User declined restart; confirm they understand migration cannot proceed without it
+ print_warning "Without restarting now, the migration cannot proceed."
+ echo -n "Restart Docker Compose now to proceed with migration? [Y/n]: "
+ read -r confirm_restart
+ confirm_restart=$(echo "$confirm_restart" | tr '[:upper:]' '[:lower:]')
+ if [[ -z "$confirm_restart" || "$confirm_restart" == "y" ]]; then
+ if restart_docker_compose; then
+ restart_success=true
+ proceed_migration=true
+ else
+ print_warning "Migration cancelled because restart was declined."
+ print_info "You can run 'docker compose down && docker compose up -d' later, then rerun this script to migrate files."
+ return 0
+ fi
+ else
+ print_warning "Migration cancelled at your request. No files were migrated."
+ print_info "You can restart later and rerun this script to migrate files."
+ return 0
+ fi
fi
else
# Try to ensure services are up without full restart
@@ -1138,22 +1391,25 @@ migrate_to_minio() {
fi
echo ""
- echo "🎉 MinIO Migration Complete!"
- echo "============================="
+ echo "🎉 Formbricks v4.0 Migration Complete!"
+ echo "======================================="
echo ""
- print_status "MinIO Configuration:"
- print_info " Files Domain: $files_domain"
- print_info " S3 Access Key: $minio_service_user"
- print_info " S3 Bucket: $minio_bucket_name"
+ print_status "Infrastructure Configuration:"
+ print_info "Redis://redis:6379"
+ print_info "Files Domain: $files_domain"
+ print_info "S3 Access Key: $minio_service_user"
+ print_info "S3 Bucket: $minio_bucket_name"
echo ""
if [[ "$restart_success" == true ]]; then
- print_status "Your Formbricks instance is now using MinIO for file storage!"
- print_info "You can check the status with: docker compose ps"
- print_info "View logs with: docker compose logs"
+ print_status "Your Formbricks instance is now ready for v4.0!"
+ print_info "- Redis is configured for caching"
+ print_info "- MinIO is configured for file storage"
+ print_info "To check that everything is running, use: docker compose ps"
+ print_info "To view logs (for troubleshooting), use: docker compose logs"
else
- print_warning "Remember to restart your Docker Compose setup:"
- print_info " docker compose down && docker compose up -d"
+ print_warning "Before migration can happen, please remember to restart your services:"
+ print_info "docker compose down && docker compose up -d"
fi
if [[ "$MIGRATE_ONLY" != true ]]; then
@@ -1211,6 +1467,6 @@ cleanup_uploads_from_compose() {
fi
# Check if script is being run directly
-if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
- migrate_to_minio
+if [[ -n "${BASH_SOURCE:-}" && "${BASH_SOURCE[0]}" == "${0}" ]]; then
+ migrate_to_v4
fi
diff --git a/docs/docs.json b/docs/docs.json
index b6cb68362a..85b2c11056 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -261,7 +261,6 @@
"group": "Advanced",
"pages": [
"self-hosting/advanced/migration",
- "self-hosting/setup/minio-migration",
"self-hosting/advanced/license",
"self-hosting/advanced/license-activation",
{
diff --git a/docs/self-hosting/advanced/migration.mdx b/docs/self-hosting/advanced/migration.mdx
index dbd1de4e7a..6e2190ae7b 100644
--- a/docs/self-hosting/advanced/migration.mdx
+++ b/docs/self-hosting/advanced/migration.mdx
@@ -4,10 +4,148 @@ description: "Formbricks Self-hosted version migration"
icon: "arrow-right"
---
-### v4.0
+## v4.0
-- Migrate local uploads to the new MinIO storage (required in 4.0):
- see [minio migration](/self-hosting/setup/minio-migration)
+
+ **Important: Migration Required**
+
+ Formbricks 4 introduces additional requirements for self-hosting setups and makes a dedicated Redis cache as well as S3-compatible file storage mandatory.
+
+
+Formbricks 4.0 is a **major milestone** that sets up the technical foundation for future iterations and feature improvements. This release focuses on modernizing core infrastructure components to improve reliability, scalability, and enable advanced features going forward.
+
+### What's New in Formbricks 4.0
+
+**🚀 New Enterprise Features:**
+- **Quotas Management**: Advanced quota controls for enterprise users
+
+**🏗️ Technical Foundation Improvements:**
+- **Enhanced File Storage**: Improved file handling with better performance and reliability
+- **Improved Caching**: New caching functionality improving speed, extensibility and reliability
+- **Database Optimization**: Removal of unused database tables and fields for better performance
+- **Future-Ready Architecture**: Standardized infrastructure components for upcoming features
+
+### What This Means for Your Self-Hosting Setup
+
+These improvements in Formbricks 4.0 also make some infrastructure requirements mandatory going forward:
+
+- **Redis** for caching
+- **MinIO or S3-compatible storage** for file uploads
+
+These services are already included in the updated one-click setup for self-hosters, but existing users need to upgrade their setup. More information on this below.
+
+### Why We Made These Changes
+
+We know this represents more moving parts in your infrastructure and might even introduce more complexity in hosting Formbricks, and we don't take this decision lightly. As Formbricks grows into a comprehensive Survey and Experience Management platform, we've reached a point where the simple, single-service approach was holding back our ability to deliver the reliable, feature-rich product our users demand and deserve.
+
+By moving to dedicated, professional-grade services for these critical functions, we're building the foundation needed to deliver:
+- **Enterprise-grade reliability** with proper redundancy and backup capabilities
+- **Advanced features** that require sophisticated caching and file processing
+- **Better performance** through optimized, dedicated services
+- **Future scalability** to support larger deployments and more complex use cases without the need to maintain two different approaches
+
+We believe this is the only path forward to build the comprehensive Survey and Experience Management software we're aiming for.
+
+### Migration Steps for v4.0
+
+Additional migration steps are needed if you are using a self-hosted Formbricks setup that uses either local file storage (not S3-compatible file storage) or doesn't already use a Redis cache.
+
+### One-Click Setup
+
+For users using our official one-click setup, we provide an automated migration using a migration script:
+
+```bash
+# Download the latest script
+curl -fsSL -o migrate-to-v4.sh \
+ https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/migrate-to-v4.sh
+
+# Make it executable
+chmod +x migrate-to-v4.sh
+
+# Launch the guided migration
+./migrate-to-v4.sh
+```
+
+This script guides you through the steps for the infrastructure migration and does the following:
+- Adds a Redis service to your setup and configures it
+- Adds a MinIO service (open source S3-alternative) to your setup, configures it and migrates local files to it
+- Pulls the latest Formbricks image and updates your instance
+
+
+### Manual Setup
+
+If you use a different setup to host your Formbricks instance, you need to make sure to make the necessary adjustments to run Formbricks 4.0.
+
+#### Redis
+
+Formbricks 4.0 requires a Redis instance to work properly. Please add a Redis instance to your Docker setup, your K8s infrastructure, or however you are hosting Formbricks at the moment. Formbricks works with the latest versions of Redis as well as Valkey.
+
+You need to configure the `REDIS_URL` environment variable and point it to your Redis instance.
+
+#### S3-compatible storage
+
+To use file storage (e.g., file upload questions, image choice questions, custom survey backgrounds, etc.), you need to have S3-compatible file storage set up and connected to Formbricks.
+
+Formbricks supports multiple storage providers (among many other S3-compatible storages):
+- AWS S3
+- Digital Ocean Spaces
+- Hetzner Object Storage
+- Custom MinIO server
+
+Please make sure to set up a storage bucket with one of these solutions and then link it to Formbricks using the following environment variables:
+
+```
+ S3_ACCESS_KEY: your-access-key
+ S3_SECRET_KEY: your-secret-key
+ S3_REGION: us-east-1
+ S3_BUCKET_NAME: formbricks-uploads
+ S3_ENDPOINT_URL: http://minio:9000 # not needed for AWS S3
+```
+#### Upgrade Process
+
+**1. Backup your Database**
+
+**Critical Step**: Create a complete database backup before proceeding. Formbricks 4.0 will automatically remove unused database tables and fields during startup.
+
+```bash
+docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v4.0_$(date +%Y%m%d_%H%M%S).dump
+```
+
+
+ If you run into "**No such container**", use `docker ps` to find your container name,
+ e.g. `formbricks_postgres_1`.
+
+
+**2. Upgrade to Formbricks 4.0**
+
+Pull the latest Docker images and restart the setup (example for docker-compose):
+
+```bash
+# Pull the latest version
+docker compose pull
+
+# Stop the current instance
+docker compose down
+
+# Start with Formbricks 4.0
+docker compose up -d
+```
+
+**3. Automatic Database Migration**
+
+When you start Formbricks 4.0 for the first time, it will **automatically**:
+- Detect and apply required database schema updates
+- Remove unused database tables and fields
+- Optimize the database structure for better performance
+
+No manual intervention is required for the database migration.
+
+**4. Verify Your Upgrade**
+
+- Access your Formbricks instance at the same URL as before
+- Test file uploads to ensure S3/MinIO integration works correctly
+- Verify that existing surveys and data are intact
+- Check that previously uploaded files are accessible
### v3.3
diff --git a/docs/self-hosting/setup/minio-migration.mdx b/docs/self-hosting/setup/minio-migration.mdx
deleted file mode 100644
index a70ecf6ada..0000000000
--- a/docs/self-hosting/setup/minio-migration.mdx
+++ /dev/null
@@ -1,106 +0,0 @@
----
-title: Migrate local uploads to MinIO
-description: One‑click migration from local file storage to the new MinIO storage required in 4.0
-icon: "cloud"
----
-
-## Why this migration is required
-
-Formbricks 4.0 uses an S3‑compatible object store for all file uploads. For One‑Click installs that previously stored files locally (named volume `uploads` or a host bind like `./uploads`), you must migrate those files to MinIO.
-
-This guide shows a safe, idempotent migration using the script `docker/migrate-to-minio.sh` included in this repository.
-
-
- This migration script is supported on Ubuntu only (Ubuntu 20.04+), requiring Bash 4+ and GNU sed/grep.
-
-
-## Prerequisites
-
-- DNS: a subdomain for object storage, e.g. `files.` pointing to your server IP
-- Your One‑Click installation directory (contains `docker-compose.yml`)
-- Docker/Compose installed and working
-
-## What the script does
-
-- Adds MinIO services (`minio`, `minio-init`) and S3 env vars to `docker-compose.yml`
-- Asks to restart your Compose stack (you should say Yes)
-- Detects your existing upload sources post‑start and migrates them to MinIO
- - Host bind mounts (e.g., `./uploads`)
- - Named volume target in the container (legacy: `/home/nextjs/apps/web/uploads` or custom target)
- - Absolute `UPLOADS_DIR` in the container if set
-- Cleans up `docker-compose.yml` after success: removes `UPLOADS_DIR`, removes all `uploads` volume mappings and the root `uploads:` definition
-- Optionally restarts again to apply the cleanup
-
-The migration is idempotent: you can re‑run it; existing objects are not duplicated.
-
-## Run the migration
-
-From the directory that contains your `docker-compose.yml`:
-
-```bash
-# Download the latest script
-curl -fsSL -o migrate-to-minio.sh \
- https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/migrate-to-minio.sh
-
-# Make it executable
-chmod +x migrate-to-minio.sh
-
-# Launch the guided migration
-./migrate-to-minio.sh
-```
-
-## Verifying the migration
-
-- Check the UI: previously uploaded images/logos/files should load
-- List bucket contents with `mc` (reuses minio‑init service env):
-
-```bash
-docker compose run --rm --entrypoint /bin/sh minio-init -lc \
-'mc alias set minio http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" && \
- mc ls -r "minio/$MINIO_BUCKET_NAME" | head -200'
-```
-
-- Confirm `docker-compose.yml` no longer contains `UPLOADS_DIR` or `uploads:` volume mappings (post‑cleanup)
-
-## Notes on sources
-
-- The script migrates from multiple sources, in order:
- - Host bind (absolute path resolved by `docker compose config`)
- - Container mount target for named volume `uploads` (e.g., `/home/nextjs/apps/web/uploads` or your custom `.../files/uploads`)
- - Absolute `UPLOADS_DIR` (container path)
-- The relative path/key structure is preserved: `${environmentId}/${accessType}/fileName.ext`
-- Re‑runs copy only new/changed files (idempotent)
-
-## Troubleshooting
-
-- MinIO 404 on file URLs
- - If you use virtual‑hosted style (`bucket.files.domain`), ensure Traefik router rule includes wildcard:
- ```
- traefik.http.routers.minio-s3.rule=Host(`files.domain`) || HostRegexp(`{any:.+}.files.domain`)
- ```
-- InvalidAccessKeyId in UI
-
- - Happens if keys rotated between runs. Re‑run the script; it reuses S3\_\* credentials and reattaches policy.
-
-- Undefined volume `minio-data`
-
- - Re‑run; the script ensures `minio-data` exists in the root `volumes:` block.
-
-- List bucket contents
- ```bash
- docker compose run --rm --entrypoint /bin/sh minio-init -lc \
- 'mc alias set minio http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; \
- mc ls -r "minio/$MINIO_BUCKET_NAME" | head -200'
- ```
-
-## Rollback
-
-- Your original `docker-compose.yml` is backed up as `docker-compose.yml.backup.`
-- To revert, restore the backup and restart Compose:
-
-```bash
-cp docker-compose.yml.backup. docker-compose.yml
-docker compose down && docker compose up -d
-```
-
-That’s it — you’re on the new MinIO storage for Formbricks 4.0. ✅