From 850c486a6eff1e58b01bde1bb3cac3bbf42c8fd7 Mon Sep 17 00:00:00 2001 From: David Londono Date: Thu, 28 Aug 2025 16:10:33 -0500 Subject: [PATCH 01/41] add appflowy --- public/svgs/appflowy.svg | 12 ++ templates/compose/appflowy.yaml | 217 ++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 public/svgs/appflowy.svg create mode 100644 templates/compose/appflowy.yaml diff --git a/public/svgs/appflowy.svg b/public/svgs/appflowy.svg new file mode 100644 index 000000000..7853ed36e --- /dev/null +++ b/public/svgs/appflowy.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/templates/compose/appflowy.yaml b/templates/compose/appflowy.yaml new file mode 100644 index 000000000..805134b71 --- /dev/null +++ b/templates/compose/appflowy.yaml @@ -0,0 +1,217 @@ +# documentation: https://docs.appflowy.io/docs/documentation/appflowy-cloud +# slogan: AppFlowy is the AI collaborative workspace where you achieve more without losing control of your data. +# tags: appflowy,notion,open-source,productivity,notes,ai,self-hosted +# logo: svgs/appflowy.svg +# category: productivity +# port: 80 + +services: + appflowy_web: + environment: + - SERVICE_FQDN_APPFLOWYWEB_80 + - APPFLOWY_BASE_URL=${SERVICE_FQDN_APPFLOWYCLOUD_8000} + - APPFLOWY_GOTRUE_BASE_URL=${SERVICE_FQDN_GOTRUE_9999} + - APPFLOWY_WS_BASE_URL=${SERVICE_FQDN_APPFLOWYCLOUD_8000}/ws/v2 + image: appflowyinc/appflowy_web:latest + depends_on: + - appflowy_cloud + + appflowy_cloud: + image: appflowyinc/appflowy_cloud:latest + labels: + - "traefik.http.middlewares.appflowy-cors.headers.customRequestHeaders.X-Request-Id={{.RequestID}}" + - "traefik.http.middlewares.appflowy-cors.headers.accessControlAllowMethods=GET,POST,PUT,DELETE,PATCH,OPTIONS" + - "traefik.http.middlewares.appflowy-cors.headers.accessControlAllowHeaders=Content-Type,Authorization,Accept,Client-Version,Device-Id" + - "traefik.http.middlewares.appflowy-cors.headers.accessControlMaxAge=3600" + - "traefik.http.middlewares.appflowy-cors.headers.accessControlAllowOriginList=*" + - "traefik.http.middlewares.appflowy-cors.headers.addvaryheader=true" + environment: + - SERVICE_FQDN_APPFLOWYCLOUD_8000 + - RUST_LOG=${RUST_LOG:-info} + - APPFLOWY_ENVIRONMENT=production + - APPFLOWY_DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-appflowy} + - APPFLOWY_REDIS_URI=redis://redis:6379 + - APPFLOWY_GOTRUE_JWT_SECRET=${SERVICE_PASSWORD_JWT} + - APPFLOWY_GOTRUE_JWT_EXP=${GOTRUE_JWT_EXP:-7200} + - APPFLOWY_GOTRUE_BASE_URL=http://gotrue:9999 + - APPFLOWY_S3_CREATE_BUCKET=${APPFLOWY_S3_CREATE_BUCKET:-true} + - APPFLOWY_S3_USE_MINIO=true + - APPFLOWY_S3_MINIO_URL=http://minio:9000 + - APPFLOWY_S3_ACCESS_KEY=${SERVICE_USER_MINIO} + - APPFLOWY_S3_SECRET_KEY=${SERVICE_PASSWORD_MINIO} + - APPFLOWY_S3_BUCKET=${APPFLOWY_S3_BUCKET:-appflowy} + - APPFLOWY_S3_REGION=${APPFLOWY_S3_REGION:-us-east-1} + - APPFLOWY_MAILER_SMTP_HOST=${APPFLOWY_MAILER_SMTP_HOST:-smtp.gmail.com} + - APPFLOWY_MAILER_SMTP_PORT=${APPFLOWY_MAILER_SMTP_PORT:-587} + - APPFLOWY_MAILER_SMTP_USERNAME=${APPFLOWY_MAILER_SMTP_USERNAME:-notify@appflowy.io} + - APPFLOWY_MAILER_SMTP_EMAIL=${APPFLOWY_MAILER_SMTP_EMAIL:-notify@appflowy.io} + - APPFLOWY_MAILER_SMTP_PASSWORD=${APPFLOWY_MAILER_SMTP_PASSWORD:-email_sender_password} + - APPFLOWY_MAILER_SMTP_TLS_KIND=${APPFLOWY_MAILER_SMTP_TLS_KIND:-none} + - APPFLOWY_ACCESS_CONTROL=true + - APPFLOWY_DATABASE_MAX_CONNECTIONS=40 + - AI_SERVER_HOST=ai + - AI_SERVER_PORT=5001 + - AI_OPENAI_API_KEY=${AI_OPENAI_API_KEY:-} + - APPFLOWY_WEB_URL=${SERVICE_FQDN_APPFLOWYWEB_80} + depends_on: + gotrue: + condition: service_healthy + postgres: + condition: service_healthy + redis: + condition: service_started + + gotrue: + image: appflowyinc/gotrue:latest + environment: + - SERVICE_FQDN_GOTRUE_9999 + - GOTRUE_API_HOST=0.0.0.0 + - GOTRUE_API_PORT=9999 + - PORT=9999 + - GOTRUE_ADMIN_EMAIL=${GOTRUE_ADMIN_EMAIL:-admin@example.com} + - GOTRUE_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN} + - GOTRUE_DISABLE_SIGNUP=false + - GOTRUE_SITE_URL=appflowy-flutter:// + - GOTRUE_URI_ALLOW_LIST=** + - GOTRUE_JWT_SECRET=${SERVICE_PASSWORD_JWT} + - GOTRUE_JWT_EXP=${GOTRUE_JWT_EXP:-7200} + - GOTRUE_JWT_ADMIN_GROUP_NAME=supabase_admin + - GOTRUE_DB_DRIVER=postgres + - API_EXTERNAL_URL=${SERVICE_FQDN_GOTRUE_9999} + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-appflowy}?search_path=auth + - GOTRUE_SMTP_HOST=${GOTRUE_SMTP_HOST} + - GOTRUE_SMTP_PORT=${GOTRUE_SMTP_PORT:-587} + - GOTRUE_SMTP_USER=${GOTRUE_SMTP_USER} + - GOTRUE_SMTP_PASS=${GOTRUE_SMTP_PASS} + - GOTRUE_MAILER_URLPATHS_CONFIRMATION=/verify + - GOTRUE_MAILER_URLPATHS_INVITE=/verify + - GOTRUE_MAILER_URLPATHS_RECOVERY=/verify + - GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=/verify + - GOTRUE_SMTP_ADMIN_EMAIL=${GOTRUE_SMTP_ADMIN_EMAIL} + - GOTRUE_SMTP_MAX_FREQUENCY=1ns + - GOTRUE_RATE_LIMIT_EMAIL_SENT=100 + - GOTRUE_MAILER_AUTOCONFIRM=true + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: "curl --fail http://127.0.0.1:9999/health || exit 1" + interval: 5s + timeout: 5s + retries: 12 + + admin_frontend: + image: appflowyinc/admin_frontend:latest + environment: + - SERVICE_FQDN_ADMINFRONTEND_3000 + - RUST_LOG=info + - ADMIN_FRONTEND_REDIS_URL=redis://redis:6379 + - ADMIN_FRONTEND_GOTRUE_URL=http://gotrue:9999 + - ADMIN_FRONTEND_APPFLOWY_CLOUD_URL=http://appflowy_cloud:8000 + - ADMIN_FRONTEND_PATH_PREFIX=/console + - ADMIN_FRONTEND_HOST=0.0.0.0 + - ADMIN_FRONTEND_PORT=3000 + - ADMIN_FRONTEND_OAUTH_CLIENT_ID=appflowy_cloud + - ADMIN_FRONTEND_OAUTH_CLIENT_SECRET=${SERVICE_PASSWORD_ADMINOAUTH} + - ADMIN_FRONTEND_OAUTH_ALLOWABLE_REDIRECT_URIS=${SERVICE_FQDN_ADMINFRONTEND_3000} + depends_on: + gotrue: + condition: service_healthy + appflowy_cloud: + condition: service_started + + ai: + image: appflowyinc/appflowy_ai:latest + environment: + - OPENAI_API_KEY=${AI_OPENAI_API_KEY:-} + - AI_SERVER_PORT=5001 + - DEFAULT_AI_MODEL=gpt-4.1-mini # Make sure the model is available in your OpenAI account + - DEFAULT_AI_COMPLETION_MODEL=gpt-4.1-mini # Make sure the model is available in your OpenAI account + - AI_APPFLOWY_HOST=${SERVICE_FQDN_APPFLOWYWEB_80} + - APPFLOWY_GOTRUE_JWT_SECRET=${SERVICE_PASSWORD_JWT} + + - AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY} + - AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT} + - AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION} + - APPFLOWY_S3_ACCESS_KEY=${SERVICE_USER_MINIO} + - APPFLOWY_S3_SECRET_KEY=${SERVICE_PASSWORD_MINIO} + - APPFLOWY_S3_BUCKET=${APPFLOWY_S3_BUCKET:-appflowy} + - APPFLOWY_S3_REGION=${APPFLOWY_S3_REGION:-us-east-1} + - AI_DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-appflowy} + - AI_REDIS_URL=redis://redis:6379 + - AI_USE_MINIO=true + - AI_MINIO_URL=http://minio:9000 + depends_on: + postgres: + condition: service_healthy + + appflowy_worker: + image: appflowyinc/appflowy_worker:latest + environment: + - RUST_LOG=info + - APPFLOWY_ENVIRONMENT=production + - APPFLOWY_WORKER_ENVIRONMENT=production + - APPFLOWY_WORKER_REDIS_URL=redis://redis:6379 + - APPFLOWY_WORKER_DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-appflowy} + - APPFLOWY_WORKER_DATABASE_NAME=${POSTGRES_DB:-appflowy} + - APPFLOWY_WORKER_IMPORT_TICK_INTERVAL=30 + - APPFLOWY_S3_USE_MINIO=true + - APPFLOWY_S3_MINIO_URL=http://minio:9000 + - APPFLOWY_S3_ACCESS_KEY=${SERVICE_USER_MINIO} + - APPFLOWY_S3_SECRET_KEY=${SERVICE_PASSWORD_MINIO} + - APPFLOWY_S3_BUCKET=${APPFLOWY_S3_BUCKET:-appflowy} + - APPFLOWY_S3_REGION=${APPFLOWY_S3_REGION:-us-east-1} + - APPFLOWY_MAILER_SMTP_HOST=${APPFLOWY_MAILER_SMTP_HOST:-smtp.gmail.com} + - APPFLOWY_MAILER_SMTP_PORT=${APPFLOWY_MAILER_SMTP_PORT:-587} + - APPFLOWY_MAILER_SMTP_USERNAME=${APPFLOWY_MAILER_SMTP_USERNAME:-notify@appflowy.io} + - APPFLOWY_MAILER_SMTP_EMAIL=${APPFLOWY_MAILER_SMTP_EMAIL:-notify@appflowy.io} + - APPFLOWY_MAILER_SMTP_PASSWORD=${APPFLOWY_MAILER_SMTP_PASSWORD:-email_sender_password} + - APPFLOWY_MAILER_SMTP_TLS_KIND=${APPFLOWY_MAILER_SMTP_TLS_KIND:-none} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + + minio: + image: minio/minio + environment: + - MINIO_ROOT_USER=${SERVICE_USER_MINIO} + - MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO} + command: server /data --console-address ":9001" + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 20s + retries: 10 + volumes: + # Coolify will manage this named volume for persistent object storage. + - minio_data:/data + + postgres: + image: pgvector/pgvector:pg16 + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-appflowy} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_HOST=postgres + volumes: + # Coolify will manage this named volume for persistent database storage. + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: [ "CMD", "pg_isready", "-U", "${SERVICE_USER_POSTGRES}", "-d", "${POSTGRES_DB:-appflowy}" ] + interval: 5s + timeout: 5s + retries: 12 + + redis: + image: redis + volumes: + - "redis_data:/data" + healthcheck: + test: + - CMD-SHELL + - "redis-cli -h localhost -p 6379 ping" + interval: 5s + timeout: 5s + retries: 3 From f7a311149ccf1f02c3b888386c9a3ab225317e6f Mon Sep 17 00:00:00 2001 From: David Londono Date: Wed, 3 Sep 2025 09:08:10 -0500 Subject: [PATCH 02/41] reverse proxy for mobile --- templates/compose/appflowy.yaml | 153 ++++++++++++++++++++++++++++---- 1 file changed, 136 insertions(+), 17 deletions(-) diff --git a/templates/compose/appflowy.yaml b/templates/compose/appflowy.yaml index 805134b71..6f9dfc579 100644 --- a/templates/compose/appflowy.yaml +++ b/templates/compose/appflowy.yaml @@ -6,12 +6,137 @@ # port: 80 services: + nginx: + image: nginx:latest + environment: + - SERVICE_URL_BACKEND + volumes: + - type: bind + source: ./nginx/nginx.conf + target: /etc/nginx/nginx.conf + read_only: true + content: | + # Please do not directly edit this file. + + error_log /var/log/nginx/error.log notice; + pid /var/run/nginx.pid; + + + events { + worker_connections 1024; + } + + + http { + resolver 127.0.0.11 valid=10s; + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + server { + listen 80; + client_max_body_size 10M; + underscores_in_headers on; + + # Backend services + set $appflowy_cloud_backend "http://appflowy_cloud:8000"; + set $gotrue_backend "http://gotrue:9999"; + # GoTrue Authentication Service + location /gotrue/ { + proxy_pass $gotrue_backend; + rewrite ^/gotrue(/.*)$ $1 break; + # Allow headers like redirect_to to be handed over to the gotrue + # for correct redirecting + proxy_set_header Host $http_host; + proxy_pass_request_headers on; + } + + # WebSocket endpoint - handle both /ws and /ws/v2 + location ~ ^/ws(/.*)?$ { + proxy_pass $appflowy_cloud_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400s; + } + + # API endpoints + location /api { + proxy_pass $appflowy_cloud_backend; + + # Headers + proxy_set_header X-Request-Id $request_id; + proxy_set_header Host $http_host; + + # Special handling for publish endpoint + location ~* ^/api/workspace/([a-zA-Z0-9_-]+)/publish$ { + proxy_pass $appflowy_cloud_backend; + proxy_request_buffering off; + client_max_body_size 256M; + } + + # Chat endpoint (streaming) + location /api/chat { + proxy_pass $appflowy_cloud_backend; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + chunked_transfer_encoding on; + proxy_buffering off; + proxy_cache off; + + proxy_read_timeout 600s; + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + } + + # Import endpoint + location /api/import { + proxy_pass $appflowy_cloud_backend; + + proxy_set_header X-Request-Id $request_id; + proxy_set_header Host $http_host; + + proxy_read_timeout 600s; + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + + proxy_request_buffering off; + proxy_buffering off; + proxy_cache off; + client_max_body_size 2G; + } + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } + } + depends_on: + - appflowy_cloud + - gotrue + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + appflowy_web: environment: - - SERVICE_FQDN_APPFLOWYWEB_80 - - APPFLOWY_BASE_URL=${SERVICE_FQDN_APPFLOWYCLOUD_8000} - - APPFLOWY_GOTRUE_BASE_URL=${SERVICE_FQDN_GOTRUE_9999} - - APPFLOWY_WS_BASE_URL=${SERVICE_FQDN_APPFLOWYCLOUD_8000}/ws/v2 + - SERVICE_URL_APPFLOWYWEB + - APPFLOWY_BASE_URL=${SERVICE_URL_CLOUD} + - APPFLOWY_GOTRUE_BASE_URL=${SERVICE_URL_GOTRUE} + - APPFLOWY_WS_BASE_URL=${SERVICE_URL_CLOUD}/ws/v2 image: appflowyinc/appflowy_web:latest depends_on: - appflowy_cloud @@ -26,8 +151,9 @@ services: - "traefik.http.middlewares.appflowy-cors.headers.accessControlAllowOriginList=*" - "traefik.http.middlewares.appflowy-cors.headers.addvaryheader=true" environment: - - SERVICE_FQDN_APPFLOWYCLOUD_8000 + - SERVICE_URL_CLOUD_8000 - RUST_LOG=${RUST_LOG:-info} + - APPFLOWY_BASE_URL=${SERVICE_URL_CLOUD} - APPFLOWY_ENVIRONMENT=production - APPFLOWY_DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-appflowy} - APPFLOWY_REDIS_URI=redis://redis:6379 @@ -52,7 +178,7 @@ services: - AI_SERVER_HOST=ai - AI_SERVER_PORT=5001 - AI_OPENAI_API_KEY=${AI_OPENAI_API_KEY:-} - - APPFLOWY_WEB_URL=${SERVICE_FQDN_APPFLOWYWEB_80} + - APPFLOWY_WEB_URL=${SERVICE_URL_APPFLOWYWEB} depends_on: gotrue: condition: service_healthy @@ -64,7 +190,7 @@ services: gotrue: image: appflowyinc/gotrue:latest environment: - - SERVICE_FQDN_GOTRUE_9999 + - SERVICE_URL_GOTRUE_9999 - GOTRUE_API_HOST=0.0.0.0 - GOTRUE_API_PORT=9999 - PORT=9999 @@ -75,9 +201,8 @@ services: - GOTRUE_URI_ALLOW_LIST=** - GOTRUE_JWT_SECRET=${SERVICE_PASSWORD_JWT} - GOTRUE_JWT_EXP=${GOTRUE_JWT_EXP:-7200} - - GOTRUE_JWT_ADMIN_GROUP_NAME=supabase_admin - GOTRUE_DB_DRIVER=postgres - - API_EXTERNAL_URL=${SERVICE_FQDN_GOTRUE_9999} + - API_EXTERNAL_URL=${SERVICE_URL_GOTRUE} - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-appflowy}?search_path=auth - GOTRUE_SMTP_HOST=${GOTRUE_SMTP_HOST} - GOTRUE_SMTP_PORT=${GOTRUE_SMTP_PORT:-587} @@ -103,17 +228,11 @@ services: admin_frontend: image: appflowyinc/admin_frontend:latest environment: - - SERVICE_FQDN_ADMINFRONTEND_3000 + - SERVICE_URL_ADMINFRONTEND_3000 - RUST_LOG=info - ADMIN_FRONTEND_REDIS_URL=redis://redis:6379 - ADMIN_FRONTEND_GOTRUE_URL=http://gotrue:9999 - ADMIN_FRONTEND_APPFLOWY_CLOUD_URL=http://appflowy_cloud:8000 - - ADMIN_FRONTEND_PATH_PREFIX=/console - - ADMIN_FRONTEND_HOST=0.0.0.0 - - ADMIN_FRONTEND_PORT=3000 - - ADMIN_FRONTEND_OAUTH_CLIENT_ID=appflowy_cloud - - ADMIN_FRONTEND_OAUTH_CLIENT_SECRET=${SERVICE_PASSWORD_ADMINOAUTH} - - ADMIN_FRONTEND_OAUTH_ALLOWABLE_REDIRECT_URIS=${SERVICE_FQDN_ADMINFRONTEND_3000} depends_on: gotrue: condition: service_healthy @@ -127,7 +246,7 @@ services: - AI_SERVER_PORT=5001 - DEFAULT_AI_MODEL=gpt-4.1-mini # Make sure the model is available in your OpenAI account - DEFAULT_AI_COMPLETION_MODEL=gpt-4.1-mini # Make sure the model is available in your OpenAI account - - AI_APPFLOWY_HOST=${SERVICE_FQDN_APPFLOWYWEB_80} + - AI_APPFLOWY_HOST=${SERVICE_URL_APPFLOWYWEB} - APPFLOWY_GOTRUE_JWT_SECRET=${SERVICE_PASSWORD_JWT} - AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY} From cf4270140cfe859c406479302c453a9afe02a4a2 Mon Sep 17 00:00:00 2001 From: David Londono Date: Thu, 16 Oct 2025 11:26:29 -0500 Subject: [PATCH 03/41] fix: update cors and versions --- templates/compose/appflowy.yaml | 95 ++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 36 deletions(-) diff --git a/templates/compose/appflowy.yaml b/templates/compose/appflowy.yaml index 6f9dfc579..93ac8fe09 100644 --- a/templates/compose/appflowy.yaml +++ b/templates/compose/appflowy.yaml @@ -7,9 +7,9 @@ services: nginx: - image: nginx:latest + image: nginx:1.29.2 environment: - - SERVICE_URL_BACKEND + - SERVICE_URL_ADMIN volumes: - type: bind source: ./nginx/nginx.conf @@ -42,10 +42,13 @@ services: # Backend services set $appflowy_cloud_backend "http://appflowy_cloud:8000"; set $gotrue_backend "http://gotrue:9999"; + set $admin_frontend_backend "http://admin_frontend:3000"; # GoTrue Authentication Service location /gotrue/ { proxy_pass $gotrue_backend; + rewrite ^/gotrue(/.*)$ $1 break; + # Allow headers like redirect_to to be handed over to the gotrue # for correct redirecting proxy_set_header Host $http_host; @@ -53,8 +56,9 @@ services: } # WebSocket endpoint - handle both /ws and /ws/v2 - location ~ ^/ws(/.*)?$ { + location /ws { proxy_pass $appflowy_cloud_backend; + proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; @@ -68,8 +72,6 @@ services: # API endpoints location /api { proxy_pass $appflowy_cloud_backend; - - # Headers proxy_set_header X-Request-Id $request_id; proxy_set_header Host $http_host; @@ -113,6 +115,33 @@ services: } } + # Admin Frontend + # Optional Module, comment this section if you did not deploy admin_frontend in docker-compose.yml + location /console { + proxy_pass $admin_frontend_backend; + + proxy_set_header X-Scheme $scheme; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + + proxy_buffering off; + proxy_cache off; + + proxy_read_timeout 60s; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + } + + # Redirect root path to /console + location = / { + return 301 /console; + } + # Health check endpoint location /health { access_log off; @@ -124,6 +153,7 @@ services: depends_on: - appflowy_cloud - gotrue + - admin_frontend healthcheck: test: ["CMD", "curl", "-f", "http://localhost/health"] interval: 30s @@ -134,31 +164,22 @@ services: appflowy_web: environment: - SERVICE_URL_APPFLOWYWEB - - APPFLOWY_BASE_URL=${SERVICE_URL_CLOUD} - - APPFLOWY_GOTRUE_BASE_URL=${SERVICE_URL_GOTRUE} - - APPFLOWY_WS_BASE_URL=${SERVICE_URL_CLOUD}/ws/v2 - image: appflowyinc/appflowy_web:latest + - APPFLOWY_BASE_URL=${SERVICE_URL_ADMIN} + - APPFLOWY_GOTRUE_BASE_URL=${SERVICE_URL_ADMIN}/gotrue + - APPFLOWY_WS_BASE_URL=${SERVICE_URL_ADMIN}/ws/v2 + image: appflowyinc/appflowy_web:0.9.132 depends_on: - appflowy_cloud appflowy_cloud: - image: appflowyinc/appflowy_cloud:latest - labels: - - "traefik.http.middlewares.appflowy-cors.headers.customRequestHeaders.X-Request-Id={{.RequestID}}" - - "traefik.http.middlewares.appflowy-cors.headers.accessControlAllowMethods=GET,POST,PUT,DELETE,PATCH,OPTIONS" - - "traefik.http.middlewares.appflowy-cors.headers.accessControlAllowHeaders=Content-Type,Authorization,Accept,Client-Version,Device-Id" - - "traefik.http.middlewares.appflowy-cors.headers.accessControlMaxAge=3600" - - "traefik.http.middlewares.appflowy-cors.headers.accessControlAllowOriginList=*" - - "traefik.http.middlewares.appflowy-cors.headers.addvaryheader=true" + image: appflowyinc/appflowy_cloud:0.9.149 environment: - - SERVICE_URL_CLOUD_8000 - RUST_LOG=${RUST_LOG:-info} - - APPFLOWY_BASE_URL=${SERVICE_URL_CLOUD} + - APPFLOWY_BASE_URL=${SERVICE_URL_ADMIN} - APPFLOWY_ENVIRONMENT=production - APPFLOWY_DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-appflowy} - APPFLOWY_REDIS_URI=redis://redis:6379 - APPFLOWY_GOTRUE_JWT_SECRET=${SERVICE_PASSWORD_JWT} - - APPFLOWY_GOTRUE_JWT_EXP=${GOTRUE_JWT_EXP:-7200} - APPFLOWY_GOTRUE_BASE_URL=http://gotrue:9999 - APPFLOWY_S3_CREATE_BUCKET=${APPFLOWY_S3_CREATE_BUCKET:-true} - APPFLOWY_S3_USE_MINIO=true @@ -179,6 +200,12 @@ services: - AI_SERVER_PORT=5001 - AI_OPENAI_API_KEY=${AI_OPENAI_API_KEY:-} - APPFLOWY_WEB_URL=${SERVICE_URL_APPFLOWYWEB} + healthcheck: + test: "curl --fail http://127.0.0.1:8000/api/health || exit 1" + interval: 5s + timeout: 5s + retries: 12 + depends_on: gotrue: condition: service_healthy @@ -188,9 +215,8 @@ services: condition: service_started gotrue: - image: appflowyinc/gotrue:latest + image: appflowyinc/gotrue:0.9.149 environment: - - SERVICE_URL_GOTRUE_9999 - GOTRUE_API_HOST=0.0.0.0 - GOTRUE_API_PORT=9999 - PORT=9999 @@ -202,7 +228,7 @@ services: - GOTRUE_JWT_SECRET=${SERVICE_PASSWORD_JWT} - GOTRUE_JWT_EXP=${GOTRUE_JWT_EXP:-7200} - GOTRUE_DB_DRIVER=postgres - - API_EXTERNAL_URL=${SERVICE_URL_GOTRUE} + - API_EXTERNAL_URL=${SERVICE_URL_ADMIN}/gotrue - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-appflowy}?search_path=auth - GOTRUE_SMTP_HOST=${GOTRUE_SMTP_HOST} - GOTRUE_SMTP_PORT=${GOTRUE_SMTP_PORT:-587} @@ -226,13 +252,10 @@ services: retries: 12 admin_frontend: - image: appflowyinc/admin_frontend:latest + image: appflowyinc/admin_frontend:0.9.149 environment: - - SERVICE_URL_ADMINFRONTEND_3000 - - RUST_LOG=info - - ADMIN_FRONTEND_REDIS_URL=redis://redis:6379 - - ADMIN_FRONTEND_GOTRUE_URL=http://gotrue:9999 - - ADMIN_FRONTEND_APPFLOWY_CLOUD_URL=http://appflowy_cloud:8000 + - APPFLOWY_GOTRUE_BASE_URL=${SERVICE_URL_ADMIN}/gotrue + - APPFLOWY_BASE_URL=${SERVICE_URL_ADMIN} depends_on: gotrue: condition: service_healthy @@ -240,7 +263,7 @@ services: condition: service_started ai: - image: appflowyinc/appflowy_ai:latest + image: appflowyinc/appflowy_ai:0.9.149 environment: - OPENAI_API_KEY=${AI_OPENAI_API_KEY:-} - AI_SERVER_PORT=5001 @@ -265,7 +288,7 @@ services: condition: service_healthy appflowy_worker: - image: appflowyinc/appflowy_worker:latest + image: appflowyinc/appflowy_worker:0.9.149 environment: - RUST_LOG=info - APPFLOWY_ENVIRONMENT=production @@ -293,16 +316,16 @@ services: condition: service_started minio: - image: minio/minio + image: minio/minio:RELEASE.2025-09-07T16-13-09Z environment: - MINIO_ROOT_USER=${SERVICE_USER_MINIO} - MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO} command: server /data --console-address ":9001" healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 5s + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s timeout: 20s - retries: 10 + retries: 3 volumes: # Coolify will manage this named volume for persistent object storage. - minio_data:/data @@ -324,7 +347,7 @@ services: retries: 12 redis: - image: redis + image: redis:8.2.2-alpine3.22 volumes: - "redis_data:/data" healthcheck: From ad148eca60bdc3d454f0178bae66ff227d3fb01a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:04:19 +0100 Subject: [PATCH 04/41] Add automatic shared dependencies for worktrees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setup Conductor to automatically share node_modules and vendor directories across all git worktrees to save disk space and speed up development. Changes: - Updated conductor-setup.sh to create symlinks to shared dependencies - Added documentation to CLAUDE.md explaining the system - Dependencies now stored in .shared-deps/ in main repository - All worktrees use the same dependency versions automatically Benefits: - Saves hundreds of MBs to GBs of disk space - No need to run npm install/composer install for each worktree - Consistent dependency versions across all worktrees Note: Add .shared-deps/ to .gitignore in the main repository 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 38 ++++++++++++++++++++++++++++++++++++++ scripts/conductor-setup.sh | 30 +++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index b7c496e42..e8c040402 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,44 @@ ## Project Overview Coolify is an open-source, self-hostable platform for deploying applications and managing servers - an alternative to Heroku/Netlify/Vercel. It's built with Laravel (PHP) and uses Docker for containerization. +## Git Worktree Shared Dependencies + +This repository uses git worktrees for parallel development with **automatic shared dependency setup** via Conductor. + +### How It Works + +The `conductor.json` setup script (`scripts/conductor-setup.sh`) automatically: +1. Creates a shared `.shared-deps/` directory in the main repository +2. Creates symlinks from `node_modules` and `vendor` to the shared location +3. This happens automatically when Conductor creates a new worktree + +### Benefits + +- **Save disk space**: Only one copy of dependencies across all worktrees +- **Faster setup**: No need to run `npm install` or `composer install` for each worktree +- **Consistent versions**: All worktrees use the same dependency versions +- **Auto-configured**: Handled by Conductor's setup script + +### Manual Setup (If Needed) + +If you need to set up symlinks manually or for non-Conductor worktrees: + +```bash +# From the worktree directory +SHARED_DEPS="../../.shared-deps" +mkdir -p "$SHARED_DEPS/node_modules" "$SHARED_DEPS/vendor" +rm -rf node_modules vendor +ln -sf "$SHARED_DEPS/node_modules" node_modules +ln -sf "$SHARED_DEPS/vendor" vendor +``` + +### Important Notes + +- Dependencies are shared at `$CONDUCTOR_ROOT_PATH/.shared-deps/` +- Run `npm install` or `composer install` from any worktree to update all +- Ensure `.shared-deps/` is in `.gitignore` (should already be there) +- If different branches need different dependency versions, this won't work - remove symlinks and use separate directories + ## Development Commands ### Frontend Development diff --git a/scripts/conductor-setup.sh b/scripts/conductor-setup.sh index 7712f88be..963c8e808 100755 --- a/scripts/conductor-setup.sh +++ b/scripts/conductor-setup.sh @@ -1 +1,29 @@ -cp $CONDUCTOR_ROOT_PATH/.env .env \ No newline at end of file +#!/bin/bash +set -e + +# Copy .env file +cp $CONDUCTOR_ROOT_PATH/.env .env + +# Setup shared dependencies via symlinks +echo "Setting up shared node_modules and vendor directories..." + +# Create shared-deps directory in main repository if it doesn't exist +SHARED_DEPS="$CONDUCTOR_ROOT_PATH/.shared-deps" +mkdir -p "$SHARED_DEPS/node_modules" +mkdir -p "$SHARED_DEPS/vendor" + +# Remove existing directories if they exist and are not symlinks +[ -d "node_modules" ] && [ ! -L "node_modules" ] && rm -rf node_modules +[ -d "vendor" ] && [ ! -L "vendor" ] && rm -rf vendor + +# Calculate relative path from worktree to shared deps +WORKTREE_PATH=$(pwd) +RELATIVE_PATH=$(python3 -c "import os.path; print(os.path.relpath('$SHARED_DEPS', '$WORKTREE_PATH'))") + +# Create symlinks +ln -sf "$RELATIVE_PATH/node_modules" node_modules +ln -sf "$RELATIVE_PATH/vendor" vendor + +echo "✓ Shared dependencies linked successfully" +echo " node_modules -> $RELATIVE_PATH/node_modules" +echo " vendor -> $RELATIVE_PATH/vendor" \ No newline at end of file From b847e3801aa1a23d9b9325767efb0b78bf481087 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:06:25 +0100 Subject: [PATCH 05/41] Use main repo directories for shared dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplified the worktree setup to use the main repository's node_modules and vendor directories directly instead of creating a separate .shared-deps directory. Changes: - Updated conductor-setup.sh to symlink directly to main repo's directories - Updated CLAUDE.md to reflect the simpler approach - Symlinks now point to ../../node_modules and ../../vendor Benefits: - Simpler setup with no extra directories - All worktrees share the main repo's dependencies - No need to add .shared-deps to .gitignore 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 16 +++++++--------- scripts/conductor-setup.sh | 15 +++++++-------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e8c040402..3982ec81c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,8 +17,8 @@ ## Git Worktree Shared Dependencies ### How It Works The `conductor.json` setup script (`scripts/conductor-setup.sh`) automatically: -1. Creates a shared `.shared-deps/` directory in the main repository -2. Creates symlinks from `node_modules` and `vendor` to the shared location +1. Creates symlinks from worktree's `node_modules` and `vendor` to the main repository's directories +2. All worktrees share the same dependencies from the main repository 3. This happens automatically when Conductor creates a new worktree ### Benefits @@ -27,6 +27,7 @@ ### Benefits - **Faster setup**: No need to run `npm install` or `composer install` for each worktree - **Consistent versions**: All worktrees use the same dependency versions - **Auto-configured**: Handled by Conductor's setup script +- **Simple**: Uses the main repo's existing directories, no extra folders ### Manual Setup (If Needed) @@ -34,18 +35,15 @@ ### Manual Setup (If Needed) ```bash # From the worktree directory -SHARED_DEPS="../../.shared-deps" -mkdir -p "$SHARED_DEPS/node_modules" "$SHARED_DEPS/vendor" rm -rf node_modules vendor -ln -sf "$SHARED_DEPS/node_modules" node_modules -ln -sf "$SHARED_DEPS/vendor" vendor +ln -sf ../../node_modules node_modules +ln -sf ../../vendor vendor ``` ### Important Notes -- Dependencies are shared at `$CONDUCTOR_ROOT_PATH/.shared-deps/` -- Run `npm install` or `composer install` from any worktree to update all -- Ensure `.shared-deps/` is in `.gitignore` (should already be there) +- Dependencies are shared from the main repository (`$CONDUCTOR_ROOT_PATH`) +- Run `npm install` or `composer install` from the main repo or any worktree to update all - If different branches need different dependency versions, this won't work - remove symlinks and use separate directories ## Development Commands diff --git a/scripts/conductor-setup.sh b/scripts/conductor-setup.sh index 963c8e808..a1172e0d0 100755 --- a/scripts/conductor-setup.sh +++ b/scripts/conductor-setup.sh @@ -4,23 +4,22 @@ set -e # Copy .env file cp $CONDUCTOR_ROOT_PATH/.env .env -# Setup shared dependencies via symlinks +# Setup shared dependencies via symlinks to main repo echo "Setting up shared node_modules and vendor directories..." -# Create shared-deps directory in main repository if it doesn't exist -SHARED_DEPS="$CONDUCTOR_ROOT_PATH/.shared-deps" -mkdir -p "$SHARED_DEPS/node_modules" -mkdir -p "$SHARED_DEPS/vendor" +# Ensure main repo has the directories +mkdir -p "$CONDUCTOR_ROOT_PATH/node_modules" +mkdir -p "$CONDUCTOR_ROOT_PATH/vendor" # Remove existing directories if they exist and are not symlinks [ -d "node_modules" ] && [ ! -L "node_modules" ] && rm -rf node_modules [ -d "vendor" ] && [ ! -L "vendor" ] && rm -rf vendor -# Calculate relative path from worktree to shared deps +# Calculate relative path from worktree to main repo WORKTREE_PATH=$(pwd) -RELATIVE_PATH=$(python3 -c "import os.path; print(os.path.relpath('$SHARED_DEPS', '$WORKTREE_PATH'))") +RELATIVE_PATH=$(python3 -c "import os.path; print(os.path.relpath('$CONDUCTOR_ROOT_PATH', '$WORKTREE_PATH'))") -# Create symlinks +# Create symlinks to main repo's node_modules and vendor ln -sf "$RELATIVE_PATH/node_modules" node_modules ln -sf "$RELATIVE_PATH/vendor" vendor From 4507d99460982f2a2516a5b7551360c371f0e575 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:15:03 +0100 Subject: [PATCH 06/41] Use absolute paths in rm commands for safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed rm -rf commands to use absolute paths ($WORKTREE_PATH) instead of relative paths to prevent accidental deletion if symlinks behave unexpectedly. Also cleaned up duplicate WORKTREE_PATH definition. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- scripts/conductor-setup.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/conductor-setup.sh b/scripts/conductor-setup.sh index a1172e0d0..b7e8ccb36 100755 --- a/scripts/conductor-setup.sh +++ b/scripts/conductor-setup.sh @@ -11,12 +11,14 @@ echo "Setting up shared node_modules and vendor directories..." mkdir -p "$CONDUCTOR_ROOT_PATH/node_modules" mkdir -p "$CONDUCTOR_ROOT_PATH/vendor" +# Get current worktree path +WORKTREE_PATH=$(pwd) + # Remove existing directories if they exist and are not symlinks -[ -d "node_modules" ] && [ ! -L "node_modules" ] && rm -rf node_modules -[ -d "vendor" ] && [ ! -L "vendor" ] && rm -rf vendor +[ -d "node_modules" ] && [ ! -L "node_modules" ] && rm -rf "$WORKTREE_PATH/node_modules" +[ -d "vendor" ] && [ ! -L "vendor" ] && rm -rf "$WORKTREE_PATH/vendor" # Calculate relative path from worktree to main repo -WORKTREE_PATH=$(pwd) RELATIVE_PATH=$(python3 -c "import os.path; print(os.path.relpath('$CONDUCTOR_ROOT_PATH', '$WORKTREE_PATH'))") # Create symlinks to main repo's node_modules and vendor From c6316272003917fa233c7ab1f00b774a74bf205e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:18:24 +0100 Subject: [PATCH 07/41] Add safety checks to prevent dangerous deletions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added multiple safety validations before executing rm -rf commands: - Check WORKTREE_PATH is not empty, /, /Users, or $HOME - Verify we're actually in a git repository (.git exists) This prevents accidental deletion of critical directories if the script is run in the wrong location or with unexpected environment variables. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- scripts/conductor-setup.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/conductor-setup.sh b/scripts/conductor-setup.sh index b7e8ccb36..effad78fc 100755 --- a/scripts/conductor-setup.sh +++ b/scripts/conductor-setup.sh @@ -14,6 +14,18 @@ mkdir -p "$CONDUCTOR_ROOT_PATH/vendor" # Get current worktree path WORKTREE_PATH=$(pwd) +# Safety check: ensure WORKTREE_PATH is valid and not a dangerous location +if [ -z "$WORKTREE_PATH" ] || [ "$WORKTREE_PATH" = "/" ] || [ "$WORKTREE_PATH" = "/Users" ] || [ "$WORKTREE_PATH" = "$HOME" ]; then + echo "ERROR: Invalid or dangerous WORKTREE_PATH: $WORKTREE_PATH" + exit 1 +fi + +# Additional safety: ensure we're in a git worktree +if [ ! -f ".git" ] && [ ! -d ".git" ]; then + echo "ERROR: Not in a git repository" + exit 1 +fi + # Remove existing directories if they exist and are not symlinks [ -d "node_modules" ] && [ ! -L "node_modules" ] && rm -rf "$WORKTREE_PATH/node_modules" [ -d "vendor" ] && [ ! -L "vendor" ] && rm -rf "$WORKTREE_PATH/vendor" From ff7b27be61217742524bf4f5161da068e59fce55 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:58:21 +0100 Subject: [PATCH 08/41] Improve worktree setup safety and cross-platform compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add validation for CONDUCTOR_ROOT_PATH environment variable - Enhance safety checks with explicit blacklist of system directories - Improve directory detection (symlink vs regular directory) - Replace Python dependency with cross-platform bash+perl for path calculation - Use absolute paths consistently to prevent symlink following - Add detailed comments explaining each safety check 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- scripts/conductor-setup.sh | 73 +++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/scripts/conductor-setup.sh b/scripts/conductor-setup.sh index effad78fc..a88b457fb 100755 --- a/scripts/conductor-setup.sh +++ b/scripts/conductor-setup.sh @@ -1,8 +1,20 @@ #!/bin/bash set -e +# Validate CONDUCTOR_ROOT_PATH is set and valid before any operations +if [ -z "$CONDUCTOR_ROOT_PATH" ]; then + echo "ERROR: CONDUCTOR_ROOT_PATH environment variable is not set" + echo "This script must be run by Conductor with CONDUCTOR_ROOT_PATH set to the main repository path" + exit 1 +fi + +if [ ! -d "$CONDUCTOR_ROOT_PATH" ]; then + echo "ERROR: CONDUCTOR_ROOT_PATH ($CONDUCTOR_ROOT_PATH) is not a valid directory" + exit 1 +fi + # Copy .env file -cp $CONDUCTOR_ROOT_PATH/.env .env +cp "$CONDUCTOR_ROOT_PATH/.env" .env # Setup shared dependencies via symlinks to main repo echo "Setting up shared node_modules and vendor directories..." @@ -14,24 +26,67 @@ mkdir -p "$CONDUCTOR_ROOT_PATH/vendor" # Get current worktree path WORKTREE_PATH=$(pwd) -# Safety check: ensure WORKTREE_PATH is valid and not a dangerous location -if [ -z "$WORKTREE_PATH" ] || [ "$WORKTREE_PATH" = "/" ] || [ "$WORKTREE_PATH" = "/Users" ] || [ "$WORKTREE_PATH" = "$HOME" ]; then - echo "ERROR: Invalid or dangerous WORKTREE_PATH: $WORKTREE_PATH" +# Safety check 1: ensure WORKTREE_PATH is valid +if [ -z "$WORKTREE_PATH" ]; then + echo "ERROR: WORKTREE_PATH is empty" exit 1 fi -# Additional safety: ensure we're in a git worktree +# Safety check 2: CRITICAL FIRST - blacklist system directories +# This check runs BEFORE the positive check to prevent dangerous operations +# even if someone misconfigures CONDUCTOR_ROOT_PATH +case "$WORKTREE_PATH" in + /|/bin|/sbin|/usr|/usr/*|/etc|/etc/*|/var|/var/*|/System|/System/*|/Library|/Library/*|/Applications|/Applications/*|"$HOME") + echo "ERROR: WORKTREE_PATH ($WORKTREE_PATH) is in a dangerous system location" + exit 1 + ;; +esac + +# Safety check 3: positive check - verify we're under CONDUCTOR_ROOT_PATH +case "$WORKTREE_PATH" in + "$CONDUCTOR_ROOT_PATH"|"$CONDUCTOR_ROOT_PATH"/.conductor/*) + # Valid: either main repo or under .conductor/ + ;; + *) + echo "ERROR: WORKTREE_PATH ($WORKTREE_PATH) is not under CONDUCTOR_ROOT_PATH ($CONDUCTOR_ROOT_PATH)" + exit 1 + ;; +esac + +# Safety check 4: verify we're in a git repository if [ ! -f ".git" ] && [ ! -d ".git" ]; then echo "ERROR: Not in a git repository" exit 1 fi -# Remove existing directories if they exist and are not symlinks -[ -d "node_modules" ] && [ ! -L "node_modules" ] && rm -rf "$WORKTREE_PATH/node_modules" -[ -d "vendor" ] && [ ! -L "vendor" ] && rm -rf "$WORKTREE_PATH/vendor" +# Remove existing directories/symlinks if they exist +# For symlinks: use 'rm' without -r to remove the symlink itself (not following it) +# For directories: use 'rm -rf' to remove the directory and contents +if [ -L "node_modules" ]; then + # It's a symlink - remove it without following (no -r flag) + rm "$WORKTREE_PATH/node_modules" +elif [ -e "node_modules" ]; then + # It's a regular directory or file - safe to use -rf + rm -rf "$WORKTREE_PATH/node_modules" +fi + +if [ -L "vendor" ]; then + # It's a symlink - remove it without following (no -r flag) + rm "$WORKTREE_PATH/vendor" +elif [ -e "vendor" ]; then + # It's a regular directory or file - safe to use -rf + rm -rf "$WORKTREE_PATH/vendor" +fi # Calculate relative path from worktree to main repo -RELATIVE_PATH=$(python3 -c "import os.path; print(os.path.relpath('$CONDUCTOR_ROOT_PATH', '$WORKTREE_PATH'))") +# Use bash-native approach: try realpath first (GNU coreutils), fallback to perl +if command -v realpath &> /dev/null && realpath --relative-to / / &> /dev/null 2>&1; then + # GNU coreutils realpath with --relative-to support + RELATIVE_PATH=$(realpath --relative-to="$WORKTREE_PATH" "$CONDUCTOR_ROOT_PATH") +else + # Fallback: use perl which is standard on macOS and most Unix systems + RELATIVE_PATH=$(perl -e 'use File::Spec; print File::Spec->abs2rel($ARGV[0], $ARGV[1])' "$CONDUCTOR_ROOT_PATH" "$WORKTREE_PATH") +fi # Create symlinks to main repo's node_modules and vendor ln -sf "$RELATIVE_PATH/node_modules" node_modules From f37eef8266488c075da6cf2e51dd159cdd4ec4be Mon Sep 17 00:00:00 2001 From: EbinJose2002 Date: Fri, 28 Nov 2025 17:08:31 +0530 Subject: [PATCH 09/41] - Made necessary changes to the migration and created new one as well. - Updated the Clickhouse service template to use the official `clickhouse/clickhouse-server` image. - Removed the usage of the deprecated `bitnamilegacy/clickhouse` image. - fixes #7110 --- app/Actions/Database/StartClickhouse.php | 14 +++-- app/Models/StandaloneClickhouse.php | 10 ++-- ...19_create_standalone_clickhouses_table.php | 3 +- ...5_10_03_154100_update_clickhouse_image.php | 32 ----------- ...1_migrate_clickhouse_to_official_image.php | 52 ++++++++++++++++++ .../ClickhouseOfficialImageMigrationTest.php | 55 +++++++++++++++++++ 6 files changed, 123 insertions(+), 43 deletions(-) delete mode 100644 database/migrations/2025_10_03_154100_update_clickhouse_image.php create mode 100644 database/migrations/2025_11_28_000001_migrate_clickhouse_to_official_image.php create mode 100644 tests/Unit/ClickhouseOfficialImageMigrationTest.php diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index 7fdfe9aeb..06cf8f719 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -51,7 +51,7 @@ public function handle(StandaloneClickhouse $database) ], 'labels' => defaultDatabaseLabels($this->database)->toArray(), 'healthcheck' => [ - 'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'", + 'test' => "clickhouse-client --user {$this->database->clickhouse_admin_user} --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'", 'interval' => '5s', 'timeout' => '5s', 'retries' => 10, @@ -152,12 +152,16 @@ private function generate_environment_variables() $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) { - $environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}"); + if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_USER'))->isEmpty()) { + $environment_variables->push("CLICKHOUSE_USER={$this->database->clickhouse_admin_user}"); } - if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) { - $environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}"); + if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_PASSWORD'))->isEmpty()) { + $environment_variables->push("CLICKHOUSE_PASSWORD={$this->database->clickhouse_admin_password}"); + } + + if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_DB'))->isEmpty()) { + $environment_variables->push("CLICKHOUSE_DB={$this->database->clickhouse_db}"); } add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 6ac685618..ce5b967c2 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -25,7 +25,7 @@ protected static function booted() static::created(function ($database) { LocalPersistentVolume::create([ 'name' => 'clickhouse-data-'.$database->uuid, - 'mount_path' => '/bitnami/clickhouse', + 'mount_path' => '/var/lib/clickhouse', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), @@ -232,8 +232,8 @@ protected function internalDbUrl(): Attribute get: function () { $encodedUser = rawurlencode($this->clickhouse_admin_user); $encodedPass = rawurlencode($this->clickhouse_admin_password); - - return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->uuid}:9000/{$this->clickhouse_db}"; + $database = $this->clickhouse_db ?? 'default'; + return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->uuid}:9000/{$database}"; }, ); } @@ -249,8 +249,8 @@ protected function externalDbUrl(): Attribute } $encodedUser = rawurlencode($this->clickhouse_admin_user); $encodedPass = rawurlencode($this->clickhouse_admin_password); - - return "clickhouse://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->clickhouse_db}"; + $database = $this->clickhouse_db ?? 'default'; + return "clickhouse://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$database}"; } return null; diff --git a/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php b/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php index 7433948b9..f7ea310cf 100644 --- a/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php +++ b/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php @@ -19,6 +19,7 @@ public function up(): void $table->string('clickhouse_admin_user')->default('default'); $table->text('clickhouse_admin_password'); + $table->string('clickhouse_db')->default('default'); $table->boolean('is_log_drain_enabled')->default(false); $table->boolean('is_include_timestamps')->default(false); @@ -26,7 +27,7 @@ public function up(): void $table->string('status')->default('exited'); - $table->string('image')->default('bitnami/clickhouse'); + $table->string('image')->default('clickhouse/clickhouse-server:latest'); $table->boolean('is_public')->default(false); $table->integer('public_port')->nullable(); diff --git a/database/migrations/2025_10_03_154100_update_clickhouse_image.php b/database/migrations/2025_10_03_154100_update_clickhouse_image.php deleted file mode 100644 index e57354037..000000000 --- a/database/migrations/2025_10_03_154100_update_clickhouse_image.php +++ /dev/null @@ -1,32 +0,0 @@ -string('image')->default('bitnamilegacy/clickhouse')->change(); - }); - // Optionally, update any existing rows with the old default to the new one - DB::table('standalone_clickhouses') - ->where('image', 'bitnami/clickhouse') - ->update(['image' => 'bitnamilegacy/clickhouse']); - } - - public function down() - { - Schema::table('standalone_clickhouses', function (Blueprint $table) { - $table->string('image')->default('bitnami/clickhouse')->change(); - }); - // Optionally, revert any changed values - DB::table('standalone_clickhouses') - ->where('image', 'bitnamilegacy/clickhouse') - ->update(['image' => 'bitnami/clickhouse']); - } -}; diff --git a/database/migrations/2025_11_28_000001_migrate_clickhouse_to_official_image.php b/database/migrations/2025_11_28_000001_migrate_clickhouse_to_official_image.php new file mode 100644 index 000000000..925b0dd09 --- /dev/null +++ b/database/migrations/2025_11_28_000001_migrate_clickhouse_to_official_image.php @@ -0,0 +1,52 @@ +string('clickhouse_db') + ->default('default') + ->after('clickhouse_admin_password'); + }); + } + StandaloneClickhouse::where(function ($query) { + $query->where('image', 'like', '%bitnami/clickhouse%') + ->orWhere('image', 'like', '%bitnamilegacy/clickhouse%'); + }) + ->update([ + 'image' => 'clickhouse/clickhouse-server:latest', + 'clickhouse_db' => DB::raw("COALESCE(clickhouse_db, 'default')") + ]); + + LocalPersistentVolume::where('resource_type', StandaloneClickhouse::class) + ->where('mount_path', '/bitnami/clickhouse') + ->update(['mount_path' => '/var/lib/clickhouse']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + StandaloneClickhouse::where('image', 'clickhouse/clickhouse-server:latest') + ->update(['image' => 'bitnami/clickhouse']); + LocalPersistentVolume::where('resource_type', StandaloneClickhouse::class) + ->where('mount_path', '/var/lib/clickhouse') + ->update(['mount_path' => '/bitnami/clickhouse']); + } +}; diff --git a/tests/Unit/ClickhouseOfficialImageMigrationTest.php b/tests/Unit/ClickhouseOfficialImageMigrationTest.php new file mode 100644 index 000000000..9edf5d09c --- /dev/null +++ b/tests/Unit/ClickhouseOfficialImageMigrationTest.php @@ -0,0 +1,55 @@ +clickhouse_admin_user = 'testuser'; + $clickhouse->clickhouse_admin_password = 'testpass'; + $clickhouse->clickhouse_db = 'mydb'; + $clickhouse->uuid = 'test-uuid'; + + $internalUrl = $clickhouse->internal_db_url; + + expect($internalUrl) + ->toContain('mydb') + ->toContain('testuser') + ->toContain('test-uuid'); +}); + +test('clickhouse defaults to default database when clickhouse_db is null', function () { + $clickhouse = new StandaloneClickhouse(); + $clickhouse->clickhouse_admin_user = 'testuser'; + $clickhouse->clickhouse_admin_password = 'testpass'; + $clickhouse->clickhouse_db = null; + $clickhouse->uuid = 'test-uuid'; + + $internalUrl = $clickhouse->internal_db_url; + + expect($internalUrl)->toContain('/default'); +}); + +test('clickhouse external url uses correct database', function () { + $clickhouse = new StandaloneClickhouse(); + $clickhouse->clickhouse_admin_user = 'admin'; + $clickhouse->clickhouse_admin_password = 'secret'; + $clickhouse->clickhouse_db = 'production'; + $clickhouse->uuid = 'prod-uuid'; + $clickhouse->is_public = true; + $clickhouse->public_port = 8123; + + $clickhouse->destination = new class { + public $server; + public function __construct() { + $this->server = new class { + public function __get($name) { + if ($name === 'getIp') return '1.2.3.4'; + } + }; + } + }; + $externalUrl = $clickhouse->external_db_url; + + expect($externalUrl)->toContain('production'); + +}); From b72f39b7ac66950c03611a26022fbab839cba8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Tue, 2 Dec 2025 18:56:06 +0100 Subject: [PATCH 10/41] migrate openpanel to v2 --- templates/compose/openpanel.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/templates/compose/openpanel.yaml b/templates/compose/openpanel.yaml index 676ae0356..3a9bfa844 100644 --- a/templates/compose/openpanel.yaml +++ b/templates/compose/openpanel.yaml @@ -7,13 +7,13 @@ services: openpanel-dashboard: - image: lindesvard/openpanel-dashboard:latest + image: lindesvard/openpanel-dashboard:2.0.0 environment: - NODE_ENV=production - - NEXT_PUBLIC_SELF_HOSTED=true + - SELF_HOSTED=true - SERVICE_URL_OPDASHBOARD_3000 - - NEXT_PUBLIC_API_URL=${SERVICE_URL_OPAPI} - - NEXT_PUBLIC_DASHBOARD_URL=${SERVICE_URL_OPDASHBOARD} + - API_URL=${SERVICE_URL_OPAPI} + - DASHBOARD_URL=${SERVICE_URL_OPDASHBOARD} - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@redis:6379 - CLICKHOUSE_URL=http://clickhouse:8123/openpanel @@ -37,7 +37,7 @@ services: start_period: 15s openpanel-api: - image: lindesvard/openpanel-api:latest + image: lindesvard/openpanel-api:2.0.0 command: > sh -c " echo 'Running migrations...' @@ -47,10 +47,10 @@ services: " environment: - NODE_ENV=production - - NEXT_PUBLIC_SELF_HOSTED=true + - SELF_HOSTED=true - SERVICE_URL_OPAPI - - NEXT_PUBLIC_API_URL=${SERVICE_URL_OPAPI} - - NEXT_PUBLIC_DASHBOARD_URL=${SERVICE_URL_OPDASHBOARD} + - API_URL=${SERVICE_URL_OPAPI} + - DASHBOARD_URL=${SERVICE_URL_OPDASHBOARD} - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public - DATABASE_URL_DIRECT=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@redis:6379 @@ -74,13 +74,13 @@ services: retries: 5 openpanel-worker: - image: lindesvard/openpanel-worker:latest + image: lindesvard/openpanel-worker:2.0.0 environment: - DISABLE_BULLBOARD=${DISABLE_BULLBOARD:-1} - NODE_ENV=production - - NEXT_PUBLIC_SELF_HOSTED=true + - SELF_HOSTED=true - SERVICE_URL_OPBULLBOARD - - NEXT_PUBLIC_API_URL=${SERVICE_URL_OPAPI} + - API_URL=${SERVICE_URL_OPAPI} - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public - DATABASE_URL_DIRECT=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${OPENPANEL_POSTGRES_DB:-openpanel-db}?schema=public - REDIS_URL=redis://default:${SERVICE_PASSWORD_REDIS}@redis:6379 @@ -129,7 +129,7 @@ services: retries: 5 clickhouse: - image: clickhouse/clickhouse-server:24.3.2-alpine + image: clickhouse/clickhouse-server:25.10.2.65 volumes: - openpanel_clickhouse_data:/var/lib/clickhouse - openpanel_clickhouse_logs:/var/log/clickhouse-server From 3bf5e3ccfb999c5d3690e56cd935afed065b0fd6 Mon Sep 17 00:00:00 2001 From: Murat Aslan <78961478+murataslan1@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:53:33 +0300 Subject: [PATCH 11/41] feat: add Soju IRC bouncer service template --- templates/compose/soju.yaml | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 templates/compose/soju.yaml diff --git a/templates/compose/soju.yaml b/templates/compose/soju.yaml new file mode 100644 index 000000000..a1da14a86 --- /dev/null +++ b/templates/compose/soju.yaml @@ -0,0 +1,49 @@ +# documentation: https://soju.im/ +# slogan: A user-friendly IRC bouncer with a modern web interface +# category: communication +# tags: irc, bouncer, chat, messaging, relay +# logo: svgs/soju.svg +# port: 80 + +services: + soju: + image: codeberg.org/emersion/soju:latest + volumes: + - soju-db:/db + - soju-uploads:/uploads + - type: bind + source: ./soju/config + target: /soju-config + content: | + db sqlite3 /db/main.db + message-store db + file-upload fs /uploads/ + listen irc+insecure:// + listen http+insecure:// + listen unix+admin:// + networks: + default: + aliases: + - gamja-backend + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 6667 || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + + gamja: + image: codeberg.org/emersion/gamja:latest + environment: + - SERVICE_FQDN_GAMJA_80 + depends_on: + soju: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + +volumes: + soju-db: + soju-uploads: From a28890b258ddb75c6e9dc18efbafc8ccb700dc67 Mon Sep 17 00:00:00 2001 From: Murat Aslan <78961478+murataslan1@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:53:45 +0300 Subject: [PATCH 12/41] feat: add Soju IRC bouncer logo --- svgs/soju.svg | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 svgs/soju.svg diff --git a/svgs/soju.svg b/svgs/soju.svg new file mode 100644 index 000000000..f05aeebee --- /dev/null +++ b/svgs/soju.svg @@ -0,0 +1,6 @@ + + + + + + From d6c5265556c22657b5e2203a7cf765c3cc56c0c4 Mon Sep 17 00:00:00 2001 From: Murat Aslan Date: Tue, 9 Dec 2025 00:16:20 +0300 Subject: [PATCH 13/41] fix: update soju config path and add WebSocket support - Fix config path to /etc/soju/config - Use explicit host:port bindings - Switch to WebSocket listener for Gamja compatibility - Add GAMJA_SERVER environment variable --- templates/compose/soju.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/templates/compose/soju.yaml b/templates/compose/soju.yaml index a1da14a86..ff9ae5228 100644 --- a/templates/compose/soju.yaml +++ b/templates/compose/soju.yaml @@ -13,14 +13,14 @@ services: - soju-uploads:/uploads - type: bind source: ./soju/config - target: /soju-config + target: /etc/soju/config content: | db sqlite3 /db/main.db message-store db file-upload fs /uploads/ - listen irc+insecure:// - listen http+insecure:// - listen unix+admin:// + listen irc+insecure://0.0.0.0:6667 + listen ws+insecure://0.0.0.0:80 + listen unix+admin:///run/soju/admin networks: default: aliases: @@ -35,6 +35,7 @@ services: image: codeberg.org/emersion/gamja:latest environment: - SERVICE_FQDN_GAMJA_80 + - GAMJA_SERVER=ws://soju:80 depends_on: soju: condition: service_healthy From 139db09fe0bdcbb372b668c6b8631b8011edb034 Mon Sep 17 00:00:00 2001 From: Murat Aslan Date: Tue, 9 Dec 2025 00:28:33 +0300 Subject: [PATCH 14/41] fix: correct soju config path and simplify template - Fix config path to /soju-config (not /etc/soju/config) - Remove unix+admin listener (causes bind error) - Remove health checks (nc/wget not available in containers) - Simplify depends_on configuration Tested locally with docker compose - both services start successfully --- templates/compose/soju.yaml | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/templates/compose/soju.yaml b/templates/compose/soju.yaml index ff9ae5228..ec746b3b7 100644 --- a/templates/compose/soju.yaml +++ b/templates/compose/soju.yaml @@ -13,37 +13,24 @@ services: - soju-uploads:/uploads - type: bind source: ./soju/config - target: /etc/soju/config + target: /soju-config content: | db sqlite3 /db/main.db message-store db file-upload fs /uploads/ listen irc+insecure://0.0.0.0:6667 listen ws+insecure://0.0.0.0:80 - listen unix+admin:///run/soju/admin networks: default: aliases: - gamja-backend - healthcheck: - test: ["CMD-SHELL", "nc -z localhost 6667 || exit 1"] - interval: 30s - timeout: 10s - retries: 5 gamja: image: codeberg.org/emersion/gamja:latest environment: - SERVICE_FQDN_GAMJA_80 - - GAMJA_SERVER=ws://soju:80 depends_on: - soju: - condition: service_healthy - healthcheck: - test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1"] - interval: 30s - timeout: 10s - retries: 5 + - soju volumes: soju-db: From d2e2ca892ce2e78db2ff4b273a456698237641ad Mon Sep 17 00:00:00 2001 From: Murat Aslan Date: Tue, 9 Dec 2025 00:35:53 +0300 Subject: [PATCH 15/41] fix: add soju-run volume for admin socket - Add soju-run volume for /run/soju directory - Re-add unix+admin listener for sojuctl user management - Tested: user creation works with sojuctl --- templates/compose/soju.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/compose/soju.yaml b/templates/compose/soju.yaml index ec746b3b7..808c61311 100644 --- a/templates/compose/soju.yaml +++ b/templates/compose/soju.yaml @@ -11,6 +11,7 @@ services: volumes: - soju-db:/db - soju-uploads:/uploads + - soju-run:/run/soju - type: bind source: ./soju/config target: /soju-config @@ -20,6 +21,7 @@ services: file-upload fs /uploads/ listen irc+insecure://0.0.0.0:6667 listen ws+insecure://0.0.0.0:80 + listen unix+admin:///run/soju/admin networks: default: aliases: @@ -35,3 +37,4 @@ services: volumes: soju-db: soju-uploads: + soju-run: From b8047a6b14ab77dccb9bdc2b5a60b2b455db1b02 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Mon, 15 Dec 2025 14:20:36 +0100 Subject: [PATCH 16/41] Infiscal no longer distributes images with the postgres on the tag --- templates/compose/infisical.yaml | 2 +- templates/test-database-detection.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/compose/infisical.yaml b/templates/compose/infisical.yaml index e7e2cfe43..32c5a44b8 100644 --- a/templates/compose/infisical.yaml +++ b/templates/compose/infisical.yaml @@ -8,7 +8,7 @@ services: backend: - image: "infisical/infisical:latest-postgres" + image: "infisical/infisical:latest" environment: - SERVICE_URL_BACKEND_8080 - SITE_URL=${SERVICE_URL_BACKEND_8080} diff --git a/templates/test-database-detection.yaml b/templates/test-database-detection.yaml index c63f92821..d57c1b270 100644 --- a/templates/test-database-detection.yaml +++ b/templates/test-database-detection.yaml @@ -233,7 +233,7 @@ services: # Infisical - secret management with postgres variant application-infisical-postgres: - image: infisical/infisical:latest-postgres + image: infisical/infisical:latest depends_on: database-postgres-main: condition: service_healthy From d1076add4e3435ae80ee29b33859a37279292ce9 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Mon, 15 Dec 2025 17:55:25 +0100 Subject: [PATCH 17/41] Pinning to the current latest --- templates/compose/infisical.yaml | 2 +- templates/test-database-detection.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/compose/infisical.yaml b/templates/compose/infisical.yaml index 32c5a44b8..7ca069b53 100644 --- a/templates/compose/infisical.yaml +++ b/templates/compose/infisical.yaml @@ -8,7 +8,7 @@ services: backend: - image: "infisical/infisical:latest" + image: "infisical/infisical:v0.154.6" environment: - SERVICE_URL_BACKEND_8080 - SITE_URL=${SERVICE_URL_BACKEND_8080} diff --git a/templates/test-database-detection.yaml b/templates/test-database-detection.yaml index d57c1b270..f242316b8 100644 --- a/templates/test-database-detection.yaml +++ b/templates/test-database-detection.yaml @@ -233,7 +233,7 @@ services: # Infisical - secret management with postgres variant application-infisical-postgres: - image: infisical/infisical:latest + image: infisical/infisical:v0.154.6 depends_on: database-postgres-main: condition: service_healthy From 327e8181af94af5f8a5d61c40ba7f9abeb22a491 Mon Sep 17 00:00:00 2001 From: Duane Adam Date: Tue, 16 Dec 2025 10:43:18 +0800 Subject: [PATCH 18/41] Add copy logs button with PII/secret sanitization Add a copy button to individual container logs that strips sensitive data before copying to clipboard. Includes sanitization for emails, database URLs with passwords, JWT tokens, API keys, private key blocks, and git access tokens. --- app/Livewire/Project/Shared/GetLogs.php | 5 + bootstrap/helpers/shared.php | 24 ++++ .../project/shared/get-logs.blade.php | 15 +++ tests/Unit/SanitizeLogsForExportTest.php | 103 ++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 tests/Unit/SanitizeLogsForExportTest.php diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index f57563330..c0ced26ef 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -179,6 +179,11 @@ public function getLogs($refresh = false) } } + public function copyLogs(): string + { + return sanitizeLogsForExport($this->outputs); + } + public function render() { return view('livewire.project.shared.get-logs'); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1066f1a63..2943432ac 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -653,6 +653,30 @@ function removeAnsiColors($text) return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text); } +function sanitizeLogsForExport(string $text): string +{ + // Use existing helper for tokens and ANSI codes + $text = remove_iip($text); + + // Database URLs with passwords - must run before email regex to prevent false matches + // (postgres://user:password@host → postgres://user:@host) + $text = preg_replace('/((?:postgres|mysql|mongodb|rediss?|mariadb):\/\/[^:]+:)[^@]+(@)/i', '$1'.REDACTED.'$2', $text); + + // Email addresses + $text = preg_replace('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', REDACTED, $text); + + // Bearer/JWT tokens + $text = preg_replace('/Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/i', 'Bearer '.REDACTED, $text); + + // API keys (common patterns) + $text = preg_replace('/(api[_-]?key|apikey|api[_-]?secret|secret[_-]?key)[=:]\s*[\'"]?[A-Za-z0-9\-_]{16,}[\'"]?/i', '$1='.REDACTED, $text); + + // Private key blocks + $text = preg_replace('/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/', REDACTED, $text); + + return $text; +} + function getTopLevelNetworks(Service|Application $resource) { if ($resource->getMorphClass() === \App\Models\Service::class) { diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index 8504a160f..f9a804aeb 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -235,6 +235,21 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text- d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /> + - - - - - diff --git a/resources/views/components/resources/breadcrumbs.blade.php b/resources/views/components/resources/breadcrumbs.blade.php index 1d6bc169d..8ea329fac 100644 --- a/resources/views/components/resources/breadcrumbs.blade.php +++ b/resources/views/components/resources/breadcrumbs.blade.php @@ -18,7 +18,7 @@
  • - {{ data_get($resource, 'environment.project.name', 'Undefined Name') }} @@ -50,7 +50,7 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
  • -