mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-15 11:41:29 -05:00
Compare commits
6 Commits
feat/langu
...
feat/repla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5053b0a9c | ||
|
|
662ea7eca3 | ||
|
|
6323250ccd | ||
|
|
a1a11b2bb8 | ||
|
|
0653c6a59f | ||
|
|
b6d793e109 |
47
.github/workflows/e2e.yml
vendored
47
.github/workflows/e2e.yml
vendored
@@ -85,12 +85,12 @@ jobs:
|
||||
echo "S3_REGION=us-east-1" >> .env
|
||||
echo "S3_BUCKET_NAME=formbricks-e2e" >> .env
|
||||
echo "S3_ENDPOINT_URL=http://localhost:9000" >> .env
|
||||
echo "S3_ACCESS_KEY=devminio" >> .env
|
||||
echo "S3_SECRET_KEY=devminio123" >> .env
|
||||
echo "S3_ACCESS_KEY=devrustfs" >> .env
|
||||
echo "S3_SECRET_KEY=devrustfs123" >> .env
|
||||
echo "S3_FORCE_PATH_STYLE=1" >> .env
|
||||
shell: bash
|
||||
|
||||
- name: Install MinIO client (mc)
|
||||
- name: Install S3 bootstrap client (mc)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
MC_VERSION="RELEASE.2025-08-13T08-35-41Z"
|
||||
@@ -106,31 +106,36 @@ jobs:
|
||||
chmod +x "${MC_BIN}"
|
||||
sudo mv "${MC_BIN}" /usr/local/bin/mc
|
||||
|
||||
- name: Start MinIO Server
|
||||
- name: Start RustFS Server
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Start MinIO server in background
|
||||
# Start RustFS server in background
|
||||
docker run -d \
|
||||
--name minio-server \
|
||||
--name rustfs-server \
|
||||
-p 9000:9000 \
|
||||
-p 9001:9001 \
|
||||
-e MINIO_ROOT_USER=devminio \
|
||||
-e MINIO_ROOT_PASSWORD=devminio123 \
|
||||
minio/minio:RELEASE.2025-09-07T16-13-09Z \
|
||||
server /data --console-address :9001
|
||||
-e RUSTFS_ACCESS_KEY=devrustfs \
|
||||
-e RUSTFS_SECRET_KEY=devrustfs123 \
|
||||
-e RUSTFS_ADDRESS=:9000 \
|
||||
-e RUSTFS_CONSOLE_ENABLE=true \
|
||||
-e RUSTFS_CONSOLE_ADDRESS=:9001 \
|
||||
rustfs/rustfs:1.0.0-alpha.93 \
|
||||
/data
|
||||
|
||||
echo "MinIO server started"
|
||||
echo "RustFS server started"
|
||||
|
||||
- name: Wait for MinIO and create S3 bucket
|
||||
- name: Wait for RustFS and create S3 bucket
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Waiting for MinIO to be ready..."
|
||||
echo "Waiting for RustFS to be ready for S3 requests..."
|
||||
ready=0
|
||||
for i in {1..60}; do
|
||||
if curl -fsS http://localhost:9000/minio/health/live >/dev/null; then
|
||||
echo "MinIO is up after ${i} seconds"
|
||||
if curl -fsS http://localhost:9000/health/ready >/dev/null && \
|
||||
mc alias set local http://localhost:9000 devrustfs devrustfs123 >/dev/null 2>&1 && \
|
||||
mc ls local >/dev/null 2>&1; then
|
||||
echo "RustFS is ready after ${i} seconds"
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
@@ -138,11 +143,11 @@ jobs:
|
||||
done
|
||||
|
||||
if [ "$ready" -ne 1 ]; then
|
||||
echo "::error::MinIO did not become ready within 60 seconds"
|
||||
echo "::error::RustFS did not become ready for S3 requests within 60 seconds"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mc alias set local http://localhost:9000 devminio devminio123
|
||||
mc alias set local http://localhost:9000 devrustfs devrustfs123
|
||||
mc mb --ignore-existing local/formbricks-e2e
|
||||
|
||||
- name: Build App
|
||||
@@ -242,8 +247,14 @@ jobs:
|
||||
if: failure()
|
||||
with:
|
||||
name: app-logs
|
||||
if-no-files-found: ignore
|
||||
path: app.log
|
||||
|
||||
- name: Output App Logs
|
||||
if: failure()
|
||||
run: cat app.log
|
||||
run: |
|
||||
if [ -f app.log ]; then
|
||||
cat app.log
|
||||
else
|
||||
echo "app.log not found because the Run App step did not execute or failed before log creation."
|
||||
fi
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Prisma } from "@prisma/client";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { DatabaseError, ResourceNotFoundError, UniqueConstraintError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull, TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -175,10 +175,34 @@ describe("createResponse V2", () => {
|
||||
).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on Prisma known request error", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
|
||||
test("should throw UniqueConstraintError on P2002 with singleUseId target", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
clientVersion: "test",
|
||||
meta: { target: ["surveyId", "singleUseId"] },
|
||||
});
|
||||
vi.mocked(mockTx.response.create).mockRejectedValue(prismaError);
|
||||
await expect(
|
||||
createResponse(mockResponseInput, mockTx as unknown as Prisma.TransactionClient)
|
||||
).rejects.toThrow(UniqueConstraintError);
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on P2002 without singleUseId target", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
clientVersion: "test",
|
||||
meta: { target: ["displayId"] },
|
||||
});
|
||||
vi.mocked(mockTx.response.create).mockRejectedValue(prismaError);
|
||||
await expect(
|
||||
createResponse(mockResponseInput, mockTx as unknown as Prisma.TransactionClient)
|
||||
).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on non-P2002 Prisma known request error", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
|
||||
code: "P2025",
|
||||
clientVersion: "test",
|
||||
});
|
||||
vi.mocked(mockTx.response.create).mockRejectedValue(prismaError);
|
||||
await expect(
|
||||
|
||||
@@ -2,7 +2,7 @@ import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { DatabaseError, ResourceNotFoundError, UniqueConstraintError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponse, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -129,6 +129,13 @@ export const createResponse = async (
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (error.code === "P2002") {
|
||||
const target = (error.meta?.target as string[]) ?? [];
|
||||
if (target?.includes("singleUseId")) {
|
||||
throw new UniqueConstraintError("Response already submitted for this single-use link");
|
||||
}
|
||||
}
|
||||
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { ZEnvironmentId } from "@formbricks/types/environment";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { InvalidInputError, UniqueConstraintError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { checkSurveyValidity } from "@/app/api/v2/client/[environmentId]/responses/lib/utils";
|
||||
import { reportApiError } from "@/app/lib/api/api-error-reporter";
|
||||
@@ -177,6 +177,10 @@ const createResponseForRequest = async ({
|
||||
return responses.badRequestResponse(error.message, undefined, true);
|
||||
}
|
||||
|
||||
if (error instanceof UniqueConstraintError) {
|
||||
return responses.conflictResponse(error.message, undefined, true);
|
||||
}
|
||||
|
||||
const response = getUnexpectedPublicErrorResponse();
|
||||
reportApiError({
|
||||
request,
|
||||
|
||||
@@ -339,6 +339,56 @@ describe("API Response Utilities", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("conflictResponse", () => {
|
||||
test("should return a conflict response", () => {
|
||||
const message = "Resource already exists";
|
||||
const details = { field: "singleUseId" };
|
||||
const response = responses.conflictResponse(message, details);
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
|
||||
return response.json().then((body) => {
|
||||
expect(body).toEqual({
|
||||
code: "conflict",
|
||||
message,
|
||||
details,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle undefined details", () => {
|
||||
const message = "Resource already exists";
|
||||
const response = responses.conflictResponse(message);
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
|
||||
return response.json().then((body) => {
|
||||
expect(body).toEqual({
|
||||
code: "conflict",
|
||||
message,
|
||||
details: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("should include CORS headers when cors is true", () => {
|
||||
const message = "Resource already exists";
|
||||
const response = responses.conflictResponse(message, undefined, true);
|
||||
|
||||
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
|
||||
expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, POST, PUT, DELETE, OPTIONS");
|
||||
expect(response.headers.get("Access-Control-Allow-Headers")).toBe("Content-Type, Authorization");
|
||||
});
|
||||
|
||||
test("should use custom cache control header when provided", () => {
|
||||
const message = "Resource already exists";
|
||||
const customCache = "no-cache";
|
||||
const response = responses.conflictResponse(message, undefined, false, customCache);
|
||||
|
||||
expect(response.headers.get("Cache-Control")).toBe(customCache);
|
||||
});
|
||||
});
|
||||
|
||||
describe("tooManyRequestsResponse", () => {
|
||||
test("should return a too many requests response", () => {
|
||||
const message = "Rate limit exceeded";
|
||||
|
||||
@@ -16,7 +16,8 @@ interface ApiErrorResponse {
|
||||
| "method_not_allowed"
|
||||
| "not_authenticated"
|
||||
| "forbidden"
|
||||
| "too_many_requests";
|
||||
| "too_many_requests"
|
||||
| "conflict";
|
||||
message: string;
|
||||
details: {
|
||||
[key: string]: string | string[] | number | number[] | boolean | boolean[];
|
||||
@@ -236,6 +237,30 @@ const internalServerErrorResponse = (
|
||||
);
|
||||
};
|
||||
|
||||
const conflictResponse = (
|
||||
message: string,
|
||||
details?: { [key: string]: string },
|
||||
cors: boolean = false,
|
||||
cache: string = "private, no-store"
|
||||
) => {
|
||||
const headers = {
|
||||
...(cors && corsHeaders),
|
||||
"Cache-Control": cache,
|
||||
};
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
code: "conflict",
|
||||
message,
|
||||
details: details || {},
|
||||
} as ApiErrorResponse,
|
||||
{
|
||||
status: 409,
|
||||
headers,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const tooManyRequestsResponse = (
|
||||
message: string,
|
||||
cors: boolean = false,
|
||||
@@ -270,4 +295,5 @@ export const responses = {
|
||||
successResponse,
|
||||
tooManyRequestsResponse,
|
||||
forbiddenResponse,
|
||||
conflictResponse,
|
||||
};
|
||||
|
||||
@@ -98,14 +98,11 @@ describe("Users Lib", () => {
|
||||
|
||||
test("returns conflict error if user with email already exists", async () => {
|
||||
(prisma.user.create as any).mockRejectedValueOnce(
|
||||
new Prisma.PrismaClientKnownRequestError(
|
||||
"Unique constraint failed on the fields: (`email`)",
|
||||
{
|
||||
code: PrismaErrorType.UniqueConstraintViolation,
|
||||
clientVersion: "1.0.0",
|
||||
meta: { target: ["email"] },
|
||||
}
|
||||
)
|
||||
new Prisma.PrismaClientKnownRequestError("Unique constraint failed on the fields: (`email`)", {
|
||||
code: PrismaErrorType.UniqueConstraintViolation,
|
||||
clientVersion: "1.0.0",
|
||||
meta: { target: ["email"] },
|
||||
})
|
||||
);
|
||||
const result = await createUser(
|
||||
{ name: "Duplicate", email: "test@example.com", role: "member" },
|
||||
|
||||
@@ -46,9 +46,9 @@ export const OpenIdButton = ({
|
||||
type="button"
|
||||
onClick={handleLogin}
|
||||
variant="secondary"
|
||||
className="relative w-full justify-center">
|
||||
{text ? text : t("auth.continue_with_openid")}
|
||||
{lastUsed && <span className="absolute right-3 text-xs opacity-50">{t("auth.last_used")}</span>}
|
||||
className="w-full items-center justify-center gap-2 px-2">
|
||||
<span className="truncate">{text || t("auth.continue_with_openid")}</span>
|
||||
{lastUsed && <span className="shrink-0 text-xs opacity-50">{t("auth.last_used")}</span>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,22 +24,63 @@ services:
|
||||
volumes:
|
||||
- valkey-data:/data
|
||||
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
|
||||
command: server /data --console-address ":9001"
|
||||
rustfs-perms:
|
||||
image: busybox:1.36.1
|
||||
user: "0:0"
|
||||
command: ["sh", "-c", "mkdir -p /data && chown -R 10001:10001 /data"]
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
|
||||
rustfs:
|
||||
image: rustfs/rustfs:1.0.0-alpha.93
|
||||
depends_on:
|
||||
rustfs-perms:
|
||||
condition: service_completed_successfully
|
||||
command: /data
|
||||
environment:
|
||||
- MINIO_ROOT_USER=devminio
|
||||
- MINIO_ROOT_PASSWORD=devminio123
|
||||
- RUSTFS_ACCESS_KEY=devrustfs
|
||||
- RUSTFS_SECRET_KEY=devrustfs123
|
||||
- RUSTFS_ADDRESS=:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_CONSOLE_ADDRESS=:9001
|
||||
ports:
|
||||
- "9000:9000" # S3 API direct access
|
||||
- "9001:9001" # Web console
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
- rustfs-data:/data
|
||||
|
||||
rustfs-init:
|
||||
image: minio/mc@sha256:95b5f3f7969a5c5a9f3a700ba72d5c84172819e13385aaf916e237cf111ab868
|
||||
depends_on:
|
||||
- rustfs
|
||||
environment:
|
||||
- RUSTFS_ADMIN_USER=devrustfs
|
||||
- RUSTFS_ADMIN_PASSWORD=devrustfs123
|
||||
- RUSTFS_BUCKET_NAME=formbricks
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo 'Waiting for RustFS to be ready...'
|
||||
attempts=0
|
||||
max_attempts=30
|
||||
until mc alias set rustfs http://rustfs:9000 "$$RUSTFS_ADMIN_USER" "$$RUSTFS_ADMIN_PASSWORD" >/dev/null 2>&1 \
|
||||
&& mc ls rustfs >/dev/null 2>&1; do
|
||||
attempts=$$((attempts + 1))
|
||||
if [ "$$attempts" -ge "$$max_attempts" ]; then
|
||||
echo "RustFS did not become ready within $${max_attempts} attempts"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
mc mb rustfs/"$$RUSTFS_BUCKET_NAME" --ignore-existing
|
||||
echo 'RustFS bucket bootstrap complete.'
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
driver: local
|
||||
valkey-data:
|
||||
driver: local
|
||||
minio-data:
|
||||
rustfs-data:
|
||||
driver: local
|
||||
|
||||
@@ -273,7 +273,7 @@ EOT
|
||||
if [[ -z $configure_uploads ]]; then configure_uploads="y"; fi
|
||||
|
||||
if [[ $configure_uploads == "y" ]]; then
|
||||
# Storage choice: External S3 vs bundled MinIO
|
||||
# Storage choice: External S3 vs bundled RustFS
|
||||
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
|
||||
@@ -286,29 +286,29 @@ EOT
|
||||
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"
|
||||
rustfs_storage="n"
|
||||
else
|
||||
minio_storage="y"
|
||||
rustfs_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 "🔑 Generating RustFS credentials..."
|
||||
rustfs_admin_user="formbricks-$(openssl rand -hex 4)"
|
||||
rustfs_admin_password=$(openssl rand -base64 20)
|
||||
rustfs_service_user="formbricks-service-$(openssl rand -hex 4)"
|
||||
rustfs_service_password=$(openssl rand -base64 20)
|
||||
rustfs_bucket_name="formbricks-uploads"
|
||||
rustfs_policy_name="formbricks-policy"
|
||||
|
||||
echo "✅ MinIO will be configured with:"
|
||||
echo " S3 Access Key (least privilege): $minio_service_user"
|
||||
echo " Bucket: $minio_bucket_name"
|
||||
echo "✅ RustFS will be configured with:"
|
||||
echo " S3 Access Key (least privilege): $rustfs_service_user"
|
||||
echo " Bucket: $rustfs_bucket_name"
|
||||
fi
|
||||
else
|
||||
minio_storage="n"
|
||||
rustfs_storage="n"
|
||||
use_external_s3="n"
|
||||
echo "⚠️ File uploads are disabled. Proceeding without S3/MinIO configuration."
|
||||
echo "⚠️ File uploads are disabled. Proceeding without S3-compatible storage configuration."
|
||||
fi
|
||||
|
||||
echo "📥 Downloading docker-compose.yml from Formbricks GitHub repository..."
|
||||
@@ -353,20 +353,20 @@ EOT
|
||||
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
|
||||
elif [[ $rustfs_storage == "y" ]]; then
|
||||
echo "🚗 Configuring bundled RustFS..."
|
||||
sed -i "s|# S3_ACCESS_KEY:|S3_ACCESS_KEY: \"$rustfs_service_user\"|" docker-compose.yml
|
||||
sed -i "s|# S3_SECRET_KEY:|S3_SECRET_KEY: \"$rustfs_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
|
||||
sed -i "s|# S3_BUCKET_NAME:|S3_BUCKET_NAME: \"$rustfs_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
|
||||
# Ensure S3_FORCE_PATH_STYLE is enabled for RustFS
|
||||
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!"
|
||||
echo "🚗 RustFS S3 configuration updated successfully!"
|
||||
fi
|
||||
|
||||
# SUPER SIMPLE: Use multiple simple operations instead of complex AWK
|
||||
@@ -400,8 +400,8 @@ EOT
|
||||
{ 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
|
||||
# Step 2: Ensure formbricks waits for rustfs-init to complete successfully (mapping depends_on)
|
||||
if [[ $rustfs_storage == "y" ]]; then
|
||||
# Remove any existing simple depends_on list and replace with mapping
|
||||
awk '
|
||||
BEGIN{in_fb=0; removing=0}
|
||||
@@ -422,7 +422,9 @@ EOT
|
||||
print " depends_on:"
|
||||
print " postgres:"
|
||||
print " condition: service_started"
|
||||
print " minio-init:"
|
||||
print " redis:"
|
||||
print " condition: service_started"
|
||||
print " rustfs-init:"
|
||||
print " condition: service_completed_successfully"
|
||||
inserted=1
|
||||
}
|
||||
@@ -437,59 +439,76 @@ EOT
|
||||
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 [[ $rustfs_storage == "y" ]]; then
|
||||
insert_rustfs_perms="y"; insert_rustfs="y"; insert_rustfs_init="y"
|
||||
if grep -q "^ rustfs-perms:" docker-compose.yml; then insert_rustfs_perms="n"; fi
|
||||
if grep -q "^ rustfs:" docker-compose.yml; then insert_rustfs="n"; fi
|
||||
if grep -q "^ rustfs-init:" docker-compose.yml; then insert_rustfs_init="n"; fi
|
||||
|
||||
if [[ $insert_minio == "y" ]]; then
|
||||
if [[ $insert_rustfs_perms == "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"
|
||||
rustfs-perms:
|
||||
image: busybox:1.36.1
|
||||
user: "0:0"
|
||||
command: ["sh", "-c", "mkdir -p /data && chown -R 10001:10001 /data"]
|
||||
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"
|
||||
- rustfs-data:/data
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [[ $insert_minio_init == "y" ]]; then
|
||||
if [[ $insert_rustfs == "y" ]]; then
|
||||
cat >> "$services_snippet_file" << EOF
|
||||
minio-init:
|
||||
rustfs:
|
||||
restart: always
|
||||
image: rustfs/rustfs:1.0.0-alpha.93
|
||||
depends_on:
|
||||
rustfs-perms:
|
||||
condition: service_completed_successfully
|
||||
command: /data
|
||||
environment:
|
||||
RUSTFS_ACCESS_KEY: "$rustfs_admin_user"
|
||||
RUSTFS_SECRET_KEY: "$rustfs_admin_password"
|
||||
RUSTFS_ADDRESS: ":9000"
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# S3 API on files subdomain
|
||||
- "traefik.http.routers.rustfs-s3.rule=Host(\`$files_domain\`)"
|
||||
- "traefik.http.routers.rustfs-s3.entrypoints=websecure"
|
||||
- "traefik.http.routers.rustfs-s3.tls=true"
|
||||
- "traefik.http.routers.rustfs-s3.tls.certresolver=default"
|
||||
- "traefik.http.routers.rustfs-s3.service=rustfs-s3"
|
||||
- "traefik.http.services.rustfs-s3.loadbalancer.server.port=9000"
|
||||
# CORS and rate limit (adjust origins if needed)
|
||||
- "traefik.http.routers.rustfs-s3.middlewares=rustfs-cors,rustfs-ratelimit"
|
||||
- "traefik.http.middlewares.rustfs-cors.headers.accesscontrolallowmethods=GET,PUT,POST,DELETE,HEAD,OPTIONS"
|
||||
- "traefik.http.middlewares.rustfs-cors.headers.accesscontrolallowheaders=*"
|
||||
- "traefik.http.middlewares.rustfs-cors.headers.accesscontrolalloworiginlist=https://$domain_name"
|
||||
- "traefik.http.middlewares.rustfs-cors.headers.accesscontrolmaxage=100"
|
||||
- "traefik.http.middlewares.rustfs-cors.headers.addvaryheader=true"
|
||||
- "traefik.http.middlewares.rustfs-ratelimit.ratelimit.average=100"
|
||||
- "traefik.http.middlewares.rustfs-ratelimit.ratelimit.burst=200"
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [[ $insert_rustfs_init == "y" ]]; then
|
||||
cat >> "$services_snippet_file" << EOF
|
||||
rustfs-init:
|
||||
image: minio/mc@sha256:95b5f3f7969a5c5a9f3a700ba72d5c84172819e13385aaf916e237cf111ab868
|
||||
depends_on:
|
||||
- minio
|
||||
- rustfs
|
||||
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"]
|
||||
RUSTFS_ADMIN_USER: "$rustfs_admin_user"
|
||||
RUSTFS_ADMIN_PASSWORD: "$rustfs_admin_password"
|
||||
RUSTFS_SERVICE_USER: "$rustfs_service_user"
|
||||
RUSTFS_SERVICE_PASSWORD: "$rustfs_service_password"
|
||||
RUSTFS_BUCKET_NAME: "$rustfs_bucket_name"
|
||||
RUSTFS_POLICY_NAME: "$rustfs_policy_name"
|
||||
entrypoint: ["/bin/sh", "/tmp/rustfs-init.sh"]
|
||||
volumes:
|
||||
- ./minio-init.sh:/tmp/minio-init.sh:ro
|
||||
- ./rustfs-init.sh:/tmp/rustfs-init.sh:ro
|
||||
EOF
|
||||
fi
|
||||
|
||||
@@ -501,7 +520,7 @@ EOF
|
||||
container_name: "traefik"
|
||||
depends_on:
|
||||
- formbricks
|
||||
- minio
|
||||
- rustfs
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -513,11 +532,11 @@ EOF
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Downgrade MinIO router to plain HTTP when HTTPS is not configured
|
||||
# Downgrade RustFS 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/traefik.http.routers.rustfs-s3.entrypoints=websecure/traefik.http.routers.rustfs-s3.entrypoints=web/' "$services_snippet_file"
|
||||
sed -i '/traefik.http.routers.rustfs-s3.tls=true/d' "$services_snippet_file"
|
||||
sed -i '/traefik.http.routers.rustfs-s3.tls.certresolver=default/d' "$services_snippet_file"
|
||||
sed -i "s|accesscontrolalloworiginlist=https://$domain_name|accesscontrolalloworiginlist=http://$domain_name|" "$services_snippet_file"
|
||||
fi
|
||||
else
|
||||
@@ -577,14 +596,14 @@ EOF
|
||||
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
|
||||
# Ensure rustfs-data if needed
|
||||
if [[ $rustfs_storage == "y" ]]; then
|
||||
if ! awk '/^volumes:/{invol=1; next} invol && (/^[^[:space:]]/ || NF==0){invol=0} invol{ if($1=="rustfs-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 }
|
||||
invol && /^[^[:space:]]/ { if(!added){ print " rustfs-data:"; print " driver: local"; added=1 } ; invol=0 }
|
||||
{ print }
|
||||
END { if (invol && !added) { print " minio-data:"; print " driver: local" } }
|
||||
END { if (invol && !added) { print " rustfs-data:"; print " driver: local" } }
|
||||
' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
|
||||
fi
|
||||
fi
|
||||
@@ -596,34 +615,34 @@ EOF
|
||||
echo " driver: local"
|
||||
echo " redis:"
|
||||
echo " driver: local"
|
||||
if [[ $minio_storage == "y" ]]; then
|
||||
echo " minio-data:"
|
||||
if [[ $rustfs_storage == "y" ]]; then
|
||||
echo " rustfs-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'
|
||||
# Create rustfs-init script outside heredoc to avoid variable expansion issues
|
||||
if [[ $rustfs_storage == "y" ]]; then
|
||||
cat > rustfs-init.sh << 'RUSTFS_SCRIPT_EOF'
|
||||
#!/bin/sh
|
||||
echo '⏳ Waiting for MinIO to be ready...'
|
||||
echo '⏳ Waiting for RustFS 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
|
||||
until mc alias set rustfs http://rustfs:9000 "$RUSTFS_ADMIN_USER" "$RUSTFS_ADMIN_PASSWORD" >/dev/null 2>&1 \
|
||||
&& mc ls rustfs >/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
|
||||
printf '❌ Failed to connect to RustFS 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 '🔗 RustFS reachable; alias configured.'
|
||||
|
||||
echo '🪣 Creating bucket (idempotent)...';
|
||||
mc mb minio/$MINIO_BUCKET_NAME --ignore-existing;
|
||||
mc mb rustfs/$RUSTFS_BUCKET_NAME --ignore-existing;
|
||||
|
||||
echo '📄 Creating JSON policy file...';
|
||||
cat > /tmp/formbricks-policy.json << EOF
|
||||
@@ -633,40 +652,40 @@ cat > /tmp/formbricks-policy.json << EOF
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:DeleteObject", "s3:GetObject", "s3:PutObject"],
|
||||
"Resource": ["arn:aws:s3:::$MINIO_BUCKET_NAME/*"]
|
||||
"Resource": ["arn:aws:s3:::$RUSTFS_BUCKET_NAME/*"]
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:ListBucket"],
|
||||
"Resource": ["arn:aws:s3:::$MINIO_BUCKET_NAME"]
|
||||
"Resource": ["arn:aws:s3:::$RUSTFS_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;
|
||||
if ! mc admin policy info rustfs "$RUSTFS_POLICY_NAME" >/dev/null 2>&1; then
|
||||
mc admin policy create rustfs "$RUSTFS_POLICY_NAME" /tmp/formbricks-policy.json || mc admin policy add rustfs "$RUSTFS_POLICY_NAME" /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";
|
||||
if ! mc admin user info rustfs "$RUSTFS_SERVICE_USER" >/dev/null 2>&1; then
|
||||
mc admin user add rustfs "$RUSTFS_SERVICE_USER" "$RUSTFS_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).';
|
||||
mc admin policy attach rustfs "$RUSTFS_POLICY_NAME" --user "$RUSTFS_SERVICE_USER" || echo 'Policy already attached or attachment failed (non-fatal).';
|
||||
|
||||
echo '✅ MinIO setup complete!';
|
||||
echo '✅ RustFS setup complete!';
|
||||
exit 0;
|
||||
MINIO_SCRIPT_EOF
|
||||
chmod +x minio-init.sh
|
||||
RUSTFS_SCRIPT_EOF
|
||||
chmod +x rustfs-init.sh
|
||||
fi
|
||||
|
||||
newgrp docker <<END
|
||||
@@ -678,10 +697,10 @@ echo "🔗 To edit more variables and deeper config, go to the formbricks/docker
|
||||
echo "🚨 Make sure you have set up the DNS records as well as inbound rules for the domain name and IP address of this instance."
|
||||
echo ""
|
||||
|
||||
if [[ $minio_storage == "y" ]]; then
|
||||
echo "🗄️ MinIO Storage Setup Complete:"
|
||||
echo " • Access Key: $minio_service_user (least privilege)"
|
||||
echo " • Bucket: $minio_bucket_name (✅ created and secured)"
|
||||
if [[ $rustfs_storage == "y" ]]; then
|
||||
echo "🗄️ RustFS Storage Setup Complete:"
|
||||
echo " • Access Key: $rustfs_service_user (least privilege)"
|
||||
echo " • Bucket: $rustfs_bucket_name (✅ created and secured)"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
@@ -736,38 +755,47 @@ get_logs() {
|
||||
sudo docker compose logs
|
||||
}
|
||||
|
||||
cleanup_minio_init() {
|
||||
echo "🧹 Cleaning up MinIO init service and references..."
|
||||
cleanup_rustfs_init() {
|
||||
echo "🧹 Cleaning up RustFS init service and references..."
|
||||
cd formbricks
|
||||
|
||||
# Remove minio-init service block from docker-compose.yml
|
||||
# Remove rustfs-init service block from docker-compose.yml
|
||||
awk '
|
||||
BEGIN{skip=0}
|
||||
/^services:[[:space:]]*$/ { print; next }
|
||||
/^ minio-init:/ { skip=1; next }
|
||||
/^ rustfs-init:/ { skip=1; next }
|
||||
/^ [A-Za-z0-9_-]+:/ { if (skip) skip=0 }
|
||||
{ if (!skip) print }
|
||||
' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
|
||||
|
||||
# Remove list-style "- minio-init" lines under depends_on (if any)
|
||||
# Remove list-style init dependencies under depends_on (if any)
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -E -i '/^[[:space:]]*-[[:space:]]*rustfs-init[[:space:]]*$/d' docker-compose.yml
|
||||
sed -E -i '/^[[:space:]]*-[[:space:]]*minio-init[[:space:]]*$/d' docker-compose.yml
|
||||
else
|
||||
sed -E -i '' '/^[[:space:]]*-[[:space:]]*rustfs-init[[:space:]]*$/d' docker-compose.yml
|
||||
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)
|
||||
# Remove the mapping style depends_on entries for init jobs
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i '/^[[:space:]]*rustfs-init:[[:space:]]*$/,/^[[:space:]]*condition:[[:space:]]*service_completed_successfully[[:space:]]*$/d' docker-compose.yml
|
||||
sed -i '/^[[:space:]]*minio-init:[[:space:]]*$/,/^[[:space:]]*condition:[[:space:]]*service_completed_successfully[[:space:]]*$/d' docker-compose.yml
|
||||
else
|
||||
sed -i '' '/^[[:space:]]*rustfs-init:[[:space:]]*$/,/^[[:space:]]*condition:[[:space:]]*service_completed_successfully[[:space:]]*$/d' docker-compose.yml
|
||||
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
|
||||
# Remove any stopped init containers and restart without orphans
|
||||
docker compose rm -f -s rustfs-init >/dev/null 2>&1 || true
|
||||
docker compose rm -f -s minio-init >/dev/null 2>&1 || true
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
echo "✅ MinIO init cleanup complete."
|
||||
echo "✅ RustFS init cleanup complete."
|
||||
}
|
||||
|
||||
cleanup_minio_init() {
|
||||
cleanup_rustfs_init
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
@@ -786,9 +814,12 @@ restart)
|
||||
logs)
|
||||
get_logs
|
||||
;;
|
||||
cleanup-minio-init)
|
||||
cleanup_minio_init
|
||||
;;
|
||||
cleanup-rustfs-init)
|
||||
cleanup_rustfs_init
|
||||
;;
|
||||
cleanup-minio-init)
|
||||
cleanup_minio_init
|
||||
;;
|
||||
uninstall)
|
||||
uninstall_formbricks
|
||||
;;
|
||||
@@ -796,4 +827,4 @@ uninstall)
|
||||
echo "🚀 Executing default step of installing Formbricks"
|
||||
install_formbricks
|
||||
;;
|
||||
esac
|
||||
esac
|
||||
|
||||
757
docker/migrate-to-rustfs.sh
Executable file
757
docker/migrate-to-rustfs.sh
Executable file
@@ -0,0 +1,757 @@
|
||||
#!/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
MC_IMAGE="minio/mc@sha256:95b5f3f7969a5c5a9f3a700ba72d5c84172819e13385aaf916e237cf111ab868"
|
||||
RUSTFS_IMAGE="rustfs/rustfs:1.0.0-alpha.93"
|
||||
|
||||
print_status() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
compose_network_flag() {
|
||||
local name
|
||||
name="$(docker network ls --format '{{.Name}}' | awk -v d="$(basename "$PWD")" '$0==d"_default"{print; exit}')" || true
|
||||
if [[ -n "$name" ]]; then
|
||||
echo --network "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
read_compose_value() {
|
||||
local key="$1"
|
||||
local val
|
||||
val=$(sed -n "s/^[[:space:]]*$key:[[:space:]]*\"\(.*\)\"[[:space:]]*$/\1/p" docker-compose.yml | head -n 1)
|
||||
if [[ -z "$val" ]]; then
|
||||
val=$(sed -n "s/^[[:space:]]*$key:[[:space:]]*\([^#][^[:space:]]*\)[[:space:]]*$/\1/p" docker-compose.yml | head -n 1)
|
||||
fi
|
||||
echo "$val"
|
||||
}
|
||||
|
||||
has_service() {
|
||||
grep -q "^ $1:[[:space:]]*$" docker-compose.yml
|
||||
}
|
||||
|
||||
check_formbricks_directory() {
|
||||
if [[ -f "docker-compose.yml" ]]; then
|
||||
if grep -q "formbricks" docker-compose.yml; then
|
||||
return 0
|
||||
fi
|
||||
print_error "This doesn't appear to be a Formbricks docker-compose.yml file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
print_error "docker-compose.yml not found in the current directory or ./formbricks/"
|
||||
exit 1
|
||||
}
|
||||
|
||||
backup_docker_compose() {
|
||||
local backup_file="docker-compose.yml.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
cp docker-compose.yml "$backup_file"
|
||||
print_status "Backed up docker-compose.yml to $backup_file"
|
||||
}
|
||||
|
||||
detect_https_setup() {
|
||||
if grep -q "websecure" docker-compose.yml || grep -q "certresolver" docker-compose.yml; then
|
||||
echo "y"
|
||||
else
|
||||
echo "n"
|
||||
fi
|
||||
}
|
||||
|
||||
get_main_domain() {
|
||||
local domain=""
|
||||
if grep -q "WEBAPP_URL:" docker-compose.yml; then
|
||||
domain=$(grep "WEBAPP_URL:" docker-compose.yml | sed 's/.*WEBAPP_URL: *"\([^"]*\)".*/\1/' 2>/dev/null)
|
||||
if [[ -z "$domain" ]]; then
|
||||
domain=$(grep "WEBAPP_URL:" docker-compose.yml | sed 's/.*WEBAPP_URL: *\([^[:space:]]*\).*/\1/')
|
||||
fi
|
||||
domain=$(echo "$domain" | sed -e 's|https://||' -e 's|http://||')
|
||||
fi
|
||||
echo "$domain"
|
||||
}
|
||||
|
||||
add_or_replace_env_var() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
|
||||
if grep -q "^[[:space:]]*$key:" docker-compose.yml; then
|
||||
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
|
||||
elif grep -q "^[[:space:]]*#[[:space:]]*$key:" docker-compose.yml; then
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
external_s3_guard() {
|
||||
local acc sec
|
||||
acc=$(read_compose_value "S3_ACCESS_KEY")
|
||||
sec=$(read_compose_value "S3_SECRET_KEY")
|
||||
|
||||
if has_service minio; then
|
||||
print_warning "Detected bundled MinIO in docker-compose.yml."
|
||||
print_error "This helper does not migrate MinIO-backed installs to RustFS."
|
||||
print_info "Use this script only for setups that still store uploads locally."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -n "$acc" && -n "$sec" ]] && ! has_service rustfs; then
|
||||
print_warning "Detected existing S3 credentials without bundled RustFS."
|
||||
print_error "This helper is intended only for local-upload installs."
|
||||
print_info "If you already use AWS S3 or another S3-compatible provider, no migration is needed."
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
generate_rustfs_credentials() {
|
||||
local existing_s3_access existing_s3_secret existing_bucket existing_endpoint
|
||||
local existing_admin_user existing_admin_password
|
||||
|
||||
existing_s3_access=$(read_compose_value "S3_ACCESS_KEY")
|
||||
existing_s3_secret=$(read_compose_value "S3_SECRET_KEY")
|
||||
existing_bucket=$(read_compose_value "S3_BUCKET_NAME")
|
||||
existing_endpoint=$(read_compose_value "S3_ENDPOINT_URL")
|
||||
existing_admin_user=$(read_compose_value "RUSTFS_ACCESS_KEY")
|
||||
existing_admin_password=$(read_compose_value "RUSTFS_SECRET_KEY")
|
||||
|
||||
rustfs_service_user="${existing_s3_access:-formbricks-service-$(openssl rand -hex 4)}"
|
||||
rustfs_service_password="${existing_s3_secret:-$(openssl rand -base64 20)}"
|
||||
rustfs_bucket_name="${existing_bucket:-formbricks-uploads}"
|
||||
rustfs_policy_name="formbricks-policy"
|
||||
rustfs_admin_user="${existing_admin_user:-formbricks-$(openssl rand -hex 4)}"
|
||||
rustfs_admin_password="${existing_admin_password:-$(openssl rand -base64 20)}"
|
||||
rustfs_existing_endpoint="$existing_endpoint"
|
||||
}
|
||||
|
||||
add_rustfs_environment_variables() {
|
||||
local files_domain="$1"
|
||||
local https_setup="$2"
|
||||
local s3_endpoint_url=""
|
||||
|
||||
if [[ "$https_setup" == "y" ]]; then
|
||||
s3_endpoint_url="https://$files_domain"
|
||||
else
|
||||
s3_endpoint_url="http://$files_domain"
|
||||
fi
|
||||
|
||||
add_or_replace_env_var "S3_ACCESS_KEY" "$rustfs_service_user"
|
||||
add_or_replace_env_var "S3_SECRET_KEY" "$rustfs_service_password"
|
||||
add_or_replace_env_var "S3_REGION" "us-east-1"
|
||||
add_or_replace_env_var "S3_BUCKET_NAME" "$rustfs_bucket_name"
|
||||
add_or_replace_env_var "S3_ENDPOINT_URL" "$s3_endpoint_url"
|
||||
add_or_replace_env_var "S3_FORCE_PATH_STYLE" "1"
|
||||
|
||||
print_status "S3 environment variables ensured in docker-compose.yml"
|
||||
}
|
||||
|
||||
add_rustfs_services() {
|
||||
local files_domain="$1"
|
||||
local main_domain="$2"
|
||||
local https_setup="$3"
|
||||
|
||||
if has_service rustfs && has_service rustfs-init && has_service rustfs-perms; then
|
||||
print_info "RustFS services already present. Skipping service injection."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local traefik_entrypoints cors_origin tls_block
|
||||
if [[ "$https_setup" == "y" ]]; then
|
||||
traefik_entrypoints="websecure"
|
||||
cors_origin="https://$main_domain"
|
||||
tls_block=$' - "traefik.http.routers.rustfs-s3.tls=true"\n - "traefik.http.routers.rustfs-s3.tls.certresolver=default"'
|
||||
else
|
||||
traefik_entrypoints="web"
|
||||
cors_origin="http://$main_domain"
|
||||
tls_block=""
|
||||
fi
|
||||
|
||||
cat > rustfs_service.tmp <<EOF
|
||||
|
||||
rustfs-perms:
|
||||
image: busybox:1.36.1
|
||||
user: "0:0"
|
||||
command: ["sh", "-c", "mkdir -p /data && chown -R 10001:10001 /data"]
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
|
||||
rustfs:
|
||||
restart: always
|
||||
image: $RUSTFS_IMAGE
|
||||
depends_on:
|
||||
rustfs-perms:
|
||||
condition: service_completed_successfully
|
||||
command: /data
|
||||
environment:
|
||||
RUSTFS_ACCESS_KEY: "$rustfs_admin_user"
|
||||
RUSTFS_SECRET_KEY: "$rustfs_admin_password"
|
||||
RUSTFS_ADDRESS: ":9000"
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.rustfs-s3.rule=Host(\`$files_domain\`)"
|
||||
- "traefik.http.routers.rustfs-s3.entrypoints=$traefik_entrypoints"
|
||||
$tls_block
|
||||
- "traefik.http.routers.rustfs-s3.service=rustfs-s3"
|
||||
- "traefik.http.services.rustfs-s3.loadbalancer.server.port=9000"
|
||||
- "traefik.http.routers.rustfs-s3.middlewares=rustfs-cors,rustfs-ratelimit"
|
||||
- "traefik.http.middlewares.rustfs-cors.headers.accesscontrolallowmethods=GET,PUT,POST,DELETE,HEAD,OPTIONS"
|
||||
- "traefik.http.middlewares.rustfs-cors.headers.accesscontrolallowheaders=*"
|
||||
- "traefik.http.middlewares.rustfs-cors.headers.accesscontrolalloworiginlist=$cors_origin"
|
||||
- "traefik.http.middlewares.rustfs-cors.headers.accesscontrolmaxage=100"
|
||||
- "traefik.http.middlewares.rustfs-cors.headers.addvaryheader=true"
|
||||
- "traefik.http.middlewares.rustfs-ratelimit.ratelimit.average=100"
|
||||
- "traefik.http.middlewares.rustfs-ratelimit.ratelimit.burst=200"
|
||||
|
||||
rustfs-init:
|
||||
image: $MC_IMAGE
|
||||
depends_on:
|
||||
- rustfs
|
||||
environment:
|
||||
RUSTFS_ADMIN_USER: "$rustfs_admin_user"
|
||||
RUSTFS_ADMIN_PASSWORD: "$rustfs_admin_password"
|
||||
RUSTFS_SERVICE_USER: "$rustfs_service_user"
|
||||
RUSTFS_SERVICE_PASSWORD: "$rustfs_service_password"
|
||||
RUSTFS_BUCKET_NAME: "$rustfs_bucket_name"
|
||||
RUSTFS_POLICY_NAME: "$rustfs_policy_name"
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
attempts=0
|
||||
max_attempts=30
|
||||
until mc alias set rustfs http://rustfs:9000 "$rustfs_admin_user" "$rustfs_admin_password" >/dev/null 2>&1 \
|
||||
&& mc ls rustfs >/dev/null 2>&1; do
|
||||
attempts=\$((attempts + 1))
|
||||
if [ "\$attempts" -ge "\$max_attempts" ]; then
|
||||
echo "RustFS did not become ready within \${max_attempts} attempts"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
mc mb rustfs/$rustfs_bucket_name --ignore-existing
|
||||
cat > /tmp/formbricks-policy.json << 'POLICY_EOF'
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:DeleteObject", "s3:GetObject", "s3:PutObject"],
|
||||
"Resource": ["arn:aws:s3:::$rustfs_bucket_name/*"]
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:ListBucket"],
|
||||
"Resource": ["arn:aws:s3:::$rustfs_bucket_name"]
|
||||
}
|
||||
]
|
||||
}
|
||||
POLICY_EOF
|
||||
if ! mc admin policy info rustfs $rustfs_policy_name >/dev/null 2>&1; then
|
||||
mc admin policy create rustfs $rustfs_policy_name /tmp/formbricks-policy.json || mc admin policy add rustfs $rustfs_policy_name /tmp/formbricks-policy.json
|
||||
fi
|
||||
if ! mc admin user info rustfs "$rustfs_service_user" >/dev/null 2>&1; then
|
||||
mc admin user add rustfs "$rustfs_service_user" "$rustfs_service_password"
|
||||
fi
|
||||
mc admin policy attach rustfs $rustfs_policy_name --user "$rustfs_service_user" || true
|
||||
EOF
|
||||
|
||||
awk '
|
||||
{
|
||||
print
|
||||
if ($0 ~ /^services:$/ && !inserted) {
|
||||
while ((getline line < "rustfs_service.tmp") > 0) print line
|
||||
close("rustfs_service.tmp")
|
||||
inserted = 1
|
||||
}
|
||||
}
|
||||
' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
|
||||
|
||||
rm -f rustfs_service.tmp
|
||||
print_status "Added RustFS services to docker-compose.yml"
|
||||
}
|
||||
|
||||
add_rustfs_volume() {
|
||||
if grep -q '^volumes:' docker-compose.yml; then
|
||||
if awk '/^volumes:/{invol=1; next} invol && NF==0{invol=0} invol{ if($1=="rustfs-data:") found=1 } END{ exit(!found) }' docker-compose.yml; then
|
||||
print_info "rustfs-data volume already present."
|
||||
else
|
||||
awk '
|
||||
/^volumes:/ { print; invol=1; next }
|
||||
invol && /^[^[:space:]]/ { if(!added){ print " rustfs-data:"; print " driver: local"; added=1 } ; invol=0 }
|
||||
{ print }
|
||||
END { if (invol && !added) { print " rustfs-data:"; print " driver: local" } }
|
||||
' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
|
||||
print_status "rustfs-data volume ensured"
|
||||
fi
|
||||
else
|
||||
{
|
||||
echo ""
|
||||
echo "volumes:"
|
||||
echo " rustfs-data:"
|
||||
echo " driver: local"
|
||||
} >> docker-compose.yml
|
||||
print_status "Added volumes section with rustfs-data"
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_rustfs_ready() {
|
||||
print_info "Waiting for RustFS to be ready..."
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
if docker run --rm $(compose_network_flag) --entrypoint /bin/sh "$MC_IMAGE" -lc \
|
||||
"mc alias set rustfs http://rustfs:9000 '$rustfs_admin_user' '$rustfs_admin_password' >/dev/null 2>&1 && mc admin info rustfs >/dev/null 2>&1"; then
|
||||
print_status "RustFS is ready!"
|
||||
return 0
|
||||
fi
|
||||
if [[ $attempt -eq $max_attempts ]]; then
|
||||
print_error "RustFS did not become ready within the expected time."
|
||||
return 1
|
||||
fi
|
||||
sleep 5
|
||||
((attempt++))
|
||||
done
|
||||
}
|
||||
|
||||
ensure_bucket_exists() {
|
||||
print_info "Ensuring bucket '$rustfs_bucket_name' exists..."
|
||||
docker run --rm $(compose_network_flag) \
|
||||
-e RUSTFS_ADMIN_USER="$rustfs_admin_user" \
|
||||
-e RUSTFS_ADMIN_PASSWORD="$rustfs_admin_password" \
|
||||
-e RUSTFS_BUCKET_NAME="$rustfs_bucket_name" \
|
||||
--entrypoint /bin/sh "$MC_IMAGE" -lc '
|
||||
mc alias set rustfs http://rustfs:9000 "$RUSTFS_ADMIN_USER" "$RUSTFS_ADMIN_PASSWORD" >/dev/null 2>&1
|
||||
mc mb rustfs/"$RUSTFS_BUCKET_NAME" --ignore-existing
|
||||
'
|
||||
}
|
||||
|
||||
ensure_service_user_and_policy() {
|
||||
print_info "Ensuring RustFS service user and policy exist..."
|
||||
docker run --rm $(compose_network_flag) \
|
||||
-e RUSTFS_ADMIN_USER="$rustfs_admin_user" \
|
||||
-e RUSTFS_ADMIN_PASSWORD="$rustfs_admin_password" \
|
||||
-e RUSTFS_SERVICE_USER="$rustfs_service_user" \
|
||||
-e RUSTFS_SERVICE_PASSWORD="$rustfs_service_password" \
|
||||
-e RUSTFS_BUCKET_NAME="$rustfs_bucket_name" \
|
||||
-e RUSTFS_POLICY_NAME="$rustfs_policy_name" \
|
||||
--entrypoint /bin/sh "$MC_IMAGE" -lc '
|
||||
mc alias set rustfs http://rustfs:9000 "$RUSTFS_ADMIN_USER" "$RUSTFS_ADMIN_PASSWORD" >/dev/null 2>&1
|
||||
if ! mc admin policy info rustfs "$RUSTFS_POLICY_NAME" >/dev/null 2>&1; then
|
||||
cat > /tmp/formbricks-policy.json << EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{ "Effect": "Allow", "Action": ["s3:DeleteObject", "s3:GetObject", "s3:PutObject"], "Resource": ["arn:aws:s3:::$RUSTFS_BUCKET_NAME/*"] },
|
||||
{ "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": ["arn:aws:s3:::$RUSTFS_BUCKET_NAME"] }
|
||||
]
|
||||
}
|
||||
EOF
|
||||
mc admin policy create rustfs "$RUSTFS_POLICY_NAME" /tmp/formbricks-policy.json >/dev/null 2>&1 || mc admin policy add rustfs "$RUSTFS_POLICY_NAME" /tmp/formbricks-policy.json >/dev/null 2>&1
|
||||
fi
|
||||
if ! mc admin user info rustfs "$RUSTFS_SERVICE_USER" >/dev/null 2>&1; then
|
||||
mc admin user add rustfs "$RUSTFS_SERVICE_USER" "$RUSTFS_SERVICE_PASSWORD" >/dev/null 2>&1 || true
|
||||
fi
|
||||
mc admin policy attach rustfs "$RUSTFS_POLICY_NAME" --user "$RUSTFS_SERVICE_USER" >/dev/null 2>&1 || true
|
||||
'
|
||||
}
|
||||
|
||||
wait_for_service_up() {
|
||||
local service_name="$1"
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
if [[ -n "$(docker compose ps --status running -q "$service_name" 2>/dev/null)" ]]; then
|
||||
print_status "$service_name is running."
|
||||
return 0
|
||||
fi
|
||||
if [[ $attempt -eq $max_attempts ]]; then
|
||||
print_error "$service_name did not become ready within the expected time."
|
||||
return 1
|
||||
fi
|
||||
sleep 5
|
||||
((attempt++))
|
||||
done
|
||||
}
|
||||
|
||||
collect_upload_sources_post_start() {
|
||||
declare -A seen
|
||||
local out=()
|
||||
local env_path
|
||||
env_path=$(read_compose_value "UPLOADS_DIR")
|
||||
local container_present=0
|
||||
|
||||
local awk_tmp
|
||||
awk_tmp=$(mktemp)
|
||||
cat > "$awk_tmp" <<'AWK_EOF'
|
||||
/^ formbricks:/ { in_svc=1; next }
|
||||
/^ [A-Za-z0-9_-]+:/ && !/^ formbricks:/ { in_svc=0 }
|
||||
in_svc && /^ volumes:/ { in_vol=1; next }
|
||||
in_svc && /^ [A-Za-z0-9_-]+:/ { if(in_vol) in_vol=0 }
|
||||
in_vol {
|
||||
if ($0 ~ /^[[:space:]]*-[[:space:]]*type:/){ tp=$0; sub(/.*type:[[:space:]]*/, "", tp); src=""; tgt="" }
|
||||
else if ($0 ~ /source:/){ line=$0; sub(/^[^:]*:[[:space:]]*/, "", line); src=line }
|
||||
else if ($0 ~ /target:/){ line=$0; sub(/^[^:]*:[[:space:]]*/, "", line); tgt=line; if (tp!="" && tgt!="") printf "%s|%s|%s\n", tp, src, tgt }
|
||||
}
|
||||
AWK_EOF
|
||||
local entries
|
||||
entries=$(docker compose config 2>/dev/null | awk -f "$awk_tmp")
|
||||
rm -f "$awk_tmp"
|
||||
|
||||
while IFS= read -r e; do
|
||||
[[ -z "$e" ]] && continue
|
||||
local tp src tgt rest
|
||||
tp="${e%%|*}"
|
||||
rest="${e#*|}"
|
||||
src="${rest%%|*}"
|
||||
tgt="${rest##*|}"
|
||||
|
||||
if [[ "$tp" == "bind" && "$tgt" == *"/uploads"* ]]; then
|
||||
if [[ -z "${seen[$src]}" ]]; then
|
||||
out+=("$src")
|
||||
seen[$src]=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$tp" == "volume" && "$src" == "uploads" ]]; then
|
||||
local key="container:$tgt"
|
||||
if [[ -z "${seen[$key]}" ]]; then
|
||||
out+=("$key")
|
||||
seen[$key]=1
|
||||
fi
|
||||
container_present=1
|
||||
fi
|
||||
done <<< "$entries"
|
||||
|
||||
if [[ -n "$env_path" && "$env_path" =~ ^/ ]]; then
|
||||
local key="container:$env_path"
|
||||
if [[ -z "${seen[$key]}" ]]; then
|
||||
out+=("$key")
|
||||
seen[$key]=1
|
||||
fi
|
||||
container_present=1
|
||||
elif [[ -n "$env_path" && -d "$env_path" && -z "${seen[$env_path]}" ]]; then
|
||||
out+=("$env_path")
|
||||
seen[$env_path]=1
|
||||
elif [[ -n "$env_path" && -d "./$env_path" && -z "${seen[./$env_path]}" ]]; then
|
||||
out+=("./$env_path")
|
||||
seen["./$env_path"]=1
|
||||
fi
|
||||
|
||||
if [[ $container_present -eq 0 ]]; then
|
||||
local legacy="container:/home/nextjs/apps/web/uploads"
|
||||
if [[ -z "${seen[$legacy]}" ]]; then
|
||||
out+=("$legacy")
|
||||
seen[$legacy]=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -d "./apps/web/uploads" && -z "${seen[./apps/web/uploads]}" ]]; then
|
||||
out+=("./apps/web/uploads")
|
||||
seen[./apps/web/uploads]=1
|
||||
fi
|
||||
|
||||
if [[ -d "./uploads" && -z "${seen[./uploads]}" ]]; then
|
||||
out+=("./uploads")
|
||||
seen[./uploads]=1
|
||||
fi
|
||||
|
||||
for s in "${out[@]}"; do
|
||||
echo "$s"
|
||||
done
|
||||
}
|
||||
|
||||
preview_upload_sources() {
|
||||
local sources="$1"
|
||||
echo ""
|
||||
echo "📋 Migration sources preview:"
|
||||
while IFS= read -r src; do
|
||||
[[ -z "$src" ]] && continue
|
||||
if [[ "$src" == container:* ]]; then
|
||||
local p="${src#container:}"
|
||||
local cnt
|
||||
cnt=$(docker compose exec -T formbricks sh -lc 'find '"$p"' -type f 2>/dev/null | wc -l' || echo 0)
|
||||
echo " - $src → $cnt files"
|
||||
else
|
||||
local cnt
|
||||
cnt=$(find "$src" -type f 2>/dev/null | wc -l || echo 0)
|
||||
echo " - $src (host) → $cnt files"
|
||||
fi
|
||||
done <<< "$sources"
|
||||
}
|
||||
|
||||
migrate_container_files_to_rustfs() {
|
||||
local container_path="$1"
|
||||
local file_count
|
||||
local formbricks_cid
|
||||
|
||||
if ! docker compose exec -T formbricks test -d "$container_path" 2>/dev/null; then
|
||||
print_warning "Container path not found, skipping: $container_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
file_count=$(docker compose exec -T formbricks find "$container_path" -type f 2>/dev/null | wc -l)
|
||||
if [[ $file_count -eq 0 ]]; then
|
||||
print_warning "No files found in $container_path to migrate."
|
||||
return 0
|
||||
fi
|
||||
|
||||
formbricks_cid=$(docker compose ps -q formbricks)
|
||||
|
||||
docker run --rm $(compose_network_flag) \
|
||||
--volumes-from "$formbricks_cid" \
|
||||
-e RUSTFS_ADMIN_USER="$rustfs_admin_user" \
|
||||
-e RUSTFS_ADMIN_PASSWORD="$rustfs_admin_password" \
|
||||
-e RUSTFS_BUCKET_NAME="$rustfs_bucket_name" \
|
||||
--entrypoint /bin/sh "$MC_IMAGE" -lc '
|
||||
mc alias set rustfs http://rustfs:9000 "$RUSTFS_ADMIN_USER" "$RUSTFS_ADMIN_PASSWORD"
|
||||
mc mirror --overwrite --preserve '"$container_path"' "rustfs/$RUSTFS_BUCKET_NAME"
|
||||
'
|
||||
|
||||
print_status "Migrated $file_count files from $container_path"
|
||||
}
|
||||
|
||||
migrate_host_files_to_rustfs() {
|
||||
local uploads_dir="$1"
|
||||
local file_count
|
||||
local host_src="$uploads_dir"
|
||||
|
||||
if [[ ! -d "$uploads_dir" ]]; then
|
||||
print_warning "Host path not found, skipping: $uploads_dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
file_count=$(find "$uploads_dir" -type f 2>/dev/null | wc -l)
|
||||
if [[ $file_count -eq 0 ]]; then
|
||||
print_warning "No files found in $uploads_dir to migrate."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$host_src" != /* ]]; then
|
||||
host_src="$PWD/$host_src"
|
||||
fi
|
||||
|
||||
docker run --rm $(compose_network_flag) \
|
||||
-v "$host_src:/source:ro" \
|
||||
-e RUSTFS_ADMIN_USER="$rustfs_admin_user" \
|
||||
-e RUSTFS_ADMIN_PASSWORD="$rustfs_admin_password" \
|
||||
-e RUSTFS_BUCKET_NAME="$rustfs_bucket_name" \
|
||||
--entrypoint /bin/sh "$MC_IMAGE" -lc '
|
||||
mc alias set rustfs http://rustfs:9000 "$RUSTFS_ADMIN_USER" "$RUSTFS_ADMIN_PASSWORD"
|
||||
mc mirror --overwrite --preserve /source "rustfs/$RUSTFS_BUCKET_NAME"
|
||||
'
|
||||
|
||||
print_status "Migrated $file_count files from $uploads_dir"
|
||||
}
|
||||
|
||||
cleanup_uploads_from_compose() {
|
||||
print_info "Cleaning docker-compose.yml uploads configuration..."
|
||||
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i 's/^\([[:space:]]*\)UPLOADS_DIR:[[:space:]].*/\1# UPLOADS_DIR:/' docker-compose.yml || true
|
||||
else
|
||||
sed -i '' 's/^\([[:space:]]*\)UPLOADS_DIR:[[:space:]].*/\1# UPLOADS_DIR:/' docker-compose.yml || true
|
||||
fi
|
||||
|
||||
awk '
|
||||
BEGIN{in_svc=0; in_vol=0}
|
||||
/^ formbricks:/ {in_svc=1}
|
||||
/^ [A-Za-z0-9_-]+:/ && !/^ formbricks:/ {in_svc=0}
|
||||
{line=$0}
|
||||
in_svc && /^ volumes:/ {in_vol=1; print; next}
|
||||
in_svc && /^ [A-Za-z0-9_-]+:/ {if(in_vol) in_vol=0}
|
||||
in_vol {
|
||||
if (line ~ /^[[:space:]]*-[[:space:]]*uploads:/) next
|
||||
if (line ~ /^[[:space:]]*-[[:space:]].*\/uploads\/?[[:space:]]*$/) next
|
||||
print
|
||||
next
|
||||
}
|
||||
{print}
|
||||
' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
|
||||
|
||||
awk '
|
||||
BEGIN{in_vol=0}
|
||||
/^volumes:[[:space:]]*$/ {print; in_vol=1; next}
|
||||
in_vol && /^ uploads:[[:space:]]*$/ {skip=1; next}
|
||||
in_vol && skip && /^[[:space:]]{4}[A-Za-z0-9_-]+:/ {next}
|
||||
in_vol && skip && /^ [A-Za-z0-9_-]+:/ {skip=0}
|
||||
{ if (!skip) print }
|
||||
' docker-compose.yml > tmp.yml && mv tmp.yml docker-compose.yml
|
||||
|
||||
print_status "Removed legacy local uploads configuration from docker-compose.yml"
|
||||
}
|
||||
|
||||
migrate_to_rustfs() {
|
||||
echo "🦀 Formbricks RustFS Migration"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
print_info "This helper provisions bundled RustFS and migrates legacy local uploads."
|
||||
print_info "MinIO-backed installs are intentionally skipped."
|
||||
echo ""
|
||||
|
||||
check_formbricks_directory
|
||||
backup_docker_compose
|
||||
external_s3_guard
|
||||
|
||||
local main_domain
|
||||
local https_setup
|
||||
local files_domain
|
||||
|
||||
main_domain=$(get_main_domain)
|
||||
https_setup=$(detect_https_setup)
|
||||
|
||||
if [[ -z "$main_domain" ]]; then
|
||||
print_error "Could not detect WEBAPP_URL from docker-compose.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
generate_rustfs_credentials
|
||||
|
||||
if has_service rustfs; then
|
||||
files_domain=$(echo "$rustfs_existing_endpoint" | sed -E 's#^https?://##')
|
||||
if [[ -z "$files_domain" ]]; then
|
||||
files_domain="files.$main_domain"
|
||||
fi
|
||||
print_info "Detected existing RustFS configuration for $files_domain"
|
||||
else
|
||||
print_warning "RustFS requires a dedicated files subdomain."
|
||||
print_info "Make sure files.$main_domain points to the same server as your Formbricks instance."
|
||||
echo -n "Do you want to continue? [Y/n]: "
|
||||
read -r continue_setup
|
||||
continue_setup=$(echo "$continue_setup" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ -n "$continue_setup" && "$continue_setup" != "y" ]]; then
|
||||
print_info "Migration cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local default_files_domain="files.$main_domain"
|
||||
echo -n "Enter the files subdomain to use for RustFS (e.g., $default_files_domain): "
|
||||
read -r files_domain
|
||||
if [[ -z "$files_domain" ]]; then
|
||||
files_domain="$default_files_domain"
|
||||
fi
|
||||
|
||||
add_rustfs_environment_variables "$files_domain" "$https_setup"
|
||||
add_rustfs_services "$files_domain" "$main_domain" "$https_setup"
|
||||
add_rustfs_volume
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -n "Restart Docker Compose now to provision RustFS and continue the migration? [Y/n]: "
|
||||
read -r restart_confirm
|
||||
restart_confirm=$(echo "$restart_confirm" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ -n "$restart_confirm" && "$restart_confirm" != "y" ]]; then
|
||||
print_warning "Migration cancelled before any file copy."
|
||||
print_info "Run 'docker compose up -d --remove-orphans' later, then rerun this script."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
wait_for_service_up formbricks
|
||||
wait_for_rustfs_ready
|
||||
ensure_service_user_and_policy
|
||||
ensure_bucket_exists
|
||||
|
||||
local sources
|
||||
sources=$(collect_upload_sources_post_start)
|
||||
preview_upload_sources "$sources"
|
||||
|
||||
if [[ -z "$sources" ]]; then
|
||||
echo -n "No upload sources were detected automatically. Enter a path (container:/path or ./path), or press Enter to skip: "
|
||||
read -r manual_src
|
||||
manual_src=$(echo "$manual_src" | xargs)
|
||||
if [[ -n "$manual_src" ]]; then
|
||||
sources="$manual_src"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$sources" ]]; then
|
||||
print_warning "No uploads directory was detected."
|
||||
cleanup_uploads_from_compose
|
||||
print_status "RustFS is configured. No local files were found to migrate."
|
||||
else
|
||||
echo -n "Proceed with the migration from the sources above? [Y/n]: "
|
||||
read -r do_migration
|
||||
do_migration=$(echo "$do_migration" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ -n "$do_migration" && "$do_migration" != "y" ]]; then
|
||||
print_warning "Skipped file migration at your request."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local migration_failed=0
|
||||
while IFS= read -r src; do
|
||||
[[ -z "$src" ]] && continue
|
||||
if [[ "$src" == container:* ]]; then
|
||||
migrate_container_files_to_rustfs "${src#container:}" || migration_failed=1
|
||||
else
|
||||
migrate_host_files_to_rustfs "$src" || migration_failed=1
|
||||
fi
|
||||
done <<< "$sources"
|
||||
|
||||
if [[ $migration_failed -eq 0 ]]; then
|
||||
cleanup_uploads_from_compose
|
||||
print_status "Local uploads have been migrated to RustFS."
|
||||
else
|
||||
print_error "Migration failed before cleanup."
|
||||
print_info "Legacy upload paths were left untouched so you can retry safely."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_status "RustFS migration complete."
|
||||
print_info "Files domain: $files_domain"
|
||||
print_info "S3 Access Key: $rustfs_service_user"
|
||||
print_info "S3 Bucket: $rustfs_bucket_name"
|
||||
print_info "RustFS admin user: $rustfs_admin_user"
|
||||
print_info "Cleanup note: keep rustfs-init if you want idempotent bootstrap on future restarts."
|
||||
}
|
||||
|
||||
migrate_to_rustfs "$@"
|
||||
@@ -39,6 +39,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
|
||||
If you run into "**No such container**", use `docker ps` to find your container name, e.g.
|
||||
`formbricks_postgres_1`.
|
||||
</Info>
|
||||
|
||||
</Tab>
|
||||
<Tab title="Kubernetes">
|
||||
If you are using the **in-cluster PostgreSQL** deployed by the Helm chart:
|
||||
@@ -52,6 +53,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
|
||||
</Info>
|
||||
|
||||
If you are using a **managed PostgreSQL** service (e.g. AWS RDS, Cloud SQL), use your provider's backup/snapshot feature or run `pg_dump` directly against the external host.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -69,6 +71,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
|
||||
# Start with Formbricks v4.7
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Kubernetes">
|
||||
```bash
|
||||
@@ -81,6 +84,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
|
||||
The Helm chart includes a migration Job that automatically runs Prisma schema migrations as a
|
||||
PreSync hook before the new pods start. No manual migration step is needed.
|
||||
</Info>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -105,6 +109,7 @@ After Formbricks starts, check the logs to see whether the value backfill was co
|
||||
```bash
|
||||
kubectl logs -n formbricks job/formbricks-migration
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -121,6 +126,7 @@ If the migration skipped the value backfill, run the standalone backfill script
|
||||
```
|
||||
|
||||
<Info>Replace `formbricks` with your actual container name if it differs. Use `docker ps` to find it.</Info>
|
||||
|
||||
</Tab>
|
||||
<Tab title="Kubernetes">
|
||||
```bash
|
||||
@@ -130,6 +136,7 @@ If the migration skipped the value backfill, run the standalone backfill script
|
||||
<Info>
|
||||
If your Formbricks deployment has a different name, run `kubectl get deploy -n formbricks` to find it.
|
||||
</Info>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -202,7 +209,7 @@ Formbricks 4.0 is a **major milestone** that sets up the technical foundation fo
|
||||
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
|
||||
- **RustFS 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.
|
||||
|
||||
@@ -223,9 +230,43 @@ We believe this is the only path forward to build the comprehensive Survey and E
|
||||
|
||||
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
|
||||
### Local uploads to bundled RustFS
|
||||
|
||||
For users using our official one-click setup, we provide an automated migration using a migration script:
|
||||
If your current self-hosted setup still stores uploads on the local filesystem and you now want to move to the
|
||||
bundled object storage setup, use the RustFS migration helper:
|
||||
|
||||
```bash
|
||||
# Download the latest script
|
||||
curl -fsSL -o migrate-to-rustfs.sh \
|
||||
https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/migrate-to-rustfs.sh
|
||||
|
||||
# Make it executable
|
||||
chmod +x migrate-to-rustfs.sh
|
||||
|
||||
# Launch the guided migration
|
||||
./migrate-to-rustfs.sh
|
||||
```
|
||||
|
||||
This helper:
|
||||
|
||||
- Adds bundled RustFS services to your Compose setup
|
||||
- Configures the bucket, policy, and least-privilege service credentials
|
||||
- Copies legacy local uploads into the RustFS bucket
|
||||
- Removes legacy local upload configuration only after the copy succeeds
|
||||
|
||||
<Note>
|
||||
This helper targets a convenient single-server RustFS setup. If you need higher availability or a
|
||||
larger-scale storage architecture, prefer external object storage or a dedicated RustFS deployment.
|
||||
</Note>
|
||||
|
||||
<Info>
|
||||
This helper does **not** migrate MinIO-backed installs to RustFS. It is designed only for setups that still
|
||||
use local uploads.
|
||||
</Info>
|
||||
|
||||
### Legacy one-click v4.0 upgrade
|
||||
|
||||
For historical v4.0 upgrades, our original one-click migration script is still available:
|
||||
|
||||
```bash
|
||||
# Download the latest script
|
||||
@@ -242,7 +283,7 @@ chmod +x 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
|
||||
- Adds a bundled MinIO service to your setup, configures it, and migrates local files to it
|
||||
- Pulls the latest Formbricks image and updates your instance
|
||||
|
||||
### Manual Setup
|
||||
@@ -264,7 +305,7 @@ Formbricks supports multiple storage providers (among many other S3-compatible s
|
||||
- AWS S3
|
||||
- Digital Ocean Spaces
|
||||
- Hetzner Object Storage
|
||||
- Custom MinIO server
|
||||
- Custom RustFS 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:
|
||||
|
||||
@@ -273,7 +314,8 @@ Please make sure to set up a storage bucket with one of these solutions and then
|
||||
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
|
||||
S3_ENDPOINT_URL: https://files.yourdomain.com # not needed for AWS S3
|
||||
S3_FORCE_PATH_STYLE: 1
|
||||
```
|
||||
|
||||
#### Upgrade Process
|
||||
@@ -319,7 +361,7 @@ 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. Check the [File Upload Troubleshooting](/self-hosting/configuration/file-uploads#troubleshooting) section if you face any issues.
|
||||
- Test file uploads to ensure S3 storage integration works correctly. Check the [File Upload Troubleshooting](/self-hosting/configuration/file-uploads#troubleshooting) section if you face any issues.
|
||||
- Verify that existing surveys and data are intact
|
||||
- Check that previously uploaded files are accessible
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description: "Configure file storage for survey images, file uploads, and projec
|
||||
icon: "upload"
|
||||
---
|
||||
|
||||
Formbricks requires S3-compatible storage for file uploads. You can use external cloud storage services or the bundled MinIO option for a self-hosted solution.
|
||||
Formbricks requires S3-compatible storage for file uploads. You can use external cloud storage services or the bundled RustFS option for a self-hosted solution.
|
||||
|
||||
## Why Configure File Uploads?
|
||||
|
||||
@@ -35,18 +35,25 @@ Use cloud storage services for production deployments:
|
||||
- **StorJ**
|
||||
- Any S3-compatible storage service
|
||||
|
||||
### 2. Bundled MinIO Storage (Self-Hosted)
|
||||
### 2. Bundled RustFS Storage (Self-Hosted)
|
||||
|
||||
<Warning>
|
||||
**Important**: MinIO requires a dedicated subdomain to function properly. You must configure a subdomain
|
||||
like `files.yourdomain.com` that points to your server. MinIO will not work without this subdomain setup.
|
||||
**Important**: Bundled RustFS requires a dedicated subdomain. You must configure a subdomain like
|
||||
`files.yourdomain.com` that points to your server so browser uploads can reach the object storage endpoint.
|
||||
</Warning>
|
||||
|
||||
MinIO provides a self-hosted S3-compatible storage solution that runs alongside Formbricks. This option:
|
||||
<Warning>
|
||||
Bundled RustFS is a convenience-oriented single-server option. It fits small-scale or lower-complexity
|
||||
self-hosted deployments, but it is not the ideal RustFS architecture for high-availability or larger-scale
|
||||
production storage. For stricter production requirements, prefer external object storage or a dedicated
|
||||
RustFS deployment.
|
||||
</Warning>
|
||||
|
||||
RustFS provides a self-hosted S3-compatible storage solution that runs alongside Formbricks. This option:
|
||||
|
||||
- Runs in a Docker container alongside Formbricks
|
||||
- Provides full S3 API compatibility
|
||||
- Requires minimal additional configuration
|
||||
- Uses the same `S3_*` environment variables as any other S3-compatible provider
|
||||
|
||||
## Configuration Methods
|
||||
|
||||
@@ -78,14 +85,14 @@ Choose this option for AWS S3, DigitalOcean Spaces, or other cloud providers:
|
||||
S3 Endpoint URL (leave empty if you are using AWS S3): https://your-endpoint.com
|
||||
```
|
||||
|
||||
#### Bundled MinIO Storage
|
||||
#### Bundled RustFS Storage
|
||||
|
||||
Choose this option for a self-hosted S3-compatible storage that runs alongside Formbricks:
|
||||
|
||||
<Note>
|
||||
**Critical Requirement**: Before proceeding, ensure you have configured a subdomain (e.g.,
|
||||
`files.yourdomain.com`) that points to your server's IP address. MinIO will not function without this
|
||||
subdomain setup.
|
||||
`files.yourdomain.com`) that points to your server's IP address. This is required so browser-direct uploads
|
||||
can reach RustFS.
|
||||
</Note>
|
||||
|
||||
```bash
|
||||
@@ -95,7 +102,7 @@ Choose this option for a self-hosted S3-compatible storage that runs alongside F
|
||||
|
||||
The script will automatically:
|
||||
|
||||
- Generate secure MinIO credentials
|
||||
- Generate separate RustFS admin and Formbricks service credentials
|
||||
- Create the storage bucket
|
||||
- Configure SSL certificates for the files subdomain
|
||||
- Configure Traefik routing for the subdomain
|
||||
@@ -122,7 +129,7 @@ S3_FORCE_PATH_STYLE=1
|
||||
|
||||
<Note>
|
||||
<strong>AWS S3 vs. third‑party S3:</strong> When using AWS S3 directly, leave `S3_ENDPOINT_URL` unset and
|
||||
set `S3_FORCE_PATH_STYLE=0` (or omit). For most third‑party S3‑compatible providers (e.g., MinIO,
|
||||
set `S3_FORCE_PATH_STYLE=0` (or omit). For most third‑party S3‑compatible providers (e.g., RustFS,
|
||||
DigitalOcean Spaces, Wasabi, Storj), you typically must set `S3_ENDPOINT_URL` to the provider's endpoint and
|
||||
set `S3_FORCE_PATH_STYLE=1`.
|
||||
</Note>
|
||||
@@ -151,11 +158,11 @@ S3_ENDPOINT_URL=https://nyc3.digitaloceanspaces.com
|
||||
S3_FORCE_PATH_STYLE=1
|
||||
```
|
||||
|
||||
### MinIO (Self-Hosted)
|
||||
### RustFS (Self-Hosted)
|
||||
|
||||
```bash
|
||||
S3_ACCESS_KEY=minio_access_key
|
||||
S3_SECRET_KEY=minio_secret_key
|
||||
S3_ACCESS_KEY=rustfs_service_access_key
|
||||
S3_SECRET_KEY=rustfs_service_secret_key
|
||||
S3_REGION=us-east-1
|
||||
S3_BUCKET_NAME=formbricks-uploads
|
||||
S3_ENDPOINT_URL=https://files.yourdomain.com
|
||||
@@ -170,14 +177,14 @@ that do not implement POST Object are not compatible with Formbricks uploads. Fo
|
||||
S3‑compatible API currently does not support POST Object and therefore will not work with Formbricks file
|
||||
uploads.
|
||||
|
||||
## Bundled MinIO Setup
|
||||
## Bundled RustFS Setup
|
||||
|
||||
When using the bundled MinIO option through the setup script, you get:
|
||||
When using the bundled RustFS option through the setup script, you get:
|
||||
|
||||
### Automatic Configuration
|
||||
|
||||
- **Storage Service**: MinIO running in a Docker container
|
||||
- **Credentials**: Auto-generated secure access keys
|
||||
- **Storage Service**: RustFS running in a Docker container
|
||||
- **Credentials**: Auto-generated admin and least-privilege service credentials
|
||||
- **Bucket**: Automatically created `formbricks-uploads` bucket
|
||||
- **SSL**: Automatic certificate generation for the files subdomain
|
||||
|
||||
@@ -186,23 +193,22 @@ When using the bundled MinIO option through the setup script, you get:
|
||||
After setup, you'll see:
|
||||
|
||||
```bash
|
||||
🗄️ MinIO Storage Setup Complete:
|
||||
• S3 API: https://files.yourdomain.com
|
||||
• Access Key: formbricks-a1b2c3d4
|
||||
🗄️ RustFS Storage Setup Complete:
|
||||
• Access Key: formbricks-service-a1b2c3d4
|
||||
• Bucket: formbricks-uploads (✅ automatically created)
|
||||
```
|
||||
|
||||
### DNS Requirements
|
||||
|
||||
<Warning>
|
||||
**Critical for MinIO**: The subdomain configuration is mandatory for MinIO to function. Without proper
|
||||
subdomain DNS setup, MinIO will fail to work entirely.
|
||||
**Critical for bundled RustFS**: The files subdomain is mandatory. Without proper DNS and reverse-proxy
|
||||
routing, browser uploads will fail.
|
||||
</Warning>
|
||||
|
||||
For the bundled MinIO setup, ensure:
|
||||
For the bundled RustFS setup, ensure:
|
||||
|
||||
1. **Main domain**: `yourdomain.com` points to your server IP
|
||||
2. **Files subdomain**: `files.yourdomain.com` points to your server IP (this is required for MinIO to work)
|
||||
2. **Files subdomain**: `files.yourdomain.com` points to your server IP
|
||||
3. **Firewall**: Ports 80 and 443 are open in your server's firewall
|
||||
4. **DNS propagation**: Allow time for DNS changes to propagate globally
|
||||
|
||||
@@ -234,23 +240,23 @@ services:
|
||||
|
||||
When using AWS S3 or S3-compatible storage providers, ensure that the IAM user associated with your `S3_ACCESS_KEY` and `S3_SECRET_KEY` credentials has the necessary permissions to interact with your bucket. Without proper permissions, file uploads and retrievals will fail.
|
||||
|
||||
The following IAM policy grants the minimum required permissions for Formbricks to function correctly. This policy is also used in the bundled MinIO integration:
|
||||
The following IAM policy grants the minimum required permissions for Formbricks to function correctly. This policy is also used in the bundled RustFS integration:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:DeleteObject", "s3:GetObject", "s3:PutObject"],
|
||||
"Effect": "Allow",
|
||||
"Resource": ["arn:aws:s3:::your-bucket-name/*"]
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:ListBucket"],
|
||||
"Effect": "Allow",
|
||||
"Resource": ["arn:aws:s3:::your-bucket-name"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"Version": "2012-10-17"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -294,7 +300,9 @@ Example least-privileged S3 bucket policy:
|
||||
```
|
||||
|
||||
<Note>
|
||||
Replace `your-bucket-name` with your actual bucket name and `arn:aws:iam::123456789012:user/formbricks-service` with the ARN of your IAM user. This policy allows public read access only to specific paths while restricting write access to your Formbricks service user.
|
||||
Replace `your-bucket-name` with your actual bucket name and
|
||||
`arn:aws:iam::123456789012:user/formbricks-service` with the ARN of your IAM user. This policy allows public
|
||||
read access only to specific paths while restricting write access to your Formbricks service user.
|
||||
</Note>
|
||||
|
||||
### S3 CORS Configuration
|
||||
@@ -306,30 +314,19 @@ Configure CORS on your S3 bucket with the following settings:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"AllowedHeaders": [
|
||||
"*"
|
||||
],
|
||||
"AllowedMethods": [
|
||||
"POST",
|
||||
"GET",
|
||||
"HEAD",
|
||||
"DELETE",
|
||||
"PUT"
|
||||
],
|
||||
"AllowedOrigins": [
|
||||
"*"
|
||||
],
|
||||
"ExposeHeaders": [
|
||||
"ETag",
|
||||
"x-amz-meta-custom-header"
|
||||
],
|
||||
"AllowedHeaders": ["*"],
|
||||
"AllowedMethods": ["POST", "GET", "HEAD", "DELETE", "PUT"],
|
||||
"AllowedOrigins": ["*"],
|
||||
"ExposeHeaders": ["ETag", "x-amz-meta-custom-header"],
|
||||
"MaxAgeSeconds": 3000
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
<Note>
|
||||
For production environments, consider restricting `AllowedOrigins` to your specific Formbricks domain(s) instead of using `"*"` for better security. For example: `["https://app.yourdomain.com", "https://yourdomain.com"]`.
|
||||
For production environments, consider restricting `AllowedOrigins` to your specific Formbricks domain(s)
|
||||
instead of using `"*"` for better security. For example: `["https://app.yourdomain.com",
|
||||
"https://yourdomain.com"]`.
|
||||
</Note>
|
||||
|
||||
**How to configure CORS:**
|
||||
@@ -338,9 +335,9 @@ Configure CORS on your S3 bucket with the following settings:
|
||||
- **DigitalOcean Spaces**: Navigate to your Space → Settings → CORS Configurations → Add CORS configuration → Paste the JSON
|
||||
- **Other S3-compatible providers**: Refer to your provider's documentation for CORS configuration
|
||||
|
||||
### MinIO Security
|
||||
### RustFS Security
|
||||
|
||||
When using bundled MinIO:
|
||||
When using bundled RustFS:
|
||||
|
||||
- Credentials are auto-generated and secure
|
||||
- Access is restricted through Traefik proxy
|
||||
@@ -359,8 +356,8 @@ When using bundled MinIO:
|
||||
3. Ensure bucket permissions allow uploads from your server
|
||||
4. Check network connectivity to S3 endpoint
|
||||
5. We use S3 presigned URLs for uploads. Make sure your CORS policy allows presigned URL uploads; otherwise, uploads will fail.
|
||||
Some providers (e.g., Hetzner’s object storage) [require a specific CORS configuration](https://github.com/formbricks/formbricks/discussions/6641#discussioncomment-14574048).
|
||||
If you’re using the bundled MinIO setup, this is already configured for you.
|
||||
Some providers (e.g., Hetzner’s object storage) [require a specific CORS configuration](https://github.com/formbricks/formbricks/discussions/6641#discussioncomment-14574048).
|
||||
If you’re using the bundled RustFS setup, this is already configured for you.
|
||||
|
||||
**Images not displaying in surveys:**
|
||||
|
||||
@@ -368,13 +365,13 @@ If you’re using the bundled MinIO setup, this is already configured for you.
|
||||
2. Check CORS configuration allows requests from your domain
|
||||
3. Ensure S3_ENDPOINT_URL is correctly set for third-party services
|
||||
|
||||
**MinIO not starting:**
|
||||
**RustFS not starting:**
|
||||
|
||||
1. **Verify subdomain DNS**: Ensure `files.yourdomain.com` points to your server IP (this is the most common issue)
|
||||
2. **Check DNS propagation**: Use tools like `nslookup` or `dig` to verify DNS resolution
|
||||
3. **Verify ports**: Ensure ports 80 and 443 are open in your firewall
|
||||
4. **SSL certificate**: Check that SSL certificate generation completed successfully
|
||||
5. **Container logs**: Check Docker container logs: `docker compose logs minio`
|
||||
5. **Container logs**: Check Docker container logs: `docker compose logs rustfs`
|
||||
|
||||
### Testing Your Configuration
|
||||
|
||||
@@ -389,8 +386,8 @@ To test if file uploads are working:
|
||||
# Check Formbricks logs
|
||||
docker compose logs formbricks
|
||||
|
||||
# Check MinIO logs (if using bundled MinIO)
|
||||
docker compose logs minio
|
||||
# Check RustFS logs (if using bundled RustFS)
|
||||
docker compose logs rustfs
|
||||
```
|
||||
|
||||
For additional help, join the conversation on [GitHub Discussions](https://github.com/formbricks/formbricks/discussions).
|
||||
|
||||
@@ -87,28 +87,24 @@ graph TD
|
||||
### Component Description
|
||||
|
||||
1. **Formbricks Cluster**
|
||||
|
||||
- Multiple Formbricks instances (1..n) running in parallel
|
||||
- Each instance is stateless and can handle any incoming request
|
||||
- Automatic failover if any instance becomes unavailable
|
||||
|
||||
2. **PostgreSQL Database**
|
||||
|
||||
- Primary database storing all survey, response, and contact data
|
||||
- Optional high-availability setup with primary-replica configuration
|
||||
- Handles all persistent data storage needs
|
||||
|
||||
3. **Redis Cluster**
|
||||
|
||||
- Acts as a distributed cache layer
|
||||
- Improves performance by caching frequently accessed data
|
||||
- Can be configured in HA mode with primary-replica setup
|
||||
- Handles session management and real-time features
|
||||
|
||||
4. **S3 Compatible Storage**
|
||||
|
||||
- Stores file uploads and attachments
|
||||
- Can be any S3-compatible storage service (AWS S3, MinIO, etc.)
|
||||
- Can be any S3-compatible storage service (AWS S3, RustFS, etc.)
|
||||
- Provides reliable and scalable file storage
|
||||
|
||||
5. **Load Balancer**
|
||||
@@ -139,7 +135,7 @@ S3_SECRET_KEY=your-secret-key
|
||||
S3_REGION=your-region
|
||||
S3_BUCKET_NAME=your-bucket-name
|
||||
|
||||
# For S3-compatible storage (e.g., StorJ, MinIO)
|
||||
# For S3-compatible storage (e.g., StorJ, RustFS)
|
||||
# Leave empty for Amazon S3
|
||||
S3_ENDPOINT_URL=https://your-s3-compatible-endpoint
|
||||
|
||||
|
||||
@@ -117,26 +117,37 @@ Please take a look at our [migration guide](/self-hosting/advanced/migration) fo
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Optional: Adding MinIO for File Storage
|
||||
## Optional: Adding RustFS for File Storage
|
||||
|
||||
MinIO provides S3-compatible object storage for file uploads in Formbricks. If you want to enable features like image uploads, file uploads in surveys, or custom logos, you can add MinIO to your Docker setup.
|
||||
RustFS provides S3-compatible object storage for file uploads in Formbricks. If you want to enable features
|
||||
like image uploads, survey file uploads, or custom logos, you can run RustFS alongside Formbricks while
|
||||
keeping the existing `S3_*` environment variables.
|
||||
|
||||
<Note>
|
||||
For detailed information about file storage options and configuration, see our [File Uploads
|
||||
For a broader overview of file storage options and required environment variables, see our [File Uploads
|
||||
Configuration](/self-hosting/configuration/file-uploads) guide.
|
||||
</Note>
|
||||
|
||||
<Warning>
|
||||
**For production deployments with HTTPS**, use the [one-click setup script](/self-hosting/setup/one-click)
|
||||
which automatically configures MinIO with Traefik, SSL certificates, and a subdomain (required for MinIO in
|
||||
production). The setups below are suitable for local development or testing only.
|
||||
which automatically configures RustFS with Traefik, SSL certificates, a dedicated `files.` subdomain, and
|
||||
least-privilege service credentials. The examples below are best suited for development, testing, or custom
|
||||
local setups.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
The bundled RustFS examples on this page are convenience-oriented single-server setups. They work well for
|
||||
development, evaluation, and smaller self-hosted deployments, but they are not the ideal RustFS architecture
|
||||
for high-availability or larger-scale production storage. For stricter production requirements, use external
|
||||
object storage or run a dedicated RustFS deployment separately.
|
||||
</Warning>
|
||||
|
||||
### Quick Start: Using docker-compose.dev.yml
|
||||
|
||||
The fastest way to test MinIO with Formbricks is to use the included `docker-compose.dev.yml` which already has MinIO pre-configured.
|
||||
The fastest way to test file uploads locally is to use the included `docker-compose.dev.yml`, which already
|
||||
starts RustFS and auto-creates the `formbricks` bucket.
|
||||
|
||||
1. **Start MinIO and Services**
|
||||
1. **Start the local stack**
|
||||
|
||||
From the repository root:
|
||||
|
||||
@@ -144,163 +155,155 @@ The fastest way to test MinIO with Formbricks is to use the included `docker-com
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
This starts PostgreSQL, Valkey (Redis), MinIO, and Mailhog.
|
||||
This starts PostgreSQL, Valkey (Redis), Mailhog, RustFS, a permissions helper, and a one-time bucket
|
||||
bootstrap job.
|
||||
|
||||
2. **Access MinIO Console**
|
||||
2. **Access the RustFS console**
|
||||
|
||||
Open http://localhost:9001 in your browser.
|
||||
Open http://localhost:9001 in your browser and sign in with:
|
||||
- Username: `devrustfs`
|
||||
- Password: `devrustfs123`
|
||||
|
||||
Login credentials:
|
||||
3. **Configure Formbricks**
|
||||
|
||||
- Username: `devminio`
|
||||
- Password: `devminio123`
|
||||
|
||||
3. **Create Bucket**
|
||||
|
||||
- Click "Buckets" in the left sidebar
|
||||
- Click "Create Bucket"
|
||||
- Name it: `formbricks`
|
||||
|
||||
4. **Configure Formbricks**
|
||||
|
||||
Update your `.env` file or environment variables with MinIO configuration:
|
||||
Update your `.env` file or environment variables:
|
||||
|
||||
```bash
|
||||
# MinIO S3 Storage
|
||||
S3_ACCESS_KEY="devminio"
|
||||
S3_SECRET_KEY="devminio123"
|
||||
S3_ACCESS_KEY="devrustfs"
|
||||
S3_SECRET_KEY="devrustfs123"
|
||||
S3_REGION="us-east-1"
|
||||
S3_BUCKET_NAME="formbricks"
|
||||
S3_ENDPOINT_URL="http://localhost:9000"
|
||||
S3_FORCE_PATH_STYLE="1"
|
||||
```
|
||||
|
||||
5. **Verify in MinIO Console**
|
||||
4. **Verify uploads**
|
||||
|
||||
After uploading files in Formbricks, view them at http://localhost:9001:
|
||||
|
||||
- Navigate to Buckets → formbricks → Browse
|
||||
- Your uploaded files will appear here
|
||||
After uploading a file in Formbricks, open http://localhost:9001 and navigate to **Buckets → formbricks**
|
||||
to confirm the object was stored successfully.
|
||||
|
||||
<Note>
|
||||
The `docker-compose.dev.yml` file includes MinIO with console access on port 9001, making it easy to
|
||||
visually verify file uploads. This is the recommended approach for development and testing.
|
||||
The development compose file also runs a `rustfs-init` job so you do not need to create the bucket manually.
|
||||
</Note>
|
||||
|
||||
### Manual MinIO Setup (Custom Configuration)
|
||||
### Manual RustFS Setup (Custom Configuration)
|
||||
|
||||
<Note>
|
||||
<strong>Recommended:</strong> If you can, use <code>docker-compose.dev.yml</code> for the fastest path. Use
|
||||
this manual approach only when you need to integrate MinIO into an existing <code>docker-compose.yml</code>{" "}
|
||||
or customize settings.
|
||||
<strong>Recommended:</strong> Prefer <code>docker-compose.dev.yml</code> for local development unless you
|
||||
need to fold RustFS into an existing custom Compose stack.
|
||||
</Note>
|
||||
|
||||
If you prefer to add MinIO to your own `docker-compose.yml`, follow these steps:
|
||||
If you want to add RustFS to your own `docker-compose.yml`, use a pinned RustFS image plus two helper
|
||||
services:
|
||||
|
||||
1. **Add the MinIO service**
|
||||
```yaml
|
||||
services:
|
||||
rustfs-perms:
|
||||
image: busybox:1.36.1
|
||||
user: "0:0"
|
||||
command: ["sh", "-c", "mkdir -p /data && chown -R 10001:10001 /data"]
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
|
||||
Add this service alongside your existing `formbricks` and `postgres` services:
|
||||
rustfs:
|
||||
image: rustfs/rustfs:1.0.0-alpha.93
|
||||
restart: always
|
||||
depends_on:
|
||||
rustfs-perms:
|
||||
condition: service_completed_successfully
|
||||
command: /data
|
||||
environment:
|
||||
RUSTFS_ACCESS_KEY: "formbricks-root"
|
||||
RUSTFS_SECRET_KEY: "change-this-secure-password"
|
||||
RUSTFS_ADDRESS: ":9000"
|
||||
RUSTFS_CONSOLE_ENABLE: "true"
|
||||
RUSTFS_CONSOLE_ADDRESS: ":9001"
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# ... your existing services (formbricks, postgres, redis/valkey, etc.)
|
||||
rustfs-init:
|
||||
image: minio/mc@sha256:95b5f3f7969a5c5a9f3a700ba72d5c84172819e13385aaf916e237cf111ab868
|
||||
depends_on:
|
||||
- rustfs
|
||||
environment:
|
||||
RUSTFS_ADMIN_USER: "formbricks-root"
|
||||
RUSTFS_ADMIN_PASSWORD: "change-this-secure-password"
|
||||
RUSTFS_BUCKET_NAME: "formbricks"
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
until mc alias set rustfs http://rustfs:9000 "$RUSTFS_ADMIN_USER" "$RUSTFS_ADMIN_PASSWORD" >/dev/null 2>&1 \
|
||||
&& mc ls rustfs >/dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
mc mb rustfs/"$RUSTFS_BUCKET_NAME" --ignore-existing
|
||||
```
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
restart: always
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: "formbricks-root"
|
||||
MINIO_ROOT_PASSWORD: "change-this-secure-password"
|
||||
ports:
|
||||
- "9000:9000" # S3 API
|
||||
- "9001:9001" # Web console
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
```
|
||||
Declare the corresponding volume:
|
||||
|
||||
<Note>
|
||||
For production pinning, consider using a digest (e.g., <code>minio/minio@sha256:...</code>) and review
|
||||
periodically with <code>docker inspect minio/minio:latest</code>.
|
||||
</Note>
|
||||
```yaml
|
||||
volumes:
|
||||
rustfs-data:
|
||||
driver: local
|
||||
```
|
||||
|
||||
2. **Declare the MinIO volume**
|
||||
Then configure Formbricks to use the RustFS credentials:
|
||||
|
||||
Add (or extend) your `volumes` block:
|
||||
```bash
|
||||
S3_ACCESS_KEY="formbricks-root"
|
||||
S3_SECRET_KEY="change-this-secure-password"
|
||||
S3_REGION="us-east-1"
|
||||
S3_BUCKET_NAME="formbricks"
|
||||
S3_ENDPOINT_URL="http://rustfs:9000"
|
||||
S3_FORCE_PATH_STYLE="1"
|
||||
```
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
postgres:
|
||||
driver: local
|
||||
redis:
|
||||
driver: local
|
||||
minio-data:
|
||||
driver: local
|
||||
```
|
||||
|
||||
3. **Start services**
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
4. **Open the MinIO Console & Create a Bucket**
|
||||
|
||||
- Visit **http://localhost:9001**
|
||||
- Log in with:
|
||||
- **Username:** `formbricks-root`
|
||||
- **Password:** `change-this-secure-password`
|
||||
- Go to **Buckets → Create Bucket**
|
||||
- Name it: **`formbricks`**
|
||||
|
||||
5. **Configure Formbricks to use MinIO**
|
||||
|
||||
In your `.env` or `formbricks` service environment, set:
|
||||
|
||||
```bash
|
||||
# MinIO S3 Storage
|
||||
S3_ACCESS_KEY="formbricks-root"
|
||||
S3_SECRET_KEY="change-this-secure-password"
|
||||
S3_REGION="us-east-1"
|
||||
S3_BUCKET_NAME="formbricks"
|
||||
S3_ENDPOINT_URL="http://minio:9000"
|
||||
S3_FORCE_PATH_STYLE="1"
|
||||
```
|
||||
|
||||
<Note>
|
||||
These credentials should match <code>MINIO_ROOT_USER</code> and <code>MINIO_ROOT_PASSWORD</code> above.
|
||||
For local/dev this is fine. For production, create a dedicated MinIO user with restricted policies.
|
||||
</Note>
|
||||
|
||||
6. **Verify uploads**
|
||||
|
||||
After uploading a file in Formbricks, check **http://localhost:9001**:
|
||||
|
||||
- **Buckets → formbricks → Browse**
|
||||
You should see your uploaded files.
|
||||
<Note>
|
||||
Using the RustFS admin credentials directly is acceptable for local development and testing. For production,
|
||||
prefer the [one-click setup script](/self-hosting/setup/one-click), which creates a separate least-privilege
|
||||
service account automatically.
|
||||
</Note>
|
||||
|
||||
#### Tips & Common Gotchas
|
||||
|
||||
- **Connection refused**: Ensure the `minio` container is running and port **9000** is reachable from the Formbricks container (use the internal URL `http://minio:9000`).
|
||||
- **Bucket not found**: Create the `formbricks` bucket in the console before uploading.
|
||||
- **Auth failed**: Confirm `S3_ACCESS_KEY`/`S3_SECRET_KEY` match MinIO credentials.
|
||||
- **Permission denied on `/data`**: Ensure the mounted directory or volume is owned by UID `10001`. The
|
||||
`rustfs-perms` helper handles this for Compose-managed volumes.
|
||||
- **Connection refused**: Ensure the `rustfs` container is running and port `9000` is reachable from the
|
||||
Formbricks container.
|
||||
- **Bucket not found**: Confirm that `rustfs-init` completed successfully or create the bucket manually with
|
||||
`mc`.
|
||||
- **Auth failed**: Confirm that `S3_ACCESS_KEY` and `S3_SECRET_KEY` match the RustFS credentials configured on
|
||||
the server.
|
||||
- **Health check**: From the Formbricks container:
|
||||
|
||||
```bash
|
||||
docker compose exec formbricks sh -c 'wget -O- http://minio:9000/minio/health/ready'
|
||||
docker compose exec formbricks sh -c 'wget -O- http://rustfs:9000/health'
|
||||
```
|
||||
|
||||
### Production Setup with Traefik
|
||||
|
||||
For production deployments, use the [one-click setup script](/self-hosting/setup/one-click) which automatically configures:
|
||||
For production deployments, use the [one-click setup script](/self-hosting/setup/one-click), which
|
||||
automatically configures:
|
||||
|
||||
- MinIO service with Traefik reverse proxy
|
||||
- Dedicated subdomain (e.g., `files.yourdomain.com`) - **required for production**
|
||||
- RustFS behind Traefik on a dedicated `files.yourdomain.com` subdomain
|
||||
- Automatic SSL certificate generation via Let's Encrypt
|
||||
- CORS configuration for your domain
|
||||
- CORS configuration scoped to your Formbricks domain
|
||||
- Rate limiting middleware
|
||||
- Secure credential generation
|
||||
- Separate RustFS admin and Formbricks service credentials
|
||||
- A `rustfs-init` job that creates the bucket and access policy
|
||||
|
||||
The production setup from [formbricks.sh](https://github.com/formbricks/formbricks/blob/main/docker/formbricks.sh) includes advanced features not covered in this manual setup. For production use, we strongly recommend using the one-click installer.
|
||||
The production setup from [formbricks.sh](https://github.com/formbricks/formbricks/blob/main/docker/formbricks.sh)
|
||||
adds the reverse proxy wiring and bootstrap automation needed for long-lived deployments.
|
||||
|
||||
<Note>
|
||||
Even in the one-click flow, bundled RustFS remains a convenience-oriented single-server deployment. For
|
||||
higher availability, stricter operational requirements, or larger storage footprints, prefer external object
|
||||
storage or a dedicated RustFS deployment managed separately from Formbricks.
|
||||
</Note>
|
||||
|
||||
## Debug
|
||||
|
||||
|
||||
@@ -14,6 +14,14 @@ If you’re looking to quickly set up a production instance of Formbricks on an
|
||||
automatic SSL management via Let’s Encrypt.
|
||||
</Note>
|
||||
|
||||
<Warning>
|
||||
The bundled RustFS option in this one-click setup is designed for convenience on a single server. It is a
|
||||
good fit for small-scale or low-complexity self-hosted deployments, but it is not the ideal RustFS
|
||||
architecture for high-availability or larger production environments. For stricter production requirements,
|
||||
use external object storage or run a dedicated RustFS deployment separately from the Formbricks one-click
|
||||
stack.
|
||||
</Warning>
|
||||
|
||||
For other operating systems or a more customized installation, please refer to the advanced installation guide with [Docker](/self-hosting/setup/docker).
|
||||
|
||||
### Requirements
|
||||
@@ -312,17 +320,22 @@ To restart Formbricks, simply run the following command:
|
||||
|
||||
The script will automatically restart all the Formbricks related containers and brings the entire stack up with the previous configuration.
|
||||
|
||||
## Cleanup MinIO init (optional)
|
||||
## Cleanup RustFS init (optional)
|
||||
|
||||
During the one-click setup, a temporary `minio-init` service configures MinIO (bucket, policy, service user). It is idempotent and safe to leave in place; it will do nothing on subsequent starts once configuration exists.
|
||||
During the one-click setup, a temporary `rustfs-init` service configures RustFS (bucket, policy, service
|
||||
user). It is idempotent and safe to leave in place; it will do nothing on subsequent starts once the
|
||||
configuration exists.
|
||||
|
||||
If you prefer to remove the `minio-init` service and its references after a successful setup, run:
|
||||
If you prefer to remove the `rustfs-init` service and its references after a successful setup, run:
|
||||
|
||||
```
|
||||
./formbricks.sh cleanup-minio-init
|
||||
./formbricks.sh cleanup-rustfs-init
|
||||
```
|
||||
|
||||
This only removes the init job and its Compose references; it does not delete any data or affect your MinIO configuration.
|
||||
`./formbricks.sh cleanup-minio-init` is still available as a backward-compatible alias.
|
||||
|
||||
This only removes the init job and its Compose references; it does not delete any data or affect your RustFS
|
||||
configuration.
|
||||
|
||||
## Uninstall
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// basic regex -- [whitespace](number)(rem)[whitespace or ;]
|
||||
const REM_REGEX = /\b(\d+(\.\d+)?)(rem)\b/gi;
|
||||
// Matches a CSS numeric value followed by "rem" — e.g. "1rem", "1.5rem", "16rem".
|
||||
// Single character-class + single quantifier: no nested quantifiers, no backtracking risk.
|
||||
const REM_REGEX = /([\d.]+)(rem)/gi; // NOSONAR -- single character-class quantifier on trusted CSS input; no backtracking risk
|
||||
const PROCESSED = Symbol("processed");
|
||||
|
||||
const remtoEm = (opts = {}) => {
|
||||
@@ -26,6 +27,36 @@ const remtoEm = (opts = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
plugins: [require("@tailwindcss/postcss"), require("autoprefixer"), remtoEm()],
|
||||
// Strips the `@layer properties { ... }` block that Tailwind v4 emits as a
|
||||
// browser-compatibility fallback for `@property` declarations.
|
||||
//
|
||||
// Problem: CSS `@layer` at-rules are globally scoped by spec — they cannot be
|
||||
// confined by a surrounding selector. Even though all other Formbricks survey
|
||||
// styles are correctly scoped to `#fbjs`, the `@layer properties` block
|
||||
// contains a bare `*, :before, :after, ::backdrop` selector that resets all
|
||||
// `--tw-*` CSS custom properties on every element of the host page. This
|
||||
// breaks shadows, rings, transforms, and other Tailwind utilities on any site
|
||||
// that uses Tailwind v4 alongside the Formbricks SDK.
|
||||
//
|
||||
// The `@property` declarations already present in the same stylesheet cover
|
||||
// the same browser-compatibility need for all supporting browsers, so removing
|
||||
// `@layer properties` does not affect survey rendering.
|
||||
//
|
||||
// See: https://github.com/formbricks/js/issues/46
|
||||
const stripLayerProperties = () => {
|
||||
return {
|
||||
postcssPlugin: "postcss-strip-layer-properties",
|
||||
AtRule: {
|
||||
layer: (atRule) => {
|
||||
if (atRule.params === "properties") {
|
||||
atRule.remove();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
stripLayerProperties.postcss = true;
|
||||
|
||||
module.exports = {
|
||||
plugins: [require("@tailwindcss/postcss"), require("autoprefixer"), remtoEm(), stripLayerProperties()],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user