From e0e7cbeeef133fa09b36f087bc3f3d138ba80046 Mon Sep 17 00:00:00 2001 From: Andrei Canta Date: Thu, 3 Jul 2025 16:01:58 +0300 Subject: [PATCH 1/6] update --- appwrite/code/.env.example | 15 ++ appwrite/code/docker-compose.yml | 123 +++++++++------ dify/code/.env.example | 41 ++++- dify/code/docker-compose-template.yaml | 40 ++++- dify/code/docker-compose.middleware.yaml | 7 +- dify/code/docker-compose.yaml | 65 ++++++-- dify/code/middleware.env.example | 10 +- plane/code/README.md | 27 +++- plane/code/restore-airgapped.sh | 144 ++++++++++++++++++ supabase/code/.env.example | 15 +- supabase/code/dev/docker-compose.dev.yml | 2 +- supabase/code/docker-compose.yml | 22 +-- supabase/code/volumes/logs/vector.yml | 28 +++- twenty/code/.env.example | 1 - twenty/code/docker-compose.yml | 5 +- .../datasources/clickhouse-datasource.yaml | 15 ++ .../otel-collector/otel-collector-config.yaml | 24 +++ twenty/code/scripts/install.sh | 2 +- twenty/code/twenty-website/Dockerfile | 4 +- twenty/code/twenty/Dockerfile | 4 +- twenty/code/twenty/entrypoint.sh | 19 ++- 21 files changed, 511 insertions(+), 102 deletions(-) create mode 100755 plane/code/restore-airgapped.sh create mode 100644 twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml create mode 100644 twenty/code/otel-collector/otel-collector-config.yaml diff --git a/appwrite/code/.env.example b/appwrite/code/.env.example index 110cf4d83..ad4417749 100644 --- a/appwrite/code/.env.example +++ b/appwrite/code/.env.example @@ -3,12 +3,17 @@ _APP_LOCALE=en _APP_OPTIONS_ABUSE=enabled _APP_OPTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled +_APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_PROTECTION=disabled _APP_OPENSSL_KEY_V1=your-secret-key _APP_DOMAIN=localhost _APP_CUSTOM_DOMAIN_DENY_LIST=example.com,test.com,app.example.com _APP_DOMAIN_FUNCTIONS=functions.localhost +_APP_DOMAIN_SITES=sites.localhost _APP_DOMAIN_TARGET=localhost +_APP_DOMAIN_TARGET_CNAME=localhost +_APP_DOMAIN_TARGET_AAAA=::1 +_APP_DOMAIN_TARGET_A=127.0.0.1 _APP_CONSOLE_WHITELIST_ROOT=enabled _APP_CONSOLE_WHITELIST_EMAILS= _APP_CONSOLE_WHITELIST_IPS= @@ -79,12 +84,16 @@ _APP_STORAGE_WASABI_SECRET= _APP_STORAGE_WASABI_REGION=eu-central-1 _APP_STORAGE_WASABI_BUCKET= _APP_FUNCTIONS_SIZE_LIMIT=30000000 +_APP_COMPUTE_SIZE_LIMIT=30000000 _APP_FUNCTIONS_BUILD_SIZE_LIMIT=2000000000 _APP_FUNCTIONS_TIMEOUT=900 _APP_FUNCTIONS_BUILD_TIMEOUT=900 +_APP_COMPUTE_BUILD_TIMEOUT=900 _APP_FUNCTIONS_CONTAINERS=10 _APP_FUNCTIONS_CPUS=0 +_APP_COMPUTE_CPUS=0 _APP_FUNCTIONS_MEMORY=0 +_APP_COMPUTE_MEMORY=0 _APP_FUNCTIONS_MEMORY_SWAP=0 _APP_FUNCTIONS_RUNTIMES=node-16.0,php-8.0,python-3.9,ruby-3.0 _APP_EXECUTOR_SECRET=your-secret-key @@ -92,14 +101,19 @@ _APP_EXECUTOR_HOST=http://exc1/v1 _APP_EXECUTOR_RUNTIME_NETWORK=appwrite_runtimes _APP_FUNCTIONS_ENVS=node-16.0,php-7.4,python-3.9,ruby-3.0 _APP_FUNCTIONS_INACTIVE_THRESHOLD=60 +_APP_COMPUTE_INACTIVE_THRESHOLD=60 DOCKERHUB_PULL_USERNAME= DOCKERHUB_PULL_PASSWORD= DOCKERHUB_PULL_EMAIL= OPEN_RUNTIMES_NETWORK=appwrite_runtimes _APP_FUNCTIONS_RUNTIMES_NETWORK=runtimes +_APP_COMPUTE_RUNTIMES_NETWORK=runtimes _APP_DOCKER_HUB_USERNAME= _APP_DOCKER_HUB_PASSWORD= _APP_FUNCTIONS_MAINTENANCE_INTERVAL=3600 +_APP_COMPUTE_MAINTENANCE_INTERVAL=3600 +_APP_SITES_TIMEOUT=900 +_APP_SITES_RUNTIMES=static-1,node-22,flutter-3.29 _APP_VCS_GITHUB_APP_NAME= _APP_VCS_GITHUB_PRIVATE_KEY= _APP_VCS_GITHUB_APP_ID= @@ -108,6 +122,7 @@ _APP_VCS_GITHUB_CLIENT_SECRET= _APP_VCS_GITHUB_WEBHOOK_SECRET= _APP_MAINTENANCE_INTERVAL=86400 _APP_MAINTENANCE_DELAY=0 +_APP_MAINTENANCE_START_TIME=00:00 _APP_MAINTENANCE_RETENTION_CACHE=2592000 _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_AUDIT=1209600 diff --git a/appwrite/code/docker-compose.yml b/appwrite/code/docker-compose.yml index f3d746a2a..1d8bde4bf 100644 --- a/appwrite/code/docker-compose.yml +++ b/appwrite/code/docker-compose.yml @@ -28,7 +28,7 @@ services: - appwrite appwrite: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 <<: *x-logging restart: unless-stopped networks: @@ -49,10 +49,13 @@ services: - traefik.http.routers.appwrite_api_https.tls=true volumes: - appwrite-uploads:/storage/uploads:rw + - appwrite-imports:/storage/imports:rw - appwrite-cache:/storage/cache:rw - appwrite-config:/storage/config:rw - appwrite-certificates:/storage/certificates:rw - appwrite-functions:/storage/functions:rw + - appwrite-sites:/storage/sites:rw + - appwrite-builds:/storage/builds:rw depends_on: - mariadb - redis @@ -74,10 +77,12 @@ services: - _APP_OPTIONS_ABUSE - _APP_OPTIONS_ROUTER_PROTECTION - _APP_OPTIONS_FORCE_HTTPS - - _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS + - _APP_OPTIONS_ROUTER_FORCE_HTTPS - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - - _APP_DOMAIN_TARGET + - _APP_DOMAIN_TARGET_CNAME + - _APP_DOMAIN_TARGET_AAAA + - _APP_DOMAIN_TARGET_A - _APP_DOMAIN_FUNCTIONS - _APP_REDIS_HOST - _APP_REDIS_PORT @@ -121,17 +126,21 @@ services: - _APP_STORAGE_WASABI_SECRET - _APP_STORAGE_WASABI_REGION - _APP_STORAGE_WASABI_BUCKET - - _APP_FUNCTIONS_SIZE_LIMIT + - _APP_COMPUTE_SIZE_LIMIT - _APP_FUNCTIONS_TIMEOUT - - _APP_FUNCTIONS_BUILD_TIMEOUT - - _APP_FUNCTIONS_CPUS - - _APP_FUNCTIONS_MEMORY + - _APP_SITES_TIMEOUT + - _APP_COMPUTE_BUILD_TIMEOUT + - _APP_COMPUTE_CPUS + - _APP_COMPUTE_MEMORY - _APP_FUNCTIONS_RUNTIMES + - _APP_SITES_RUNTIMES + - _APP_DOMAIN_SITES - _APP_EXECUTOR_SECRET - _APP_EXECUTOR_HOST - _APP_LOGGING_CONFIG - _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_DELAY + - _APP_MAINTENANCE_START_TIME - _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_CACHE - _APP_MAINTENANCE_RETENTION_ABUSE @@ -153,9 +162,10 @@ services: - _APP_MIGRATIONS_FIREBASE_CLIENT_ID - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET - _APP_ASSISTANT_OPENAI_API_KEY + appwrite-console: <<: *x-logging - image: appwrite/console:5.2.58 + image: appwrite/console:6.0.13 restart: unless-stopped networks: - appwrite @@ -175,7 +185,7 @@ services: - traefik.http.routers.appwrite_console_https.tls=true appwrite-realtime: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: realtime <<: *x-logging restart: unless-stopped @@ -217,7 +227,7 @@ services: - _APP_LOGGING_CONFIG appwrite-worker-audits: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-audits <<: *x-logging restart: unless-stopped @@ -242,7 +252,7 @@ services: - _APP_LOGGING_CONFIG appwrite-worker-webhooks: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-webhooks <<: *x-logging restart: unless-stopped @@ -269,7 +279,7 @@ services: - _APP_LOGGING_CONFIG appwrite-worker-deletes: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-deletes <<: *x-logging restart: unless-stopped @@ -282,6 +292,7 @@ services: - appwrite-uploads:/storage/uploads:rw - appwrite-cache:/storage/cache:rw - appwrite-functions:/storage/functions:rw + - appwrite-sites:/storage/sites:rw - appwrite-builds:/storage/builds:rw - appwrite-certificates:/storage/certificates:rw environment: @@ -330,7 +341,7 @@ services: - _APP_EMAIL_CERTIFICATES appwrite-worker-databases: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-databases <<: *x-logging restart: unless-stopped @@ -355,7 +366,7 @@ services: - _APP_LOGGING_CONFIG appwrite-worker-builds: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-builds <<: *x-logging restart: unless-stopped @@ -366,7 +377,9 @@ services: - mariadb volumes: - appwrite-functions:/storage/functions:rw + - appwrite-sites:/storage/sites:rw - appwrite-builds:/storage/builds:rw + - appwrite-uploads:/storage/uploads:rw environment: - _APP_ENV - _APP_WORKER_PER_CORE @@ -387,12 +400,13 @@ services: - _APP_VCS_GITHUB_PRIVATE_KEY - _APP_VCS_GITHUB_APP_ID - _APP_FUNCTIONS_TIMEOUT - - _APP_FUNCTIONS_BUILD_TIMEOUT - - _APP_FUNCTIONS_CPUS - - _APP_FUNCTIONS_MEMORY - - _APP_FUNCTIONS_SIZE_LIMIT + - _APP_SITES_TIMEOUT + - _APP_COMPUTE_BUILD_TIMEOUT + - _APP_COMPUTE_CPUS + - _APP_COMPUTE_MEMORY + - _APP_COMPUTE_SIZE_LIMIT - _APP_OPTIONS_FORCE_HTTPS - - _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS + - _APP_OPTIONS_ROUTER_FORCE_HTTPS - _APP_DOMAIN - _APP_STORAGE_DEVICE - _APP_STORAGE_S3_ACCESS_KEY @@ -416,9 +430,10 @@ services: - _APP_STORAGE_WASABI_SECRET - _APP_STORAGE_WASABI_REGION - _APP_STORAGE_WASABI_BUCKET + - _APP_DOMAIN_SITES appwrite-worker-certificates: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-certificates <<: *x-logging restart: unless-stopped @@ -435,7 +450,9 @@ services: - _APP_WORKER_PER_CORE - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - - _APP_DOMAIN_TARGET + - _APP_DOMAIN_TARGET_CNAME + - _APP_DOMAIN_TARGET_AAAA + - _APP_DOMAIN_TARGET_A - _APP_DOMAIN_FUNCTIONS - _APP_EMAIL_CERTIFICATES - _APP_REDIS_HOST @@ -450,7 +467,7 @@ services: - _APP_LOGGING_CONFIG appwrite-worker-functions: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-functions <<: *x-logging restart: unless-stopped @@ -476,9 +493,10 @@ services: - _APP_DB_USER - _APP_DB_PASS - _APP_FUNCTIONS_TIMEOUT - - _APP_FUNCTIONS_BUILD_TIMEOUT - - _APP_FUNCTIONS_CPUS - - _APP_FUNCTIONS_MEMORY + - _APP_SITES_TIMEOUT + - _APP_COMPUTE_BUILD_TIMEOUT + - _APP_COMPUTE_CPUS + - _APP_COMPUTE_MEMORY - _APP_EXECUTOR_SECRET - _APP_EXECUTOR_HOST - _APP_USAGE_STATS @@ -487,7 +505,7 @@ services: - _APP_LOGGING_CONFIG appwrite-worker-mails: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-mails <<: *x-logging restart: unless-stopped @@ -520,7 +538,7 @@ services: - _APP_OPTIONS_FORCE_HTTPS appwrite-worker-messaging: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-messaging <<: *x-logging restart: unless-stopped @@ -570,12 +588,14 @@ services: - _APP_STORAGE_WASABI_BUCKET appwrite-worker-migrations: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-migrations <<: *x-logging restart: unless-stopped networks: - appwrite + volumes: + - appwrite-imports:/storage/imports:rw depends_on: - mariadb environment: @@ -583,7 +603,9 @@ services: - _APP_WORKER_PER_CORE - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - - _APP_DOMAIN_TARGET + - _APP_DOMAIN_TARGET_CNAME + - _APP_DOMAIN_TARGET_AAAA + - _APP_DOMAIN_TARGET_A - _APP_EMAIL_SECURITY - _APP_REDIS_HOST - _APP_REDIS_PORT @@ -599,7 +621,7 @@ services: - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET appwrite-task-maintenance: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: maintenance <<: *x-logging restart: unless-stopped @@ -611,7 +633,9 @@ services: - _APP_ENV - _APP_WORKER_PER_CORE - _APP_DOMAIN - - _APP_DOMAIN_TARGET + - _APP_DOMAIN_TARGET_CNAME + - _APP_DOMAIN_TARGET_AAAA + - _APP_DOMAIN_TARGET_A - _APP_DOMAIN_FUNCTIONS - _APP_OPENSSL_KEY_V1 - _APP_REDIS_HOST @@ -633,14 +657,12 @@ services: - _APP_MAINTENANCE_RETENTION_SCHEDULES appwrite-task-stats-resources: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: stats-resources <<: *x-logging + restart: unless-stopped networks: - appwrite - volumes: - - ./app:/usr/src/code/app - - ./src:/usr/src/code/src depends_on: - redis - mariadb @@ -663,7 +685,7 @@ services: - _APP_STATS_RESOURCES_INTERVAL appwrite-worker-stats-resources: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-stats-resources <<: *x-logging restart: unless-stopped @@ -690,7 +712,7 @@ services: - _APP_STATS_RESOURCES_INTERVAL appwrite-worker-stats-usage: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: worker-stats-usage <<: *x-logging restart: unless-stopped @@ -717,7 +739,7 @@ services: - _APP_USAGE_AGGREGATION_INTERVAL appwrite-task-scheduler-functions: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: schedule-functions <<: *x-logging restart: unless-stopped @@ -741,7 +763,7 @@ services: - _APP_DB_PASS appwrite-task-scheduler-executions: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: schedule-executions <<: *x-logging restart: unless-stopped @@ -765,7 +787,7 @@ services: - _APP_DB_PASS appwrite-task-scheduler-messages: - image: appwrite/appwrite:1.6.2 + image: appwrite/appwrite:1.7.4 entrypoint: schedule-messages <<: *x-logging restart: unless-stopped @@ -797,12 +819,19 @@ services: environment: - _APP_ASSISTANT_OPENAI_API_KEY + appwrite-browser: + image: appwrite/browser:0.2.4 + <<: *x-logging + restart: unless-stopped + networks: + - appwrite + openruntimes-executor: hostname: exc1 <<: *x-logging restart: unless-stopped stop_signal: SIGINT - image: openruntimes/executor:0.6.11 + image: openruntimes/executor:0.7.14 networks: - appwrite - runtimes @@ -810,18 +839,20 @@ services: - /var/run/docker.sock:/var/run/docker.sock - appwrite-builds:/storage/builds:rw - appwrite-functions:/storage/functions:rw + - appwrite-sites:/storage/sites:rw # Host mount nessessary to share files between executor and runtimes. # It's not possible to share mount file between 2 containers without host mount (copying is too slow) - /tmp:/tmp:rw environment: - - OPR_EXECUTOR_INACTIVE_TRESHOLD=$_APP_FUNCTIONS_INACTIVE_THRESHOLD - - OPR_EXECUTOR_MAINTENANCE_INTERVAL=$_APP_FUNCTIONS_MAINTENANCE_INTERVAL - - OPR_EXECUTOR_NETWORK=$_APP_FUNCTIONS_RUNTIMES_NETWORK + - OPR_EXECUTOR_INACTIVE_TRESHOLD=$_APP_COMPUTE_INACTIVE_THRESHOLD + - OPR_EXECUTOR_MAINTENANCE_INTERVAL=$_APP_COMPUTE_MAINTENANCE_INTERVAL + - OPR_EXECUTOR_NETWORK=$_APP_COMPUTE_RUNTIMES_NETWORK - OPR_EXECUTOR_DOCKER_HUB_USERNAME=$_APP_DOCKER_HUB_USERNAME - OPR_EXECUTOR_DOCKER_HUB_PASSWORD=$_APP_DOCKER_HUB_PASSWORD - OPR_EXECUTOR_ENV=$_APP_ENV - - OPR_EXECUTOR_RUNTIMES=$_APP_FUNCTIONS_RUNTIMES + - OPR_EXECUTOR_RUNTIMES=$_APP_FUNCTIONS_RUNTIMES,$_APP_SITES_RUNTIMES - OPR_EXECUTOR_SECRET=$_APP_EXECUTOR_SECRET + - OPR_EXECUTOR_RUNTIME_VERSIONS=v5 - OPR_EXECUTOR_LOGGING_CONFIG=$_APP_LOGGING_CONFIG - OPR_EXECUTOR_STORAGE_DEVICE=$_APP_STORAGE_DEVICE - OPR_EXECUTOR_STORAGE_S3_ACCESS_KEY=$_APP_STORAGE_S3_ACCESS_KEY @@ -895,7 +926,9 @@ volumes: appwrite-redis: appwrite-cache: appwrite-uploads: + appwrite-imports: appwrite-certificates: appwrite-functions: + appwrite-sites: appwrite-builds: appwrite-config: diff --git a/dify/code/.env.example b/dify/code/.env.example index aacef0e1f..a024566c8 100644 --- a/dify/code/.env.example +++ b/dify/code/.env.example @@ -285,6 +285,7 @@ BROKER_USE_SSL=false # If you are using Redis Sentinel for high availability, configure the following settings. CELERY_USE_SENTINEL=false CELERY_SENTINEL_MASTER_NAME= +CELERY_SENTINEL_PASSWORD= CELERY_SENTINEL_SOCKET_TIMEOUT=0.1 # ------------------------------ @@ -399,7 +400,7 @@ SUPABASE_URL=your-server-url # ------------------------------ # The type of vector store to use. -# Supported values are `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `oceanbase`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`. +# Supported values are `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `oceanbase`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`. VECTOR_STORE=weaviate # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. @@ -412,6 +413,7 @@ QDRANT_API_KEY=difyai123456 QDRANT_CLIENT_TIMEOUT=20 QDRANT_GRPC_ENABLED=false QDRANT_GRPC_PORT=6334 +QDRANT_REPLICATION_FACTOR=1 # Milvus configuration. Only available when VECTOR_STORE is `milvus`. # The milvus uri. @@ -489,6 +491,13 @@ TIDB_VECTOR_USER= TIDB_VECTOR_PASSWORD= TIDB_VECTOR_DATABASE=dify +# Matrixone vector configurations. +MATRIXONE_HOST=matrixone +MATRIXONE_PORT=6001 +MATRIXONE_USER=dump +MATRIXONE_PASSWORD=111 +MATRIXONE_DATABASE=dify + # Tidb on qdrant configuration, only available when VECTOR_STORE is `tidb_on_qdrant` TIDB_ON_QDRANT_URL=http://127.0.0.1 TIDB_ON_QDRANT_API_KEY=dify @@ -531,6 +540,7 @@ RELYT_DATABASE=postgres OPENSEARCH_HOST=opensearch OPENSEARCH_PORT=9200 OPENSEARCH_SECURE=true +OPENSEARCH_VERIFY_CERTS=true OPENSEARCH_AUTH_METHOD=basic OPENSEARCH_USER=admin OPENSEARCH_PASSWORD=admin @@ -717,10 +727,11 @@ NOTION_INTERNAL_SECRET= # Mail related configuration # ------------------------------ -# Mail type, support: resend, smtp +# Mail type, support: resend, smtp, sendgrid MAIL_TYPE=resend # Default send from email address, if not specified +# If using SendGrid, use the 'from' field for authentication if necessary. MAIL_DEFAULT_SEND_FROM= # API-Key for the Resend email provider, used when MAIL_TYPE is `resend`. @@ -736,6 +747,9 @@ SMTP_PASSWORD= SMTP_USE_TLS=true SMTP_OPPORTUNISTIC_TLS=false +# Sendgid configuration +SENDGRID_API_KEY= + # ------------------------------ # Others Configuration # ------------------------------ @@ -785,6 +799,9 @@ HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760 HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576 HTTP_REQUEST_NODE_SSL_VERIFY=True +# Respect X-* headers to redirect clients +RESPECT_XFORWARD_HEADERS_ENABLED=false + # SSRF Proxy server HTTP URL SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128 # SSRF Proxy server HTTPS URL @@ -800,7 +817,7 @@ MAX_TOOLS_NUM=10 MAX_PARALLEL_LIMIT=10 # The maximum number of iterations for agent setting -MAX_ITERATIONS_NUM=5 +MAX_ITERATIONS_NUM=99 # ------------------------------ # Environment Variables for web Service @@ -813,7 +830,8 @@ TEXT_GENERATION_TIMEOUT_MS=60000 # Environment Variables for db Service # ------------------------------ -PGUSER=${DB_USERNAME} +# The name of the default postgres user. +POSTGRES_USER=${DB_USERNAME} # The password for the default postgres user. POSTGRES_PASSWORD=${DB_PASSWORD} # The name of the default postgres database. @@ -1055,7 +1073,7 @@ PLUGIN_MAX_EXECUTION_TIMEOUT=600 PIP_MIRROR_URL= # https://github.com/langgenius/dify-plugin-daemon/blob/main/.env.example -# Plugin storage type, local aws_s3 tencent_cos azure_blob aliyun_oss +# Plugin storage type, local aws_s3 tencent_cos azure_blob aliyun_oss volcengine_tos PLUGIN_STORAGE_TYPE=local PLUGIN_STORAGE_LOCAL_ROOT=/app/storage PLUGIN_WORKING_PATH=/app/storage/cwd @@ -1065,6 +1083,7 @@ PLUGIN_MEDIA_CACHE_PATH=assets # Plugin oss bucket PLUGIN_STORAGE_OSS_BUCKET= # Plugin oss s3 credentials +PLUGIN_S3_USE_AWS=false PLUGIN_S3_USE_AWS_MANAGED_IAM=false PLUGIN_S3_ENDPOINT= PLUGIN_S3_USE_PATH_STYLE=false @@ -1085,6 +1104,11 @@ PLUGIN_ALIYUN_OSS_ACCESS_KEY_ID= PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET= PLUGIN_ALIYUN_OSS_AUTH_VERSION=v4 PLUGIN_ALIYUN_OSS_PATH= +# Plugin oss volcengine tos +PLUGIN_VOLCENGINE_TOS_ENDPOINT= +PLUGIN_VOLCENGINE_TOS_ACCESS_KEY= +PLUGIN_VOLCENGINE_TOS_SECRET_KEY= +PLUGIN_VOLCENGINE_TOS_REGION= # ------------------------------ # OTLP Collector Configuration @@ -1104,3 +1128,10 @@ OTEL_METRIC_EXPORT_TIMEOUT=30000 # Prevent Clickjacking ALLOW_EMBED=false + +# Dataset queue monitor configuration +QUEUE_MONITOR_THRESHOLD=200 +# You can configure multiple ones, separated by commas. eg: test1@dify.ai,test2@dify.ai +QUEUE_MONITOR_ALERT_EMAILS= +# Monitor interval in minutes, default is 30 minutes +QUEUE_MONITOR_INTERVAL=30 diff --git a/dify/code/docker-compose-template.yaml b/dify/code/docker-compose-template.yaml index ceb32e4ab..d45f8f8bf 100644 --- a/dify/code/docker-compose-template.yaml +++ b/dify/code/docker-compose-template.yaml @@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.4.0 + image: langgenius/dify-api:1.5.1 restart: always environment: # Use the shared environment variables. @@ -31,7 +31,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.4.0 + image: langgenius/dify-api:1.5.1 restart: always environment: # Use the shared environment variables. @@ -57,7 +57,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.4.0 + image: langgenius/dify-web:1.5.1 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -75,7 +75,7 @@ services: LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} - MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5} + MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99} ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true} ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true} ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true} @@ -84,7 +84,7 @@ services: image: postgres:15-alpine restart: always environment: - PGUSER: ${PGUSER:-postgres} + POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456} POSTGRES_DB: ${POSTGRES_DB:-dify} PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} @@ -142,7 +142,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.0.10-local + image: langgenius/dify-plugin-daemon:0.1.3-local restart: always environment: # Use the shared environment variables. @@ -168,6 +168,7 @@ services: PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-false} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} @@ -184,6 +185,10 @@ services: ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-} + VOLCENGINE_TOS_ENDPOINT: ${PLUGIN_VOLCENGINE_TOS_ENDPOINT:-} + VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-} + VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-} + VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-} ports: - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" volumes: @@ -430,7 +435,7 @@ services: # OceanBase vector database oceanbase: - image: oceanbase/oceanbase-ce:4.3.5.1-101000042025031818 + image: oceanbase/oceanbase-ce:4.3.5-lts container_name: oceanbase profiles: - oceanbase @@ -444,9 +449,16 @@ services: OB_SYS_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456} OB_TENANT_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456} OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai} - MODE: MINI + OB_SERVER_IP: 127.0.0.1 + MODE: mini ports: - "${OCEANBASE_VECTOR_PORT:-2881}:2881" + healthcheck: + test: [ 'CMD-SHELL', 'obclient -h127.0.0.1 -P2881 -uroot@test -p$${OB_TENANT_PASSWORD} -e "SELECT 1;"' ] + interval: 10s + retries: 30 + start_period: 30s + timeout: 10s # Oracle vector database oracle: @@ -605,6 +617,18 @@ services: ports: - ${MYSCALE_PORT:-8123}:${MYSCALE_PORT:-8123} + # Matrixone vector store. + matrixone: + hostname: matrixone + image: matrixorigin/matrixone:2.1.1 + profiles: + - matrixone + restart: always + volumes: + - ./volumes/matrixone/data:/mo-data + ports: + - ${MATRIXONE_PORT:-6001}:${MATRIXONE_PORT:-6001} + # https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html # https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-prod-prerequisites elasticsearch: diff --git a/dify/code/docker-compose.middleware.yaml b/dify/code/docker-compose.middleware.yaml index 8690a5092..0b1885755 100644 --- a/dify/code/docker-compose.middleware.yaml +++ b/dify/code/docker-compose.middleware.yaml @@ -71,7 +71,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.0.10-local + image: langgenius/dify-plugin-daemon:0.1.3-local restart: always env_file: - ./middleware.env @@ -104,6 +104,7 @@ services: PLUGIN_PACKAGE_CACHE_PATH: ${PLUGIN_PACKAGE_CACHE_PATH:-plugin_packages} PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-false} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} @@ -121,6 +122,10 @@ services: ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-} + VOLCENGINE_TOS_ENDPOINT: ${PLUGIN_VOLCENGINE_TOS_ENDPOINT:-} + VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-} + VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-} + VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-} ports: - "${EXPOSE_PLUGIN_DAEMON_PORT:-5002}:${PLUGIN_DAEMON_PORT:-5002}" - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" diff --git a/dify/code/docker-compose.yaml b/dify/code/docker-compose.yaml index 90dd29cec..9b65af5d3 100644 --- a/dify/code/docker-compose.yaml +++ b/dify/code/docker-compose.yaml @@ -79,6 +79,7 @@ x-shared-env: &shared-api-worker-env BROKER_USE_SSL: ${BROKER_USE_SSL:-false} CELERY_USE_SENTINEL: ${CELERY_USE_SENTINEL:-false} CELERY_SENTINEL_MASTER_NAME: ${CELERY_SENTINEL_MASTER_NAME:-} + CELERY_SENTINEL_PASSWORD: ${CELERY_SENTINEL_PASSWORD:-} CELERY_SENTINEL_SOCKET_TIMEOUT: ${CELERY_SENTINEL_SOCKET_TIMEOUT:-0.1} WEB_API_CORS_ALLOW_ORIGINS: ${WEB_API_CORS_ALLOW_ORIGINS:-*} CONSOLE_CORS_ALLOW_ORIGINS: ${CONSOLE_CORS_ALLOW_ORIGINS:-*} @@ -138,6 +139,7 @@ x-shared-env: &shared-api-worker-env QDRANT_CLIENT_TIMEOUT: ${QDRANT_CLIENT_TIMEOUT:-20} QDRANT_GRPC_ENABLED: ${QDRANT_GRPC_ENABLED:-false} QDRANT_GRPC_PORT: ${QDRANT_GRPC_PORT:-6334} + QDRANT_REPLICATION_FACTOR: ${QDRANT_REPLICATION_FACTOR:-1} MILVUS_URI: ${MILVUS_URI:-http://host.docker.internal:19530} MILVUS_DATABASE: ${MILVUS_DATABASE:-} MILVUS_TOKEN: ${MILVUS_TOKEN:-} @@ -194,6 +196,11 @@ x-shared-env: &shared-api-worker-env TIDB_VECTOR_USER: ${TIDB_VECTOR_USER:-} TIDB_VECTOR_PASSWORD: ${TIDB_VECTOR_PASSWORD:-} TIDB_VECTOR_DATABASE: ${TIDB_VECTOR_DATABASE:-dify} + MATRIXONE_HOST: ${MATRIXONE_HOST:-matrixone} + MATRIXONE_PORT: ${MATRIXONE_PORT:-6001} + MATRIXONE_USER: ${MATRIXONE_USER:-dump} + MATRIXONE_PASSWORD: ${MATRIXONE_PASSWORD:-111} + MATRIXONE_DATABASE: ${MATRIXONE_DATABASE:-dify} TIDB_ON_QDRANT_URL: ${TIDB_ON_QDRANT_URL:-http://127.0.0.1} TIDB_ON_QDRANT_API_KEY: ${TIDB_ON_QDRANT_API_KEY:-dify} TIDB_ON_QDRANT_CLIENT_TIMEOUT: ${TIDB_ON_QDRANT_CLIENT_TIMEOUT:-20} @@ -227,6 +234,7 @@ x-shared-env: &shared-api-worker-env OPENSEARCH_HOST: ${OPENSEARCH_HOST:-opensearch} OPENSEARCH_PORT: ${OPENSEARCH_PORT:-9200} OPENSEARCH_SECURE: ${OPENSEARCH_SECURE:-true} + OPENSEARCH_VERIFY_CERTS: ${OPENSEARCH_VERIFY_CERTS:-true} OPENSEARCH_AUTH_METHOD: ${OPENSEARCH_AUTH_METHOD:-basic} OPENSEARCH_USER: ${OPENSEARCH_USER:-admin} OPENSEARCH_PASSWORD: ${OPENSEARCH_PASSWORD:-admin} @@ -320,6 +328,7 @@ x-shared-env: &shared-api-worker-env SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_USE_TLS: ${SMTP_USE_TLS:-true} SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false} + SENDGRID_API_KEY: ${SENDGRID_API_KEY:-} INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000} INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72} RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5} @@ -347,14 +356,15 @@ x-shared-env: &shared-api-worker-env HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760} HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576} HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True} + RESPECT_XFORWARD_HEADERS_ENABLED: ${RESPECT_XFORWARD_HEADERS_ENABLED:-false} SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128} SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128} LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} - MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5} + MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99} TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} - PGUSER: ${PGUSER:-${DB_USERNAME}} + POSTGRES_USER: ${POSTGRES_USER:-${DB_USERNAME}} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-${DB_PASSWORD}} POSTGRES_DB: ${POSTGRES_DB:-${DB_DATABASE}} PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} @@ -465,6 +475,7 @@ x-shared-env: &shared-api-worker-env PLUGIN_PACKAGE_CACHE_PATH: ${PLUGIN_PACKAGE_CACHE_PATH:-plugin_packages} PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} + PLUGIN_S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-false} PLUGIN_S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} PLUGIN_S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} PLUGIN_S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} @@ -482,6 +493,10 @@ x-shared-env: &shared-api-worker-env PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} PLUGIN_ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} PLUGIN_ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-} + PLUGIN_VOLCENGINE_TOS_ENDPOINT: ${PLUGIN_VOLCENGINE_TOS_ENDPOINT:-} + PLUGIN_VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-} + PLUGIN_VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-} + PLUGIN_VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-} ENABLE_OTEL: ${ENABLE_OTEL:-false} OTLP_BASE_ENDPOINT: ${OTLP_BASE_ENDPOINT:-http://localhost:4318} OTLP_API_KEY: ${OTLP_API_KEY:-} @@ -495,11 +510,14 @@ x-shared-env: &shared-api-worker-env OTEL_BATCH_EXPORT_TIMEOUT: ${OTEL_BATCH_EXPORT_TIMEOUT:-10000} OTEL_METRIC_EXPORT_TIMEOUT: ${OTEL_METRIC_EXPORT_TIMEOUT:-30000} ALLOW_EMBED: ${ALLOW_EMBED:-false} + QUEUE_MONITOR_THRESHOLD: ${QUEUE_MONITOR_THRESHOLD:-200} + QUEUE_MONITOR_ALERT_EMAILS: ${QUEUE_MONITOR_ALERT_EMAILS:-} + QUEUE_MONITOR_INTERVAL: ${QUEUE_MONITOR_INTERVAL:-30} services: # API service api: - image: langgenius/dify-api:1.4.0 + image: langgenius/dify-api:1.5.1 restart: always environment: # Use the shared environment variables. @@ -528,7 +546,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.4.0 + image: langgenius/dify-api:1.5.1 restart: always environment: # Use the shared environment variables. @@ -554,7 +572,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.4.0 + image: langgenius/dify-web:1.5.1 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -572,7 +590,7 @@ services: LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} - MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5} + MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99} ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true} ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true} ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true} @@ -581,7 +599,7 @@ services: image: postgres:15-alpine restart: always environment: - PGUSER: ${PGUSER:-postgres} + POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456} POSTGRES_DB: ${POSTGRES_DB:-dify} PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} @@ -649,7 +667,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.0.10-local + image: langgenius/dify-plugin-daemon:0.1.3-local restart: always environment: # Use the shared environment variables. @@ -675,6 +693,7 @@ services: PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-false} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} @@ -691,6 +710,10 @@ services: ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-} + VOLCENGINE_TOS_ENDPOINT: ${PLUGIN_VOLCENGINE_TOS_ENDPOINT:-} + VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-} + VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-} + VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-} volumes: - ./volumes/plugin_daemon:/app/storage depends_on: @@ -949,7 +972,7 @@ services: # OceanBase vector database oceanbase: - image: oceanbase/oceanbase-ce:4.3.5.1-101000042025031818 + image: oceanbase/oceanbase-ce:4.3.5-lts profiles: - oceanbase restart: always @@ -962,7 +985,19 @@ services: OB_SYS_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456} OB_TENANT_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456} OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai} - MODE: MINI + OB_SERVER_IP: 127.0.0.1 + MODE: mini + healthcheck: + test: + [ + 'CMD-SHELL', + 'obclient -h127.0.0.1 -P2881 -uroot@test -p$${OB_TENANT_PASSWORD} -e + "SELECT 1;"' + ] + interval: 10s + retries: 30 + start_period: 30s + timeout: 10s # Oracle vector database oracle: @@ -1110,6 +1145,16 @@ services: - ./volumes/myscale/log:/var/log/clickhouse-server - ./volumes/myscale/config/users.d/custom_users_config.xml:/etc/clickhouse-server/users.d/custom_users_config.xml + # Matrixone vector store. + matrixone: + hostname: matrixone + image: matrixorigin/matrixone:2.1.1 + profiles: + - matrixone + restart: always + volumes: + - ./volumes/matrixone/data:/mo-data + # https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html # https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-prod-prerequisites elasticsearch: diff --git a/dify/code/middleware.env.example b/dify/code/middleware.env.example index 2437026ee..2eba62f59 100644 --- a/dify/code/middleware.env.example +++ b/dify/code/middleware.env.example @@ -1,7 +1,7 @@ # ------------------------------ # Environment Variables for db Service # ------------------------------ -PGUSER=postgres +POSTGRES_USER=postgres # The password for the default postgres user. POSTGRES_PASSWORD=difyai123456 # The name of the default postgres database. @@ -109,7 +109,7 @@ EXPOSE_PLUGIN_DEBUGGING_HOST=localhost EXPOSE_PLUGIN_DEBUGGING_PORT=5003 PLUGIN_DIFY_INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1 -PLUGIN_DIFY_INNER_API_URL=http://api:5001 +PLUGIN_DIFY_INNER_API_URL=http://host.docker.internal:5001 MARKETPLACE_ENABLED=true MARKETPLACE_API_URL=https://marketplace.dify.ai @@ -133,6 +133,7 @@ PLUGIN_MEDIA_CACHE_PATH=assets PLUGIN_STORAGE_OSS_BUCKET= # Plugin oss s3 credentials PLUGIN_S3_USE_AWS_MANAGED_IAM=false +PLUGIN_S3_USE_AWS=false PLUGIN_S3_ENDPOINT= PLUGIN_S3_USE_PATH_STYLE=false PLUGIN_AWS_ACCESS_KEY= @@ -152,3 +153,8 @@ PLUGIN_ALIYUN_OSS_ACCESS_KEY_ID= PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET= PLUGIN_ALIYUN_OSS_AUTH_VERSION=v4 PLUGIN_ALIYUN_OSS_PATH= +# Plugin oss volcengine tos +PLUGIN_VOLCENGINE_TOS_ENDPOINT= +PLUGIN_VOLCENGINE_TOS_ACCESS_KEY= +PLUGIN_VOLCENGINE_TOS_SECRET_KEY= +PLUGIN_VOLCENGINE_TOS_REGION= diff --git a/plane/code/README.md b/plane/code/README.md index e5a8089e6..ba7af0e6c 100644 --- a/plane/code/README.md +++ b/plane/code/README.md @@ -486,7 +486,7 @@ When you want to restore the previously backed-up data, follow the instructions 1. Download the restore script using the command below. We suggest downloading it in the same folder as `setup.sh`. ```bash - curl -fsSL -o restore.sh https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/restore.sh + curl -fsSL -o restore.sh https://github.com/makeplane/plane/releases/latest/download/restore.sh chmod +x restore.sh ``` @@ -529,6 +529,31 @@ When you want to restore the previously backed-up data, follow the instructions --- +### Restore for Commercial Air-Gapped (Docker Compose) + +When you want to restore the previously backed-up data on Plane Commercial Air-Gapped version, follow the instructions below. + +1. Download the restore script using the command below + + ```bash + curl -fsSL -o restore-airgapped.sh https://github.com/makeplane/plane/releases/latest/download/restore-airgapped.sh + chmod +x restore-airgapped.sh + ``` + +1. Copy the backup folder and the `restore-airgapped.sh` to `Commercial Airgapped Edition` server + +1. Make sure that Plane Commercial (Airgapped) is extracted and ready to get started. In case it is running, you would need to stop that. + +1. Execute the command below to restore your data. + + ```bash + ./restore-airgapped.sh + ``` + +1. After restoration, you are ready to start Plane Commercial (Airgapped) will all your previously saved data. + +--- +

Upgrading from v0.13.2 to v0.14.x

diff --git a/plane/code/restore-airgapped.sh b/plane/code/restore-airgapped.sh new file mode 100755 index 000000000..9da02fd86 --- /dev/null +++ b/plane/code/restore-airgapped.sh @@ -0,0 +1,144 @@ +#!/bin/bash ++set -euo pipefail + +function print_header() { +clear + +cat <<"EOF" +-------------------------------------------- + ____ _ ///////// +| _ \| | __ _ _ __ ___ ///////// +| |_) | |/ _` | '_ \ / _ \ ///// ///// +| __/| | (_| | | | | __/ ///// ///// +|_| |_|\__,_|_| |_|\___| //// + //// +-------------------------------------------- +Project management tool from the future +-------------------------------------------- +EOF +} + +function restoreData() { + + echo "" + echo "****************************************************" + echo "We are about to restore your data from the backup files." + echo "****************************************************" + echo "" + + # set the backup folder path + BACKUP_FOLDER=${1} + + if [ -z "$BACKUP_FOLDER" ]; then + BACKUP_FOLDER="$PWD/backup" + read -p "Enter the backup folder path [$BACKUP_FOLDER]: " BACKUP_FOLDER + if [ -z "$BACKUP_FOLDER" ]; then + BACKUP_FOLDER="$PWD/backup" + fi + fi + + # check if the backup folder exists + if [ ! -d "$BACKUP_FOLDER" ]; then + echo "Error: Backup folder not found at $BACKUP_FOLDER" + exit 1 + fi + + # check if there are any .tar.gz files in the backup folder + if ! ls "$BACKUP_FOLDER"/*.tar.gz 1> /dev/null 2>&1; then + echo "Error: Backup folder does not contain .tar.gz files" + exit 1 + fi + + echo "" + echo "Using backup folder: $BACKUP_FOLDER" + echo "" + + # ask for current install path + AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped" + read -p "Enter the airgapped instance install path [$AIRGAPPED_INSTALL_PATH]: " AIRGAPPED_INSTALL_PATH + if [ -z "$AIRGAPPED_INSTALL_PATH" ]; then + AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped" + fi + + # check if the airgapped instance install path exists + if [ ! -d "$AIRGAPPED_INSTALL_PATH" ]; then + echo "Error: Airgapped instance install path not found at $AIRGAPPED_INSTALL_PATH" + exit 1 + fi + + echo "" + echo "Using airgapped instance install path: $AIRGAPPED_INSTALL_PATH" + echo "" + + # check if the docker-compose.yaml exists + if [ ! -f "$AIRGAPPED_INSTALL_PATH/docker-compose.yml" ]; then + echo "Error: docker-compose.yml not found at $AIRGAPPED_INSTALL_PATH/docker-compose.yml" + exit 1 + fi + + local dockerServiceStatus + if command -v jq &> /dev/null; then + dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped --format=json | jq -r .[0].Status) + else + dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped | grep -o "running" | head -n 1) + fi + + if [[ $dockerServiceStatus == "running" ]]; then + echo "Plane Airgapped is running. Please STOP the Plane Airgapped before restoring data." + exit 1 + fi + + CURRENT_USER_ID=$(id -u) + CURRENT_GROUP_ID=$(id -g) + + # if the data folder not exists, create it + if [ ! -d "$AIRGAPPED_INSTALL_PATH/data" ]; then + mkdir -p "$AIRGAPPED_INSTALL_PATH/data" + chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data" + fi + + for BACKUP_FILE in "$BACKUP_FOLDER/*.tar.gz"; do + if [ -e "$BACKUP_FILE" ]; then + + # get the basefilename without the extension + BASE_FILE_NAME=$(basename "$BACKUP_FILE" ".tar.gz") + + # extract the restoreFile to the airgapped instance install path + echo "Restoring $BASE_FILE_NAME" + rm -rf "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" || true + + tar -xvzf "$BACKUP_FILE" -C "$AIRGAPPED_INSTALL_PATH/data/" + if [ $? -ne 0 ]; then + echo "Error: Failed to extract $BACKUP_FILE" + exit 1 + fi + chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" + if [ $? -ne 0 ]; then + echo "Error: Failed to change ownership of $AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" + exit 1 + fi + else + echo "No .tar.gz files found in the current directory." + echo "" + echo "Please provide the path to the backup file." + echo "" + echo "Usage: $0 /path/to/backup" + exit 1 + fi + done + + echo "" + echo "Restore completed successfully." + echo "" +} + +# if docker-compose is installed +if command -v docker-compose &> /dev/null +then + COMPOSE_CMD="docker-compose" +else + COMPOSE_CMD="docker compose" +fi + +print_header +restoreData "$@" diff --git a/supabase/code/.env.example b/supabase/code/.env.example index bb7450087..8ee5f75c6 100644 --- a/supabase/code/.env.example +++ b/supabase/code/.env.example @@ -26,10 +26,17 @@ POSTGRES_PORT=5432 ############ # Supavisor -- Database pooler ############ +# Port Supavisor listens on for transaction pooling connections POOLER_PROXY_PORT_TRANSACTION=6543 +# Maximum number of PostgreSQL connections Supavisor opens per pool POOLER_DEFAULT_POOL_SIZE=20 +# Maximum number of client connections Supavisor accepts per pool POOLER_MAX_CLIENT_CONN=100 +# Unique tenant identifier POOLER_TENANT_ID=your-tenant-id +# Pool size for internal metadata storage used by Supavisor +# This is separate from client connections and used only by Supavisor itself +POOLER_DB_POOL_SIZE=5 ############ @@ -106,14 +113,14 @@ FUNCTIONS_VERIFY_JWT=false ############ -# Logs - Configuration for Logflare +# Logs - Configuration for Analytics # Please refer to https://supabase.com/docs/reference/self-hosting-analytics/introduction ############ -LOGFLARE_LOGGER_BACKEND_API_KEY=your-super-secret-and-long-logflare-key - # Change vector.toml sinks to reflect this change -LOGFLARE_API_KEY=your-super-secret-and-long-logflare-key +# these cannot be the same value +LOGFLARE_PUBLIC_ACCESS_TOKEN=your-super-secret-and-long-logflare-key-public +LOGFLARE_PRIVATE_ACCESS_TOKEN=your-super-secret-and-long-logflare-key-private # Docker socket location - this value will differ depending on your OS DOCKER_SOCKET_LOCATION=/var/run/docker.sock diff --git a/supabase/code/dev/docker-compose.dev.yml b/supabase/code/dev/docker-compose.dev.yml index ca19a0ad7..f8b3ba787 100644 --- a/supabase/code/dev/docker-compose.dev.yml +++ b/supabase/code/dev/docker-compose.dev.yml @@ -4,7 +4,7 @@ services: studio: build: context: .. - dockerfile: studio/Dockerfile + dockerfile: apps/studio/Dockerfile target: dev ports: - 8082:8082 diff --git a/supabase/code/docker-compose.yml b/supabase/code/docker-compose.yml index a8d8584d9..9a4e48ec0 100644 --- a/supabase/code/docker-compose.yml +++ b/supabase/code/docker-compose.yml @@ -10,7 +10,7 @@ name: supabase services: studio: - image: supabase/studio:2025.05.19-sha-3487831 + image: supabase/studio:2025.06.30-sha-6f5982d restart: unless-stopped healthcheck: test: @@ -41,7 +41,7 @@ services: SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} AUTH_JWT_SECRET: ${JWT_SECRET} - LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN} LOGFLARE_URL: http://analytics:4000 NEXT_PUBLIC_ENABLE_LOGS: true # Comment to use Big Query backend for analytics @@ -75,7 +75,7 @@ services: /docker-entrypoint.sh kong docker-start' auth: - image: supabase/gotrue:v2.172.1 + image: supabase/gotrue:v2.176.1 restart: unless-stopped healthcheck: test: @@ -221,7 +221,7 @@ services: # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up storage: - image: supabase/storage-api:v1.22.17 + image: supabase/storage-api:v1.24.7 restart: unless-stopped volumes: - ./volumes/storage:/var/lib/storage:z @@ -279,7 +279,7 @@ services: IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} meta: - image: supabase/postgres-meta:v0.89.0 + image: supabase/postgres-meta:v0.89.3 restart: unless-stopped depends_on: db: @@ -314,7 +314,7 @@ services: command: [ "start", "--main-service", "/home/deno/functions/main" ] analytics: - image: supabase/logflare:1.12.0 + image: supabase/logflare:1.14.2 restart: unless-stopped # Uncomment to use Big Query backend for analytics # volumes: @@ -339,7 +339,8 @@ services: DB_PORT: ${POSTGRES_PORT} DB_PASSWORD: ${POSTGRES_PASSWORD} DB_SCHEMA: _analytics - LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} + LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN} LOGFLARE_SINGLE_TENANT: true LOGFLARE_SUPABASE_MODE: true LOGFLARE_MIN_CLUSTER_SIZE: 1 @@ -421,14 +422,14 @@ services: interval: 5s retries: 3 environment: - LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} command: [ "--config", "/etc/vector/vector.yml" ] security_opt: - "label=disable" # Update the DATABASE_URL if you are using an external Postgres database supavisor: - image: supabase/supavisor:2.5.1 + image: supabase/supavisor:2.5.6 restart: unless-stopped volumes: - ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z @@ -456,7 +457,7 @@ services: POSTGRES_PORT: ${POSTGRES_PORT} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/_supabase + DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase CLUSTER_POSTGRES: true SECRET_KEY_BASE: ${SECRET_KEY_BASE} VAULT_ENC_KEY: ${VAULT_ENC_KEY} @@ -468,6 +469,7 @@ services: POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE} POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN} POOLER_POOL_MODE: transaction + DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE} command: [ "/bin/sh", diff --git a/supabase/code/volumes/logs/vector.yml b/supabase/code/volumes/logs/vector.yml index cce46df43..1c438a8ec 100644 --- a/supabase/code/volumes/logs/vector.yml +++ b/supabase/code/volumes/logs/vector.yml @@ -165,7 +165,9 @@ sinks: method: 'post' request: retry_max_duration_secs: 10 - uri: 'http://analytics:4000/api/logs?source_name=gotrue.logs.prod&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + headers: + x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required} + uri: 'http://analytics:4000/api/logs?source_name=gotrue.logs.prod' logflare_realtime: type: 'http' inputs: @@ -175,7 +177,9 @@ sinks: method: 'post' request: retry_max_duration_secs: 10 - uri: 'http://analytics:4000/api/logs?source_name=realtime.logs.prod&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + headers: + x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required} + uri: 'http://analytics:4000/api/logs?source_name=realtime.logs.prod' logflare_rest: type: 'http' inputs: @@ -185,7 +189,9 @@ sinks: method: 'post' request: retry_max_duration_secs: 10 - uri: 'http://analytics:4000/api/logs?source_name=postgREST.logs.prod&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + headers: + x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required} + uri: 'http://analytics:4000/api/logs?source_name=postgREST.logs.prod' logflare_db: type: 'http' inputs: @@ -195,10 +201,12 @@ sinks: method: 'post' request: retry_max_duration_secs: 10 + headers: + x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required} # We must route the sink through kong because ingesting logs before logflare is fully initialised will # lead to broken queries from studio. This works by the assumption that containers are started in the # following order: vector > db > logflare > kong - uri: 'http://kong:8000/analytics/v1/api/logs?source_name=postgres.logs&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + uri: 'http://kong:8000/analytics/v1/api/logs?source_name=postgres.logs' logflare_functions: type: 'http' inputs: @@ -208,7 +216,9 @@ sinks: method: 'post' request: retry_max_duration_secs: 10 - uri: 'http://analytics:4000/api/logs?source_name=deno-relay-logs&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + headers: + x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required} + uri: 'http://analytics:4000/api/logs?source_name=deno-relay-logs' logflare_storage: type: 'http' inputs: @@ -218,7 +228,9 @@ sinks: method: 'post' request: retry_max_duration_secs: 10 - uri: 'http://analytics:4000/api/logs?source_name=storage.logs.prod.2&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + headers: + x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required} + uri: 'http://analytics:4000/api/logs?source_name=storage.logs.prod.2' logflare_kong: type: 'http' inputs: @@ -229,4 +241,6 @@ sinks: method: 'post' request: retry_max_duration_secs: 10 - uri: 'http://analytics:4000/api/logs?source_name=cloudflare.logs.prod&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + headers: + x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required} + uri: 'http://analytics:4000/api/logs?source_name=cloudflare.logs.prod' diff --git a/twenty/code/.env.example b/twenty/code/.env.example index 941e7dfe0..d53881426 100644 --- a/twenty/code/.env.example +++ b/twenty/code/.env.example @@ -7,7 +7,6 @@ TAG=latest REDIS_URL=redis://redis:6379 SERVER_URL=https://$(PRIMARY_DOMAIN) -SIGN_IN_PREFILLED=false # Use openssl rand -base64 32 for each secret APP_SECRET=replace_me_with_a_random_string diff --git a/twenty/code/docker-compose.yml b/twenty/code/docker-compose.yml index 2c0e1884c..55dfc6d58 100644 --- a/twenty/code/docker-compose.yml +++ b/twenty/code/docker-compose.yml @@ -10,6 +10,8 @@ services: PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default SERVER_URL: ${SERVER_URL} REDIS_URL: ${REDIS_URL:-redis://redis:6379} + DISABLE_DB_MIGRATIONS: ${DISABLE_DB_MIGRATIONS} + DISABLE_CRON_JOBS_REGISTRATION: ${DISABLE_CRON_JOBS_REGISTRATION} STORAGE_TYPE: ${STORAGE_TYPE} STORAGE_S3_REGION: ${STORAGE_S3_REGION} @@ -54,13 +56,14 @@ services: worker: image: twentycrm/twenty:${TAG:-latest} volumes: - - server-local-data:/app/packages/twenty-server/${STORAGE_LOCAL_PATH:-.local-storage} + - server-local-data:/app/packages/twenty-server/.local-storage command: [ "yarn", "worker:prod" ] environment: PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default SERVER_URL: ${SERVER_URL} REDIS_URL: ${REDIS_URL:-redis://redis:6379} DISABLE_DB_MIGRATIONS: "true" # it already runs on the server + DISABLE_CRON_JOBS_REGISTRATION: "true" # it already runs on the server STORAGE_TYPE: ${STORAGE_TYPE} STORAGE_S3_REGION: ${STORAGE_S3_REGION} diff --git a/twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml b/twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml new file mode 100644 index 000000000..bb4279ee8 --- /dev/null +++ b/twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml @@ -0,0 +1,15 @@ +apiVersion: 1 + +datasources: + - name: ClickHouse + type: grafana-clickhouse-datasource + uid: clickhouse_datasource + jsonData: + server: twenty_clickhouse + defaultDatabase: twenty_dev + port: 9000 + protocol: native + tlsSkipVerify: true + username: default + secureJsonData: + password: devPassword \ No newline at end of file diff --git a/twenty/code/otel-collector/otel-collector-config.yaml b/twenty/code/otel-collector/otel-collector-config.yaml new file mode 100644 index 000000000..1d6eb9d78 --- /dev/null +++ b/twenty/code/otel-collector/otel-collector-config.yaml @@ -0,0 +1,24 @@ +receivers: + otlp: + protocols: + http: + endpoint: "0.0.0.0:4318" + +exporters: + clickhouse: + endpoint: tcp://twenty_clickhouse:9000 + database: twenty_dev + username: default + password: devPassword + debug: + verbosity: detailed + +processors: + batch: + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [clickhouse, debug] \ No newline at end of file diff --git a/twenty/code/scripts/install.sh b/twenty/code/scripts/install.sh index 320696d13..2ae7bb4e1 100755 --- a/twenty/code/scripts/install.sh +++ b/twenty/code/scripts/install.sh @@ -94,7 +94,7 @@ echo "# === Randomly generated secret ===" >> .env echo "APP_SECRET=$(openssl rand -base64 32)" >> .env echo "" >> .env -echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> .env +echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 32)" >> .env echo -e "\t• .env configuration completed" diff --git a/twenty/code/twenty-website/Dockerfile b/twenty/code/twenty-website/Dockerfile index 3f64a074c..c760216b2 100644 --- a/twenty/code/twenty-website/Dockerfile +++ b/twenty/code/twenty-website/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.17.1-alpine as twenty-website-build +FROM node:22-alpine as twenty-website-build WORKDIR /app @@ -23,7 +23,7 @@ COPY ./packages/twenty-ui /app/packages/twenty-ui COPY ./packages/twenty-website /app/packages/twenty-website RUN npx nx build twenty-website -FROM node:18.17.1-alpine as twenty-website +FROM node:22-alpine as twenty-website WORKDIR /app/packages/twenty-website diff --git a/twenty/code/twenty/Dockerfile b/twenty/code/twenty/Dockerfile index 410069433..d5f71978c 100644 --- a/twenty/code/twenty/Dockerfile +++ b/twenty/code/twenty/Dockerfile @@ -1,5 +1,5 @@ # Base image for common dependencies -FROM node:18.17.1-alpine as common-deps +FROM node:22-alpine as common-deps WORKDIR /app @@ -49,7 +49,7 @@ RUN npx nx build twenty-front # Final stage: Run the application -FROM node:18.17.1-alpine as twenty +FROM node:22-alpine as twenty # Used to run healthcheck in docker RUN apk add --no-cache curl jq diff --git a/twenty/code/twenty/entrypoint.sh b/twenty/code/twenty/entrypoint.sh index a7de30a37..999049125 100755 --- a/twenty/code/twenty/entrypoint.sh +++ b/twenty/code/twenty/entrypoint.sh @@ -12,7 +12,7 @@ setup_and_migrate_db() { PGPASS=$(echo $PG_DATABASE_URL | awk -F ':' '{print $3}' | awk -F '@' '{print $1}') PGHOST=$(echo $PG_DATABASE_URL | awk -F '@' '{print $2}' | awk -F ':' '{print $1}') PGPORT=$(echo $PG_DATABASE_URL | awk -F ':' '{print $4}' | awk -F '/' '{print $1}') - PGDATABASE=$(echo $PG_DATABASE_URL | awk -F ':' '{print $4}' | awk -F '/' '{print $2}') + PGDATABASE=$(echo $PG_DATABASE_URL | awk -F '/' '{print $NF}' | cut -d'?' -f1) # Creating the database if it doesn't exist db_count=$(PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -tAc "SELECT COUNT(*) FROM pg_database WHERE datname = '${PGDATABASE}'") @@ -22,12 +22,29 @@ setup_and_migrate_db() { # Run setup and migration scripts NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts + yarn database:migrate:prod fi yarn command:prod upgrade echo "Successfully migrated DB!" } + +register_background_jobs() { + if [ "${DISABLE_CRON_JOBS_REGISTRATION}" = "true" ]; then + echo "Cron job registration is disabled, skipping..." + return + fi + + echo "Registering background sync jobs..." + if yarn command:prod cron:register:all; then + echo "Successfully registered all background sync jobs!" + else + echo "Warning: Failed to register background jobs, but continuing startup..." + fi +} + setup_and_migrate_db +register_background_jobs # Continue with the original Docker command exec "$@" From fa0b9127f12b3d7bff02b20db0614e289f844a6c Mon Sep 17 00:00:00 2001 From: Andrei Canta Date: Thu, 28 Aug 2025 15:52:36 +0300 Subject: [PATCH 2/6] run updates --- appwrite/code/.env.example | 2 +- appwrite/update.js | 6 + appwrite/update.sh | 14 -- dify/code/.env.example | 148 ++++++++++++++++++- dify/code/README.md | 103 +++++++------ dify/code/certbot/README.md | 8 +- dify/code/docker-compose-template.yaml | 37 ++++- dify/code/docker-compose.middleware.yaml | 4 +- dify/code/docker-compose.yaml | 104 ++++++++++++- dify/code/nginx/conf.d/default.conf.template | 5 +- dify/update.js | 6 + dify/update.sh | 13 -- plane/update.js | 24 +++ plane/update.sh | 14 -- supabase/code/.env.example | 2 +- supabase/code/docker-compose.yml | 8 +- supabase/update.js | 6 + supabase/update.sh | 11 -- twenty/code/twenty-website/Dockerfile | 5 +- twenty/code/twenty/Dockerfile | 19 +-- twenty/code/twenty/entrypoint.sh | 6 +- twenty/update.sh | 13 -- 22 files changed, 399 insertions(+), 159 deletions(-) delete mode 100644 appwrite/update.sh delete mode 100644 dify/update.sh delete mode 100644 plane/update.sh delete mode 100644 supabase/update.sh delete mode 100644 twenty/update.sh diff --git a/appwrite/code/.env.example b/appwrite/code/.env.example index ad4417749..ad6634857 100644 --- a/appwrite/code/.env.example +++ b/appwrite/code/.env.example @@ -6,7 +6,7 @@ _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_PROTECTION=disabled _APP_OPENSSL_KEY_V1=your-secret-key -_APP_DOMAIN=localhost +_APP_DOMAIN=$(PRIMARY_DOMAIN)localhost _APP_CUSTOM_DOMAIN_DENY_LIST=example.com,test.com,app.example.com _APP_DOMAIN_FUNCTIONS=functions.localhost _APP_DOMAIN_SITES=sites.localhost diff --git a/appwrite/update.js b/appwrite/update.js index c8308dae5..67a4d1595 100644 --- a/appwrite/update.js +++ b/appwrite/update.js @@ -11,3 +11,9 @@ await utils.downloadFile( await utils.removeContainerNames("./code/docker-compose.yml"); await utils.removePorts("./code/docker-compose.yml"); + +await utils.searchReplace( + "./code/.env.example", + "_APP_DOMAIN=", + "_APP_DOMAIN=$(PRIMARY_DOMAIN)" +); diff --git a/appwrite/update.sh b/appwrite/update.sh deleted file mode 100644 index 2ae62446e..000000000 --- a/appwrite/update.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -if [ ! -d "./repo" ]; then - git clone --depth 1 --branch main --single-branch https://github.com/appwrite/appwrite.git repo -else - cd repo - git pull - cd .. -fi - -curl -s https://appwrite.io/install/compose > ./code/docker-compose.yml -curl -s https://appwrite.io/install/env > ./code/.env.example - - diff --git a/dify/code/.env.example b/dify/code/.env.example index a024566c8..31aed9758 100644 --- a/dify/code/.env.example +++ b/dify/code/.env.example @@ -34,7 +34,7 @@ APP_API_URL= # used to display WebAPP API Base Url to the front-end. # If empty, it is the same domain. # Example: https://app.dify.ai -APP_WEB_URL= +APP_WEB_URL=https://$(PRIMARY_DOMAIN) # File preview or download Url prefix. # used to display File preview or download Url to the front-end or as Multi-model inputs; @@ -47,6 +47,16 @@ APP_WEB_URL= # ensuring port 5001 is externally accessible (see docker-compose.yaml). FILES_URL= +# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network. +# Set this to the internal Docker service URL for proper plugin file access. +# Example: INTERNAL_FILES_URL=http://api:5001 +INTERNAL_FILES_URL= + +# Ensure UTF-8 encoding +LANG=en_US.UTF-8 +LC_ALL=en_US.UTF-8 +PYTHONIOENCODING=utf-8 + # ------------------------------ # Server Configuration # ------------------------------ @@ -205,10 +215,16 @@ DB_DATABASE=dify # The size of the database connection pool. # The default is 30 connections, which can be appropriately increased. SQLALCHEMY_POOL_SIZE=30 +# The default is 10 connections, which allows temporary overflow beyond the pool size. +SQLALCHEMY_MAX_OVERFLOW=10 # Database connection pool recycling time, the default is 3600 seconds. SQLALCHEMY_POOL_RECYCLE=3600 # Whether to print SQL, default is false. SQLALCHEMY_ECHO=false +# If True, will test connections for liveness upon each checkout +SQLALCHEMY_POOL_PRE_PING=false +# Whether to enable the Last in first out option or use default FIFO queue if is false +SQLALCHEMY_POOL_USE_LIFO=false # Maximum number of connections to the database # Default is 100 @@ -250,6 +266,15 @@ REDIS_PORT=6379 REDIS_USERNAME= REDIS_PASSWORD=difyai123456 REDIS_USE_SSL=false +# SSL configuration for Redis (when REDIS_USE_SSL=true) +REDIS_SSL_CERT_REQS=CERT_NONE +# Options: CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED +REDIS_SSL_CA_CERTS= +# Path to CA certificate file for SSL verification +REDIS_SSL_CERTFILE= +# Path to client certificate file for SSL authentication +REDIS_SSL_KEYFILE= +# Path to client private key file for SSL authentication REDIS_DB=0 # Whether to use Redis Sentinel mode. @@ -274,12 +299,14 @@ REDIS_CLUSTERS_PASSWORD= # Celery Configuration # ------------------------------ -# Use redis as the broker, and redis db 1 for celery broker. -# Format as follows: `redis://:@:/` +# Use standalone redis as the broker, and redis db 1 for celery broker. (redis_username is usually set by defualt as empty) +# Format as follows: `redis://:@:/`. # Example: redis://:difyai123456@redis:6379/1 -# If use Redis Sentinel, format as follows: `sentinel://:@:/` -# Example: sentinel://localhost:26379/1;sentinel://localhost:26380/1;sentinel://localhost:26381/1 +# If use Redis Sentinel, format as follows: `sentinel://:@:/` +# For high availability, you can configure multiple Sentinel nodes (if provided) separated by semicolons like below example: +# Example: sentinel://:difyai123456@localhost:26379/1;sentinel://:difyai12345@localhost:26379/1;sentinel://:difyai12345@localhost:26379/1 CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1 +CELERY_BACKEND=redis BROKER_USE_SSL=false # If you are using Redis Sentinel for high availability, configure the following settings. @@ -317,6 +344,25 @@ OPENDAL_SCHEME=fs # Configurations for OpenDAL Local File System. OPENDAL_FS_ROOT=storage +# ClickZetta Volume Configuration (for storage backend) +# To use ClickZetta Volume as storage backend, set STORAGE_TYPE=clickzetta-volume +# Note: ClickZetta Volume will reuse the existing CLICKZETTA_* connection parameters + +# Volume type selection (three types available): +# - user: Personal/small team use, simple config, user-level permissions +# - table: Enterprise multi-tenant, smart routing, table-level + user-level permissions +# - external: Data lake integration, external storage connection, volume-level + storage-level permissions +CLICKZETTA_VOLUME_TYPE=user + +# External Volume name (required only when TYPE=external) +CLICKZETTA_VOLUME_NAME= + +# Table Volume table prefix (used only when TYPE=table) +CLICKZETTA_VOLUME_TABLE_PREFIX=dataset_ + +# Dify file directory prefix (isolates from other apps, recommended to keep default) +CLICKZETTA_VOLUME_DIFY_PREFIX=dify_km + # S3 Configuration # S3_ENDPOINT= @@ -400,8 +446,10 @@ SUPABASE_URL=your-server-url # ------------------------------ # The type of vector store to use. -# Supported values are `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `oceanbase`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`. +# Supported values are `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `oceanbase`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`, `clickzetta`. VECTOR_STORE=weaviate +# Prefix used to create collection name in vector database +VECTOR_INDEX_NAME_PREFIX=Vector_index # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. WEAVIATE_ENDPOINT=http://weaviate:8080 @@ -565,6 +613,17 @@ ELASTICSEARCH_USERNAME=elastic ELASTICSEARCH_PASSWORD=elastic KIBANA_PORT=5601 +# Using ElasticSearch Cloud Serverless, or not. +ELASTICSEARCH_USE_CLOUD=false +ELASTICSEARCH_CLOUD_URL=YOUR-ELASTICSEARCH_CLOUD_URL +ELASTICSEARCH_API_KEY=YOUR-ELASTICSEARCH_API_KEY + +ELASTICSEARCH_VERIFY_CERTS=False +ELASTICSEARCH_CA_CERTS= +ELASTICSEARCH_REQUEST_TIMEOUT=100000 +ELASTICSEARCH_RETRY_ON_TIMEOUT=True +ELASTICSEARCH_MAX_RETRIES=10 + # baidu vector configurations, only available when VECTOR_STORE is `baidu` BAIDU_VECTOR_DB_ENDPOINT=http://127.0.0.1:5287 BAIDU_VECTOR_DB_CONNECTION_TIMEOUT_MS=30000 @@ -624,6 +683,21 @@ TABLESTORE_ENDPOINT=https://instance-name.cn-hangzhou.ots.aliyuncs.com TABLESTORE_INSTANCE_NAME=instance-name TABLESTORE_ACCESS_KEY_ID=xxx TABLESTORE_ACCESS_KEY_SECRET=xxx +TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE=false + +# Clickzetta configuration, only available when VECTOR_STORE is `clickzetta` +CLICKZETTA_USERNAME= +CLICKZETTA_PASSWORD= +CLICKZETTA_INSTANCE= +CLICKZETTA_SERVICE=api.clickzetta.com +CLICKZETTA_WORKSPACE=quick_start +CLICKZETTA_VCLUSTER=default_ap +CLICKZETTA_SCHEMA=dify +CLICKZETTA_BATCH_SIZE=100 +CLICKZETTA_ENABLE_INVERTED_INDEX=true +CLICKZETTA_ANALYZER_TYPE=chinese +CLICKZETTA_ANALYZER_MODE=smart +CLICKZETTA_VECTOR_DISTANCE_FUNCTION=cosine_distance # ------------------------------ # Knowledge Configuration @@ -705,6 +779,12 @@ API_SENTRY_PROFILES_SAMPLE_RATE=1.0 # If not set, Sentry error reporting will be disabled. WEB_SENTRY_DSN= +# Plugin_daemon Service Sentry DSN address, default is empty, when empty, +# all monitoring information is not reported to Sentry. +# If not set, Sentry error reporting will be disabled. +PLUGIN_SENTRY_ENABLED=false +PLUGIN_SENTRY_DSN= + # ------------------------------ # Notion Integration Configuration # Variables can be obtained by applying for Notion integration: https://www.notion.so/my-integrations @@ -763,6 +843,8 @@ INVITE_EXPIRY_HOURS=72 # Reset password token valid time (minutes), RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 +CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 +OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 # The sandbox service endpoint. CODE_EXECUTION_ENDPOINT=http://sandbox:8194 @@ -794,6 +876,33 @@ WORKFLOW_FILE_UPLOAD_LIMIT=10 # hybrid: Save new data to object storage, read from both object storage and RDBMS WORKFLOW_NODE_EXECUTION_STORAGE=rdbms +# Repository configuration +# Core workflow execution repository implementation +# Options: +# - core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository (default) +# - core.repositories.celery_workflow_execution_repository.CeleryWorkflowExecutionRepository +CORE_WORKFLOW_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository + +# Core workflow node execution repository implementation +# Options: +# - core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository (default) +# - core.repositories.celery_workflow_node_execution_repository.CeleryWorkflowNodeExecutionRepository +CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository + +# API workflow run repository implementation +API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository + +# API workflow node execution repository implementation +API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository + +# Workflow log cleanup configuration +# Enable automatic cleanup of workflow run logs to manage database size +WORKFLOW_LOG_CLEANUP_ENABLED=false +# Number of days to retain workflow run logs (default: 30 days) +WORKFLOW_LOG_RETENTION_DAYS=30 +# Batch size for workflow log cleanup operations (default: 100) +WORKFLOW_LOG_CLEANUP_BATCH_SIZE=100 + # HTTP request node in workflow configuration HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760 HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576 @@ -826,6 +935,12 @@ MAX_ITERATIONS_NUM=99 # The timeout for the text generation in millisecond TEXT_GENERATION_TIMEOUT_MS=60000 +# Allow rendering unsafe URLs which have "data:" scheme. +ALLOW_UNSAFE_DATA_SCHEME=false + +# Maximum number of tree depth in the workflow +MAX_TREE_DEPTH=50 + # ------------------------------ # Environment Variables for db Service # ------------------------------ @@ -958,7 +1073,7 @@ NGINX_SSL_PROTOCOLS=TLSv1.1 TLSv1.2 TLSv1.3 # Nginx performance tuning NGINX_WORKER_PROCESSES=auto -NGINX_CLIENT_MAX_BODY_SIZE=15M +NGINX_CLIENT_MAX_BODY_SIZE=100M NGINX_KEEPALIVE_TIMEOUT=65 # Proxy settings @@ -1067,6 +1182,9 @@ MARKETPLACE_API_URL=https://marketplace.dify.ai FORCE_VERIFYING_SIGNATURE=true +PLUGIN_STDIO_BUFFER_SIZE=1024 +PLUGIN_STDIO_MAX_BUFFER_SIZE=5242880 + PLUGIN_PYTHON_ENV_INIT_TIMEOUT=120 PLUGIN_MAX_EXECUTION_TIMEOUT=600 # PIP_MIRROR_URL=https://pypi.tuna.tsinghua.edu.cn/simple @@ -1114,6 +1232,8 @@ PLUGIN_VOLCENGINE_TOS_REGION= # OTLP Collector Configuration # ------------------------------ ENABLE_OTEL=false +OTLP_TRACE_ENDPOINT= +OTLP_METRIC_ENDPOINT= OTLP_BASE_ENDPOINT=http://localhost:4318 OTLP_API_KEY= OTEL_EXPORTER_OTLP_PROTOCOL= @@ -1135,3 +1255,17 @@ QUEUE_MONITOR_THRESHOLD=200 QUEUE_MONITOR_ALERT_EMAILS= # Monitor interval in minutes, default is 30 minutes QUEUE_MONITOR_INTERVAL=30 + +# Swagger UI configuration +SWAGGER_UI_ENABLED=true +SWAGGER_UI_PATH=/swagger-ui.html + +# Celery schedule tasks configuration +ENABLE_CLEAN_EMBEDDING_CACHE_TASK=false +ENABLE_CLEAN_UNUSED_DATASETS_TASK=false +ENABLE_CREATE_TIDB_SERVERLESS_TASK=false +ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK=false +ENABLE_CLEAN_MESSAGES=false +ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK=false +ENABLE_DATASETS_QUEUE_MONITOR=false +ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK=true diff --git a/dify/code/README.md b/dify/code/README.md index 22dfe2c91..b5c46eb9f 100644 --- a/dify/code/README.md +++ b/dify/code/README.md @@ -4,7 +4,7 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T ### What's Updated -- **Certbot Container**: `docker-compose.yaml` now contains `certbot` for managing SSL certificates. This container automatically renews certificates and ensures secure HTTPS connections. +- **Certbot Container**: `docker-compose.yaml` now contains `certbot` for managing SSL certificates. This container automatically renews certificates and ensures secure HTTPS connections.\ For more information, refer `docker/certbot/README.md`. - **Persistent Environment Variables**: Environment variables are now managed through a `.env` file, ensuring that your configurations persist across deployments. @@ -13,43 +13,44 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T > The `.env` file is a crucial component in Docker and Docker Compose environments, serving as a centralized configuration file where you can define environment variables that are accessible to the containers at runtime. This file simplifies the management of environment settings across different stages of development, testing, and production, providing consistency and ease of configuration to deployments. - **Unified Vector Database Services**: All vector database services are now managed from a single Docker Compose file `docker-compose.yaml`. You can switch between different vector databases by setting the `VECTOR_STORE` environment variable in your `.env` file. + - **Mandatory .env File**: A `.env` file is now required to run `docker compose up`. This file is crucial for configuring your deployment and for any custom settings to persist through upgrades. ### How to Deploy Dify with `docker-compose.yaml` 1. **Prerequisites**: Ensure Docker and Docker Compose are installed on your system. -2. **Environment Setup**: - - Navigate to the `docker` directory. - - Copy the `.env.example` file to a new file named `.env` by running `cp .env.example .env`. - - Customize the `.env` file as needed. Refer to the `.env.example` file for detailed configuration options. -3. **Running the Services**: - - Execute `docker compose up` from the `docker` directory to start the services. - - To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`. -4. **SSL Certificate Setup**: - - Refer `docker/certbot/README.md` to set up SSL certificates using Certbot. -5. **OpenTelemetry Collector Setup**: +1. **Environment Setup**: + - Navigate to the `docker` directory. + - Copy the `.env.example` file to a new file named `.env` by running `cp .env.example .env`. + - Customize the `.env` file as needed. Refer to the `.env.example` file for detailed configuration options. +1. **Running the Services**: + - Execute `docker compose up` from the `docker` directory to start the services. + - To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`. +1. **SSL Certificate Setup**: + - Refer `docker/certbot/README.md` to set up SSL certificates using Certbot. +1. **OpenTelemetry Collector Setup**: - Change `ENABLE_OTEL` to `true` in `.env`. - Configure `OTLP_BASE_ENDPOINT` properly. ### How to Deploy Middleware for Developing Dify 1. **Middleware Setup**: - - Use the `docker-compose.middleware.yaml` for setting up essential middleware services like databases and caches. - - Navigate to the `docker` directory. - - Ensure the `middleware.env` file is created by running `cp middleware.env.example middleware.env` (refer to the `middleware.env.example` file). -2. **Running Middleware Services**: - - Navigate to the `docker` directory. - - Execute `docker compose -f docker-compose.middleware.yaml --profile weaviate -p dify up -d` to start the middleware services. (Change the profile to other vector database if you are not using weaviate) + - Use the `docker-compose.middleware.yaml` for setting up essential middleware services like databases and caches. + - Navigate to the `docker` directory. + - Ensure the `middleware.env` file is created by running `cp middleware.env.example middleware.env` (refer to the `middleware.env.example` file). +1. **Running Middleware Services**: + - Navigate to the `docker` directory. + - Execute `docker compose -f docker-compose.middleware.yaml --profile weaviate -p dify up -d` to start the middleware services. (Change the profile to other vector database if you are not using weaviate) ### Migration for Existing Users For users migrating from the `docker-legacy` setup: 1. **Review Changes**: Familiarize yourself with the new `.env` configuration and Docker Compose setup. -2. **Transfer Customizations**: - - If you have customized configurations such as `docker-compose.yaml`, `ssrf_proxy/squid.conf`, or `nginx/conf.d/default.conf`, you will need to reflect these changes in the `.env` file you create. -3. **Data Migration**: - - Ensure that data from services like databases and caches is backed up and migrated appropriately to the new structure if necessary. +1. **Transfer Customizations**: + - If you have customized configurations such as `docker-compose.yaml`, `ssrf_proxy/squid.conf`, or `nginx/conf.d/default.conf`, you will need to reflect these changes in the `.env` file you create. +1. **Data Migration**: + - Ensure that data from services like databases and caches is backed up and migrated appropriately to the new structure if necessary. ### Overview of `.env` @@ -64,39 +65,49 @@ For users migrating from the `docker-legacy` setup: The `.env.example` file provided in the Docker setup is extensive and covers a wide range of configuration options. It is structured into several sections, each pertaining to different aspects of the application and its services. Here are some of the key sections and variables: 1. **Common Variables**: - - `CONSOLE_API_URL`, `SERVICE_API_URL`: URLs for different API services. - - `APP_WEB_URL`: Frontend application URL. - - `FILES_URL`: Base URL for file downloads and previews. -2. **Server Configuration**: - - `LOG_LEVEL`, `DEBUG`, `FLASK_DEBUG`: Logging and debug settings. - - `SECRET_KEY`: A key for encrypting session cookies and other sensitive data. + - `CONSOLE_API_URL`, `SERVICE_API_URL`: URLs for different API services. + - `APP_WEB_URL`: Frontend application URL. + - `FILES_URL`: Base URL for file downloads and previews. + +1. **Server Configuration**: + + - `LOG_LEVEL`, `DEBUG`, `FLASK_DEBUG`: Logging and debug settings. + - `SECRET_KEY`: A key for encrypting session cookies and other sensitive data. + +1. **Database Configuration**: + + - `DB_USERNAME`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`: PostgreSQL database credentials and connection details. + +1. **Redis Configuration**: + + - `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`: Redis server connection settings. + +1. **Celery Configuration**: + + - `CELERY_BROKER_URL`: Configuration for Celery message broker. + +1. **Storage Configuration**: + + - `STORAGE_TYPE`, `S3_BUCKET_NAME`, `AZURE_BLOB_ACCOUNT_NAME`: Settings for file storage options like local, S3, Azure Blob, etc. + +1. **Vector Database Configuration**: -3. **Database Configuration**: - - `DB_USERNAME`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`: PostgreSQL database credentials and connection details. + - `VECTOR_STORE`: Type of vector database (e.g., `weaviate`, `milvus`). + - Specific settings for each vector store like `WEAVIATE_ENDPOINT`, `MILVUS_URI`. -4. **Redis Configuration**: - - `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`: Redis server connection settings. +1. **CORS Configuration**: -5. **Celery Configuration**: - - `CELERY_BROKER_URL`: Configuration for Celery message broker. + - `WEB_API_CORS_ALLOW_ORIGINS`, `CONSOLE_CORS_ALLOW_ORIGINS`: Settings for cross-origin resource sharing. -6. **Storage Configuration**: - - `STORAGE_TYPE`, `S3_BUCKET_NAME`, `AZURE_BLOB_ACCOUNT_NAME`: Settings for file storage options like local, S3, Azure Blob, etc. +1. **OpenTelemetry Configuration**: -7. **Vector Database Configuration**: - - `VECTOR_STORE`: Type of vector database (e.g., `weaviate`, `milvus`). - - Specific settings for each vector store like `WEAVIATE_ENDPOINT`, `MILVUS_URI`. + - `ENABLE_OTEL`: Enable OpenTelemetry collector in api. + - `OTLP_BASE_ENDPOINT`: Endpoint for your OTLP exporter. -8. **CORS Configuration**: - - `WEB_API_CORS_ALLOW_ORIGINS`, `CONSOLE_CORS_ALLOW_ORIGINS`: Settings for cross-origin resource sharing. +1. **Other Service-Specific Environment Variables**: -9. **OpenTelemetry Configuration**: - - `ENABLE_OTEL`: Enable OpenTelemetry collector in api. - - `OTLP_BASE_ENDPOINT`: Endpoint for your OTLP exporter. - -10. **Other Service-Specific Environment Variables**: - - Each service like `nginx`, `redis`, `db`, and vector databases have specific environment variables that are directly referenced in the `docker-compose.yaml`. + - Each service like `nginx`, `redis`, `db`, and vector databases have specific environment variables that are directly referenced in the `docker-compose.yaml`. ### Additional Information diff --git a/dify/code/certbot/README.md b/dify/code/certbot/README.md index 21be34b33..62b1eee39 100644 --- a/dify/code/certbot/README.md +++ b/dify/code/certbot/README.md @@ -2,12 +2,12 @@ ## Short description -docker compose certbot configurations with Backward compatibility (without certbot container). +docker compose certbot configurations with Backward compatibility (without certbot container).\ Use `docker compose --profile certbot up` to use this features. ## The simplest way for launching new servers with SSL certificates -1. Get letsencrypt certs +1. Get letsencrypt certs\ set `.env` values ```properties NGINX_SSL_CERT_FILENAME=fullchain.pem @@ -25,7 +25,7 @@ Use `docker compose --profile certbot up` to use this features. ```shell docker compose exec -it certbot /bin/sh /update-cert.sh ``` -2. Edit `.env` file and `docker compose --profile certbot up` again. +1. Edit `.env` file and `docker compose --profile certbot up` again.\ set `.env` value additionally ```properties NGINX_HTTPS_ENABLED=true @@ -34,7 +34,7 @@ Use `docker compose --profile certbot up` to use this features. ```shell docker compose --profile certbot up -d --no-deps --force-recreate nginx ``` - Then you can access your serve with HTTPS. + Then you can access your serve with HTTPS.\ [https://your_domain.com](https://your_domain.com) ## SSL certificates renewal diff --git a/dify/code/docker-compose-template.yaml b/dify/code/docker-compose-template.yaml index d45f8f8bf..0e695e4fc 100644 --- a/dify/code/docker-compose-template.yaml +++ b/dify/code/docker-compose-template.yaml @@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.5.1 + image: langgenius/dify-api:1.8.0 restart: always environment: # Use the shared environment variables. @@ -31,7 +31,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.5.1 + image: langgenius/dify-api:1.8.0 restart: always environment: # Use the shared environment variables. @@ -55,9 +55,28 @@ services: - ssrf_proxy_network - default + # worker_beat service + # Celery beat for scheduling periodic tasks. + worker_beat: + image: langgenius/dify-api:1.8.0 + restart: always + environment: + # Use the shared environment variables. + <<: *shared-api-worker-env + # Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks. + MODE: beat + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + networks: + - ssrf_proxy_network + - default + # Frontend web application. web: - image: langgenius/dify-web:1.5.1 + image: langgenius/dify-web:1.8.0 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -67,6 +86,7 @@ services: TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} CSP_WHITELIST: ${CSP_WHITELIST:-} ALLOW_EMBED: ${ALLOW_EMBED:-false} + ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false} MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai} MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai} TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-} @@ -76,6 +96,7 @@ services: MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99} + MAX_TREE_DEPTH: ${MAX_TREE_DEPTH:-50} ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true} ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true} ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true} @@ -142,7 +163,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.1.3-local + image: langgenius/dify-plugin-daemon:0.2.0-local restart: always environment: # Use the shared environment variables. @@ -160,6 +181,8 @@ services: FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true} PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120} PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600} + PLUGIN_STDIO_BUFFER_SIZE: ${PLUGIN_STDIO_BUFFER_SIZE:-1024} + PLUGIN_STDIO_MAX_BUFFER_SIZE: ${PLUGIN_STDIO_MAX_BUFFER_SIZE:-5242880} PIP_MIRROR_URL: ${PIP_MIRROR_URL:-} PLUGIN_STORAGE_TYPE: ${PLUGIN_STORAGE_TYPE:-local} PLUGIN_STORAGE_LOCAL_ROOT: ${PLUGIN_STORAGE_LOCAL_ROOT:-/app/storage} @@ -189,6 +212,8 @@ services: VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-} VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-} VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-} + SENTRY_ENABLED: ${PLUGIN_SENTRY_ENABLED:-false} + SENTRY_DSN: ${PLUGIN_SENTRY_DSN:-} ports: - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" volumes: @@ -265,7 +290,7 @@ services: NGINX_SSL_CERT_KEY_FILENAME: ${NGINX_SSL_CERT_KEY_FILENAME:-dify.key} NGINX_SSL_PROTOCOLS: ${NGINX_SSL_PROTOCOLS:-TLSv1.1 TLSv1.2 TLSv1.3} NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto} - NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-15M} + NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M} NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65} NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} @@ -518,7 +543,7 @@ services: milvus-standalone: container_name: milvus-standalone - image: milvusdb/milvus:v2.5.0-beta + image: milvusdb/milvus:v2.5.15 profiles: - milvus command: [ 'milvus', 'run', 'standalone' ] diff --git a/dify/code/docker-compose.middleware.yaml b/dify/code/docker-compose.middleware.yaml index 0b1885755..9f7cc7258 100644 --- a/dify/code/docker-compose.middleware.yaml +++ b/dify/code/docker-compose.middleware.yaml @@ -20,7 +20,7 @@ services: ports: - "${EXPOSE_POSTGRES_PORT:-5432}:5432" healthcheck: - test: [ "CMD", "pg_isready" ] + test: [ 'CMD', 'pg_isready', '-h', 'db', '-U', '${PGUSER:-postgres}', '-d', '${POSTGRES_DB:-dify}' ] interval: 1s timeout: 3s retries: 30 @@ -71,7 +71,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.1.3-local + image: langgenius/dify-plugin-daemon:0.2.0-local restart: always env_file: - ./middleware.env diff --git a/dify/code/docker-compose.yaml b/dify/code/docker-compose.yaml index 9b65af5d3..7f24227a1 100644 --- a/dify/code/docker-compose.yaml +++ b/dify/code/docker-compose.yaml @@ -11,6 +11,10 @@ x-shared-env: &shared-api-worker-env APP_API_URL: ${APP_API_URL:-} APP_WEB_URL: ${APP_WEB_URL:-} FILES_URL: ${FILES_URL:-} + INTERNAL_FILES_URL: ${INTERNAL_FILES_URL:-} + LANG: ${LANG:-en_US.UTF-8} + LC_ALL: ${LC_ALL:-en_US.UTF-8} + PYTHONIOENCODING: ${PYTHONIOENCODING:-utf-8} LOG_LEVEL: ${LOG_LEVEL:-INFO} LOG_FILE: ${LOG_FILE:-/app/logs/server.log} LOG_FILE_MAX_SIZE: ${LOG_FILE_MAX_SIZE:-20} @@ -53,8 +57,11 @@ x-shared-env: &shared-api-worker-env DB_PORT: ${DB_PORT:-5432} DB_DATABASE: ${DB_DATABASE:-dify} SQLALCHEMY_POOL_SIZE: ${SQLALCHEMY_POOL_SIZE:-30} + SQLALCHEMY_MAX_OVERFLOW: ${SQLALCHEMY_MAX_OVERFLOW:-10} SQLALCHEMY_POOL_RECYCLE: ${SQLALCHEMY_POOL_RECYCLE:-3600} SQLALCHEMY_ECHO: ${SQLALCHEMY_ECHO:-false} + SQLALCHEMY_POOL_PRE_PING: ${SQLALCHEMY_POOL_PRE_PING:-false} + SQLALCHEMY_POOL_USE_LIFO: ${SQLALCHEMY_POOL_USE_LIFO:-false} POSTGRES_MAX_CONNECTIONS: ${POSTGRES_MAX_CONNECTIONS:-100} POSTGRES_SHARED_BUFFERS: ${POSTGRES_SHARED_BUFFERS:-128MB} POSTGRES_WORK_MEM: ${POSTGRES_WORK_MEM:-4MB} @@ -65,6 +72,10 @@ x-shared-env: &shared-api-worker-env REDIS_USERNAME: ${REDIS_USERNAME:-} REDIS_PASSWORD: ${REDIS_PASSWORD:-difyai123456} REDIS_USE_SSL: ${REDIS_USE_SSL:-false} + REDIS_SSL_CERT_REQS: ${REDIS_SSL_CERT_REQS:-CERT_NONE} + REDIS_SSL_CA_CERTS: ${REDIS_SSL_CA_CERTS:-} + REDIS_SSL_CERTFILE: ${REDIS_SSL_CERTFILE:-} + REDIS_SSL_KEYFILE: ${REDIS_SSL_KEYFILE:-} REDIS_DB: ${REDIS_DB:-0} REDIS_USE_SENTINEL: ${REDIS_USE_SENTINEL:-false} REDIS_SENTINELS: ${REDIS_SENTINELS:-} @@ -76,6 +87,7 @@ x-shared-env: &shared-api-worker-env REDIS_CLUSTERS: ${REDIS_CLUSTERS:-} REDIS_CLUSTERS_PASSWORD: ${REDIS_CLUSTERS_PASSWORD:-} CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://:difyai123456@redis:6379/1} + CELERY_BACKEND: ${CELERY_BACKEND:-redis} BROKER_USE_SSL: ${BROKER_USE_SSL:-false} CELERY_USE_SENTINEL: ${CELERY_USE_SENTINEL:-false} CELERY_SENTINEL_MASTER_NAME: ${CELERY_SENTINEL_MASTER_NAME:-} @@ -86,6 +98,10 @@ x-shared-env: &shared-api-worker-env STORAGE_TYPE: ${STORAGE_TYPE:-opendal} OPENDAL_SCHEME: ${OPENDAL_SCHEME:-fs} OPENDAL_FS_ROOT: ${OPENDAL_FS_ROOT:-storage} + CLICKZETTA_VOLUME_TYPE: ${CLICKZETTA_VOLUME_TYPE:-user} + CLICKZETTA_VOLUME_NAME: ${CLICKZETTA_VOLUME_NAME:-} + CLICKZETTA_VOLUME_TABLE_PREFIX: ${CLICKZETTA_VOLUME_TABLE_PREFIX:-dataset_} + CLICKZETTA_VOLUME_DIFY_PREFIX: ${CLICKZETTA_VOLUME_DIFY_PREFIX:-dify_km} S3_ENDPOINT: ${S3_ENDPOINT:-} S3_REGION: ${S3_REGION:-us-east-1} S3_BUCKET_NAME: ${S3_BUCKET_NAME:-difyai} @@ -132,6 +148,7 @@ x-shared-env: &shared-api-worker-env SUPABASE_API_KEY: ${SUPABASE_API_KEY:-your-access-key} SUPABASE_URL: ${SUPABASE_URL:-your-server-url} VECTOR_STORE: ${VECTOR_STORE:-weaviate} + VECTOR_INDEX_NAME_PREFIX: ${VECTOR_INDEX_NAME_PREFIX:-Vector_index} WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080} WEAVIATE_API_KEY: ${WEAVIATE_API_KEY:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih} QDRANT_URL: ${QDRANT_URL:-http://qdrant:6333} @@ -253,6 +270,14 @@ x-shared-env: &shared-api-worker-env ELASTICSEARCH_USERNAME: ${ELASTICSEARCH_USERNAME:-elastic} ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD:-elastic} KIBANA_PORT: ${KIBANA_PORT:-5601} + ELASTICSEARCH_USE_CLOUD: ${ELASTICSEARCH_USE_CLOUD:-false} + ELASTICSEARCH_CLOUD_URL: ${ELASTICSEARCH_CLOUD_URL:-YOUR-ELASTICSEARCH_CLOUD_URL} + ELASTICSEARCH_API_KEY: ${ELASTICSEARCH_API_KEY:-YOUR-ELASTICSEARCH_API_KEY} + ELASTICSEARCH_VERIFY_CERTS: ${ELASTICSEARCH_VERIFY_CERTS:-False} + ELASTICSEARCH_CA_CERTS: ${ELASTICSEARCH_CA_CERTS:-} + ELASTICSEARCH_REQUEST_TIMEOUT: ${ELASTICSEARCH_REQUEST_TIMEOUT:-100000} + ELASTICSEARCH_RETRY_ON_TIMEOUT: ${ELASTICSEARCH_RETRY_ON_TIMEOUT:-True} + ELASTICSEARCH_MAX_RETRIES: ${ELASTICSEARCH_MAX_RETRIES:-10} BAIDU_VECTOR_DB_ENDPOINT: ${BAIDU_VECTOR_DB_ENDPOINT:-http://127.0.0.1:5287} BAIDU_VECTOR_DB_CONNECTION_TIMEOUT_MS: ${BAIDU_VECTOR_DB_CONNECTION_TIMEOUT_MS:-30000} BAIDU_VECTOR_DB_ACCOUNT: ${BAIDU_VECTOR_DB_ACCOUNT:-root} @@ -296,6 +321,19 @@ x-shared-env: &shared-api-worker-env TABLESTORE_INSTANCE_NAME: ${TABLESTORE_INSTANCE_NAME:-instance-name} TABLESTORE_ACCESS_KEY_ID: ${TABLESTORE_ACCESS_KEY_ID:-xxx} TABLESTORE_ACCESS_KEY_SECRET: ${TABLESTORE_ACCESS_KEY_SECRET:-xxx} + TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE: ${TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE:-false} + CLICKZETTA_USERNAME: ${CLICKZETTA_USERNAME:-} + CLICKZETTA_PASSWORD: ${CLICKZETTA_PASSWORD:-} + CLICKZETTA_INSTANCE: ${CLICKZETTA_INSTANCE:-} + CLICKZETTA_SERVICE: ${CLICKZETTA_SERVICE:-api.clickzetta.com} + CLICKZETTA_WORKSPACE: ${CLICKZETTA_WORKSPACE:-quick_start} + CLICKZETTA_VCLUSTER: ${CLICKZETTA_VCLUSTER:-default_ap} + CLICKZETTA_SCHEMA: ${CLICKZETTA_SCHEMA:-dify} + CLICKZETTA_BATCH_SIZE: ${CLICKZETTA_BATCH_SIZE:-100} + CLICKZETTA_ENABLE_INVERTED_INDEX: ${CLICKZETTA_ENABLE_INVERTED_INDEX:-true} + CLICKZETTA_ANALYZER_TYPE: ${CLICKZETTA_ANALYZER_TYPE:-chinese} + CLICKZETTA_ANALYZER_MODE: ${CLICKZETTA_ANALYZER_MODE:-smart} + CLICKZETTA_VECTOR_DISTANCE_FUNCTION: ${CLICKZETTA_VECTOR_DISTANCE_FUNCTION:-cosine_distance} UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15} UPLOAD_FILE_BATCH_LIMIT: ${UPLOAD_FILE_BATCH_LIMIT:-5} ETL_TYPE: ${ETL_TYPE:-dify} @@ -314,6 +352,8 @@ x-shared-env: &shared-api-worker-env API_SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0} API_SENTRY_PROFILES_SAMPLE_RATE: ${API_SENTRY_PROFILES_SAMPLE_RATE:-1.0} WEB_SENTRY_DSN: ${WEB_SENTRY_DSN:-} + PLUGIN_SENTRY_ENABLED: ${PLUGIN_SENTRY_ENABLED:-false} + PLUGIN_SENTRY_DSN: ${PLUGIN_SENTRY_DSN:-} NOTION_INTEGRATION_TYPE: ${NOTION_INTEGRATION_TYPE:-public} NOTION_CLIENT_SECRET: ${NOTION_CLIENT_SECRET:-} NOTION_CLIENT_ID: ${NOTION_CLIENT_ID:-} @@ -332,6 +372,8 @@ x-shared-env: &shared-api-worker-env INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000} INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72} RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5} + CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: ${CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES:-5} + OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES: ${OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES:-5} CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194} CODE_EXECUTION_API_KEY: ${CODE_EXECUTION_API_KEY:-dify-sandbox} CODE_MAX_NUMBER: ${CODE_MAX_NUMBER:-9223372036854775807} @@ -353,6 +395,13 @@ x-shared-env: &shared-api-worker-env WORKFLOW_PARALLEL_DEPTH_LIMIT: ${WORKFLOW_PARALLEL_DEPTH_LIMIT:-3} WORKFLOW_FILE_UPLOAD_LIMIT: ${WORKFLOW_FILE_UPLOAD_LIMIT:-10} WORKFLOW_NODE_EXECUTION_STORAGE: ${WORKFLOW_NODE_EXECUTION_STORAGE:-rdbms} + CORE_WORKFLOW_EXECUTION_REPOSITORY: ${CORE_WORKFLOW_EXECUTION_REPOSITORY:-core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository} + CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY: ${CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY:-core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository} + API_WORKFLOW_RUN_REPOSITORY: ${API_WORKFLOW_RUN_REPOSITORY:-repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository} + API_WORKFLOW_NODE_EXECUTION_REPOSITORY: ${API_WORKFLOW_NODE_EXECUTION_REPOSITORY:-repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository} + WORKFLOW_LOG_CLEANUP_ENABLED: ${WORKFLOW_LOG_CLEANUP_ENABLED:-false} + WORKFLOW_LOG_RETENTION_DAYS: ${WORKFLOW_LOG_RETENTION_DAYS:-30} + WORKFLOW_LOG_CLEANUP_BATCH_SIZE: ${WORKFLOW_LOG_CLEANUP_BATCH_SIZE:-100} HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760} HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576} HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True} @@ -364,6 +413,8 @@ x-shared-env: &shared-api-worker-env MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99} TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} + ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false} + MAX_TREE_DEPTH: ${MAX_TREE_DEPTH:-50} POSTGRES_USER: ${POSTGRES_USER:-${DB_USERNAME}} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-${DB_PASSWORD}} POSTGRES_DB: ${POSTGRES_DB:-${DB_DATABASE}} @@ -420,7 +471,7 @@ x-shared-env: &shared-api-worker-env NGINX_SSL_CERT_KEY_FILENAME: ${NGINX_SSL_CERT_KEY_FILENAME:-dify.key} NGINX_SSL_PROTOCOLS: ${NGINX_SSL_PROTOCOLS:-TLSv1.1 TLSv1.2 TLSv1.3} NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto} - NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-15M} + NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M} NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65} NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} @@ -465,6 +516,8 @@ x-shared-env: &shared-api-worker-env MARKETPLACE_ENABLED: ${MARKETPLACE_ENABLED:-true} MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai} FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true} + PLUGIN_STDIO_BUFFER_SIZE: ${PLUGIN_STDIO_BUFFER_SIZE:-1024} + PLUGIN_STDIO_MAX_BUFFER_SIZE: ${PLUGIN_STDIO_MAX_BUFFER_SIZE:-5242880} PLUGIN_PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120} PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600} PIP_MIRROR_URL: ${PIP_MIRROR_URL:-} @@ -498,6 +551,8 @@ x-shared-env: &shared-api-worker-env PLUGIN_VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-} PLUGIN_VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-} ENABLE_OTEL: ${ENABLE_OTEL:-false} + OTLP_TRACE_ENDPOINT: ${OTLP_TRACE_ENDPOINT:-} + OTLP_METRIC_ENDPOINT: ${OTLP_METRIC_ENDPOINT:-} OTLP_BASE_ENDPOINT: ${OTLP_BASE_ENDPOINT:-http://localhost:4318} OTLP_API_KEY: ${OTLP_API_KEY:-} OTEL_EXPORTER_OTLP_PROTOCOL: ${OTEL_EXPORTER_OTLP_PROTOCOL:-} @@ -513,11 +568,21 @@ x-shared-env: &shared-api-worker-env QUEUE_MONITOR_THRESHOLD: ${QUEUE_MONITOR_THRESHOLD:-200} QUEUE_MONITOR_ALERT_EMAILS: ${QUEUE_MONITOR_ALERT_EMAILS:-} QUEUE_MONITOR_INTERVAL: ${QUEUE_MONITOR_INTERVAL:-30} + SWAGGER_UI_ENABLED: ${SWAGGER_UI_ENABLED:-true} + SWAGGER_UI_PATH: ${SWAGGER_UI_PATH:-/swagger-ui.html} + ENABLE_CLEAN_EMBEDDING_CACHE_TASK: ${ENABLE_CLEAN_EMBEDDING_CACHE_TASK:-false} + ENABLE_CLEAN_UNUSED_DATASETS_TASK: ${ENABLE_CLEAN_UNUSED_DATASETS_TASK:-false} + ENABLE_CREATE_TIDB_SERVERLESS_TASK: ${ENABLE_CREATE_TIDB_SERVERLESS_TASK:-false} + ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK: ${ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK:-false} + ENABLE_CLEAN_MESSAGES: ${ENABLE_CLEAN_MESSAGES:-false} + ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK: ${ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK:-false} + ENABLE_DATASETS_QUEUE_MONITOR: ${ENABLE_DATASETS_QUEUE_MONITOR:-false} + ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK: ${ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK:-true} services: # API service api: - image: langgenius/dify-api:1.5.1 + image: langgenius/dify-api:1.8.0 restart: always environment: # Use the shared environment variables. @@ -546,7 +611,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.5.1 + image: langgenius/dify-api:1.8.0 restart: always environment: # Use the shared environment variables. @@ -570,9 +635,28 @@ services: - ssrf_proxy_network - default + # worker_beat service + # Celery beat for scheduling periodic tasks. + worker_beat: + image: langgenius/dify-api:1.8.0 + restart: always + environment: + # Use the shared environment variables. + <<: *shared-api-worker-env + # Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks. + MODE: beat + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + networks: + - ssrf_proxy_network + - default + # Frontend web application. web: - image: langgenius/dify-web:1.5.1 + image: langgenius/dify-web:1.8.0 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -582,6 +666,7 @@ services: TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} CSP_WHITELIST: ${CSP_WHITELIST:-} ALLOW_EMBED: ${ALLOW_EMBED:-false} + ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false} MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai} MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai} TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-} @@ -591,6 +676,7 @@ services: MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99} + MAX_TREE_DEPTH: ${MAX_TREE_DEPTH:-50} ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true} ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true} ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true} @@ -667,7 +753,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.1.3-local + image: langgenius/dify-plugin-daemon:0.2.0-local restart: always environment: # Use the shared environment variables. @@ -685,6 +771,8 @@ services: FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true} PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120} PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600} + PLUGIN_STDIO_BUFFER_SIZE: ${PLUGIN_STDIO_BUFFER_SIZE:-1024} + PLUGIN_STDIO_MAX_BUFFER_SIZE: ${PLUGIN_STDIO_MAX_BUFFER_SIZE:-5242880} PIP_MIRROR_URL: ${PIP_MIRROR_URL:-} PLUGIN_STORAGE_TYPE: ${PLUGIN_STORAGE_TYPE:-local} PLUGIN_STORAGE_LOCAL_ROOT: ${PLUGIN_STORAGE_LOCAL_ROOT:-/app/storage} @@ -714,6 +802,8 @@ services: VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-} VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-} VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-} + SENTRY_ENABLED: ${PLUGIN_SENTRY_ENABLED:-false} + SENTRY_DSN: ${PLUGIN_SENTRY_DSN:-} volumes: - ./volumes/plugin_daemon:/app/storage depends_on: @@ -802,7 +892,7 @@ services: NGINX_SSL_CERT_KEY_FILENAME: ${NGINX_SSL_CERT_KEY_FILENAME:-dify.key} NGINX_SSL_PROTOCOLS: ${NGINX_SSL_PROTOCOLS:-TLSv1.1 TLSv1.2 TLSv1.3} NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto} - NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-15M} + NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M} NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65} NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} @@ -1055,7 +1145,7 @@ services: - milvus milvus-standalone: - image: milvusdb/milvus:v2.5.0-beta + image: milvusdb/milvus:v2.5.15 profiles: - milvus command: [ 'milvus', 'run', 'standalone' ] diff --git a/dify/code/nginx/conf.d/default.conf.template b/dify/code/nginx/conf.d/default.conf.template index a458412d1..48d7da8cf 100644 --- a/dify/code/nginx/conf.d/default.conf.template +++ b/dify/code/nginx/conf.d/default.conf.template @@ -39,7 +39,10 @@ server { proxy_pass http://web:3000; include proxy.conf; } - + location /mcp { + proxy_pass http://api:5001; + include proxy.conf; + } # placeholder for acme challenge location ${ACME_CHALLENGE_LOCATION} diff --git a/dify/update.js b/dify/update.js index faff6b0be..527f58152 100644 --- a/dify/update.js +++ b/dify/update.js @@ -4,3 +4,9 @@ await utils.cloneOrPullRepo({ repo: "https://github.com/langgenius/dify.git" }); await utils.copyDir("./repo/docker", "./code"); await utils.removeContainerNames("./code/docker-compose.yaml"); await utils.removePorts("./code/docker-compose.yaml"); + +await utils.searchReplace( + "./code/.env.example", + "APP_WEB_URL=", + "APP_WEB_URL=https://$(PRIMARY_DOMAIN)" +); diff --git a/dify/update.sh b/dify/update.sh deleted file mode 100644 index 433d23c2d..000000000 --- a/dify/update.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -if [ ! -d "./repo" ]; then - git clone --depth 1 --branch main --single-branch https://github.com/langgenius/dify.git repo -else - cd repo - git pull - cd .. -fi - -cp -r ./repo/docker/. ./code - - diff --git a/plane/update.js b/plane/update.js index 5abe74f8b..a5ef43fc4 100644 --- a/plane/update.js +++ b/plane/update.js @@ -1,5 +1,11 @@ import utils from "../utils.js"; +console.log( + "Plan file structure has changed. This script needs to be reworked." +); + +process.exit(); + await utils.cloneOrPullRepo({ repo: "https://github.com/makeplane/plane.git", path: "./repo", @@ -11,3 +17,21 @@ await utils.renameFile("./code/variables.env", "./code/.env.example"); await utils.removeContainerNames("./code/docker-compose.yml"); await utils.removePorts("./code/docker-compose.yml"); + +await utils.searchReplace( + "./code/.env.example", + "APP_DOMAIN=localhost", + "APP_DOMAIN=$(PRIMARY_DOMAIN)" +); + +await utils.searchReplace( + "./code/.env.example", + "WEB_URL=http://${APP_DOMAIN}", + "WEB_URL=https://$(PRIMARY_DOMAIN)" +); + +await utils.searchReplace( + "./code/.env.example", + "CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}", + "CORS_ALLOWED_ORIGINS=https://$(PRIMARY_DOMAIN)" +); diff --git a/plane/update.sh b/plane/update.sh deleted file mode 100644 index 3bd731389..000000000 --- a/plane/update.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -if [ ! -d "./repo" ]; then - git clone --depth 1 --branch preview --single-branch https://github.com/makeplane/plane.git repo -else - cd repo - git pull - cd .. -fi - -cp -r ./repo/deploy/selfhost/. ./code -mv ./code/variables.env ./code/.env.example - - diff --git a/supabase/code/.env.example b/supabase/code/.env.example index 8ee5f75c6..fcb631072 100644 --- a/supabase/code/.env.example +++ b/supabase/code/.env.example @@ -59,7 +59,7 @@ PGRST_DB_SCHEMAS=public,storage,graphql_public ############ ## General -SITE_URL=http://localhost:3000 +SITE_URL=https://$(PRIMARY_DOMAIN) ADDITIONAL_REDIRECT_URLS= JWT_EXPIRY=3600 DISABLE_SIGNUP=false diff --git a/supabase/code/docker-compose.yml b/supabase/code/docker-compose.yml index 9a4e48ec0..b1fbf8641 100644 --- a/supabase/code/docker-compose.yml +++ b/supabase/code/docker-compose.yml @@ -75,7 +75,7 @@ services: /docker-entrypoint.sh kong docker-start' auth: - image: supabase/gotrue:v2.176.1 + image: supabase/gotrue:v2.177.0 restart: unless-stopped healthcheck: test: @@ -221,7 +221,7 @@ services: # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up storage: - image: supabase/storage-api:v1.24.7 + image: supabase/storage-api:v1.25.7 restart: unless-stopped volumes: - ./volumes/storage:/var/lib/storage:z @@ -279,7 +279,7 @@ services: IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} meta: - image: supabase/postgres-meta:v0.89.3 + image: supabase/postgres-meta:v0.91.0 restart: unless-stopped depends_on: db: @@ -429,7 +429,7 @@ services: # Update the DATABASE_URL if you are using an external Postgres database supavisor: - image: supabase/supavisor:2.5.6 + image: supabase/supavisor:2.5.7 restart: unless-stopped volumes: - ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z diff --git a/supabase/update.js b/supabase/update.js index ca3df4fa7..fdcaa71db 100644 --- a/supabase/update.js +++ b/supabase/update.js @@ -5,3 +5,9 @@ await utils.copyDir("./repo/docker", "./code"); await utils.removeContainerNames("./code/docker-compose.yml"); await utils.removePorts("./code/docker-compose.yml"); + +await utils.searchReplace( + "./code/.env.example", + "SITE_URL=http://localhost:3000", + "SITE_URL=https://$(PRIMARY_DOMAIN)" +); diff --git a/supabase/update.sh b/supabase/update.sh deleted file mode 100644 index 24abd5a02..000000000 --- a/supabase/update.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -if [ ! -d "./repo" ]; then - git clone --depth 1 --branch master --single-branch https://github.com/supabase/supabase repo -else - cd repo - git pull - cd .. -fi - -cp -r ./repo/docker/. ./code diff --git a/twenty/code/twenty-website/Dockerfile b/twenty/code/twenty-website/Dockerfile index c760216b2..2ff7cb808 100644 --- a/twenty/code/twenty-website/Dockerfile +++ b/twenty/code/twenty-website/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine as twenty-website-build +FROM node:24-alpine AS twenty-website-build WORKDIR /app @@ -7,6 +7,7 @@ COPY ./package.json . COPY ./yarn.lock . COPY ./.yarnrc.yml . COPY ./.yarn/releases /app/.yarn/releases +COPY ./.yarn/patches /app/.yarn/patches COPY ./tools/eslint-rules /app/tools/eslint-rules COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/ COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/ @@ -23,7 +24,7 @@ COPY ./packages/twenty-ui /app/packages/twenty-ui COPY ./packages/twenty-website /app/packages/twenty-website RUN npx nx build twenty-website -FROM node:22-alpine as twenty-website +FROM node:24-alpine AS twenty-website WORKDIR /app/packages/twenty-website diff --git a/twenty/code/twenty/Dockerfile b/twenty/code/twenty/Dockerfile index d5f71978c..7818be742 100644 --- a/twenty/code/twenty/Dockerfile +++ b/twenty/code/twenty/Dockerfile @@ -1,11 +1,12 @@ # Base image for common dependencies -FROM node:22-alpine as common-deps +FROM node:24-alpine AS common-deps WORKDIR /app # Copy only the necessary files for dependency resolution COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/ COPY ./.yarn/releases /app/.yarn/releases +COPY ./.yarn/patches /app/.yarn/patches COPY ./.prettierrc /app/ COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/ @@ -20,7 +21,7 @@ RUN yarn && yarn cache clean && npx nx reset # Build the back -FROM common-deps as twenty-server-build +FROM common-deps AS twenty-server-build # Copy sourcecode after installing dependences to accelerate subsequents builds COPY ./packages/twenty-emails /app/packages/twenty-emails @@ -28,17 +29,11 @@ COPY ./packages/twenty-shared /app/packages/twenty-shared COPY ./packages/twenty-server /app/packages/twenty-server RUN npx nx run twenty-server:build -RUN mv /app/packages/twenty-server/dist /app/packages/twenty-server/build -RUN npx nx run twenty-server:build:packageJson -RUN mv /app/packages/twenty-server/dist/package.json /app/packages/twenty-server/package.json -RUN rm -rf /app/packages/twenty-server/dist -RUN mv /app/packages/twenty-server/build /app/packages/twenty-server/dist RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-server - # Build the front -FROM common-deps as twenty-front-build +FROM common-deps AS twenty-front-build ARG REACT_APP_SERVER_BASE_URL @@ -49,7 +44,7 @@ RUN npx nx build twenty-front # Final stage: Run the application -FROM node:22-alpine as twenty +FROM node:24-alpine AS twenty # Used to run healthcheck in docker RUN apk add --no-cache curl jq @@ -63,10 +58,10 @@ RUN chmod +x /app/entrypoint.sh WORKDIR /app/packages/twenty-server ARG REACT_APP_SERVER_BASE_URL -ENV REACT_APP_SERVER_BASE_URL $REACT_APP_SERVER_BASE_URL +ENV REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL ARG APP_VERSION -ENV APP_VERSION $APP_VERSION +ENV APP_VERSION=$APP_VERSION # Copy built applications from previous stages COPY --chown=1000 --from=twenty-server-build /app /app diff --git a/twenty/code/twenty/entrypoint.sh b/twenty/code/twenty/entrypoint.sh index 999049125..0175a0a4f 100755 --- a/twenty/code/twenty/entrypoint.sh +++ b/twenty/code/twenty/entrypoint.sh @@ -19,8 +19,12 @@ setup_and_migrate_db() { if [ "$db_count" = "0" ]; then echo "Database ${PGDATABASE} does not exist, creating..." PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -c "CREATE DATABASE \"${PGDATABASE}\"" + fi - # Run setup and migration scripts + # Run setup and migration scripts + has_schema=$(PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d ${PGDATABASE} -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')") + if [ "$has_schema" = "f" ]; then + echo "Database appears to be empty, running migrations." NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts yarn database:migrate:prod fi diff --git a/twenty/update.sh b/twenty/update.sh deleted file mode 100644 index 468d07655..000000000 --- a/twenty/update.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -if [ ! -d "./repo" ]; then - git clone --depth 1 --branch main --single-branch git@github.com:twentyhq/twenty.git repo -else - cd repo - git pull - cd .. -fi - -cp -r ./repo/packages/twenty-docker/. ./code - - From ee335fa22ee9db464fff67f68bddf035125590ce Mon Sep 17 00:00:00 2001 From: Andrei Canta Date: Thu, 28 Aug 2025 16:05:36 +0300 Subject: [PATCH 3/6] remove secret key --- dify/code/.env.example | 2 +- dify/update.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dify/code/.env.example b/dify/code/.env.example index 31aed9758..226a7b732 100644 --- a/dify/code/.env.example +++ b/dify/code/.env.example @@ -91,7 +91,7 @@ ENABLE_REQUEST_LOGGING=False # A secret key that is used for securely signing the session cookie # and encrypting sensitive information on the database. # You can generate a strong key using `openssl rand -base64 42`. -SECRET_KEY=sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U +SECRET_KEY= # Password for admin user initialization. # If left unset, admin user will not be prompted for a password diff --git a/dify/update.js b/dify/update.js index 527f58152..48d1867a2 100644 --- a/dify/update.js +++ b/dify/update.js @@ -10,3 +10,9 @@ await utils.searchReplace( "APP_WEB_URL=", "APP_WEB_URL=https://$(PRIMARY_DOMAIN)" ); + +await utils.searchReplace( + "./code/.env.example", + "SECRET_KEY=sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U", + "SECRET_KEY=" +); From 854efbcff0399222f61cfd693d282c4ba38f687b Mon Sep 17 00:00:00 2001 From: Ahson Shaikh Date: Thu, 28 Aug 2025 18:49:17 +0500 Subject: [PATCH 4/6] Removed Twenty, Updated Plane --- plane/README.md | 44 +- plane/code/.env.example | 29 +- plane/code/README.md | 630 ------------------ plane/code/build.yml | 30 - plane/code/docker-compose.yml | 53 +- plane/code/images/download.png | Bin 22985 -> 0 bytes plane/code/images/migrate-error.png | Bin 14139 -> 0 bytes plane/code/images/restart.png | Bin 15664 -> 0 bytes plane/code/images/started.png | Bin 21489 -> 0 bytes plane/code/images/stopped.png | Bin 16638 -> 0 bytes plane/code/images/upgrade.png | Bin 55240 -> 0 bytes plane/code/migration-0.13-0.14.sh | 118 ---- plane/code/restore-airgapped.sh | 144 ---- plane/code/restore.sh | 123 ---- plane/code/swarm.sh | 612 ----------------- plane/{code/install.sh => setup.sh} | 16 +- plane/update.js | 19 +- twenty/README.md | 4 - twenty/code/.env.example | 18 - twenty/code/Makefile | 27 - twenty/code/docker-compose.yml | 126 ---- .../datasources/clickhouse-datasource.yaml | 15 - twenty/code/k8s/README.md | 116 ---- twenty/code/k8s/manifests/deployment-db.yaml | 58 -- .../code/k8s/manifests/deployment-redis.yaml | 45 -- .../code/k8s/manifests/deployment-server.yaml | 76 --- .../code/k8s/manifests/deployment-worker.yaml | 57 -- twenty/code/k8s/manifests/ingress.yaml | 24 - twenty/code/k8s/manifests/pv-db.yaml | 11 - twenty/code/k8s/manifests/pv-docker-data.yaml | 11 - twenty/code/k8s/manifests/pv-server.yaml | 12 - twenty/code/k8s/manifests/pvc-db.yaml | 13 - .../code/k8s/manifests/pvc-docker-data.yaml | 13 - twenty/code/k8s/manifests/pvc-server.yaml | 13 - twenty/code/k8s/manifests/service-db.yaml | 18 - twenty/code/k8s/manifests/service-redis.yaml | 18 - twenty/code/k8s/manifests/service-server.yaml | 19 - twenty/code/k8s/terraform/.terraform-docs.yml | 48 -- twenty/code/k8s/terraform/README.md | 73 -- twenty/code/k8s/terraform/deployment-db.tf | 87 --- twenty/code/k8s/terraform/deployment-redis.tf | 60 -- .../code/k8s/terraform/deployment-server.tf | 138 ---- .../code/k8s/terraform/deployment-worker.tf | 99 --- twenty/code/k8s/terraform/ingress.tf | 30 - twenty/code/k8s/terraform/main.tf | 23 - twenty/code/k8s/terraform/namespace.tf | 9 - twenty/code/k8s/terraform/pv-db.tf | 19 - twenty/code/k8s/terraform/pv-docker-data.tf | 19 - twenty/code/k8s/terraform/pv-server.tf | 19 - twenty/code/k8s/terraform/pvc-db.tf | 15 - twenty/code/k8s/terraform/pvc-docker-data.tf | 15 - twenty/code/k8s/terraform/pvc-server.tf | 15 - twenty/code/k8s/terraform/secret.tf | 28 - twenty/code/k8s/terraform/service-db.tf | 18 - twenty/code/k8s/terraform/service-redis.tf | 18 - twenty/code/k8s/terraform/service-server.tf | 19 - twenty/code/k8s/terraform/variables.tf | 136 ---- .../otel-collector/otel-collector-config.yaml | 24 - twenty/code/podman/README.md | 48 -- .../code/podman/install-systemd-user-service | 11 - .../manual-steps-to-deploy-twenty-on-podman | 71 -- twenty/code/podman/podman-compose.yml | 56 -- twenty/code/podman/twentycrm.service | 15 - twenty/code/scripts/1-click.sh | 22 - twenty/code/scripts/install.sh | 167 ----- twenty/code/twenty-postgres-spilo/Dockerfile | 68 -- twenty/code/twenty-website/Dockerfile | 43 -- twenty/code/twenty/Dockerfile | 82 --- twenty/code/twenty/entrypoint.sh | 54 -- twenty/update.js | 25 - utils.js | 14 + 71 files changed, 128 insertions(+), 3972 deletions(-) delete mode 100644 plane/code/README.md delete mode 100644 plane/code/build.yml delete mode 100644 plane/code/images/download.png delete mode 100644 plane/code/images/migrate-error.png delete mode 100644 plane/code/images/restart.png delete mode 100644 plane/code/images/started.png delete mode 100644 plane/code/images/stopped.png delete mode 100644 plane/code/images/upgrade.png delete mode 100755 plane/code/migration-0.13-0.14.sh delete mode 100755 plane/code/restore-airgapped.sh delete mode 100755 plane/code/restore.sh delete mode 100755 plane/code/swarm.sh rename plane/{code/install.sh => setup.sh} (95%) delete mode 100644 twenty/README.md delete mode 100644 twenty/code/.env.example delete mode 100644 twenty/code/Makefile delete mode 100644 twenty/code/docker-compose.yml delete mode 100644 twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml delete mode 100644 twenty/code/k8s/README.md delete mode 100644 twenty/code/k8s/manifests/deployment-db.yaml delete mode 100644 twenty/code/k8s/manifests/deployment-redis.yaml delete mode 100644 twenty/code/k8s/manifests/deployment-server.yaml delete mode 100644 twenty/code/k8s/manifests/deployment-worker.yaml delete mode 100644 twenty/code/k8s/manifests/ingress.yaml delete mode 100644 twenty/code/k8s/manifests/pv-db.yaml delete mode 100644 twenty/code/k8s/manifests/pv-docker-data.yaml delete mode 100644 twenty/code/k8s/manifests/pv-server.yaml delete mode 100644 twenty/code/k8s/manifests/pvc-db.yaml delete mode 100644 twenty/code/k8s/manifests/pvc-docker-data.yaml delete mode 100644 twenty/code/k8s/manifests/pvc-server.yaml delete mode 100644 twenty/code/k8s/manifests/service-db.yaml delete mode 100644 twenty/code/k8s/manifests/service-redis.yaml delete mode 100644 twenty/code/k8s/manifests/service-server.yaml delete mode 100644 twenty/code/k8s/terraform/.terraform-docs.yml delete mode 100644 twenty/code/k8s/terraform/README.md delete mode 100644 twenty/code/k8s/terraform/deployment-db.tf delete mode 100644 twenty/code/k8s/terraform/deployment-redis.tf delete mode 100644 twenty/code/k8s/terraform/deployment-server.tf delete mode 100644 twenty/code/k8s/terraform/deployment-worker.tf delete mode 100644 twenty/code/k8s/terraform/ingress.tf delete mode 100644 twenty/code/k8s/terraform/main.tf delete mode 100644 twenty/code/k8s/terraform/namespace.tf delete mode 100644 twenty/code/k8s/terraform/pv-db.tf delete mode 100644 twenty/code/k8s/terraform/pv-docker-data.tf delete mode 100644 twenty/code/k8s/terraform/pv-server.tf delete mode 100644 twenty/code/k8s/terraform/pvc-db.tf delete mode 100644 twenty/code/k8s/terraform/pvc-docker-data.tf delete mode 100644 twenty/code/k8s/terraform/pvc-server.tf delete mode 100644 twenty/code/k8s/terraform/secret.tf delete mode 100644 twenty/code/k8s/terraform/service-db.tf delete mode 100644 twenty/code/k8s/terraform/service-redis.tf delete mode 100644 twenty/code/k8s/terraform/service-server.tf delete mode 100644 twenty/code/k8s/terraform/variables.tf delete mode 100644 twenty/code/otel-collector/otel-collector-config.yaml delete mode 100644 twenty/code/podman/README.md delete mode 100755 twenty/code/podman/install-systemd-user-service delete mode 100644 twenty/code/podman/manual-steps-to-deploy-twenty-on-podman delete mode 100644 twenty/code/podman/podman-compose.yml delete mode 100644 twenty/code/podman/twentycrm.service delete mode 100644 twenty/code/scripts/1-click.sh delete mode 100755 twenty/code/scripts/install.sh delete mode 100644 twenty/code/twenty-postgres-spilo/Dockerfile delete mode 100644 twenty/code/twenty-website/Dockerfile delete mode 100644 twenty/code/twenty/Dockerfile delete mode 100755 twenty/code/twenty/entrypoint.sh delete mode 100644 twenty/update.js diff --git a/plane/README.md b/plane/README.md index 86611bc18..ce423fb4c 100644 --- a/plane/README.md +++ b/plane/README.md @@ -1,4 +1,42 @@ -# Plane +# Plane - Project Management Script -- copied from https://github.com/makeplane/plane -- removed `ports` +This directory contains the Plane project management tool deployment for Easypanel. + +## Update Process + +To update Plane to the latest version, follow these steps in order: + +### Step 1: Run the Setup Script for Upgrade + +First, run the `setup.sh` script and choose the upgrade option: + +``` +./setup.sh +``` + +When prompted, select option **5** for "Upgrade". This will: + +* Check for the latest available release +* Download the latest stable version + +### Step 2: Run the Update Script + +After the setup script completes successfully, run the `update.js` script: + +``` +node update.js +``` + +This script will: + +* Removes the existing code directory and changes the name for the newly fetched directory to code.  +* Rename `plane.env` to `.env.example` +* Rename `docker-compose.yaml` to `docker-compose.yml` +* Remove container names and ports from docker-compose.yml +* Update environment variables to use Easypanel's `PRIMARY_DOMAIN` variable + +## Important Notes + +* **Always run setup.sh first** - This ensures you get the latest official Plane release +* **Then run update.js** - This applies Easypanel-specific customizations +* The update process will preserve your existing data and configuration \ No newline at end of file diff --git a/plane/code/.env.example b/plane/code/.env.example index 78031a4ac..fd92cb43c 100644 --- a/plane/code/.env.example +++ b/plane/code/.env.example @@ -1,5 +1,6 @@ -APP_DOMAIN=localhost -APP_RELEASE=stable +APP_DOMAIN=$(PRIMARY_DOMAIN) +APP_RELEASE=v0.28.0 +SSL=false WEB_REPLICAS=1 SPACE_REPLICAS=1 @@ -9,10 +10,12 @@ WORKER_REPLICAS=1 BEAT_WORKER_REPLICAS=1 LIVE_REPLICAS=1 -NGINX_PORT=80 -WEB_URL=http://${APP_DOMAIN} +LISTEN_HTTP_PORT=80 +LISTEN_HTTPS_PORT=443 + +WEB_URL=https://$(PRIMARY_DOMAIN) DEBUG=0 -CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN} +CORS_ALLOWED_ORIGINS=https://$(PRIMARY_DOMAIN) API_BASE_URL=http://api:8000 #DB SETTINGS @@ -38,6 +41,19 @@ RABBITMQ_PASSWORD=plane RABBITMQ_VHOST=plane AMQP_URL= +# If SSL Cert to be generated, set CERT_EMAIl="email " +CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory +TRUSTED_PROXIES=0.0.0.0/0 +SITE_ADDRESS=:80 +CERT_EMAIL= + + + +# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL +# CERT_ACME_DNS="acme_dns " +CERT_ACME_DNS= + + # Secret Key SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5 @@ -61,3 +77,6 @@ MINIO_ENDPOINT_SSL=0 # API key rate limit API_KEY_RATE_LIMIT=60/minute +DOCKERHUB_USER=artifacts.plane.so/makeplane +PULL_POLICY=if_not_present +CUSTOM_BUILD=false diff --git a/plane/code/README.md b/plane/code/README.md deleted file mode 100644 index ba7af0e6c..000000000 --- a/plane/code/README.md +++ /dev/null @@ -1,630 +0,0 @@ -# Self Hosting - -In this guide, we will walk you through the process of setting up a self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization. - -We will cover two main options for setting up your self-hosted environment: using a cloud server or using your desktop. For the cloud server, we will use an AWS EC2 instance. For the desktop, we will use Docker to create a local environment. - -Let's get started! - -## Setting up Docker Environment - -
- Option 1 - Using Cloud Server -

Best way to start is to create EC2 machine on AWS. It must have minimum of 2vCPU and 4GB RAM.

-

Run the below command to install docker engine.

- -`curl -fsSL https://get.docker.com | sh -` - -
- ---- - -
- Option 2 - Using Desktop - -#### For Mac - -
    -
  1. Download Docker Desktop for Mac from the Docker Hub.
  2. -
  3. Double-click the downloaded `.dmg` file and drag the Docker app icon to the Applications folder.
  4. -
  5. Open Docker Desktop from the Applications folder. You might be asked to provide your system password to install additional software.
  6. -
- -#### For Windows: - -
    -
  1. Download Docker Desktop for Windows from the Docker Hub.
  2. -
  3. Run the installer and follow the instructions. You might be asked to enable Hyper-V and "Containers" Windows features.
  4. -
  5. Open Docker Desktop. You might be asked to log out and log back in, or restart your machine, for changes to take effect.
  6. -
- -After installation, you can verify the installation by opening a terminal (Command Prompt on Windows, Terminal app on Mac) and running the command `docker --version`. This should display the installed version of Docker. - -
- ---- - -## Installing Plane - -Installing plane is a very easy and minimal step process. - -### Prerequisite - -- Docker installed and running -- OS with bash scripting enabled (Ubuntu, Linux AMI, macos). Windows systems need to have [gitbash](https://git-scm.com/download/win) -- User context used must have access to docker services. In most cases, use sudo su to switch as root user -- Use the terminal (or gitbash) window to run all the future steps - -### Downloading Latest Release - -``` -mkdir plane-selfhost - -cd plane-selfhost -``` - -#### For *Docker Compose* based setup - -``` -curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh - -chmod +x setup.sh -``` - -#### For *Docker Swarm* based setup - -``` -curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/swarm.sh - -chmod +x setup.sh -``` - ---- - -### Proceed with setup - -Above steps will set you ready to install and start plane services. - -Lets get started by running the `./setup.sh` command. - -This will prompt you with the below options. - -#### Docker Compose -```bash -Select an Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 1 -``` - -For the 1st time setup, type "1" as action input. - -This will create a folder `plane-app` and will download 2 files inside that - -- `docker-compose.yaml` -- `plane.env` - -Again the `options [1-8]` will be popped up, and this time hit `8` to exit. - -#### Docker Swarm - -```bash -Select an Action you want to perform: - 1) Deploy Stack - 2) Remove Stack - 3) View Stack Status - 4) Redeploy Stack - 5) Upgrade - 6) View Logs - 7) Exit - -Action [3]: 1 -``` - -For the 1st time setup, type "1" as action input. - -This will create a create a folder `plane-app` and will download 2 files inside that - -- `docker-compose.yaml` -- `plane.env` - -Again the `options [1-7]` will be popped up, and this time hit `7` to exit. - ---- - -### Continue with setup - Environment Settings - -Before proceeding, we suggest used to review `.env` file and set the values. -Below are the most import keys you must refer to. _You can use any text editor to edit this file_. - -> `NGINX_PORT` - This is default set to `80`. Make sure the port you choose to use is not preoccupied. (e.g `NGINX_PORT=8080`) - -> `WEB_URL` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`) - -> `CORS_ALLOWED_ORIGINS` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`) - -There are many other settings you can play with, but we suggest you configure `EMAIL SETTINGS` as it will enable you to invite your teammates onto the platform. - ---- - -### Continue with setup - Start Server (Docker Compose) - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `2` to start the sevices - -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 2 -``` - -Expect something like this. -![Downloading docker images](images/download.png) - -Be patient as it might take sometime based on download speed and system configuration. If all goes well, you must see something like this - -![Downloading completed](images/started.png) - -This is the confirmation that all images were downloaded and the services are up & running. - -You have successfully self hosted `Plane` instance. Access the application by going to IP or domain you have configured it (e.g `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`) - ---- - -### Stopping the Server / Remove Stack - -In case you want to make changes to `plane.env` variables, we suggest you to stop the services before doing that. - -#### Docker Compose - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `3` to stop the sevices - -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 3 -``` - -If all goes well, you must see something like this - -![Stop Services](images/stopped.png) - -#### Docker Swarm - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `2` to stop the sevices - -```bash -Select an Action you want to perform: - 1) Deploy Stack - 2) Remove Stack - 3) View Stack Status - 4) Redeploy Stack - 5) Upgrade - 6) View Logs - 7) Exit - -Action [3]: 2 -``` - -If all goes well, you will see the confirmation from docker cli - ---- - -### Restarting the Server / Redeploy Stack - -In case you want to make changes to `plane.env` variables, without stopping the server or you noticed some abnormalies in services, you can restart the services with `RESTART` / `REDEPLOY` option. - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `4` to restart the sevices - -#### Docker Compose -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 4 -``` - -If all goes well, you must see something like this - -![Restart Services](images/restart.png) - -#### Docker Swarm - -```bash - 1) Deploy Stack - 2) Remove Stack - 3) View Stack Status - 4) Redeploy Stack - 5) Upgrade - 6) View Logs - 7) Exit - -Action [3]: 4 -``` - -If all goes well, you will see the confirmation from docker cli - ---- - -### Upgrading Plane Version - -It is always advised to keep Plane up to date with the latest release. - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release. - -#### Docker Compose - -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 5 -``` - -By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`. - -You must expect the below message - -![Alt text](images/upgrade.png) - -Once done, choose `8` to exit from prompt. - -> It is very important for you to validate the `plane.env` for the new changes. - -Once done with making changes in `plane.env` file, jump on to `Start Server` - -#### Docker Swarm - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release. - -```bash - 1) Deploy Stack - 2) Remove Stack - 3) View Stack Status - 4) Redeploy Stack - 5) Upgrade - 6) View Logs - 7) Exit - -Action [3]: 5 -``` - -By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`. - -Once done, choose `7` to exit from prompt. - -> It is very important for you to validate the `plane.env` for the new changes. - -Once done with making changes in `plane.env` file, jump on to `Redeploy Stack` - ---- - -### View Logs - -There would a time when you might want to check what is happening inside the API, Worker or any other container. - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. - -This time select `6` to view logs. - -#### Docker Compose - -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 6 -``` - -#### Docker Swarm - - -```bash - 1) Deploy Stack - 2) Remove Stack - 3) View Stack Status - 4) Redeploy Stack - 5) Upgrade - 6) View Logs - 7) Exit - -Action [3]: 6 -``` - -#### Service Menu Options for Logs -This will further open sub-menu with list of services -```bash -Select a Service you want to view the logs for: - 1) Web - 2) Space - 3) API - 4) Worker - 5) Beat-Worker - 6) Migrator - 7) Proxy - 8) Redis - 9) Postgres - 10) Minio - 11) RabbitMQ - 0) Back to Main Menu - -Service: 3 -``` - -Select any of the service to view the logs e.g. `3`. Expect something similar to this -```bash -api-1 | Waiting for database... -api-1 | Database available! -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | No migrations Pending. Starting processes ... -api-1 | Instance registered -api-1 | ENABLE_SIGNUP loaded with value from environment variable. -api-1 | ENABLE_EMAIL_PASSWORD loaded with value from environment variable. -api-1 | ENABLE_MAGIC_LINK_LOGIN loaded with value from environment variable. -api-1 | GOOGLE_CLIENT_ID loaded with value from environment variable. -api-1 | GITHUB_CLIENT_ID loaded with value from environment variable. -api-1 | GITHUB_CLIENT_SECRET loaded with value from environment variable. -api-1 | EMAIL_HOST loaded with value from environment variable. -api-1 | EMAIL_HOST_USER loaded with value from environment variable. -api-1 | EMAIL_HOST_PASSWORD loaded with value from environment variable. -api-1 | EMAIL_PORT loaded with value from environment variable. -api-1 | EMAIL_FROM loaded with value from environment variable. -api-1 | EMAIL_USE_TLS loaded with value from environment variable. -api-1 | EMAIL_USE_SSL loaded with value from environment variable. -api-1 | OPENAI_API_KEY loaded with value from environment variable. -api-1 | GPT_ENGINE loaded with value from environment variable. -api-1 | UNSPLASH_ACCESS_KEY loaded with value from environment variable. -api-1 | Checking bucket... -api-1 | Bucket 'uploads' does not exist. Creating bucket... -api-1 | Bucket 'uploads' created successfully. -api-1 | Public read access policy set for bucket 'uploads'. -api-1 | Cache Cleared -api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Starting gunicorn 21.2.0 -api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) -api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Using worker: uvicorn.workers.UvicornWorker -api-1 | [2024-05-02 03:56:01 +0000] [25] [INFO] Booting worker with pid: 25 -api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Started server process [25] -api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Waiting for application startup. -api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] ASGI 'lifespan' protocol appears unsupported. -api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Application startup complete. - -``` - -To exit this, use `CTRL+C` and then you will land on to the main-menu with the list of actions. - -Similarly, you can view the logs of other services. - ---- - -### Backup Data (Docker Compose) - -There would a time when you might want to backup your data from docker volumes to external storage like S3 or drives. - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `7` to Backup the data. - -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 7 -``` - -In response, you can find the backup folder - -```bash -Backing Up plane-app_pgdata -Backing Up plane-app_redisdata -Backing Up plane-app_uploads - -Backup completed successfully. Backup files are stored in /....../plane-app/backup/20240502-1120 -``` - ---- - -### Restore Data (Docker Compose) - -When you want to restore the previously backed-up data, follow the instructions below. - -1. Make sure that Plane-CE is installed, started, and then stopped. This ensures that the Docker volumes are created. - -1. Download the restore script using the command below. We suggest downloading it in the same folder as `setup.sh`. - - ```bash - curl -fsSL -o restore.sh https://github.com/makeplane/plane/releases/latest/download/restore.sh - chmod +x restore.sh - ``` - -1. Execute the command below to restore your data. - - ```bash - ./restore.sh - ``` - - As an example, for a backup folder `/opt/plane-selfhost/plane-app/backup/20240722-0914`, expect the response below: - - ```bash - -------------------------------------------- - ____ _ ///////// - | _ \| | __ _ _ __ ___ ///////// - | |_) | |/ _` | '_ \ / _ \ ///// ///// - | __/| | (_| | | | | __/ ///// ///// - |_| |_|\__,_|_| |_|\___| //// - //// - -------------------------------------------- - Project management tool from the future - -------------------------------------------- - Found /opt/plane-selfhost/plane-app/backup/20240722-0914/pgdata.tar.gz - .....Restoring plane-app_pgdata - .....Successfully restored volume plane-app_pgdata from pgdata.tar.gz - - Found /opt/plane-selfhost/plane-app/backup/20240722-0914/redisdata.tar.gz - .....Restoring plane-app_redisdata - .....Successfully restored volume plane-app_redisdata from redisdata.tar.gz - - Found /opt/plane-selfhost/plane-app/backup/20240722-0914/uploads.tar.gz - .....Restoring plane-app_uploads - .....Successfully restored volume plane-app_uploads from uploads.tar.gz - - - Restore completed successfully. - ``` - -1. Start the Plane instance using `./setup.sh start`. - ---- - -### Restore for Commercial Air-Gapped (Docker Compose) - -When you want to restore the previously backed-up data on Plane Commercial Air-Gapped version, follow the instructions below. - -1. Download the restore script using the command below - - ```bash - curl -fsSL -o restore-airgapped.sh https://github.com/makeplane/plane/releases/latest/download/restore-airgapped.sh - chmod +x restore-airgapped.sh - ``` - -1. Copy the backup folder and the `restore-airgapped.sh` to `Commercial Airgapped Edition` server - -1. Make sure that Plane Commercial (Airgapped) is extracted and ready to get started. In case it is running, you would need to stop that. - -1. Execute the command below to restore your data. - - ```bash - ./restore-airgapped.sh - ``` - -1. After restoration, you are ready to start Plane Commercial (Airgapped) will all your previously saved data. - ---- - -
-

Upgrading from v0.13.2 to v0.14.x

- -This is one time activity for users who are upgrading from v0.13.2 to v0.14.0 - -As there has been significant changes to Self Hosting process, this step mainly covers the data migration from current (v0.13.2) docker volumes from newly created volumes - -> Before we begin with migration, make sure your v0.14.0 was started and then stopped. This is required to know the newly created docker volume names. - -Begin with downloading the migration script using below command - -``` - -curl -fsSL -o migrate.sh https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/migration-0.13-0.14.sh - -chmod +x migrate.sh - -``` - -Now run the `./migrate.sh` command and expect the instructions as below - -``` -****************************************************************** - -This script is solely for the migration purpose only. -This is a 1 time migration of volume data from v0.13.2 => v0.14.x - -Assumption: -1. Postgres data volume name ends with _pgdata -2. Minio data volume name ends with _uploads -3. Redis data volume name ends with _redisdata - -Any changes to this script can break the migration. - -Before you proceed, make sure you run the below command -to know the docker volumes - -docker volume ls -q | grep -i "_pgdata" -docker volume ls -q | grep -i "_uploads" -docker volume ls -q | grep -i "_redisdata" - -******************************************************* - -Given below list of REDIS volumes, identify the prefix of source and destination volumes leaving "_redisdata" ---------------------- -plane-app_redisdata -v0132_redisdata - -Provide the Source Volume Prefix : -``` - -**Open another terminal window**, and run the mentioned 3 command. This may be different for users who have changed the volume names in their previous setup (v0.13.2) - -For every command you must see 2 records something like shown in above example of `redisdata` - -To move forward, you would need PREFIX of old setup and new setup. As per above example, `v0132` is the prefix of v0.13.2 and `plane-app` is the prefix of v0.14.0 setup - -**Back to original terminal window**, _Provide the Source Volume Prefix_ and hit ENTER. - -Now you will be prompted to _Provide Destination Volume Prefix_. Provide the value and hit ENTER - -``` -Provide the Source Volume Prefix : v0132 -Provide the Destination Volume Prefix : plane-app -``` - -In case the suffixes are wrong or the mentioned volumes are not found, you will receive the error shown below. The image below displays an error for source volumes. - -![Migrate Error](images/migrate-error.png) - -In case of successful migration, it will be a silent exit without error. - -Now its time to restart v0.14.0 setup. -
\ No newline at end of file diff --git a/plane/code/build.yml b/plane/code/build.yml deleted file mode 100644 index b65d297e9..000000000 --- a/plane/code/build.yml +++ /dev/null @@ -1,30 +0,0 @@ -services: - web: - image: ${DOCKERHUB_USER:-local}/plane-frontend:${APP_RELEASE:-latest} - build: - context: . - dockerfile: ./web/Dockerfile.web - - space: - image: ${DOCKERHUB_USER:-local}/plane-space:${APP_RELEASE:-latest} - build: - context: ./ - dockerfile: ./space/Dockerfile.space - - admin: - image: ${DOCKERHUB_USER:-local}/plane-admin:${APP_RELEASE:-latest} - build: - context: ./ - dockerfile: ./admin/Dockerfile.admin - - api: - image: ${DOCKERHUB_USER:-local}/plane-backend:${APP_RELEASE:-latest} - build: - context: ./apiserver - dockerfile: ./Dockerfile.api - - proxy: - image: ${DOCKERHUB_USER:-local}/plane-proxy:${APP_RELEASE:-latest} - build: - context: ./nginx - dockerfile: ./Dockerfile diff --git a/plane/code/docker-compose.yml b/plane/code/docker-compose.yml index 65fb84e10..284b80ef9 100644 --- a/plane/code/docker-compose.yml +++ b/plane/code/docker-compose.yml @@ -24,9 +24,16 @@ x-aws-s3-env: &aws-s3-env AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} x-proxy-env: &proxy-env - NGINX_PORT: ${NGINX_PORT:-80} - BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} + SSL: ${SSL:-false} + APP_DOMAIN: ${APP_DOMAIN:-localhost} FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} + CERT_EMAIL: ${CERT_EMAIL} + CERT_ACME_CA: ${CERT_ACME_CA} + CERT_ACME_DNS: ${CERT_ACME_DNS} + LISTEN_HTTP_PORT: ${LISTEN_HTTP_PORT:-80} + LISTEN_HTTPS_PORT: ${LISTEN_HTTPS_PORT:-443} + BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} + SITE_ADDRESS: ${SITE_ADDRESS:-:80} x-mq-env: # RabbitMQ Settings @@ -55,8 +62,7 @@ x-app-env: &app-env services: web: - image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-stable} - command: node web/server.js web + image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-v0.28.0} deploy: replicas: ${WEB_REPLICAS:-1} restart_policy: @@ -66,8 +72,7 @@ services: - worker space: - image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-stable} - command: node space/server.js space + image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-v0.28.0} deploy: replicas: ${SPACE_REPLICAS:-1} restart_policy: @@ -78,8 +83,7 @@ services: - web admin: - image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-stable} - command: node admin/server.js admin + image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-v0.28.0} deploy: replicas: ${ADMIN_REPLICAS:-1} restart_policy: @@ -89,8 +93,7 @@ services: - web live: - image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-stable} - command: node live/dist/server.js live + image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-v0.28.0} environment: <<: [ *live-env ] deploy: @@ -102,7 +105,7 @@ services: - web api: - image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable} + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v0.28.0} command: ./bin/docker-entrypoint-api.sh deploy: replicas: ${API_REPLICAS:-1} @@ -118,7 +121,7 @@ services: - plane-mq worker: - image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable} + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v0.28.0} command: ./bin/docker-entrypoint-worker.sh deploy: replicas: ${WORKER_REPLICAS:-1} @@ -135,7 +138,7 @@ services: - plane-mq beat-worker: - image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable} + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v0.28.0} command: ./bin/docker-entrypoint-beat.sh deploy: replicas: ${BEAT_WORKER_REPLICAS:-1} @@ -152,7 +155,7 @@ services: - plane-mq migrator: - image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable} + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v0.28.0} command: ./bin/docker-entrypoint-migrator.sh deploy: replicas: 1 @@ -214,17 +217,31 @@ services: # Comment this if you already have a reverse proxy running proxy: - image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-stable} - environment: - <<: *proxy-env + image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-v0.28.0} + command: + [ + "caddy", + "run", + "--config", + "/etc/caddy/Caddyfile", + "--adapter", + "caddyfile" + ] deploy: replicas: 1 restart_policy: condition: on-failure + environment: + <<: *proxy-env + volumes: + - proxy_config:/config + - proxy_data:/data depends_on: - web - api - space + - admin + - live volumes: pgdata: @@ -235,3 +252,5 @@ volumes: logs_beat-worker: logs_migrator: rabbitmq_data: + proxy_config: + proxy_data: diff --git a/plane/code/images/download.png b/plane/code/images/download.png deleted file mode 100644 index bb0d1183e68a8d455ce7959b6689bb97da679ab0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22985 zcmZVm1z6q8);2u!i`|a!6 zBs}SD10e_K?6+1ydK-DZnMHQq( zMTry~?MyAKO+Y}XzPWsrmjN?C8YFD~NJ`G z!?juDP}I2$QQ-#tW~ox9I56;VqcU(RiA=6d&VM(V_aZ%$8vgZq?=Wyn^;^(~MRe zsjU8kM))E+Q3eQ)n@9&|$YBP!=>6R4E5r+wVW@H(}I7X$=6J`@BR*rEVmu^h0!XTj)l!2j-pWW8S~tSl-e1#Fd#98FAYoy_f= z>#BRlfvy%TR5YA5WIyv5+1W4{7~2_|Fu2>;zc&HlbLRneZA_dEh}>u%^v@;ER{SIyvI<0^c8(@Q91KhhOe6wuL_|be`uN@5cKZ4Uh6Coy++ zw&!7FbaQiKaARe#b2MXQ=H}*RWMW}tVW9`Epm*}HbvAIPw{;@@PbYu-5i@ZzaQ7M*n{PGfoqCi~rrp*6F{G1w0_*`x!=N1}4UT`v#iwy&vUK zuy8lA))cd_0mK8`Lx7o`i|?Q7|DQAeyW{_9sqw#-?Cc!>x8?si^Z(yc)yc$B)XoOD zrL(~QX6C<*|L@8FHsoV`fAaq$iT{ZCpQC`B1>pD?|D7`dIJC<}SP&2)5GgTX6?f3H zFVHDy0}DUuzd=G^B()fS&G8R_j_hS5ii8zbR1_kE%Oim!;Zp#GB_ZhniHBs?2NU*? zhMG-yKtf0VHV)Q{g6SHN*(DdlzPh?PJ3GtDyISvcem~({QnspU=2>R9S=KhB+GMTC zS<3_#4MN%t^Ur2WA0iJ=hqDUj?}7K@D4-k~4-)ziSfCRA$nTpW(r&1z9Y2yPqW@i1 z)6;wZZ#UgATm(QPyf;(_NbJ9@B>X>ObVC2ziA47$Oij)0*Q_1NOIhcPUK2MyZCkH(%wK_N1cEsfOgdK976JSKvY992^Tu zsVCK4Sx_Hw$gbPgZF%gsKzyE#iW^KPaAfv^Mw~Z&?vV)|a5=1DBw{w}2z?(6IQU*) zpY1$vrX}kWt)}xJ`5$)^8f;cMa0I;Fo7nJFN)!S%+;=|Wu$sc#thIbwX|$5O-}DnC zCnH0$Pv>=Gpx3M;ljIiY`6~f27O*6E47Q7zLYdIB3dMY=m&b$bPzzVNSS;qiyu7>; z$vA?!X1mQAEIKBTU%!5xs&HrXNmlZWa`0`=+(Io{d69F zbXwRv&Lw<)Pd6547U#Z~zXnQuU!Lk>xFQVb^n=^7xSb>n_Z?dg?^f;ZleY2y&Loi_ z5mG}>yf#ru*zdcu)je{C_EVN#NvT|skP`Fhyep(ttE$$}{qa;$!M8uG>%fR<(#Lmb z&yw|gb8C%;P@)T1Oh(HU87N32wUo*S8ii;ASgT`p+bee;}! z(2uyI8BA(Kqg3(GzMUIB_g^e(y6pp))e1f#xBa;qD-{hxn8^pvVfI(OkYIyq6C}--tD_1ajc@t~ z)OtT&eckd;J8LCI!1uJTt_#64P8TaSkXr)UZ?Yx+`v_UYG7~tF9U)7($q6 zyz!UiMyq6w59h3Z^&xVcKRu!hmsU(}oYTS2X`?uP3dXQplz!&`%wn(m-`gdD+w4r6 zkXi8X;07BJIHAnr72KMwjwamBp^p@emh}oa6 z*b?yjhlhp0l0rcp@4@DFEMU9oeE!Yr!y#b$uN-gyIbhgCA*b&F8e=gFZdo8YDU84~ z{G#UV&-G-f*<{vLuFu7mu*XOEZ&cI2cQWoY+N)*zj&pW*jSEHOLI;#~erEEHIdzUs zq%e!(C}|LfpbH{>4rND$SLDAMCcO&sp5nJ!pzGVl{b5$@^L*Ffa-zm9`N{7iJY^}m zG*f}D`!=Kx#{U@+0cf&@;x8|{z3hDhU04doUjgO=`TkNZ?p&fpa+x@F+a!lUl-tAK zNS9oK^{*VNb$=AN^-G87dkcTh-KqXuZ#rS@jSi35({4nDrPjNK4qx~`NW*vk6zOn??0DAEI5q5 zVq*6LGrO>;6b~=5$bwKZX(tPnG8#t(>>A@U-+$THN|*ot_92W-m+kvhY78}qD@c*q zdADM9188NgLYr2tIG2-!5tAu;t%k3cs@)ouAJgreFCVWD#%76R=b|*zMselTEL>za zSpKe1IUypN6lw35+rqT@9N}2HNDu5RXePuyB#|2(asLvy zt=x@9D|Nf9kp#+evU4?NCR#8ty2nfon>1rJ-4^>?y7LJ-*NVfref8&Xm8imz62_Kr zwB~s1@&+2*L$yZgjEb3ng>TpYz9Fpm?TupI^h_kbPgNr&$nHt1pq zGW-BzOwNheA;?toyYtzWM!hP7h9sFX{piN@rZ(t-?SbgCV1KmgN*G*M@$^sodgzt_qO?KNV{P`SD`+94-kZw z0E1A}dx7mB**X%QV(+-^Ye&JymnHPMLGA(Z_s`Tk8yXx`vb411XiQ-=Bulx8Y^B*5 zic1!i+Im*ZJ+8IGSer)o;%oTaEE&Zi z?{At9WXsv;HlCNm!&6{MI_;0=DiunPoIZ7Y>f6<6K)rToF#I0sK~?fRudYWQ_Os^g zcl)_#)X{CZk$aK?W{q?L~AD*W5n{$==x97mMEVReP?x(@O zlfL-ZF%Li@+WLej1G6$f0|7QpGtmtCU!Txd*#BeFpDP(h-oH+JfOoWierRw76Nco9 z-E^)fv{sWfb|#O@sP^re{uc&UY(+z<-0U<6`a1Mo9l%OLi*_77Axj$lFu7q94I?Aq$rbLZ+F6 z)_Ksb7I00(VP2_@{|x(DI=|@RaF4DSZ*mrAdB?g9O@z8&VrNi&j( zkO_(8D$_V#U`98;zC5G$3j6ODd}(*}wX#}}Oe>@w$UXJ?E(e&DZ4n|W#3)0n!7_rD zx*oS3c_cvynJDCGj7yOV*N7$RR|p5y|O!dnPn$q0tI zRKM};VLsw=L`#2Ahaj+2tA-vp1er?pCxvxT78-t-QA8D-B@IcnsTAvagf-36wn@1kel~07d zOmeymNM33Y|{J`{8C`f1+}xS(n?e z#NI@9gUf}+pw}XM7KL{z%Sk;v+488H>3XuP*4#8&@XHevMUr=an;IfHq2>~0h-C{!;>J<;T+uNyef|ya>=xH#G=xfA%lf`q*7py z#($%aQ7KekB!1K-SS0o0WTuW3nidrb&rkO;;U~U~(tvUD{fc-LjJ(2-*ptOGw}r9I zx~Fc@N8hS~7RJ01?YN-1;&b7ZSEumM;yb+LK{$qAEBUPc8Q=HCsjgfvL_mZKEZ3eR zt3<_UD58ebZig4PI_+qBSoiei4y)X3R zm29Q!*$T_4U^K#4wBzNp%R8_yo}iTRNu-~cxk(8}si4eBGu=_aL|H<;rULrL0*BkQ z6f4Gti_tqweodg^b9OMQZ4`}uzg$XJE$^T9H$zl9(Ti0FO?O zLO)D28!U_?N-3v{Vlz@eupR}wLmBl3yO)i9Sh`j=Phf|{$EMg@CH$K0=PD(P!8Y9s6%r9ZFZMNF=dTB>g!2!Ulxj-G^k}<`P65BO?)U$joC{iwZK-| z3ko8}-%dXIpZ*|jYbv0?WlAR}H0QwfC?Kswg>P;fdN1DdcMq4miLp8E;B{QNJt-vN-;sJF?t~K z2;0u8bmXr%H@p2m&X>^j4wg=m@|_XIp*^L%BAAKcd>Lh7T@@p!2wOt0}2 z;a@-?80oOU0&8Q!XI9>jb)_$ag6VWx6y$ianG6E?2JyJyKU$Si1kO{VOLk+1j;37l zgZWZb*wo}ej=n_s&{|InLr=@K1wo4m{>e3?lX%5pw;Yy39t0UQGPso#H;FOF zX2y6nveNBvuPqj#*z}|@W6do4vSb*oi_J-K{6p%ZB* zhgm0wPPsyE@FG%G_%%4V!XW*4KJPHH4R$Vn5p?Blmb`8M*gQ>uUQ_>Uy2bPiQn)Nd z3OtLN_!vhr2+FJD3p9exckIC!L5h^TdR*$2wt}Ml&#QD-&7C_r8Q`pr?kN1zoKjeH z&kY0CXYDL?ReN$>R39-5hovmafLW~fMGtJMa24cSN}F)*{nQHJ3v7G}@(q(^04%507CNojN5!gjo3|LUL|q&L zU0{_gc650-{eww7sx~p?!{l-O6W>Ocpy=l!dWIx%uV@Tr8gg=KYOQ0un~Jb*#d7_D z(OGAwD8m`tI?oBnR0z@i-jL6pa=X<^%lW&%IUEda6&spLwtb6ZVS{1&!-?yCo;|h+ zFx6D33JS2GEHzSDXkqujFx@g8HMKIoC#jZ)uLgl4+QEf-6Qx9jBtx?k6C8B!gqVyn z^(Mr}n*;a-7rpvGcgr-R6M{ss3f{Kf_yfVVAd$M@B2vwnK~FjgMN3&L2G+uIok>(- zWS(f>6f^|S1@t@|4^u@$PZ=m*$F}X@*U9bxaFZrLw~tmDk`D&B>Ul-yl6~)U6mTK9 zHmHcyWgJaC>JJv_*A-rGv~~F2*Dne$PA#kpZ&9d+;=Pe(#0m{hsss zB0bh*=CPNkZVq@48zT)GE_sOn zKr}3h#g4_HbgjD5Y$vbXY@2v`w(@7Gxm>w=XkM`5W49bNQ z{@MscN4zaf`>M*Kx%;hrr2qn!P^|+X}#n+ zw?B%94kGZXX34;c!{`f#kt2r@?!EE|KA`{3O9z?z*uxgdO-}TJ%j-(lXg=L*cf!OLbl5~h zFjvP=9vC^|adSBB2m}53r!?AZEl*P$B<(Q(p1^_*B8hY8RFp{EDp!<*cYJU)t&t`r zfYeY#R+Dw_{@uKe;ordW{$1(R_ANw{Prhm-`!I7q8{x7CePWI~zGk8=BS8Ki zT9^?bloh;xKhTB;pWwuF!4nhwvDOd(AjplnA!ZpE#{LR;7ZbwTOCt~z#6fW!BEh|n zS~7N*|D*<)){={P%j#diL{5UJ=0l z-zV;d*%biuU?DRu*1TVHLiD*`cVoGW%Dft2AeE*&%H#K1ON!iF;EA=Ik4*3od_JW+ zTeYjqAm20i$>#Iq+&fy)(7@*TupQRteY?frHiYk{5J$jYfbzLRx877%2+qmKS7|y= z95w*;{(O^A;fo__z2@_1BCRAfHFc|l=Fl;lkrJnj@<+oYmkreazkYJ#v{OI;rZ8D>#D>+-|?|T#n}=(%7ve04TbFulPl)#h!Q_5D&y3LXTZ! zsU-jdSN8y76_=aW=kr-0d-OwGE{ji!OVI2m-Z^)pGJrGz?h~`o;3pQj`nyJ}#n|6n zZ@$?4*yX>GwmnAT$)yt~Z2+THDxWQ&(ro($(7=SZ=bM6(y8I01KkjuutrT}5)mNM# zZyNz?ss=v{a>_te9tf37bboj{b^%GISkaqG&e6zN-bfz7x@?>;UC9!$bsRl+6QyP) z>gQeL{Qf&QCix;cWK%MyLtzK6F+=*XvW%Z(syZr}dL19@qQvU_E7Ud@YwXi=*Mlq`Q3{TipRl=Kumlo*-Q8 zAJ+avr`w^CrB`bRDK04~0~gAB-l3e;r!4?+km%0=28C#j3k3F}D_apXllgH1(kTZJ z`}X0Q&pfWDR39q=BQ&*8sSPU;kN%$RfDN4nVBGq7d2O{gnUa;_AE7H@mr$S8t8}O> zg!IE76M(a`06yUK$Cb(~spzJc3J=H$@a(>8M=qFv9C#ppfsZ9@Ij&_#?TBGFpCXdP z$OxMb;_LtfHqva-k$KF!(*Ce-w2z`fBO@a-q;*z{45cDR1E5#slZ}hIG8!JC-N8ukW8JKZ zK>ivGloB4Xl4vuF-wOad1mcU4`4njfqA-h}Zck>z2z?Lv&&qwaZX+WizNugn;SAUl zag}d8~yedkkvJ;ttZ|7uu#$t|m)<{hlol2?tQ|_vqYx ztm}8%NuBCjS$P$Sb0?FluJ5BsWK>lCqOMmAfGB6iI=GX&KZ}|{zLJdFbc^NU;~lOh1Nd~`WPSd6LX{q3TAwZEA zu8kNZTc`Ge{vH9cmD6=5V;xd0dNNV9Qt(#2C;5oRFr=V}(pZ@8529UaOkvP$SX!B# zD`EbzNc}v4-9mxg8dsEle=fr&={oI&1N2u9uJhi=n8w-!gSj%1sjy&{I5ei1p+^@w zyzXsBX5h`YsV&x8n73_i0Lu!|2e~H9>w5Y%3!~%97eLu*U$8mPq)dXCnzx+TwH;B+ z>Xm-RmcIrdrhL#z0yKvE`WuaNvcLm!ERWX#)CP|+$hIe2IgS95_0kl=BI5%G{zA!fIO)D>FuYr!P+Jj}(D zxCTe$BRZF5rwhJHIgN&Gd+7ReAo!+8^Ftd;`NC3P4!(nWluHTw0weBOm$q{SI-iu3 zGzfTGB(d^TovglZ=glJzcQWeakS^UlmO|N+E z8N(2%KyNADZjuOtP^a7ADOanc@dI^NOk=Yk5e!4XAKgz{>;&!23HWA2QCga1kmGS) zG}CsAX>oW>x!YIZ^SsqTcRo%HK(V^tc-*85C>Fh(VYOvOGg>M8r-U=y^C>J^xZ7S#)@^?~7vq zQ=`xz_7Tgo#~y57R$mv60%Ox{(`SP&g%m1oy#2iMgg*?w{?32_?ik7l@>)^<0U9-W z8Xi-Xy8TRfFQ$nJr@6&3br{?Z;7ptaB#46?3^Le7b(Z4D~bP z5{rQAiX(C0zp2H@`+-)4awxANTf%Kh+LMC=0<;3T~hm(oDOX zodsW?)8#u%>VzfHQ)9B(>9fZiZHcvn6^||G+rbBImxF=@2@9(^ND6O`*9S5^cC|MK zOXwGf33`SHgQfLQZ9bs)%*3oN`!l2`B$tBy9Lh6#rE0!$VI63)@Ts0jywBC$H)YO5sqxRN}}Y->kIZ=m5oC!-O4o= z<~3#^8M(MjW>D4yH>m4>u>waJvs;`vMUejoE9ewkutbjA&)Cf5p%^xx>;Jmk`O?uX zrJ+dtwdFwTTYIb7Cn{fknbA4;vy?)`kzP$v5it2P1*NJ2$R#!S7p z6+aCPAg}_~M7x{905Fg>Ex>@FMH%?Oit>d&`U?s`*wt#VL>7i1b)d^y`);~LJ|7x zrIUt4$|b42YeZp>H>(@GVf0$Iirn@I)un`OSal!bb)W|<33JJP`dKECCIxXOX3V?H zLXbvH)aZm698MG0K3&r^cLo)XTL#koCdEP?t*o90>l;iyoMoB|Ok{EQ?`s(x<)TMa zJzHtC09aFaBfOAUS}H&mu&=bB?uM^KM}{t(xHhX zjxvCY$|H)!7w~DmOtHbM*#R0WoFEZ1im~4roTo^eEa)bQ{#Svp(9RR4qFLg;sLL^+pj_V>D~qMPtNJcC zHl;l;R13V-wP2S$>M9-6MnCDcLnyOa^DB~lK@ZA^;5%FqVGs(z{EaJ!$A$aXVG$9R z&R_;%xNEV}TKFopDF)BtKwpLCj+9nC-&)o9Ekj~5+=WHXN=%|QdS<1P_qXBtjz zh0|8+#rC8+jRwFJLHV`mb($iEyucTT3h;acNMX?9O;rV|VOK@Od zO_b{x5cVSL5~ltL?0FTFFai45RWZ){^W5yhg5MV>KW64u=(V_{)@q~|LMq9BhLF@D zL!YGjS_j153cvvKzE0UNOSy%^MEFh2!#12$JA2j`{d}ZyTgR4U6ku!#;K(Q(%4SRP zDW4sIpW+dEU{XiwRAn?U*v-)ALgl<-EvFH= zhu+tiIM|~v91fD7=&3g9L?8hIvlQDGzqy?**c?bC7md0-vreKLO=%n1IS~XMfpkkT zW_bIU3c5nT+koxFR*YS5b8`OU6Yt=aL$Dmk2(<|D;fUNBlTMgO@vOu6hfu3xqu$#w zL73oieTU{!`QPoh58_Y(n@DDmc76{;r%foqgwUWQ8)!Gy6Z-K3+(>-^$mrt}S{_#; zl;r93Gr7=q%1D0jQ(E{dF9JS@BZqZiN^Gc#?AR1?RMbgW#ZhwXw4sp{+*|8`KS68; z5hg4+uo}TT!84VGYPGuFUY}Xe1VZh*xz>whaqY;#O0VrQ0kvj&Kjc_m2A_<9(;WA}`)ne6Lp7;lB=MSF99Ul~ViC?jDOelh? zv;8h@!ijin5-7wETYLO)zc;v*b+Zbk*NQr&9TdR@E3R(X9EK~oBZ&z82%xHq1QQeQ zme9(9U(B6~IFX194%%L+up2msLRahv9Fv;a66fXzK2P^F2e+2#WlMs^*{NmO`?}vSWWLkiUn}rWS17QMr zNPeLNX9Ov#@&SV`9OAGg7TMpwQmXm4nhVuk7L>EYjzDCULIy*{Dz&CxWrG_KOUI`} z65lxl`Cart2Q%$R>)TU-x_$VC=MBWqEY^Y@j6-7Pnp4Z7-ELq#gwQ%-p3^Ks<;|@8 z;0>M8T|2t`A3h)?n6Er_UC$lwYxK0EkZ%wflEMB;s_gv1@_G{p+V98sR8 zQa1>{ua1W@lzEMiD42mi0dE^!04NVXe?rkD9|a8hq!fOSw^Y)B4YYBKpD2{M9INRd z0y9I;pHPo47D2=MEYa;xhYhR149a@G4oWcdu-Dkg!H6P z)o?s$E#}#t$J9~}MeG_(L*lUT$nernZfW9@QXdiY#lY=@}W`hOH$Q`s|nydlAEk? zAu&`b1rzJreKKDawqs?K)7)8at^>ElYB8G*=zajn%~gA+@JN9}+I`mVgY3zO2iPxC zcd9Ru0hQX#qk;ox&6^DMBGBQ76-+|3>rXZ#VkkU$RKo5GTjD(eKV{v|*w!EXo_}P| z%|$gB7^RsQK`t6V9g4vPr_Dw$H{ISlk>T$czyMhjY@(F=(0O;6WQJRgK1}ci z#?TXeXHQ0)9^PEj7LF5bgMx0aSWIR3lUzms%@+Y!SD%_w+2e;@4pr{#e`8p_!+qR? z5eP7Ya2>Q7A3=KH2;sresg=YCjDEtYK?`pw0`Y+(z_RQ`LZsNHan)fOrI8_M8;Zsj z1xRcpEtEYV_#cWyh$BIt-XV;k&d$MT8X)nA0w0F+czmMFb_)5Cwf58V@fcjg^`x_m zgU0tVT)XgZ@KfY6gYMWl#qu&JebQOXL`*{JrFinBy3)CYdhTk}swG=_1KDbM)ydg; zxszt|+iz(DpP&vO+~dwlv2xU_uD(BCK1;JX&KXn_sD_b8jVZU*)m{mM#7%d&#m%nH z@{8Kg#4X=tx@~;>{lvNH&8p4e2HCSghm}JN1e^8DLQYx__Mop=96ds5L_6A|953N1}B-!8D9sOV393ELBh`GrnEKGf9e^qUP2V6!xN&@-x9WFXJ zg^d-F?xr5Y6U#TW!1*r?=88uQbH@QaV5AVXvRD|FYnpqCRR*j@rsGb_jkJ_rHQ3+{ zGueyUfOU=MUSZgA7NEtX>vPe4Lu^=7?}J-$>b$FD(SgpJU2G(|^|nQ_OuMaguMH09 z?}Z5l1N9OW5!QWtxZL56Mz54EIEg_o*s&QM*AONkHG(`)R#Mt!Lc7bP`6Vv)ndYY^ zFOyIlj;2HhcUCdKSByHvnc!B@dONEV1_!s(O^J4YJZwd@Qdr?Gz^7nzIbBlBj9+ab zkPSapCvtNn8Q`BuVX^vtSLCGSZXZAi*7Z@~+u@hX4JJeAz7J|90F*yhEKm5T$4QGC zF2~CMEyZEezZIfusO{mWTvPK~Gp5h_ z4*nrx4P(M^jp~nJhC7n}C1!z25beS&X)oYiS-PWzMq;yZmKB@e{y50;Is_t6(#8hH zsgp#;gFI2{K~Y|bwv@ePn8ac3q0^E5F7$dzv!S61LN=MPs)0YBR@w-k6O~>sP`-~cMdBS5REeS%5qd*EgzLA zMESz;O*897WcLt$JnQt)g9#Ls&rqYM*&8H7YI+3p{$5 zmLyD+(J`n1_)W7%EzDkZvwp-?dSt{%EI|V_+V$9^_1N~N))V2={wbg^*KYtlUa43v zv!iSrOZ-PFyz8IZm_T|Q;+{*4LPTS;9Jws%PvJ#`obHC16&en@S?zzW%=MP(GtH6? zCU!TFxzZh4uyg2NQV=H4;EIg;?@IK#I{*0PrRGi@DYVq7exSvz4F~Z-dx*1b-NQ%~ zaQm$zLYNORJ_z{x%3)1SraqZnuj31ltw^LurVxP~8es297Gn;pc5H1{!!~~>7|;YH zYlJcG#1!VpGCm1yKkIyZyZl*Ud!BJcgY>j0!J$U+7I$jvw(5EJ&HMmc0XrFLJ0 z6Em3UyC(!Ox)443U}(%yUIGkclvsuB6AH5g0*86w>-n0=?_~q(vBT0qS2oWN zcb!e5S-l*$x=`vVp<}U=^&z~8^{r-Xn5-vC!Wkt@^%DCZFL#C#7@JHB-mboRAqoSU!vpICP~j-{rJ#)8=_qS_3;#mCMNixyQvK z?zFKab|L+8Kse2%@U8f;N+{Y0ya-T?JR0FbVq@`u^&3Y-MC1ZlnB#e4UMOs0p485` z#IFauZqhl2RG2=U{ZBPW^?Z^HV@^%!#Jva?OvZP+Q?KU0gKP0=YWE0rCxt;;cN`gY zH39hrjmxbpHj8*MtOVoBgLycL$v}2zK*#GYD8N7ZyI|)%Pz!x65o?cgI>r)}q6%xJ zKjz!-dq72ECb@$<_W!BH=q{>cBeY)8ADzm3&c**A1iNw}YI5}D@vfb8^>#Mjd<1U<^IwnExJjUO)&CrjsYjSAR)fXTte&jy7`j=TaoD_){$( zs>!@$xg-sS9-=Fi@XK>zQWvpCRxKYdTKr{ia{L~4RL86&-Pqt&2H}iIfTHfPJHQ7Z z^J00$M5^$dmIio=yTqZAJs=%GF^tDrr;k^!H{<8D#sAnc0kxN}ow3<|udO;?!aIMK z2#WgV`zjnUO9PYg=hjS`5lG%qk|Nzr{dvVoo#?LXT{tc8z|JqJX5or!Wh@h&J_v$_ z8B7)wk??7iM*jlPTyH-$KV3Z;nR>W#jIq{J-m|kG&QF!Ub~3;Wjc4f~`I^`ol_1sZ z8Ndz<%Ag4NtlEb`3!ewJbrdH zgoGC^M`>hKZ2MYMn?GAo&3ZCXZ0l0X5qfZl&pRLdo%zaB`>u&Yx}RdvlA^~6eca8Jy1 z0LnPUWf|lOasaE+T&T++-;g+qo_Sqn)cdHw&P&|;#{ttk#{HX@WmGXRbe-uoPyI^4N>?A973mj!H#qltDEL zn~iq3bWs-TepIMoU`=rP&B|>=W!@7AdGmBqt(-03Q@5^ZYJIQoA>sEtLYX~vIGt~z zr(LPlXubF7j|_te29%l*;jY!2#Z}PWp&x{6?!DBqlm3uR=V&k*LhuCQsvmg5lY(y@ z@K9m;Hb4-W59DB_s)+2Ly7h7R+-o{cfig-eQc_VI-mzrH;^1J2Qu%pUMb~DV)glBR zTfbLwTK_bG1kE&o>iHe-zP647uGUzuLuehZSQ9&eVkthyt$6&xa2`0w6Bbr(Owt-| zT3?p_^j=9=bLs9+gwaGTtbUlsaQZ;Nx&Wg`~R~#`K zVUW8*045C!Kps24Qrej=BAF7Lwo*z_whbNC7W<;sfx?-2gQiO4RsJU>EXs>fq9O!7 zHo9mbT@rn-8Ttj> z|EyVv-&EB!tlZGAJ0Vdc%hD6kOTWw2s~MtzTFYw3len$w0^MGo46F z6csK>&2ZA`-h!Z#xB+|)3=g<#2xc8fdCm`{Uz3571hQb}2_VC+TF2O~!w?uX8wto~ z;l$qr5v3NIz+K2w8c0J?*6R<$fXy)e*=EwjBtT;~CPN*-ykfVQ5%V#9!4U5(;g-OjhG+*x7_}p+s5QNbKeg<* z%o&CZA55Er_^|_3rD~p-!=RGhoF@4xCjr(-{SDw$qEKui;BtJ<>T|f>pEv?UQO(zM z#cnSI+g7N`Y`5UHS^7!O$0suMhkfAqOb6s=*DGM4XZICIyUig(cL0@8Uu~ty9mj;MkqP_HjA6no-PJ&7YqozL3c#U- ziGO#&fK}FoXHx+(`KP-L@6<$W0y!4{Ucaa?w;ms>+zaO)rAjFDQ^_FASeido>bx^q+D^7n zpmv#9^}vz|OaR%w7kt@lk4u=O#&`t9a=r|Gpuc~(H6R7*zf?0ql+p6gE0=!@10{$Y zkzv}y_OiTwe=B#bX(JMZ3p_GlNuP%u&g~ZJzsMK4WB@QOCyPA%mvI^!Xm$o=B;;_m zCYxZ_sln3Gv(0g90y$eIhx9Lu&8wE;UD)|enrSm%rk0!fY-{w@o{naVO{TbSNrMfa zr$iAIfe49g$BOD^d49T7nGzs>XlTG=&{eU*V zWO*!5Rc_1$SMf)u$vRbO&n+taSYF_64k-;CY`opx6n?pBUD5N=I9=V%!$35&VfYk{`>30-M!=Kv@H z;eQI^BmlZ)9??EZIiZl!NA?uTct8BRug+QkymZEe! za-pbiss+|Vu}}08dHlZuB*9Wrwx?2HvfHm!(@h+U_HNnEaS1`J^jU9x-vLs#{vnr; zdGF2M12=K)lk^TT+t*(w$8~qG_>Pc!AFt1%d_T=Kemu@?Sr*={2eeaa=4WPFUOWDP zD#CV#-YlXwsyMu}WOj((jk@_^abSurO*N=N?v8106u&@#B+K?ysF)jhV1?55e=wT^ z6Y)UYy@JpRl?ta0a^JN|Ka1ImJmf*H&&PUc9t7M=<)24&Ik7M$T@?lslr zkOmTSE!VCpFRDg!s=_=i6_er0?9#wjEPlAP|Gd$I%8k7cURJmk`vnB(E^w2o@?$=? zWI>KpWj8wnBh)ybN^FpYnotG1S*!+Fx9~pMU;iOvs&K|=LtEVA1pv8u z5iAF43#V_79r~b&C2O`QG*iOxNEA~H31TDwQtR2eq3~8(PiZZ*P+wsEY%f=Zv^dRd z3{={8B(kiPA>8(>>ANg+uDX=6iiq3icC}$4bJ>sRb##nxE(cng-=nlx+&|Fy%jnP? zUSwcxH41%aVID`yNf>0<1q#PHzojoSFC4pU80=!?=aUeHT7y+kO;Ser%hrB@cJ+*`^9v{9~gd*=mtSY-(19?*iHyG6Ic3W^rf9cqtdc6PFDH&ygo zd=_ybOP%4k4+TZq) zERf{bc)0HEr@i8;X217-_Twv@T}IefsYAnP%N#QMN8yUO_7;V+c4+i6+_KS;nuPWq+0``NCoH?S*^hh7gsL%%qKB8Mmk%h9;164X#&K z4c230*G3a@v`SZ+*V%q2I66{7{5qmM@&j-JV&7PzP)OtyX?YB-v=jxPznKMU=2+xz}bbJdB+VlF0)m?#S4PxI}-OY{HeDX>!;x&QbnvA_C$7R3>N=6sxz zT!9{0ma-Jl`i^}Eqf2zXX+hMAa{=CYcu>+P-ou;Rk>fRh{kRFet-B_+U@@ zs5VLRDR{g}oWk%^_(A%p;*HjTDLm;T9($cNP1S$|0P7du&IA9FXtgejdWR`1N*=qH zE9O=0jJqdj>bq|P$kGqKXPh%at{H+tw4QJY6qSB>^36%Y|&CktN6(D4gq*n3u4F9&m7@(V-PsZ4D2fk420XjX3z9 zwL!#U`a`HXuV*V6Z{_#OV5>C*7Ot+>Bj!BUIhf*ji7#Z_Q+Hj&2GN?H{I%>%y(P)I znE0B4{3r3Lf}k%CgzmN&S|yH9SdqxvVgs|l?zE#B5<^XHv!f3m)Vfi zYrDUa_`MeKjcSC_HiqY@kLH0v_cnbg4e9JQ2D{#m7+A1|pIUz+aJ0$R;RFy`s*MJ3 zxa=mTrV$jZ80NW1O08Jqqp9MSjr7Col<=tw6kxs&E27yh*7j?%;*PJP*d~FJP`^?7 ztV?&}xaE>9d|(Il>g|C01F%Znr>US`DUWfSibO^AT9tg;olYkNm5n6{lq?CPOY;-R z?0E#n+Vt6~s}`u!^S~Exr7F+RLghS}42T^i_r&$$dj*rw+mBCa>osD0DxVq#cx911WQcPBddP};E$*)yS#;aI>1Wbj zdwr$h+0h$J;(tY>%CI8{1Z!;3AK|av$zg=I;~AZVtbVZTh2gtHZny%Qe2a*=cTqpR z@-^8cWSCqhofEgwwJw(p!~wm;2}j#KXIDM2~fJ71(3 zCV$Y8w5BruP4x8x_!75)$r5_A`I5BNH)$4ZYm<@_8qmUO_C2u&>G7u%&P|v*boq?T zhl4lU+)QUY1t!MScm+m_x92<&FKt#HzP~h69qi4xX<_qo9c2R|YhI@=s=S9~7q#%j zvVkEE>v5bp--;-GOTvCS#OO55airX^Ul8Z^mw#XMoCmu=D04Nt@oq~|a@q4Kay${r zoE{auOi7~!amkwHM_saLElDB|dE)JODCSro%(6h1ZGxK5XdZJdEMeX%h`-)2v+$Y! z>!bt#*ji|SqYJ;T)^}=*s`Ah3qg-~Q@<@L_Q=6p>%9h$RGqnFxnx*hDCuJ*-z${i0Ud#+fr@aEiN9OvD zc2|Rz7}FkEh<*WC=k8tto+uqFkQ|`nRH4VlC{E}P6ah~}&Q=L!ym^!*6zueHD8M&+ z6M4xHc7?}gXFneP*&Y+J&48zo?7$`DiJgQ&7eWazJvyA z2P`3BWo!hUs`%AY@#C(XSQmz%^X}S;O@TCNW?qp{ZRR|+oTP#ug%zdnyuylF-xo6U z1)<+z?*`qvRT)$O*rg&oBp(bl@a}j4FZtG@$7lbtW>rp3p_ea&N$3&3w-FzzHtUqL z)GHz1Sn%_b;a7}h@YfxVoImV}OlOM5`GH&`<>oPE&F?GH4jzQ=t&P^52C@1sK9V>g zBw+|KA3`BO{DNfw_n|C1;xn79RBP+FGZ}z`wDl^@IoU~*TBB8g@=O>Z{BnnD^Q zR{Z$auKaMO2`e#QT=koHiV!g=v3AuXKxQrWVYr zYO_B3x#Wt={FK>w2M>H4F=8}$VHB%2bY-_G@210adhhGxwmrn~JNH|GrI+MAO)VNn zZp4mUPhBiUcP)lP224KUHg`-vUApVDO`4J-gfV2P^mtSJc;J(>+}OE4PnBGW`csez zeflI!$gs!ias{9SNLoF4F*adoz+l%In#co(XIzIUrw4%pDAfcM1l}>efrUN)9*3;Y zU<9ULVUA=U@`j{Q(4D6J$rZDuB>wN2#vj6UU&hG%0m8a^x)WhhFnD1M`!qedk6^z; zX%m95j6(GDDp&7u>Kw(~1@3+P4wqsI8`OK3zNX8~6bcR(jz1j}!9@QT*>5H4770Y= zw||+}m=u?Dw@N-e%6C1WW+C0&yuvB7Qz2)d^ibkv%*QtcZqG%QC#0W09ebLXbMb`$ zyRFLSF-kS_JM0n0!=+v7%U}0jBQxoTHj+8oJPQg$Hw3)nDfvPA!9mj76ulrWn(XJG z%P6Yp{DV=3cpBczi$#MAAVHU)OHUO*)YUf6>)$61ajHuT={wYon5G@Tv~fSHzzhOr`Fdacl+s2bo(N+O_UOCIl!oH#pdqVE;@1K9xES;! zJtzjAF+GE$(kL`F5$*4Q4FdCFn@QpxGw9;3Wy_be4p%^U)44d=t*{HZe)u`DgoYjR zyAT>fWk@NpUzgH~4ww$b)(f2=Cjh;1;)FXrGpf|ncLwlm;&uEOvqkwep*WjO zX_m*1^1TL(dHM?+vQf+N#g441oL%e!Uy>49v83ijOmnrZ-%7TyrE#rp?Ia0s^&NQ z(s>BPof+U;mWS;IdXA=1WLH-N&D2UY7R6^fd+L0CY$=fKMMdBqbyJe|b%?r?=}@&v zA21^yNm;;0OM1#K+}FgUN%Rpj62zWcgL`yFX@1#MtPVB>zG8UkD7$pTTh*w6Soshf z9F=AuZec@i57ENBy?IkF4$7PoOQB-jdtYO_2{n&ysMnU(GaM)walg`?EblHA6N^jV z$?CQ*th&+jJ617U12Xx>p-FJjoT%7PgnwDz0F0S~F0HX8Q`v z@mJ(SX$GMwq@RvJ)S*&z1~suHslCA78v+~&x2kCJKe+%pF4K7^TE)cMq!X)(hMZoW zFB2&JsZb-2zO+kNIQacFp#hYO5`tpNfj(6x%923L*d?Sd6KOQ78p?*C{*hNoM?o)x zr3(r`m;!XzhR#qi)*b8T?}6y2QLg?{^z-VwTs~sD;gX@XQqHv{_9j>fIH+vgBtl!6Rpo!{HyLMZ2At`;7&7_QjxxzO4c1ydSifL}% zK;}gQJg{L8ua;zJ*%Ml3c0N(?Mqm7^BlD6#JnP__MW&8l5S%<tg;)R)iM)UN18pV_XK0gZ6X#@t03_VkN#y03xUN~uPvPECa)$W)_GM&2Et ztwrP7;{!bf<)45y*fs}nFd~lqkh#fhJ)b1Gw|*`CT)PX#=?Y9=U|qeVaPc+@<%5E& zxVTSu%Yf*;KtvLkw7oxj%Lv|LuQS zhd{z<2swy|tR!IqSg?Q4nb1aUAm*sCBe%T&A0-t=24z5ocaZ63MgCpqul>3O1*Vu= z(BS6(X|;GNi|ar diff --git a/plane/code/images/migrate-error.png b/plane/code/images/migrate-error.png deleted file mode 100644 index f42ec441a7d1c7cb52d8dd24b4b1bd4311b843fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14139 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 V5bMF<+1r<7l@-+GD`ZVW{~rQdd)NQ~ diff --git a/plane/code/images/started.png b/plane/code/images/started.png deleted file mode 100644 index d6a0a0baaee767c3e8d852926e1a2ec788b90f60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21489 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 diff --git a/plane/code/images/upgrade.png b/plane/code/images/upgrade.png deleted file mode 100644 index b78fbbb6093b60a8855f863fcddc329b8472292b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55240 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 J!m~C3{}1cA2Fd^c diff --git a/plane/code/migration-0.13-0.14.sh b/plane/code/migration-0.13-0.14.sh deleted file mode 100755 index d03f87780..000000000 --- a/plane/code/migration-0.13-0.14.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/bin/bash - -echo ' -****************************************************************** - -This script is solely for the migration purpose only. -This is a 1 time migration of volume data from v0.13.2 => v0.14.x - -Assumption: -1. Postgres data volume name ends with _pgdata -2. Minio data volume name ends with _uploads -3. Redis data volume name ends with _redisdata - -Any changes to this script can break the migration. - -Before you proceed, make sure you run the below command -to know the docker volumes - -docker volume ls -q | grep -i "_pgdata" -docker volume ls -q | grep -i "_uploads" -docker volume ls -q | grep -i "_redisdata" - -******************************************************* -' - -DOWNLOAD_FOL=./download -rm -rf ${DOWNLOAD_FOL} -mkdir -p ${DOWNLOAD_FOL} - -function volumeExists { - if [ "$(docker volume ls -f name=$1 | awk '{print $NF}' | grep -E '^'$1'$')" ]; then - return 0 - else - return 1 - fi -} - -function readPrefixes(){ - echo '' - echo 'Given below list of REDIS volumes, identify the prefix of source and destination volumes leaving "_redisdata" ' - echo '---------------------' - docker volume ls -q | grep -i "_redisdata" - echo '' - - read -p "Provide the Source Volume Prefix : " SRC_VOL_PREFIX - until [ "$SRC_VOL_PREFIX" ]; do - read -p "Provide the Source Volume Prefix : " SRC_VOL_PREFIX - done - - read -p "Provide the Destination Volume Prefix : " DEST_VOL_PREFIX - until [ "$DEST_VOL_PREFIX" ]; do - read -p "Provide the Source Volume Prefix : " DEST_VOL_PREFIX - done - - echo '' - echo 'Prefix Provided ' - echo " Source : ${SRC_VOL_PREFIX}" - echo " Destination : ${DEST_VOL_PREFIX}" - echo '---------------------------------------' -} - -function migrate(){ - - SRC_VOLUME=${SRC_VOL_PREFIX}_${VOL_NAME_SUFFIX} - DEST_VOLUME=${DEST_VOL_PREFIX}_${VOL_NAME_SUFFIX} - - if volumeExists $SRC_VOLUME; then - if volumeExists $DEST_VOLUME; then - GOOD_TO_GO=1 - else - echo "Destination Volume '$DEST_VOLUME' does not exist" - echo '' - fi - else - echo "Source Volume '$SRC_VOLUME' does not exist" - echo '' - fi - - if [ $GOOD_TO_GO = 1 ]; then - - echo "MIGRATING ${VOL_NAME_SUFFIX} FROM ${SRC_VOLUME} => ${DEST_VOLUME}" - - TEMP_CONTAINER=$(docker run -d -v $SRC_VOLUME:$CONTAINER_VOL_FOLDER busybox true) - docker cp -q $TEMP_CONTAINER:$CONTAINER_VOL_FOLDER ${DOWNLOAD_FOL}/${VOL_NAME_SUFFIX} - docker rm $TEMP_CONTAINER &> /dev/null - - TEMP_CONTAINER=$(docker run -d -v $DEST_VOLUME:$CONTAINER_VOL_FOLDER busybox true) - if [ "$VOL_NAME_SUFFIX" = "pgdata" ]; then - docker cp -q ${DOWNLOAD_FOL}/${VOL_NAME_SUFFIX} $TEMP_CONTAINER:$CONTAINER_VOL_FOLDER/_temp - docker run --rm -v $DEST_VOLUME:$CONTAINER_VOL_FOLDER \ - -e DATA_FOLDER="${CONTAINER_VOL_FOLDER}" \ - busybox /bin/sh -c 'cp -Rf $DATA_FOLDER/_temp/* $DATA_FOLDER ' - else - docker cp -q ${DOWNLOAD_FOL}/${VOL_NAME_SUFFIX} $TEMP_CONTAINER:$CONTAINER_VOL_FOLDER - fi - docker rm $TEMP_CONTAINER &> /dev/null - - echo '' - fi -} - -readPrefixes - -# MIGRATE DB -CONTAINER_VOL_FOLDER=/var/lib/postgresql/data -VOL_NAME_SUFFIX=pgdata -migrate - -# MIGRATE REDIS -CONTAINER_VOL_FOLDER=/data -VOL_NAME_SUFFIX=redisdata -migrate - -# MIGRATE MINIO -CONTAINER_VOL_FOLDER=/export -VOL_NAME_SUFFIX=uploads -migrate - diff --git a/plane/code/restore-airgapped.sh b/plane/code/restore-airgapped.sh deleted file mode 100755 index 9da02fd86..000000000 --- a/plane/code/restore-airgapped.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/bin/bash -+set -euo pipefail - -function print_header() { -clear - -cat <<"EOF" --------------------------------------------- - ____ _ ///////// -| _ \| | __ _ _ __ ___ ///////// -| |_) | |/ _` | '_ \ / _ \ ///// ///// -| __/| | (_| | | | | __/ ///// ///// -|_| |_|\__,_|_| |_|\___| //// - //// --------------------------------------------- -Project management tool from the future --------------------------------------------- -EOF -} - -function restoreData() { - - echo "" - echo "****************************************************" - echo "We are about to restore your data from the backup files." - echo "****************************************************" - echo "" - - # set the backup folder path - BACKUP_FOLDER=${1} - - if [ -z "$BACKUP_FOLDER" ]; then - BACKUP_FOLDER="$PWD/backup" - read -p "Enter the backup folder path [$BACKUP_FOLDER]: " BACKUP_FOLDER - if [ -z "$BACKUP_FOLDER" ]; then - BACKUP_FOLDER="$PWD/backup" - fi - fi - - # check if the backup folder exists - if [ ! -d "$BACKUP_FOLDER" ]; then - echo "Error: Backup folder not found at $BACKUP_FOLDER" - exit 1 - fi - - # check if there are any .tar.gz files in the backup folder - if ! ls "$BACKUP_FOLDER"/*.tar.gz 1> /dev/null 2>&1; then - echo "Error: Backup folder does not contain .tar.gz files" - exit 1 - fi - - echo "" - echo "Using backup folder: $BACKUP_FOLDER" - echo "" - - # ask for current install path - AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped" - read -p "Enter the airgapped instance install path [$AIRGAPPED_INSTALL_PATH]: " AIRGAPPED_INSTALL_PATH - if [ -z "$AIRGAPPED_INSTALL_PATH" ]; then - AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped" - fi - - # check if the airgapped instance install path exists - if [ ! -d "$AIRGAPPED_INSTALL_PATH" ]; then - echo "Error: Airgapped instance install path not found at $AIRGAPPED_INSTALL_PATH" - exit 1 - fi - - echo "" - echo "Using airgapped instance install path: $AIRGAPPED_INSTALL_PATH" - echo "" - - # check if the docker-compose.yaml exists - if [ ! -f "$AIRGAPPED_INSTALL_PATH/docker-compose.yml" ]; then - echo "Error: docker-compose.yml not found at $AIRGAPPED_INSTALL_PATH/docker-compose.yml" - exit 1 - fi - - local dockerServiceStatus - if command -v jq &> /dev/null; then - dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped --format=json | jq -r .[0].Status) - else - dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped | grep -o "running" | head -n 1) - fi - - if [[ $dockerServiceStatus == "running" ]]; then - echo "Plane Airgapped is running. Please STOP the Plane Airgapped before restoring data." - exit 1 - fi - - CURRENT_USER_ID=$(id -u) - CURRENT_GROUP_ID=$(id -g) - - # if the data folder not exists, create it - if [ ! -d "$AIRGAPPED_INSTALL_PATH/data" ]; then - mkdir -p "$AIRGAPPED_INSTALL_PATH/data" - chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data" - fi - - for BACKUP_FILE in "$BACKUP_FOLDER/*.tar.gz"; do - if [ -e "$BACKUP_FILE" ]; then - - # get the basefilename without the extension - BASE_FILE_NAME=$(basename "$BACKUP_FILE" ".tar.gz") - - # extract the restoreFile to the airgapped instance install path - echo "Restoring $BASE_FILE_NAME" - rm -rf "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" || true - - tar -xvzf "$BACKUP_FILE" -C "$AIRGAPPED_INSTALL_PATH/data/" - if [ $? -ne 0 ]; then - echo "Error: Failed to extract $BACKUP_FILE" - exit 1 - fi - chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" - if [ $? -ne 0 ]; then - echo "Error: Failed to change ownership of $AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" - exit 1 - fi - else - echo "No .tar.gz files found in the current directory." - echo "" - echo "Please provide the path to the backup file." - echo "" - echo "Usage: $0 /path/to/backup" - exit 1 - fi - done - - echo "" - echo "Restore completed successfully." - echo "" -} - -# if docker-compose is installed -if command -v docker-compose &> /dev/null -then - COMPOSE_CMD="docker-compose" -else - COMPOSE_CMD="docker compose" -fi - -print_header -restoreData "$@" diff --git a/plane/code/restore.sh b/plane/code/restore.sh deleted file mode 100755 index 23b8de6cf..000000000 --- a/plane/code/restore.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash - -function print_header() { -clear - -cat <<"EOF" --------------------------------------------- - ____ _ ///////// -| _ \| | __ _ _ __ ___ ///////// -| |_) | |/ _` | '_ \ / _ \ ///// ///// -| __/| | (_| | | | | __/ ///// ///// -|_| |_|\__,_|_| |_|\___| //// - //// --------------------------------------------- -Project management tool from the future --------------------------------------------- -EOF -} - -function restoreSingleVolume() { - selectedVolume=$1 - backupFolder=$2 - restoreFile=$3 - - docker volume rm "$selectedVolume" > /dev/null 2>&1 - - if [ $? -ne 0 ]; then - echo "Error: Failed to remove volume $selectedVolume" - echo "" - return 1 - fi - - docker volume create "$selectedVolume" > /dev/null 2>&1 - if [ $? -ne 0 ]; then - echo "Error: Failed to create volume $selectedVolume" - echo "" - return 1 - fi - - docker run --rm \ - -e TAR_NAME="$restoreFile" \ - -v "$selectedVolume":"/vol" \ - -v "$backupFolder":/backup \ - busybox sh -c 'mkdir -p /restore && tar -xzf "/backup/${TAR_NAME}.tar.gz" -C /restore && mv /restore/${TAR_NAME}/* /vol' - - if [ $? -ne 0 ]; then - echo "Error: Failed to restore volume ${selectedVolume} from ${restoreFile}.tar.gz" - echo "" - return 1 - fi - echo ".....Successfully restored volume $selectedVolume from ${restoreFile}.tar.gz" - echo "" -} - -function restoreData() { - print_header - local BACKUP_FOLDER=${1:-$PWD} - - local dockerServiceStatus - dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-app --format=json | jq -r .[0].Status) - local dockerServicePrefix - dockerServicePrefix="running" - - if [[ $dockerServiceStatus == $dockerServicePrefix* ]]; then - echo "Plane App is running. Please STOP the Plane App before restoring data." - exit 1 - fi - - local volume_suffix - volume_suffix="_pgdata|_redisdata|_uploads|_rabbitmq_data" - local volumes - volumes=$(docker volume ls -f "name=plane-app" --format "{{.Name}}" | grep -E "$volume_suffix") - # Check if there are any matching volumes - if [ -z "$volumes" ]; then - echo ".....No volumes found starting with 'plane-app'" - exit 1 - fi - - - for BACKUP_FILE in $BACKUP_FOLDER/*.tar.gz; do - if [ -e "$BACKUP_FILE" ]; then - - local restoreFileName - restoreFileName=$(basename "$BACKUP_FILE") - restoreFileName="${restoreFileName%.tar.gz}" - - local restoreVolName - restoreVolName="plane-app_${restoreFileName}" - echo "Found $BACKUP_FILE" - - local docVol - docVol=$(docker volume ls -f "name=$restoreVolName" --format "{{.Name}}" | grep -E "$volume_suffix") - - if [ -z "$docVol" ]; then - echo "Skipping: No volume found with name $restoreVolName" - else - echo ".....Restoring $docVol" - restoreSingleVolume "$docVol" "$BACKUP_FOLDER" "$restoreFileName" - fi - else - echo "No .tar.gz files found in the current directory." - echo "" - echo "Please provide the path to the backup file." - echo "" - echo "Usage: ./restore.sh /path/to/backup" - exit 1 - fi - done - - echo "" - echo "Restore completed successfully." - echo "" -} - -# if docker-compose is installed -if command -v docker-compose &> /dev/null -then - COMPOSE_CMD="docker-compose" -else - COMPOSE_CMD="docker compose" -fi - -restoreData "$@" \ No newline at end of file diff --git a/plane/code/swarm.sh b/plane/code/swarm.sh deleted file mode 100755 index c58f05e51..000000000 --- a/plane/code/swarm.sh +++ /dev/null @@ -1,612 +0,0 @@ -#!/bin/bash - -BRANCH=${BRANCH:-master} -SERVICE_FOLDER=plane-app -SCRIPT_DIR=$PWD -PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER -export APP_RELEASE="stable" -export DOCKERHUB_USER=artifacts.plane.so/makeplane - -export GH_REPO=makeplane/plane -export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download" -export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deploy/selfhost" - -OS_NAME=$(uname) - -# Create necessary directories -mkdir -p $PLANE_INSTALL_DIR/archive - -DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yml -DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env - -function print_header() { -clear - -cat <<"EOF" --------------------------------------------- - ____ _ ///////// -| _ \| | __ _ _ __ ___ ///////// -| |_) | |/ _` | '_ \ / _ \ ///// ///// -| __/| | (_| | | | | __/ ///// ///// -|_| |_|\__,_|_| |_|\___| //// - //// --------------------------------------------- -Project management tool from the future --------------------------------------------- -EOF -} - -function checkLatestRelease(){ - echo "Checking for the latest release..." >&2 - local latest_release=$(curl -s https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g') - if [ -z "$latest_release" ]; then - echo "Failed to check for the latest release. Exiting..." >&2 - exit 1 - fi - - echo $latest_release -} - -# Function to read stack name from env file -function readStackName() { - if [ -f "$DOCKER_ENV_PATH" ]; then - local saved_stack_name=$(grep "^STACK_NAME=" "$DOCKER_ENV_PATH" | cut -d'=' -f2) - if [ -n "$saved_stack_name" ]; then - stack_name=$saved_stack_name - return 1 - fi - fi - return 0 -} - -# Function to get stack name (either from env or user input) -function getStackName() { - read -p "Enter stack name [plane]: " input_stack_name - if [ -z "$input_stack_name" ]; then - input_stack_name="plane" - fi - stack_name=$input_stack_name - updateEnvFile "STACK_NAME" "$stack_name" "$DOCKER_ENV_PATH" - echo "Using stack name: $stack_name" -} - -function syncEnvFile(){ - echo "Syncing environment variables..." >&2 - if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then - # READ keys of plane.env and update the values from plane.env.bak - while IFS= read -r line - do - # ignore if the line is empty or starts with # - if [ -z "$line" ] || [[ $line == \#* ]]; then - continue - fi - key=$(echo "$line" | cut -d'=' -f1) - value=$(getEnvValue "$key" "$PLANE_INSTALL_DIR/plane.env.bak") - if [ -n "$value" ]; then - updateEnvFile "$key" "$value" "$DOCKER_ENV_PATH" - fi - done < "$DOCKER_ENV_PATH" - - value=$(getEnvValue "STACK_NAME" "$PLANE_INSTALL_DIR/plane.env.bak") - if [ -n "$value" ]; then - updateEnvFile "STACK_NAME" "$value" "$DOCKER_ENV_PATH" - fi - fi - echo "Environment variables synced successfully" >&2 - rm -f $PLANE_INSTALL_DIR/plane.env.bak -} - -function getEnvValue() { - local key=$1 - local file=$2 - - if [ -z "$key" ] || [ -z "$file" ]; then - echo "Invalid arguments supplied" - exit 1 - fi - - if [ -f "$file" ]; then - grep -q "^$key=" "$file" - if [ $? -eq 0 ]; then - local value - value=$(grep "^$key=" "$file" | cut -d'=' -f2) - echo "$value" - else - echo "" - fi - fi -} - -function updateEnvFile() { - local key=$1 - local value=$2 - local file=$3 - - if [ -z "$key" ] || [ -z "$value" ] || [ -z "$file" ]; then - echo "Invalid arguments supplied" - exit 1 - fi - - if [ -f "$file" ]; then - # check if key exists in the file - grep -q "^$key=" "$file" - if [ $? -ne 0 ]; then - echo "$key=$value" >> "$file" - return - else - if [ "$OS_NAME" == "Darwin" ]; then - value=$(echo "$value" | sed 's/|/\\|/g') - sed -i '' "s|^$key=.*|$key=$value|g" "$file" - else - sed -i "s/^$key=.*/$key=$value/g" "$file" - fi - fi - else - echo "File not found: $file" - exit 1 - fi -} - -function download() { - cd $SCRIPT_DIR || exit 1 - TS=$(date +%s) - if [ -f "$PLANE_INSTALL_DIR/docker-compose.yml" ] - then - mv $PLANE_INSTALL_DIR/docker-compose.yml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml - fi - - echo $RELEASE_DOWNLOAD_URL - echo $FALLBACK_DOWNLOAD_URL - echo $APP_RELEASE - - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)") - BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') - STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') - - if [ "$STATUS" -eq 200 ]; then - echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yml - else - # Fallback to download from the raw github url - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)") - BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') - STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') - - if [ "$STATUS" -eq 200 ]; then - echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yml - else - echo "Failed to download docker-compose.yml. HTTP Status: $STATUS" - echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml" - mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml $PLANE_INSTALL_DIR/docker-compose.yml - exit 1 - fi - fi - - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)") - BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') - STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') - - if [ "$STATUS" -eq 200 ]; then - echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env - else - # Fallback to download from the raw github url - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)") - BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') - STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') - - if [ "$STATUS" -eq 200 ]; then - echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env - else - echo "Failed to download variables.env. HTTP Status: $STATUS" - echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env" - mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml $PLANE_INSTALL_DIR/docker-compose.yml - exit 1 - fi - fi - - if [ -f "$DOCKER_ENV_PATH" ]; - then - cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env" - cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak" - fi - - mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH - - syncEnvFile - - updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH" - -} -function deployStack() { - # Check if docker compose file and env file exist - if [ ! -f "$DOCKER_FILE_PATH" ] || [ ! -f "$DOCKER_ENV_PATH" ]; then - echo "Configuration files not found" - echo "Downloading it now......" - APP_RELEASE=$(checkLatestRelease) - download - fi - if [ -z "$stack_name" ]; then - getStackName - fi - echo "Starting ${stack_name} stack..." - - # Pull envs - if [ -f "$DOCKER_ENV_PATH" ]; then - set -o allexport; source $DOCKER_ENV_PATH; set +o allexport; - else - echo "Environment file not found: $DOCKER_ENV_PATH" - exit 1 - fi - - # Deploy the stack - docker stack deploy -c $DOCKER_FILE_PATH $stack_name - - echo "Waiting for services to be deployed..." - sleep 10 - - # Check migrator service - local migrator_service=$(docker service ls --filter name=${stack_name}_migrator -q) - if [ -n "$migrator_service" ]; then - echo ">> Waiting for Data Migration to finish" - while docker service ls --filter name=${stack_name}_migrator | grep -q "running"; do - echo -n "." - sleep 1 - done - echo "" - - # Get the most recent container for the migrator service - local migrator_container=$(docker ps -a --filter name=${stack_name}_migrator --latest -q) - - if [ -n "$migrator_container" ]; then - # Get the exit code of the container - local exit_code=$(docker inspect --format='{{.State.ExitCode}}' $migrator_container) - - if [ "$exit_code" != "0" ]; then - echo "Server failed to start ❌" - echo "Migration failed with exit code: $exit_code" - echo "Please check the logs for the 'migrator' service and resolve the issue(s)." - echo "Stop the services by running the command: ./swarm.sh stop" - exit 1 - else - echo " Data Migration completed successfully ✅" - fi - else - echo "Warning: Could not find migrator container to check exit status" - fi - fi - - # Check API service - local api_service=$(docker service ls --filter name=${stack_name}_api -q) - while docker service ls --filter name=${stack_name}_api | grep -q "running"; do - local running_container=$(docker ps --filter "name=${stack_name}_api" --filter "status=running" -q) - if [ -n "$running_container" ]; then - if docker container logs $running_container 2>/dev/null | grep -q "Application Startup Complete"; then - break - fi - fi - sleep 2 - done - - if [ -z "$api_service" ]; then - echo "Plane Server failed to start ❌" - echo "Please check the logs for the 'api' service and resolve the issue(s)." - echo "Stop the services by running the command: ./swarm.sh stop" - exit 1 - fi - echo " Plane Server started successfully ✅" - echo "" - echo " You can access the application at $WEB_URL" - echo "" -} - -function removeStack() { - if [ -z "$stack_name" ]; then - echo "Stack name not found" - exit 1 - fi - echo "Removing ${stack_name} stack..." - docker stack rm "$stack_name" - echo "Waiting for services to be removed..." - while docker stack ls | grep -q "$stack_name"; do - sleep 1 - done - sleep 20 - echo "Services stopped successfully ✅" -} - -function viewStatus() { - echo "Checking status of ${stack_name} stack..." - if [ -z "$stack_name" ]; then - echo "Stack name not found" - exit 1 - fi - docker stack ps "$stack_name" -} - -function redeployStack() { - removeStack - echo "ReDeploying ${stack_name} stack..." - deployStack -} - -function upgrade() { - - echo "Checking status of ${stack_name} stack..." - if [ -z "$stack_name" ]; then - echo "Stack name not found" - exit 1 - fi - - local latest_release=$(checkLatestRelease) - - echo "" - echo "Current release: $APP_RELEASE" - - if [ "$latest_release" == "$APP_RELEASE" ]; then - echo "" - echo "You are already using the latest release" - exit 0 - fi - - echo "Latest release: $latest_release" - echo "" - - # Check for confirmation to upgrade - echo "Do you want to upgrade to the latest release ($latest_release)?" - read -p "Continue? [y/N]: " confirm - - if [[ ! "$confirm" =~ ^[Yy]$ ]]; then - echo "Exiting..." - exit 0 - fi - - export APP_RELEASE=$latest_release - - # check if stack exists - echo "Upgrading ${stack_name} stack..." - - # check env file and take backup - if [ -f "$DOCKER_ENV_PATH" ]; then - cp "$DOCKER_ENV_PATH" "${DOCKER_ENV_PATH}.bak" - fi - - download - redeployStack -} - -function viewSpecificLogs() { - local service=$1 - - # Input validation - if [ -z "$service" ]; then - echo "Error: Please specify a service name" - return 1 - fi - - # Main loop for service logs - while true; do - # Get all running containers for the service - local running_containers=$(docker ps --filter "name=${stack_name}_${service}" --filter "status=running" -q) - - # If no running containers found, try service logs - if [ -z "$running_containers" ]; then - echo "No running containers found for ${stack_name}_${service}, checking service logs..." - if docker service inspect ${stack_name}_${service} >/dev/null 2>&1; then - echo "Press Ctrl+C or 'q' to exit logs" - docker service logs ${stack_name}_${service} -f - break - else - echo "Error: No running containers or services found for ${stack_name}_${service}" - return 1 - fi - return - fi - - # If multiple containers are running, let user choose - if [ $(echo "$running_containers" | grep -v '^$' | wc -l) -gt 1 ]; then - clear - echo "Multiple containers found for ${stack_name}_${service}:" - local i=1 - # Use regular arrays instead of associative arrays - container_ids=() - container_names=() - - while read -r container_id; do - if [ -n "$container_id" ]; then - local container_name=$(docker inspect --format '{{.Name}}' "$container_id" | sed 's/\///') - container_ids[$i]=$container_id - container_names[$i]=$container_name - echo "[$i] ${container_names[$i]} (${container_ids[$i]})" - i=$((i+1)) - fi - done <<< "$running_containers" - - echo -e "\nPlease select a container number:" - read -r selection - - if [[ "$selection" =~ ^[0-9]+$ ]] && [ -n "${container_ids[$selection]}" ]; then - local selected_container=${container_ids[$selection]} - clear - echo "Showing logs for container: ${container_names[$selection]}" - echo "Press Ctrl+C or 'q' to return to container selection" - - # Start watching logs in the background - docker container logs -f "$selected_container" & - local log_pid=$! - - while true; do - read -r -n 1 input - if [[ $input == "q" ]]; then - kill $log_pid 2>/dev/null - wait $log_pid 2>/dev/null - break - fi - done - clear - else - echo "Error: Invalid selection" - sleep 2 - fi - else - # Single container case - local container_name=$(docker inspect --format '{{.Name}}' "$running_containers" | sed 's/\///') - echo "Showing logs for container: $container_name" - echo "Press Ctrl+C or 'q' to exit logs" - docker container logs -f "$running_containers" & - local log_pid=$! - - while true; do - read -r -n 1 input - if [[ $input == "q" ]]; then - kill $log_pid 2>/dev/null - wait $log_pid 2>/dev/null - break - fi - done - break - fi - done -} - -function viewLogs(){ - - ARG_SERVICE_NAME=$2 - if [ -z "$ARG_SERVICE_NAME" ]; - then - echo - echo "Select a Service you want to view the logs for:" - echo " 1) Web" - echo " 2) Space" - echo " 3) API" - echo " 4) Worker" - echo " 5) Beat-Worker" - echo " 6) Migrator" - echo " 7) Proxy" - echo " 8) Redis" - echo " 9) Postgres" - echo " 10) Minio" - echo " 11) RabbitMQ" - echo " 0) Back to Main Menu" - echo - read -p "Service: " DOCKER_SERVICE_NAME - - until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 11 )); do - echo "Invalid selection. Please enter a number between 0 and 11." - read -p "Service: " DOCKER_SERVICE_NAME - done - - if [ -z "$DOCKER_SERVICE_NAME" ]; - then - echo "INVALID SERVICE NAME SUPPLIED" - else - case $DOCKER_SERVICE_NAME in - 1) viewSpecificLogs "web";; - 2) viewSpecificLogs "space";; - 3) viewSpecificLogs "api";; - 4) viewSpecificLogs "worker";; - 5) viewSpecificLogs "beat-worker";; - 6) viewSpecificLogs "migrator";; - 7) viewSpecificLogs "proxy";; - 8) viewSpecificLogs "plane-redis";; - 9) viewSpecificLogs "plane-db";; - 10) viewSpecificLogs "plane-minio";; - 11) viewSpecificLogs "plane-mq";; - 0) askForAction;; - *) echo "INVALID SERVICE NAME SUPPLIED";; - esac - fi - elif [ -n "$ARG_SERVICE_NAME" ]; - then - ARG_SERVICE_NAME=$(echo "$ARG_SERVICE_NAME" | tr '[:upper:]' '[:lower:]') - case $ARG_SERVICE_NAME in - web) viewSpecificLogs "web";; - space) viewSpecificLogs "space";; - api) viewSpecificLogs "api";; - worker) viewSpecificLogs "worker";; - beat-worker) viewSpecificLogs "beat-worker";; - migrator) viewSpecificLogs "migrator";; - proxy) viewSpecificLogs "proxy";; - redis) viewSpecificLogs "plane-redis";; - postgres) viewSpecificLogs "plane-db";; - minio) viewSpecificLogs "plane-minio";; - rabbitmq) viewSpecificLogs "plane-mq";; - *) echo "INVALID SERVICE NAME SUPPLIED";; - esac - else - echo "INVALID SERVICE NAME SUPPLIED" - fi -} - - - -function askForAction() { - # Rest of askForAction remains the same but use $stack_name instead of $STACK_NAME - local DEFAULT_ACTION=$1 - - if [ -z "$DEFAULT_ACTION" ]; then - echo - echo "Select an Action you want to perform:" - echo " 1) Deploy Stack" - echo " 2) Remove Stack" - echo " 3) View Stack Status" - echo " 4) Redeploy Stack" - echo " 5) Upgrade" - echo " 6) View Logs" - echo " 7) Exit" - echo - read -p "Action [3]: " ACTION - until [[ -z "$ACTION" || "$ACTION" =~ ^[1-6]$ ]]; do - echo "$ACTION: invalid selection." - read -p "Action [3]: " ACTION - done - - if [ -z "$ACTION" ]; then - ACTION=3 - fi - echo - fi - - if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "deploy" ]; then - deployStack - elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "remove" ]; then - removeStack - elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "status" ]; then - viewStatus - elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "redeploy" ]; then - redeployStack - elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ]; then - upgrade - elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ]; then - viewLogs "$@" - elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "exit" ]; then - exit 0 - else - echo "INVALID ACTION SUPPLIED" - fi -} - -# Initialize stack name at script start - -if [ -z "$stack_name" ]; then - readStackName -fi - -# Sync environment variables -if [ -f "$DOCKER_ENV_PATH" ]; then - DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH") - APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH") - - if [ -z "$DOCKERHUB_USER" ]; then - DOCKERHUB_USER=artifacts.plane.so/makeplane - updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH" - fi - - if [ -z "$APP_RELEASE" ]; then - APP_RELEASE=stable - updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH" - fi -fi - - -# Main execution -print_header -askForAction "$@" diff --git a/plane/code/install.sh b/plane/setup.sh similarity index 95% rename from plane/code/install.sh rename to plane/setup.sh index 9f0065f66..3460d35f1 100755 --- a/plane/code/install.sh +++ b/plane/setup.sh @@ -2,14 +2,14 @@ BRANCH=${BRANCH:-master} SCRIPT_DIR=$PWD -SERVICE_FOLDER=plane-app +SERVICE_FOLDER=plane-code PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER export APP_RELEASE=stable export DOCKERHUB_USER=artifacts.plane.so/makeplane export PULL_POLICY=${PULL_POLICY:-if_not_present} export GH_REPO=makeplane/plane export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download" -export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deploy/selfhost" +export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deployments/cli/community" CPU_ARCH=$(uname -m) OS_NAME=$(uname) @@ -57,7 +57,7 @@ function spinner() { function checkLatestRelease(){ echo "Checking for the latest release..." >&2 - local latest_release=$(curl -s https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g') + local latest_release=$(curl -fsSL https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g') if [ -z "$latest_release" ]; then echo "Failed to check for the latest release. Exiting..." >&2 exit 1 @@ -196,7 +196,7 @@ function buildYourOwnImage(){ REPO=https://github.com/$GH_REPO.git git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1 - cp "$PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml" + cp "$PLANE_TEMP_CODE_DIR/deployments/cli/community/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml" cd "$PLANE_TEMP_CODE_DIR" || exit @@ -247,7 +247,7 @@ function download() { mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml fi - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)") + RESPONSE=$(curl -fsSL -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)") BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') @@ -255,7 +255,7 @@ function download() { echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml else # Fallback to download from the raw github url - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)") + RESPONSE=$(curl -fsSL -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)") BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') @@ -269,7 +269,7 @@ function download() { fi fi - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)") + RESPONSE=$(curl -fsSL -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)") BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') @@ -277,7 +277,7 @@ function download() { echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env else # Fallback to download from the raw github url - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)") + RESPONSE=$(curl -fsSL -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)") BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') diff --git a/plane/update.js b/plane/update.js index a5ef43fc4..51317d885 100644 --- a/plane/update.js +++ b/plane/update.js @@ -1,19 +1,10 @@ import utils from "../utils.js"; -console.log( - "Plan file structure has changed. This script needs to be reworked." -); - -process.exit(); - -await utils.cloneOrPullRepo({ - repo: "https://github.com/makeplane/plane.git", - path: "./repo", - branch: "preview", -}); - -await utils.copyDir("./repo/deploy/selfhost", "./code"); -await utils.renameFile("./code/variables.env", "./code/.env.example"); +await utils.removeDir("./code"); +await utils.renameDir("./plane-code", "./code"); +await utils.removeDir("./code/archive"); +await utils.renameFile("./code/plane.env", "./code/.env.example"); +await utils.renameFile("./code/docker-compose.yaml", "./code/docker-compose.yml"); await utils.removeContainerNames("./code/docker-compose.yml"); await utils.removePorts("./code/docker-compose.yml"); diff --git a/twenty/README.md b/twenty/README.md deleted file mode 100644 index 1fa12fb2a..000000000 --- a/twenty/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Twenty - -- copied from https://github.com/twentyhq/twenty -- removed `ports` diff --git a/twenty/code/.env.example b/twenty/code/.env.example deleted file mode 100644 index d53881426..000000000 --- a/twenty/code/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -TAG=latest - -#PG_DATABASE_USER=postgres -#PG_DATABASE_PASSWORD=replace_me_with_a_strong_password_without_special_characters -#PG_DATABASE_HOST=db -#PG_DATABASE_PORT=5432 -REDIS_URL=redis://redis:6379 - -SERVER_URL=https://$(PRIMARY_DOMAIN) - -# Use openssl rand -base64 32 for each secret -APP_SECRET=replace_me_with_a_random_string - -STORAGE_TYPE=local - -# STORAGE_S3_REGION=eu-west3 -# STORAGE_S3_NAME=my-bucket -# STORAGE_S3_ENDPOINT= diff --git a/twenty/code/Makefile b/twenty/code/Makefile deleted file mode 100644 index bdda023dd..000000000 --- a/twenty/code/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -# Makefile for building Twenty CRM docker images. -# Set the tag and/or target build platform using make command-line variables assignments. -# -# Optional make variables: -# PLATFORM - defaults to 'linux/amd64' -# TAG - defaults to 'latest' -# -# Example: make -# Example: make PLATFORM=linux/aarch64 TAG=my-tag - -PLATFORM ?= linux/amd64 -TAG ?= latest - -prod-build: - @cd ../.. && docker build -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd - - -prod-run: - @docker run -d -p 3000:3000 --name twenty twenty:$(TAG) - -prod-postgres-run: - @docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres --name twenty-postgres twenty-postgres:$(TAG) - -prod-website-build: - @cd ../.. && docker build -f ./packages/twenty-docker/twenty-website/Dockerfile --platform $(PLATFORM) --tag twenty-website:$(TAG) . && cd - - -prod-website-run: - @docker run -d -p 3000:3000 --name twenty-website twenty-website:$(TAG) diff --git a/twenty/code/docker-compose.yml b/twenty/code/docker-compose.yml deleted file mode 100644 index 55dfc6d58..000000000 --- a/twenty/code/docker-compose.yml +++ /dev/null @@ -1,126 +0,0 @@ -name: twenty - -services: - server: - image: twentycrm/twenty:${TAG:-latest} - volumes: - - server-local-data:/app/packages/twenty-server/.local-storage - environment: - NODE_PORT: 3000 - PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default - SERVER_URL: ${SERVER_URL} - REDIS_URL: ${REDIS_URL:-redis://redis:6379} - DISABLE_DB_MIGRATIONS: ${DISABLE_DB_MIGRATIONS} - DISABLE_CRON_JOBS_REGISTRATION: ${DISABLE_CRON_JOBS_REGISTRATION} - - STORAGE_TYPE: ${STORAGE_TYPE} - STORAGE_S3_REGION: ${STORAGE_S3_REGION} - STORAGE_S3_NAME: ${STORAGE_S3_NAME} - STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT} - - APP_SECRET: ${APP_SECRET:-replace_me_with_a_random_string} - # MESSAGING_PROVIDER_GMAIL_ENABLED: ${MESSAGING_PROVIDER_GMAIL_ENABLED} - # CALENDAR_PROVIDER_GOOGLE_ENABLED: ${CALENDAR_PROVIDER_GOOGLE_ENABLED} - # AUTH_GOOGLE_CLIENT_ID: ${AUTH_GOOGLE_CLIENT_ID} - # AUTH_GOOGLE_CLIENT_SECRET: ${AUTH_GOOGLE_CLIENT_SECRET} - # AUTH_GOOGLE_CALLBACK_URL: ${AUTH_GOOGLE_CALLBACK_URL} - # AUTH_GOOGLE_APIS_CALLBACK_URL: ${AUTH_GOOGLE_APIS_CALLBACK_URL} - - # CALENDAR_PROVIDER_MICROSOFT_ENABLED: ${CALENDAR_PROVIDER_MICROSOFT_ENABLED} - # MESSAGING_PROVIDER_MICROSOFT_ENABLED: ${MESSAGING_PROVIDER_MICROSOFT_ENABLED} - # AUTH_MICROSOFT_ENABLED: ${AUTH_MICROSOFT_ENABLED} - # AUTH_MICROSOFT_CLIENT_ID: ${AUTH_MICROSOFT_CLIENT_ID} - # AUTH_MICROSOFT_CLIENT_SECRET: ${AUTH_MICROSOFT_CLIENT_SECRET} - # AUTH_MICROSOFT_CALLBACK_URL: ${AUTH_MICROSOFT_CALLBACK_URL} - # AUTH_MICROSOFT_APIS_CALLBACK_URL: ${AUTH_MICROSOFT_APIS_CALLBACK_URL} - - # EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com} - # EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"} - # EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com} - # EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp} - # EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com} - # EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465} - # EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-} - # EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-} - - depends_on: - db: - condition: service_healthy - healthcheck: - test: curl --fail http://localhost:3000/healthz - interval: 5s - timeout: 5s - retries: 20 - restart: always - - worker: - image: twentycrm/twenty:${TAG:-latest} - volumes: - - server-local-data:/app/packages/twenty-server/.local-storage - command: [ "yarn", "worker:prod" ] - environment: - PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default - SERVER_URL: ${SERVER_URL} - REDIS_URL: ${REDIS_URL:-redis://redis:6379} - DISABLE_DB_MIGRATIONS: "true" # it already runs on the server - DISABLE_CRON_JOBS_REGISTRATION: "true" # it already runs on the server - - STORAGE_TYPE: ${STORAGE_TYPE} - STORAGE_S3_REGION: ${STORAGE_S3_REGION} - STORAGE_S3_NAME: ${STORAGE_S3_NAME} - STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT} - - APP_SECRET: ${APP_SECRET:-replace_me_with_a_random_string} - # MESSAGING_PROVIDER_GMAIL_ENABLED: ${MESSAGING_PROVIDER_GMAIL_ENABLED} - # CALENDAR_PROVIDER_GOOGLE_ENABLED: ${CALENDAR_PROVIDER_GOOGLE_ENABLED} - # AUTH_GOOGLE_CLIENT_ID: ${AUTH_GOOGLE_CLIENT_ID} - # AUTH_GOOGLE_CLIENT_SECRET: ${AUTH_GOOGLE_CLIENT_SECRET} - # AUTH_GOOGLE_CALLBACK_URL: ${AUTH_GOOGLE_CALLBACK_URL} - # AUTH_GOOGLE_APIS_CALLBACK_URL: ${AUTH_GOOGLE_APIS_CALLBACK_URL} - - # CALENDAR_PROVIDER_MICROSOFT_ENABLED: ${CALENDAR_PROVIDER_MICROSOFT_ENABLED} - # MESSAGING_PROVIDER_MICROSOFT_ENABLED: ${MESSAGING_PROVIDER_MICROSOFT_ENABLED} - # AUTH_MICROSOFT_ENABLED: ${AUTH_MICROSOFT_ENABLED} - # AUTH_MICROSOFT_CLIENT_ID: ${AUTH_MICROSOFT_CLIENT_ID} - # AUTH_MICROSOFT_CLIENT_SECRET: ${AUTH_MICROSOFT_CLIENT_SECRET} - # AUTH_MICROSOFT_CALLBACK_URL: ${AUTH_MICROSOFT_CALLBACK_URL} - # AUTH_MICROSOFT_APIS_CALLBACK_URL: ${AUTH_MICROSOFT_APIS_CALLBACK_URL} - - # EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com} - # EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"} - # EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com} - # EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp} - # EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com} - # EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465} - # EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-} - # EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-} - - depends_on: - db: - condition: service_healthy - server: - condition: service_healthy - restart: always - - db: - image: postgres:16 - volumes: - - db-data:/var/lib/postgresql/data - environment: - POSTGRES_USER: ${PG_DATABASE_USER:-postgres} - POSTGRES_PASSWORD: ${PG_DATABASE_PASSWORD:-postgres} - healthcheck: - test: pg_isready -U ${PG_DATABASE_USER:-postgres} -h localhost -d postgres - interval: 5s - timeout: 5s - retries: 10 - restart: always - - redis: - image: redis - restart: always - command: [ "--maxmemory-policy", "noeviction" ] - -volumes: - db-data: - server-local-data: diff --git a/twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml b/twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml deleted file mode 100644 index bb4279ee8..000000000 --- a/twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: 1 - -datasources: - - name: ClickHouse - type: grafana-clickhouse-datasource - uid: clickhouse_datasource - jsonData: - server: twenty_clickhouse - defaultDatabase: twenty_dev - port: 9000 - protocol: native - tlsSkipVerify: true - username: default - secureJsonData: - password: devPassword \ No newline at end of file diff --git a/twenty/code/k8s/README.md b/twenty/code/k8s/README.md deleted file mode 100644 index d538d21ba..000000000 --- a/twenty/code/k8s/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# README -DISCLAIMER: The k8s and podman deployments are not maintained by the core team. -These files are provided and maintained by the community. Twenty core team -maintains support for docker deployment. - -## Overview - -This repository contains Kubernetes manifests and Terraform files to help you deploy and manage the TwentyCRM application. The files are located in the `packages/twenty-docker/k8s` directory. - -## Prerequisites - -Before using these files, ensure you have the following installed and configured on your system: - -- Kubernetes cluster (e.g., Minikube, EKS, GKE) -- kubectl -- Terraform -- Docker - -## Setup Instructions - -### Step 1: Clone the Repository - -Clone the repository to your local machine: - -``` bash -git clone https://github.com/twentyhq/twenty.git -cd twentycrm/packages/twenty-docker/k8s -``` - -### Step 2: Customize the Manifests and Terraform Files - -**Important:** These files require customization for your specific implementation. Update the placeholders and configurations according to your environment and requirements. - -### Step 3: Deploy with Terraform - -1. Navigate to the Terraform directory: - - ```bash - cd terraform - ``` - -2. Initialize Terraform: - - ```bash - terraform init - ``` - -3. Plan the deployment: - - ```bash - terraform plan - ``` - -4. Apply the deployment: - - ```bash - terraform apply - ``` - -## OR - -### Step 3: Deploy with Kubernetes Manifests - -1. Navigate to the Kubernetes manifests directory: - - ```bash - cd ../k8s - ``` - -2. Create Server Secret - - ``` bash - kubectl create secret generic -n twentycrm tokens --from-literal accessToken=changeme --from-literal loginToken="changeme" --from-literal refreshToken="changeme" --from-literal fileToken="changeme" - ``` - -3. Apply the manifests: - - ```bash - kubectl apply -f . - ``` - -## Customization - -### Kubernetes Manifests - -- **Namespace:** Update the `namespace` in the manifests as needed. -- **Resource Limits:** Adjust the resource limits and requests according to your application's requirements. -- **Environment Variables:** Configure server tokens in the `Secret` command above. - -### Terraform Files - -- **Variables:** Update the variables in the `variables.tf` file to match your environment. -- **Locals:** Update the locals in the `main.tf` file to match your environment. -- **Providers:** Ensure the provider configurations (e.g., AWS, GCP) are correct for your setup. -- **Resources:** Modify the resource definitions as needed to fit your infrastructure. - -## Troubleshooting - -### Common Issues - -- **Connectivity:** Ensure your Kubernetes cluster is accessible and configured correctly. -- **Permissions:** Verify that you have the necessary permissions to deploy resources in your cloud provider. -- **Resource Limits:** Adjust resource limits if you encounter issues related to insufficient resources. - -### Logs and Debugging - -- Use `kubectl logs` to check the logs of your Kubernetes pods. -- Use `terraform show` and `terraform state` to inspect your Terraform state and configurations. - -## Conclusion - -This setup provides a basic structure for deploying the TwentyCRM application using Kubernetes and Terraform. Ensure you thoroughly customize the manifests and Terraform files to suit your specific needs. For any issues or questions, please refer to the official documentation of Kubernetes and Terraform or seek support from your cloud provider. - ---- - -Feel free to contribute and improve this repository by submitting pull requests or opening issues. Happy deploying! diff --git a/twenty/code/k8s/manifests/deployment-db.yaml b/twenty/code/k8s/manifests/deployment-db.yaml deleted file mode 100644 index c797972e0..000000000 --- a/twenty/code/k8s/manifests/deployment-db.yaml +++ /dev/null @@ -1,58 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: twentycrm-db - name: twentycrm-db - namespace: twentycrm -spec: - progressDeadlineSeconds: 600 - replicas: 1 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - selector: - matchLabels: - app: twentycrm-db - template: - metadata: - labels: - app: twentycrm-db - spec: - volumes: - - name: twentycrm-db-data - persistentVolumeClaim: - claimName: twentycrm-db-pvc - containers: - - name: twentycrm - image: twentycrm/twenty-postgres-spilo:latest - imagePullPolicy: Always - env: - - name: PGUSER_SUPERUSER - value: "postgres" - - name: PGPASSWORD_SUPERUSER - value: "postgres" - - name: SPILO_PROVIDER - value: "local" - - name: ALLOW_NOSSL - value: "true" - ports: - - containerPort: 5432 - name: tcp - protocol: TCP - resources: - requests: - memory: "256Mi" - cpu: "250m" - limits: - memory: "1024Mi" - cpu: "1000m" - stdin: true - tty: true - volumeMounts: - - mountPath: /home/postgres/pgdata - name: twentycrm-db-data - dnsPolicy: ClusterFirst - restartPolicy: Always diff --git a/twenty/code/k8s/manifests/deployment-redis.yaml b/twenty/code/k8s/manifests/deployment-redis.yaml deleted file mode 100644 index bd7aaa3cf..000000000 --- a/twenty/code/k8s/manifests/deployment-redis.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: twentycrm-redis - name: twentycrm-redis - namespace: twentycrm -spec: - progressDeadlineSeconds: 600 - replicas: 1 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - selector: - matchLabels: - app: twentycrm-redis - template: - metadata: - labels: - app: twentycrm-redis - spec: - containers: - - name: redis - image: redis/redis-stack-server:latest - imagePullPolicy: Always - args: ["--maxmemory-policy", "noeviction"] - env: - - name: PORT - value: "6379" - ports: - - containerPort: 6379 - name: redis - protocol: TCP - resources: - requests: - memory: "1024Mi" - cpu: "250m" - limits: - memory: "2048Mi" - cpu: "500m" - - dnsPolicy: ClusterFirst - restartPolicy: Always diff --git a/twenty/code/k8s/manifests/deployment-server.yaml b/twenty/code/k8s/manifests/deployment-server.yaml deleted file mode 100644 index 9d219936e..000000000 --- a/twenty/code/k8s/manifests/deployment-server.yaml +++ /dev/null @@ -1,76 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: twentycrm-server - name: twentycrm-server - namespace: twentycrm -spec: - progressDeadlineSeconds: 600 - replicas: 1 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - selector: - matchLabels: - app: twentycrm-server - template: - metadata: - labels: - app: twentycrm-server - spec: - volumes: - - name: twentycrm-server-data - persistentVolumeClaim: - claimName: twentycrm-server-pvc - - name: twentycrm-docker-data - persistentVolumeClaim: - claimName: twentycrm-docker-data-pvc - containers: - - name: twentycrm - image: twentycrm/twenty:latest - imagePullPolicy: Always - env: - - name: NODE_PORT - value: 3000 - - name: SERVER_URL - value: "https://crm.example.com:443" - - name: "PG_DATABASE_URL" - value: "postgres://postgres:postgres@twentycrm-db.twentycrm.svc.cluster.local/default" - - name: "REDIS_URL" - value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379" - - name: SIGN_IN_PREFILLED - value: "false" - - name: STORAGE_TYPE - value: "local" - - name: "ACCESS_TOKEN_EXPIRES_IN" - value: "7d" - - name: "LOGIN_TOKEN_EXPIRES_IN" - value: "1h" - - name: APP_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: accessToken - ports: - - containerPort: 3000 - name: http-tcp - protocol: TCP - resources: - requests: - memory: "256Mi" - cpu: "250m" - limits: - memory: "1024Mi" - cpu: "1000m" - stdin: true - tty: true - volumeMounts: - - mountPath: /app/docker-data - name: twentycrm-docker-data - - mountPath: /app/packages/twenty-server/.local-storage - name: twentycrm-server-data - dnsPolicy: ClusterFirst - restartPolicy: Always diff --git a/twenty/code/k8s/manifests/deployment-worker.yaml b/twenty/code/k8s/manifests/deployment-worker.yaml deleted file mode 100644 index fbee27781..000000000 --- a/twenty/code/k8s/manifests/deployment-worker.yaml +++ /dev/null @@ -1,57 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: twentycrm-worker - name: twentycrm-worker - namespace: twentycrm -spec: - progressDeadlineSeconds: 600 - replicas: 1 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - selector: - matchLabels: - app: twentycrm-worker - template: - metadata: - labels: - app: twentycrm-worker - spec: - containers: - - name: twentycrm - image: twentycrm/twenty:latest - imagePullPolicy: Always - env: - - name: SERVER_URL - value: "https://crm.example.com:443" - - name: PG_DATABASE_URL - value: "postgres://postgres:postgres@twentycrm-db.twentycrm.svc.cluster.local/default" - - name: DISABLE_DB_MIGRATIONS - value: "false" # it already runs on the server - - name: STORAGE_TYPE - value: "local" - - name: "REDIS_URL" - value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379" - - name: APP_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: accessToken - command: - - yarn - - worker:prod - resources: - requests: - memory: "1024Mi" - cpu: "250m" - limits: - memory: "2048Mi" - cpu: "1000m" - stdin: true - tty: true - dnsPolicy: ClusterFirst - restartPolicy: Always diff --git a/twenty/code/k8s/manifests/ingress.yaml b/twenty/code/k8s/manifests/ingress.yaml deleted file mode 100644 index 0bbae11dd..000000000 --- a/twenty/code/k8s/manifests/ingress.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: twentycrm - namespace: twentycrm - annotations: - nginx.ingress.kubernetes.io/configuration-snippet: | - more_set_headers "X-Forwarded-For $http_x_forwarded_for"; - nginx.ingress.kubernetes.io/force-ssl-redirect: "false" - kubernetes.io/ingress.class: "nginx" - nginx.ingress.kubernetes.io/backend-protocol: "HTTP" -spec: - ingressClassName: nginx - rules: - - host: crm.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: twentycrm-server - port: - name: http-tcp diff --git a/twenty/code/k8s/manifests/pv-db.yaml b/twenty/code/k8s/manifests/pv-db.yaml deleted file mode 100644 index 9caa4ca4d..000000000 --- a/twenty/code/k8s/manifests/pv-db.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: twentycrm-db-pv -spec: - storageClassName: default - capacity: - storage: 10Gi - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain diff --git a/twenty/code/k8s/manifests/pv-docker-data.yaml b/twenty/code/k8s/manifests/pv-docker-data.yaml deleted file mode 100644 index 95fc52a26..000000000 --- a/twenty/code/k8s/manifests/pv-docker-data.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: twentycrm-docker-data-pv -spec: - storageClassName: default - capacity: - storage: 100Mi - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain diff --git a/twenty/code/k8s/manifests/pv-server.yaml b/twenty/code/k8s/manifests/pv-server.yaml deleted file mode 100644 index 721de7d56..000000000 --- a/twenty/code/k8s/manifests/pv-server.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: twentycrm-server-pv - namespace: twentycrm -spec: - storageClassName: default - capacity: - storage: 10Gi - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain diff --git a/twenty/code/k8s/manifests/pvc-db.yaml b/twenty/code/k8s/manifests/pvc-db.yaml deleted file mode 100644 index 146596ea1..000000000 --- a/twenty/code/k8s/manifests/pvc-db.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: twentycrm-db-pvc - namespace: twentycrm -spec: - storageClassName: default - volumeName: twentycrm-db-pv - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi diff --git a/twenty/code/k8s/manifests/pvc-docker-data.yaml b/twenty/code/k8s/manifests/pvc-docker-data.yaml deleted file mode 100644 index 12dd071a7..000000000 --- a/twenty/code/k8s/manifests/pvc-docker-data.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: twentycrm-docker-data-pvc - namespace: twentycrm -spec: - storageClassName: default - volumeName: twentycrm-docker-data-pv - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi diff --git a/twenty/code/k8s/manifests/pvc-server.yaml b/twenty/code/k8s/manifests/pvc-server.yaml deleted file mode 100644 index f265057cf..000000000 --- a/twenty/code/k8s/manifests/pvc-server.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: twentycrm-server-pvc - namespace: twentycrm -spec: - storageClassName: default - volumeName: twentycrm-server-pv - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi diff --git a/twenty/code/k8s/manifests/service-db.yaml b/twenty/code/k8s/manifests/service-db.yaml deleted file mode 100644 index 89dbd1464..000000000 --- a/twenty/code/k8s/manifests/service-db.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: twentycrm-db - namespace: twentycrm -spec: - internalTrafficPolicy: Cluster - ports: - - port: 5432 - protocol: TCP - targetPort: 5432 - selector: - app: twentycrm-db - sessionAffinity: ClientIP - sessionAffinityConfig: - clientIP: - timeoutSeconds: 10800 - type: ClusterIP diff --git a/twenty/code/k8s/manifests/service-redis.yaml b/twenty/code/k8s/manifests/service-redis.yaml deleted file mode 100644 index 49f508897..000000000 --- a/twenty/code/k8s/manifests/service-redis.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: twentycrm-redis - namespace: twentycrm -spec: - internalTrafficPolicy: Cluster - ports: - - port: 6379 - protocol: TCP - targetPort: 6379 - selector: - app: twentycrm-redis - sessionAffinity: ClientIP - sessionAffinityConfig: - clientIP: - timeoutSeconds: 10800 - type: ClusterIP diff --git a/twenty/code/k8s/manifests/service-server.yaml b/twenty/code/k8s/manifests/service-server.yaml deleted file mode 100644 index b45b28f31..000000000 --- a/twenty/code/k8s/manifests/service-server.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: twentycrm-server - namespace: twentycrm -spec: - internalTrafficPolicy: Cluster - ports: - - name: http-tcp - port: 3000 - protocol: TCP - targetPort: 3000 - selector: - app: twentycrm-server - sessionAffinity: ClientIP - sessionAffinityConfig: - clientIP: - timeoutSeconds: 10800 - type: ClusterIP diff --git a/twenty/code/k8s/terraform/.terraform-docs.yml b/twenty/code/k8s/terraform/.terraform-docs.yml deleted file mode 100644 index 792c543f4..000000000 --- a/twenty/code/k8s/terraform/.terraform-docs.yml +++ /dev/null @@ -1,48 +0,0 @@ -formatter: "markdown table" # this is required - -version: "" - -header-from: main.tf - -recursive: - enabled: false - path: modules - -output: - file: "README.md" - mode: inject - template: |- - - # TwentyCRM Terraform Docs - - This file was generated by [terraform-docs](https://terraform-docs.io/), for more information on how to install, configure, and use visit their website. - - To update this `README.md` after changes to the Terraform code in this folder, run: `terraform-docs -c `./.terraform-docs.yml .` - - To make configuration changes to how this doc is generated, see `./.terraform-docs.yml` - - {{ .Content }} - - -output-values: - enabled: false - from: "outputs.tf" - -sort: - enabled: true - by: required - -settings: - anchor: true - color: true - default: true - description: true - escape: true - hide-empty: true - html: true - indent: 2 - lockfile: true - read-comments: true - required: true - sensitive: true - type: true diff --git a/twenty/code/k8s/terraform/README.md b/twenty/code/k8s/terraform/README.md deleted file mode 100644 index 32facfd18..000000000 --- a/twenty/code/k8s/terraform/README.md +++ /dev/null @@ -1,73 +0,0 @@ - -# TwentyCRM Terraform Docs - -This file was generated by [terraform-docs](https://terraform-docs.io/), for more information on how to install, configure, and use visit their website. - -To update this `README.md` after changes to the Terraform code in this folder, run: `terraform-docs -c `./.terraform-docs.yml .` - -To make configuration changes to how this doc is generated, see `./.terraform-docs.yml` - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.9.2 | -| [kubernetes](#requirement\_kubernetes) | >= 2.32.0 | -| [random](#requirement\_random) | >= 3.6.3 | - -## Providers - -| Name | Version | -|------|---------| -| [kubernetes](#provider\_kubernetes) | >= 2.32.0 | -| [random](#provider\_random) | >= 3.6.3 | - -## Resources - -| Name | Type | -|------|------| -| [kubernetes_deployment.twentycrm_db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | -| [kubernetes_deployment.twentycrm_redis](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | -| [kubernetes_deployment.twentycrm_server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | -| [kubernetes_deployment.twentycrm_worker](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | -| [kubernetes_ingress.twentycrm](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/ingress) | resource | -| [kubernetes_namespace.twentycrm](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | -| [kubernetes_persistent_volume.db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource | -| [kubernetes_persistent_volume.docker_data](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource | -| [kubernetes_persistent_volume.server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource | -| [kubernetes_persistent_volume_claim.db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource | -| [kubernetes_persistent_volume_claim.docker_data](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource | -| [kubernetes_persistent_volume_claim.server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource | -| [kubernetes_secret.twentycrm_tokens](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | -| [kubernetes_service.twentycrm_db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource | -| [kubernetes_service.twentycrm_redis](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource | -| [kubernetes_service.twentycrm_server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource | -| [random_bytes.this](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/bytes) | resource | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [twentycrm\_app\_hostname](#input\_twentycrm\_app\_hostname) | The protocol, DNS fully qualified hostname, and port used to access TwentyCRM in your environment. Ex: https://crm.example.com:443 | `string` | n/a | yes | -| [twentycrm\_pgdb\_admin\_password](#input\_twentycrm\_pgdb\_admin\_password) | TwentyCRM password for postgres database. | `string` | n/a | yes | -| [twentycrm\_app\_name](#input\_twentycrm\_app\_name) | A friendly name prefix to use for every component deployed. | `string` | `"twentycrm"` | no | -| [twentycrm\_db\_image](#input\_twentycrm\_db\_image) | TwentyCRM image for database deployment. This defaults to latest. | `string` | `"twentycrm/twenty-postgres-spilo:latest"` | no | -| [twentycrm\_db\_pv\_capacity](#input\_twentycrm\_db\_pv\_capacity) | Storage capacity provisioned for database persistent volume. | `string` | `"10Gi"` | no | -| [twentycrm\_db\_pv\_path](#input\_twentycrm\_db\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no | -| [twentycrm\_db\_pvc\_requests](#input\_twentycrm\_db\_pvc\_requests) | Storage capacity reservation for database persistent volume claim. | `string` | `"10Gi"` | no | -| [twentycrm\_db\_replicas](#input\_twentycrm\_db\_replicas) | Number of replicas for the TwentyCRM database deployment. This defaults to 1. | `number` | `1` | no | -| [twentycrm\_docker\_data\_mount\_path](#input\_twentycrm\_docker\_data\_mount\_path) | TwentyCRM mount path for servers application data. Defaults to '/app/docker-data'. | `string` | `"/app/docker-data"` | no | -| [twentycrm\_docker\_data\_pv\_capacity](#input\_twentycrm\_docker\_data\_pv\_capacity) | Storage capacity provisioned for server persistent volume. | `string` | `"10Gi"` | no | -| [twentycrm\_docker\_data\_pv\_path](#input\_twentycrm\_docker\_data\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no | -| [twentycrm\_docker\_data\_pvc\_requests](#input\_twentycrm\_docker\_data\_pvc\_requests) | Storage capacity reservation for server persistent volume claim. | `string` | `"10Gi"` | no | -| [twentycrm\_namespace](#input\_twentycrm\_namespace) | Namespace for all TwentyCRM resources | `string` | `"twentycrm"` | no | -| [twentycrm\_redis\_image](#input\_twentycrm\_redis\_image) | TwentyCRM image for Redis deployment. This defaults to latest. | `string` | `"redis/redis-stack-server:latest"` | no | -| [twentycrm\_redis\_replicas](#input\_twentycrm\_redis\_replicas) | Number of replicas for the TwentyCRM Redis deployment. This defaults to 1. | `number` | `1` | no | -| [twentycrm\_server\_data\_mount\_path](#input\_twentycrm\_server\_data\_mount\_path) | TwentyCRM mount path for servers application data. Defaults to '/app/packages/twenty-server/.local-storage'. | `string` | `"/app/packages/twenty-server/.local-storage"` | no | -| [twentycrm\_server\_image](#input\_twentycrm\_server\_image) | TwentyCRM server image for the server deployment. This defaults to latest. This value is also used for the workers image. | `string` | `"twentycrm/twenty:latest"` | no | -| [twentycrm\_server\_pv\_capacity](#input\_twentycrm\_server\_pv\_capacity) | Storage capacity provisioned for server persistent volume. | `string` | `"10Gi"` | no | -| [twentycrm\_server\_pv\_path](#input\_twentycrm\_server\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no | -| [twentycrm\_server\_pvc\_requests](#input\_twentycrm\_server\_pvc\_requests) | Storage capacity reservation for server persistent volume claim. | `string` | `"10Gi"` | no | -| [twentycrm\_server\_replicas](#input\_twentycrm\_server\_replicas) | Number of replicas for the TwentyCRM server deployment. This defaults to 1. | `number` | `1` | no | -| [twentycrm\_worker\_replicas](#input\_twentycrm\_worker\_replicas) | Number of replicas for the TwentyCRM worker deployment. This defaults to 1. | `number` | `1` | no | - diff --git a/twenty/code/k8s/terraform/deployment-db.tf b/twenty/code/k8s/terraform/deployment-db.tf deleted file mode 100644 index 62c61a298..000000000 --- a/twenty/code/k8s/terraform/deployment-db.tf +++ /dev/null @@ -1,87 +0,0 @@ -resource "kubernetes_deployment" "twentycrm_db" { - metadata { - name = "${var.twentycrm_app_name}-db" - namespace = kubernetes_namespace.twentycrm.metadata.0.name - labels = { - app = "${var.twentycrm_app_name}-db" - } - } - - spec { - replicas = var.twentycrm_db_replicas - selector { - match_labels = { - app = "${var.twentycrm_app_name}-db" - } - } - - strategy { - type = "RollingUpdate" - rolling_update { - max_surge = "1" - max_unavailable = "1" - } - } - - template { - metadata { - labels = { - app = "${var.twentycrm_app_name}-db" - } - } - - spec { - container { - image = var.twentycrm_db_image - name = var.twentycrm_app_name - stdin = true - tty = true - security_context { - allow_privilege_escalation = true - } - - env { - name = "POSTGRES_PASSWORD" - value = var.twentycrm_pgdb_admin_password - } - env { - name = "BITNAMI_DEBUG" - value = true - } - - port { - container_port = 5432 - protocol = "TCP" - } - - resources { - requests = { - cpu = "250m" - memory = "256Mi" - } - limits = { - cpu = "1000m" - memory = "1024Mi" - } - } - - volume_mount { - name = "db-data" - mount_path = "/bitnami/postgresql" - } - } - - volume { - name = "db-data" - - persistent_volume_claim { - claim_name = kubernetes_persistent_volume_claim.db.metadata.0.name - } - } - - dns_policy = "ClusterFirst" - restart_policy = "Always" - } - } - } -} diff --git a/twenty/code/k8s/terraform/deployment-redis.tf b/twenty/code/k8s/terraform/deployment-redis.tf deleted file mode 100644 index d867dac76..000000000 --- a/twenty/code/k8s/terraform/deployment-redis.tf +++ /dev/null @@ -1,60 +0,0 @@ -resource "kubernetes_deployment" "twentycrm_redis" { - metadata { - name = "${var.twentycrm_app_name}-redis" - namespace = kubernetes_namespace.twentycrm.metadata.0.name - - labels = { - app = "${var.twentycrm_app_name}-redis" - } - } - - spec { - replicas = var.twentycrm_redis_replicas - selector { - match_labels = { - app = "${var.twentycrm_app_name}-redis" - } - } - - strategy { - type = "RollingUpdate" - rolling_update { - max_surge = "1" - max_unavailable = "1" - } - } - - template { - metadata { - labels = { - app = "${var.twentycrm_app_name}-redis" - } - } - - spec { - container { - image = var.twentycrm_redis_image - name = "redis" - - port { - container_port = 6379 - protocol = "TCP" - } - - resources { - requests = { - cpu = "250m" - memory = "1024Mi" - } - limits = { - cpu = "500m" - memory = "2048Mi" - } - } - } - dns_policy = "ClusterFirst" - restart_policy = "Always" - } - } - } -} diff --git a/twenty/code/k8s/terraform/deployment-server.tf b/twenty/code/k8s/terraform/deployment-server.tf deleted file mode 100644 index a8bb43b95..000000000 --- a/twenty/code/k8s/terraform/deployment-server.tf +++ /dev/null @@ -1,138 +0,0 @@ -resource "kubernetes_deployment" "twentycrm_server" { - metadata { - name = "${var.twentycrm_app_name}-server" - namespace = kubernetes_namespace.twentycrm.metadata.0.name - labels = { - app = "${var.twentycrm_app_name}-server" - } - } - - spec { - replicas = var.twentycrm_server_replicas - selector { - match_labels = { - app = "${var.twentycrm_app_name}-server" - } - } - - strategy { - type = "RollingUpdate" - rolling_update { - max_surge = "1" - max_unavailable = "1" - } - } - - template { - metadata { - labels = { - app = "${var.twentycrm_app_name}-server" - } - } - - spec { - container { - image = var.twentycrm_server_image - name = var.twentycrm_app_name - stdin = true - tty = true - - env { - name = "NODE_PORT" - value = "3000" - } - - env { - name = "SERVER_URL" - value = var.twentycrm_app_hostname - } - - env { - name = "PG_DATABASE_URL" - value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default" - } - env { - name = "REDIS_URL" - value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379" - } - env { - name = "DISABLE_DB_MIGRATIONS" - value = "false" - } - - env { - name = "STORAGE_TYPE" - value = "local" - } - env { - name = "ACCESS_TOKEN_EXPIRES_IN" - value = "7d" - } - env { - name = "LOGIN_TOKEN_EXPIRES_IN" - value = "1h" - } - env { - name = "APP_SECRET" - value_from { - secret_key_ref { - name = "tokens" - key = "accessToken" - } - } - } - - port { - container_port = 3000 - protocol = "TCP" - } - - resources { - requests = { - cpu = "250m" - memory = "256Mi" - } - limits = { - cpu = "1000m" - memory = "1024Mi" - } - } - - volume_mount { - name = "server-data" - mount_path = var.twentycrm_server_data_mount_path - } - - volume_mount { - name = "docker-data" - mount_path = var.twentycrm_docker_data_mount_path - } - } - - volume { - name = "server-data" - - persistent_volume_claim { - claim_name = kubernetes_persistent_volume_claim.server.metadata.0.name - } - } - - volume { - name = "docker-data" - - persistent_volume_claim { - claim_name = kubernetes_persistent_volume_claim.docker_data.metadata.0.name - } - } - - dns_policy = "ClusterFirst" - restart_policy = "Always" - } - } - } - depends_on = [ - kubernetes_deployment.twentycrm_db, - kubernetes_deployment.twentycrm_redis, - kubernetes_secret.twentycrm_tokens - ] -} diff --git a/twenty/code/k8s/terraform/deployment-worker.tf b/twenty/code/k8s/terraform/deployment-worker.tf deleted file mode 100644 index 9e4da3e8c..000000000 --- a/twenty/code/k8s/terraform/deployment-worker.tf +++ /dev/null @@ -1,99 +0,0 @@ -resource "kubernetes_deployment" "twentycrm_worker" { - metadata { - name = "${var.twentycrm_app_name}-worker" - namespace = kubernetes_namespace.twentycrm.metadata.0.name - labels = { - app = "${var.twentycrm_app_name}-worker" - } - } - - spec { - replicas = var.twentycrm_worker_replicas - selector { - match_labels = { - app = "${var.twentycrm_app_name}-worker" - } - } - - strategy { - type = "RollingUpdate" - rolling_update { - max_surge = "1" - max_unavailable = "1" - } - } - - template { - metadata { - labels = { - app = "${var.twentycrm_app_name}-worker" - } - } - - spec { - container { - image = var.twentycrm_server_image - name = var.twentycrm_app_name - stdin = true - tty = true - command = ["yarn", "worker:prod"] - - env { - name = "SERVER_URL" - value = var.twentycrm_app_hostname - } - - env { - name = "PG_DATABASE_URL" - value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default" - } - - env { - name = "REDIS_URL" - value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379" - } - - env { - name = "DISABLE_DB_MIGRATIONS" - value = "true" #it already runs on the server - } - - env { - name = "STORAGE_TYPE" - value = "local" - } - - env { - name = "APP_SECRET" - value_from { - secret_key_ref { - name = "tokens" - key = "accessToken" - } - } - } - - resources { - requests = { - cpu = "250m" - memory = "1024Mi" - } - limits = { - cpu = "1000m" - memory = "2048Mi" - } - } - } - - dns_policy = "ClusterFirst" - restart_policy = "Always" - } - } - } - depends_on = [ - kubernetes_deployment.twentycrm_db, - kubernetes_deployment.twentycrm_redis, - kubernetes_deployment.twentycrm_server, - kubernetes_secret.twentycrm_tokens, - ] -} diff --git a/twenty/code/k8s/terraform/ingress.tf b/twenty/code/k8s/terraform/ingress.tf deleted file mode 100644 index f8a28779c..000000000 --- a/twenty/code/k8s/terraform/ingress.tf +++ /dev/null @@ -1,30 +0,0 @@ -resource "kubernetes_ingress" "twentycrm" { - wait_for_load_balancer = true - metadata { - name = "${var.twentycrm_app_name}-ingress" - namespace = kubernetes_namespace.twentycrm.metadata.0.name - annotations = { - "kubernetes.io/ingress.class" = "nginx" - "nginx.ingress.kubernetes.io/configuration-snippet" = </dev/null; then - echo -e "\t❌ Docker is not installed or not in PATH. Please install Docker first.\n\t\tSee https://docs.docker.com/get-docker/" - exit 1 -fi -# Check if docker compose plugin is installed -if ! docker compose version &>/dev/null; then - echo -e "\t❌ Docker Compose is not installed or not in PATH (n.b. docker-compose is deprecated)\n\t\tUpdate docker or install docker-compose-plugin\n\t\tOn Linux: sudo apt-get install docker-compose-plugin\n\t\tSee https://docs.docker.com/compose/install/" - exit 1 -fi -# Check if docker is started -if ! docker info &>/dev/null; then - echo -e "\t❌ Docker is not running.\n\t\tPlease start Docker Desktop, Docker or check documentation at https://docs.docker.com/config/daemon/start/" - exit 1 -fi -if ! command -v curl &>/dev/null; then - echo -e "\t❌ Curl is not installed or not in PATH.\n\t\tOn macOS: brew install curl\n\t\tOn Linux: sudo apt install curl" - exit 1 -fi - -# Check if docker compose version is >= 2 -if [ "$(docker compose version --short | cut -d' ' -f3 | cut -d'.' -f1)" -lt 2 ]; then - echo -e "\t❌ Docker Compose is outdated. Please update Docker Compose to version 2 or higher.\n\t\tSee https://docs.docker.com/compose/install/linux/" - exit 1 -fi -# Check if docker-compose is installed, if so issue a warning if version is < 2 -if command -v docker-compose &>/dev/null; then - if [ "$(docker-compose version --short | cut -d' ' -f3 | cut -d'.' -f1)" -lt 2 ]; then - echo -e "\n\t⚠️ 'docker-compose' is installed but outdated. Make sure to use 'docker compose' or to upgrade 'docker-compose' to version 2.\n\t\tSee https://docs.docker.com/compose/install/standalone/\n" - fi -fi - -# Catch errors -set -e -function on_exit { - # $? is the exit status of the last command executed - local exit_status=$? - if [ $exit_status -ne 0 ]; then - echo "❌ Something went wrong, exiting: $exit_status" - fi -} -trap on_exit EXIT - -# Use environment variables VERSION and BRANCH, with defaults if not set -version=${VERSION:-$(curl -s "https://hub.docker.com/v2/repositories/twentycrm/twenty/tags" | grep -o '"name":"[^"]*"' | grep -v 'latest' | cut -d'"' -f4 | sort -V | tail -n1)} -branch=${BRANCH:-$(curl -s https://api.github.com/repos/twentyhq/twenty/tags | grep '"name":' | head -n 1 | cut -d '"' -f 4)} - -echo "🚀 Using docker version $version and Github branch $branch" - -dir_name="twenty" -function ask_directory { - read -p "📁 Enter the directory name to setup the project (default: $dir_name): " answer - if [ -n "$answer" ]; then - dir_name=$answer - fi -} - -ask_directory - -while [ -d "$dir_name" ]; do - read -p "🚫 Directory '$dir_name' already exists. Do you want to overwrite it? (y/N) " answer - if [ "$answer" = "y" ]; then - break - else - ask_directory - fi -done - -# Create a directory named twenty -echo "📁 Creating directory '$dir_name'" -mkdir -p "$dir_name" && cd "$dir_name" || { echo "❌ Failed to create/access directory '$dir_name'"; exit 1; } - -# Copy twenty/packages/twenty-docker/docker-compose.yml in it -echo -e "\t• Copying docker-compose.yml" -curl -sLo docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/$branch/packages/twenty-docker/docker-compose.yml - -# Copy twenty/packages/twenty-docker/.env.example to .env -echo -e "\t• Setting up .env file" -curl -sLo .env https://raw.githubusercontent.com/twentyhq/twenty/$branch/packages/twenty-docker/.env.example - -# Replace TAG=latest by TAG= -if [[ $(uname) == "Darwin" ]]; then - # Running on macOS - sed -i '' "s/TAG=latest/TAG=$version/g" .env -else - # Assuming Linux - sed -i'' "s/TAG=latest/TAG=$version/g" .env -fi - -# Generate random strings for secrets -echo "# === Randomly generated secret ===" >> .env -echo "APP_SECRET=$(openssl rand -base64 32)" >> .env - -echo "" >> .env -echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 32)" >> .env - -echo -e "\t• .env configuration completed" - -port=3000 -# Check if command nc is available -if command -v nc &> /dev/null; then - # Check if port 3000 is already in use, propose to change it - while nc -zv localhost $port &>/dev/null; do - read -p "🚫 Port $port is already in use. Do you want to use another port? (Y/n) " answer - if [ "$answer" = "n" ]; then - continue - fi - read -p "Enter a new port number: " new_port - if [[ $(uname) == "Darwin" ]]; then - sed -i '' "s/$port:$port/$new_port:$port/g" docker-compose.yml - sed -E -i '' "s|^SERVER_URL=http://localhost:[0-9]+|SERVER_URL=http://localhost:$new_port|g" .env - else - sed -i'' "s/$port:$port/$new_port:$port/g" docker-compose.yml - sed -E -i'' "s|^SERVER_URL=http://localhost:[0-9]+|SERVER_URL=http://localhost:$new_port|g" .env - fi - port=$new_port - done -fi - -# Ask user if they want to start the project -read -p "🚀 Do you want to start the project now? (Y/n) " answer -if [ "$answer" = "n" ]; then - echo "✅ Project setup completed. Run 'docker compose up -d' to start." - exit 0 -else - echo "🐳 Starting Docker containers..." - docker compose up -d - # Check if port is listening - echo "Waiting for server to be healthy, it might take a few minutes while we initialize the database..." - # Tail logs of the server until it's ready - docker compose logs -f server & - pid=$! - while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do - sleep 1 - done - kill $pid - echo "" - echo "✅ Server is up and running" -fi - -function ask_open_browser { - read -p "🌐 Do you want to open the project in your browser? (Y/n) " answer - if [ "$answer" = "n" ]; then - echo "✅ Setup completed. Access your project at http://localhost:$port" - exit 0 - fi -} - -# Ask user if they want to open the project -# Running on macOS -if [[ $(uname) == "Darwin" ]]; then - ask_open_browser - - open "http://localhost:$port" -# Assuming Linux -else - # xdg-open is not installed, we could be running in a non gui environment - if command -v xdg-open >/dev/null 2>&1; then - ask_open_browser - - xdg-open "http://localhost:$port" - else - echo "✅ Setup completed. Your project is available at http://localhost:$port" - fi -fi diff --git a/twenty/code/twenty-postgres-spilo/Dockerfile b/twenty/code/twenty-postgres-spilo/Dockerfile deleted file mode 100644 index 9a84c120d..000000000 --- a/twenty/code/twenty-postgres-spilo/Dockerfile +++ /dev/null @@ -1,68 +0,0 @@ -ARG POSTGRES_VERSION=15 -ARG SPILO_VERSION=3.2-p1 -ARG WRAPPERS_VERSION=0.2.0 - -# Build the mysql_fdw extension -FROM debian:bookworm AS build-mysql_fdw -ARG POSTGRES_VERSION - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && \ - apt install -y \ - build-essential \ - git \ - postgresql-server-dev-${POSTGRES_VERSION} \ - default-libmysqlclient-dev && \ - rm -rf /var/lib/apt/lists/* - -# Install mysql_fdw -RUN git clone https://github.com/EnterpriseDB/mysql_fdw.git -WORKDIR /mysql_fdw -RUN make USE_PGXS=1 - - -# Build libssl for wrappers -FROM ubuntu:22.04 AS build-libssl - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && \ - apt install -y \ - build-essential \ - git && \ - rm -rf /var/lib/apt/lists/* - -WORKDIR /build -RUN git clone --branch OpenSSL_1_1_1-stable https://github.com/openssl/openssl.git -WORKDIR /build/openssl -RUN ./config && make && make install - - -# Extend the Spilo image with the mysql_fdw extensions -FROM ghcr.io/zalando/spilo-${POSTGRES_VERSION}:${SPILO_VERSION} -ARG POSTGRES_VERSION -ARG WRAPPERS_VERSION -ARG TARGETARCH - -# Install precompiled supabase wrappers extensions -RUN set -eux; \ - ARCH="$(dpkg --print-architecture)"; \ - case "${ARCH}" in \ - aarch64|arm64) TARGETARCH='arm64';; \ - amd64|x86_64) TARGETARCH='amd64';; \ - *) echo "Unsupported arch: ${ARCH}"; exit 1;; \ - esac; - -RUN apt update && apt install default-libmysqlclient-dev -y && rm -rf /var/lib/apt/lists/* - -RUN curl -L "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_VERSION}/wrappers-v${WRAPPERS_VERSION}-pg${POSTGRES_VERSION}-${TARGETARCH}-linux-gnu.deb" -o wrappers.deb && \ - dpkg --install wrappers.deb && \ - rm wrappers.deb - -COPY --from=build-libssl /usr/local/lib/libssl* /usr/local/lib/libcrypto* /usr/lib/ -COPY --from=build-libssl /usr/local/lib/engines-1.1 /usr/lib/engines-1.1 - -# Copy mysql_fdw -COPY --from=build-mysql_fdw /mysql_fdw/mysql_fdw.so \ - /usr/lib/postgresql/${POSTGRES_VERSION}/lib/mysql_fdw.so -COPY --from=build-mysql_fdw /mysql_fdw/mysql_fdw*.sql /mysql_fdw/mysql_fdw.control /mysql_fdw/mysql_fdw_pushdown.config \ - /usr/share/postgresql/${POSTGRES_VERSION}/extension/ diff --git a/twenty/code/twenty-website/Dockerfile b/twenty/code/twenty-website/Dockerfile deleted file mode 100644 index 2ff7cb808..000000000 --- a/twenty/code/twenty-website/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM node:24-alpine AS twenty-website-build - - -WORKDIR /app - -COPY ./package.json . -COPY ./yarn.lock . -COPY ./.yarnrc.yml . -COPY ./.yarn/releases /app/.yarn/releases -COPY ./.yarn/patches /app/.yarn/patches -COPY ./tools/eslint-rules /app/tools/eslint-rules -COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/ -COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/ -COPY ./packages/twenty-website/package.json /app/packages/twenty-website/package.json - -RUN yarn - -ENV KEYSTATIC_GITHUB_CLIENT_ID="" -ENV KEYSTATIC_GITHUB_CLIENT_SECRET="" -ENV KEYSTATIC_SECRET="" -ENV NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG="" - -COPY ./packages/twenty-ui /app/packages/twenty-ui -COPY ./packages/twenty-website /app/packages/twenty-website -RUN npx nx build twenty-website - -FROM node:24-alpine AS twenty-website - -WORKDIR /app/packages/twenty-website - -COPY --from=twenty-website-build /app /app - -WORKDIR /app/packages/twenty-website - -LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty -LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the website." - -RUN chown -R 1000 /app - -# Use non root user with uid 1000 -USER 1000 - -CMD ["/bin/sh", "-c", "npx nx start"] \ No newline at end of file diff --git a/twenty/code/twenty/Dockerfile b/twenty/code/twenty/Dockerfile deleted file mode 100644 index 7818be742..000000000 --- a/twenty/code/twenty/Dockerfile +++ /dev/null @@ -1,82 +0,0 @@ -# Base image for common dependencies -FROM node:24-alpine AS common-deps - -WORKDIR /app - -# Copy only the necessary files for dependency resolution -COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/ -COPY ./.yarn/releases /app/.yarn/releases -COPY ./.yarn/patches /app/.yarn/patches - -COPY ./.prettierrc /app/ -COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/ -COPY ./packages/twenty-server/package.json /app/packages/twenty-server/ -COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches -COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/ -COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/ -COPY ./packages/twenty-front/package.json /app/packages/twenty-front/ - -# Install all dependencies -RUN yarn && yarn cache clean && npx nx reset - - -# Build the back -FROM common-deps AS twenty-server-build - -# Copy sourcecode after installing dependences to accelerate subsequents builds -COPY ./packages/twenty-emails /app/packages/twenty-emails -COPY ./packages/twenty-shared /app/packages/twenty-shared -COPY ./packages/twenty-server /app/packages/twenty-server - -RUN npx nx run twenty-server:build - -RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-server - -# Build the front -FROM common-deps AS twenty-front-build - -ARG REACT_APP_SERVER_BASE_URL - -COPY ./packages/twenty-front /app/packages/twenty-front -COPY ./packages/twenty-ui /app/packages/twenty-ui -COPY ./packages/twenty-shared /app/packages/twenty-shared -RUN npx nx build twenty-front - - -# Final stage: Run the application -FROM node:24-alpine AS twenty - -# Used to run healthcheck in docker -RUN apk add --no-cache curl jq - -RUN npm install -g tsx - -RUN apk add --no-cache postgresql-client - -COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh -RUN chmod +x /app/entrypoint.sh -WORKDIR /app/packages/twenty-server - -ARG REACT_APP_SERVER_BASE_URL -ENV REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL - -ARG APP_VERSION -ENV APP_VERSION=$APP_VERSION - -# Copy built applications from previous stages -COPY --chown=1000 --from=twenty-server-build /app /app -COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server -COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front - -# Set metadata and labels -LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty -LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the backend and frontend, ensuring it deploys faster and runs the same way regardless of the deployment environment." - -RUN mkdir -p /app/.local-storage /app/packages/twenty-server/.local-storage && \ - chown -R 1000:1000 /app - -# Use non root user with uid 1000 -USER 1000 - -CMD ["node", "dist/src/main"] -ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/twenty/code/twenty/entrypoint.sh b/twenty/code/twenty/entrypoint.sh deleted file mode 100755 index 0175a0a4f..000000000 --- a/twenty/code/twenty/entrypoint.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/sh -set -e - -setup_and_migrate_db() { - if [ "${DISABLE_DB_MIGRATIONS}" = "true" ]; then - echo "Database setup and migrations are disabled, skipping..." - return - fi - - echo "Running database setup and migrations..." - PGUSER=$(echo $PG_DATABASE_URL | awk -F '//' '{print $2}' | awk -F ':' '{print $1}') - PGPASS=$(echo $PG_DATABASE_URL | awk -F ':' '{print $3}' | awk -F '@' '{print $1}') - PGHOST=$(echo $PG_DATABASE_URL | awk -F '@' '{print $2}' | awk -F ':' '{print $1}') - PGPORT=$(echo $PG_DATABASE_URL | awk -F ':' '{print $4}' | awk -F '/' '{print $1}') - PGDATABASE=$(echo $PG_DATABASE_URL | awk -F '/' '{print $NF}' | cut -d'?' -f1) - - # Creating the database if it doesn't exist - db_count=$(PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -tAc "SELECT COUNT(*) FROM pg_database WHERE datname = '${PGDATABASE}'") - if [ "$db_count" = "0" ]; then - echo "Database ${PGDATABASE} does not exist, creating..." - PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -c "CREATE DATABASE \"${PGDATABASE}\"" - fi - - # Run setup and migration scripts - has_schema=$(PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d ${PGDATABASE} -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')") - if [ "$has_schema" = "f" ]; then - echo "Database appears to be empty, running migrations." - NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts - yarn database:migrate:prod - fi - - yarn command:prod upgrade - echo "Successfully migrated DB!" -} - -register_background_jobs() { - if [ "${DISABLE_CRON_JOBS_REGISTRATION}" = "true" ]; then - echo "Cron job registration is disabled, skipping..." - return - fi - - echo "Registering background sync jobs..." - if yarn command:prod cron:register:all; then - echo "Successfully registered all background sync jobs!" - else - echo "Warning: Failed to register background jobs, but continuing startup..." - fi -} - -setup_and_migrate_db -register_background_jobs - -# Continue with the original Docker command -exec "$@" diff --git a/twenty/update.js b/twenty/update.js deleted file mode 100644 index b6131b7b6..000000000 --- a/twenty/update.js +++ /dev/null @@ -1,25 +0,0 @@ -import utils from "../utils.js"; - -await utils.cloneOrPullRepo({ repo: "git@github.com:twentyhq/twenty.git" }); -await utils.copyDir("./repo/packages/twenty-docker", "./code"); - -await utils.removeContainerNames("./code/docker-compose.yml"); -await utils.removePorts("./code/docker-compose.yml"); - -await utils.searchReplace( - "./code/.env.example", - "#REDIS_URL=redis://redis:6379", - "REDIS_URL=redis://redis:6379" -); - -await utils.searchReplace( - "./code/.env.example", - "SERVER_URL=http://localhost:3000", - "SERVER_URL=https://$(PRIMARY_DOMAIN)" -); - -await utils.searchReplace( - "./code/.env.example", - "# APP_SECRET=replace_me_with_a_random_string", - "APP_SECRET=replace_me_with_a_random_string" -); diff --git a/utils.js b/utils.js index d22ef2013..2644f3196 100644 --- a/utils.js +++ b/utils.js @@ -81,6 +81,18 @@ async function searchReplace(path, search, replace) { await fs.promises.writeFile(path, newFile); } +async function removeDir(path) { + console.log(`Removing directory ${path}`); + + await execa("rm", ["-rf", path]); +} + +async function renameDir(src, dest) { + console.log(`Renaming directory ${src} to ${dest}`); + + await execa("mv", [src, dest]); +} + export default { cloneOrPullRepo, removeContainerNames, @@ -89,4 +101,6 @@ export default { downloadFile, renameFile, searchReplace, + removeDir, + renameDir, }; From c6b41076eaf45cff201b7dbcb84b2c384e1459a9 Mon Sep 17 00:00:00 2001 From: Ahson Shaikh Date: Thu, 28 Aug 2025 22:03:39 +0500 Subject: [PATCH 5/6] Updated Appwrite Env --- appwrite/code/.env.example | 2 +- appwrite/update.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/appwrite/code/.env.example b/appwrite/code/.env.example index ad6634857..71d2007e6 100644 --- a/appwrite/code/.env.example +++ b/appwrite/code/.env.example @@ -6,7 +6,7 @@ _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_PROTECTION=disabled _APP_OPENSSL_KEY_V1=your-secret-key -_APP_DOMAIN=$(PRIMARY_DOMAIN)localhost +_APP_DOMAIN=$(PRIMARY_DOMAIN) _APP_CUSTOM_DOMAIN_DENY_LIST=example.com,test.com,app.example.com _APP_DOMAIN_FUNCTIONS=functions.localhost _APP_DOMAIN_SITES=sites.localhost diff --git a/appwrite/update.js b/appwrite/update.js index 67a4d1595..50e07435d 100644 --- a/appwrite/update.js +++ b/appwrite/update.js @@ -14,6 +14,6 @@ await utils.removePorts("./code/docker-compose.yml"); await utils.searchReplace( "./code/.env.example", - "_APP_DOMAIN=", + "_APP_DOMAIN=localhost", "_APP_DOMAIN=$(PRIMARY_DOMAIN)" ); From a121ba9baadab98f8f05aef4028be0bd1df50ff8 Mon Sep 17 00:00:00 2001 From: riwkindo123 <227522252+ryukenshin546-a11y@users.noreply.github.com> Date: Sun, 2 Nov 2025 14:13:21 +0700 Subject: [PATCH 6/6] Add LINE OAuth environment variables to auth service --- supabase/code/docker-compose.yml | 6 + supabase/code/docker-compose.yml.backup | 482 ++++++++++++++++++++++++ 2 files changed, 488 insertions(+) create mode 100644 supabase/code/docker-compose.yml.backup diff --git a/supabase/code/docker-compose.yml b/supabase/code/docker-compose.yml index b1fbf8641..fe77a0c3f 100644 --- a/supabase/code/docker-compose.yml +++ b/supabase/code/docker-compose.yml @@ -136,6 +136,12 @@ services: GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP} GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM} + # LINE OAuth Configuration + GOTRUE_EXTERNAL_LINE_ENABLED: ${GOTRUE_EXTERNAL_LINE_ENABLED} + GOTRUE_EXTERNAL_LINE_CHANNEL_ID: ${GOTRUE_EXTERNAL_LINE_CHANNEL_ID} + GOTRUE_EXTERNAL_LINE_CHANNEL_SECRET: ${GOTRUE_EXTERNAL_LINE_CHANNEL_SECRET} + GOTRUE_EXTERNAL_LINE_SCOPE: ${GOTRUE_EXTERNAL_LINE_SCOPE} + GOTRUE_EXTERNAL_LINE_REDIRECT_URI: ${GOTRUE_EXTERNAL_LINE_REDIRECT_URI} # Uncomment to enable custom access token hook. Please see: https://supabase.com/docs/guides/auth/auth-hooks for full list of hooks and additional details about custom_access_token_hook # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: "true" diff --git a/supabase/code/docker-compose.yml.backup b/supabase/code/docker-compose.yml.backup new file mode 100644 index 000000000..b1fbf8641 --- /dev/null +++ b/supabase/code/docker-compose.yml.backup @@ -0,0 +1,482 @@ +# Usage +# Start: docker compose up +# With helpers: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up +# Stop: docker compose down +# Destroy: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans +# Reset everything: ./reset.sh + +name: supabase + +services: + + studio: + image: supabase/studio:2025.06.30-sha-6f5982d + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://studio:3000/api/platform/profile').then((r) => {if + (r.status !== 200) throw new Error(r.status)})" + ] + timeout: 10s + interval: 5s + retries: 3 + depends_on: + analytics: + condition: service_healthy + environment: + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + + DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + + SUPABASE_URL: http://kong:8000 + SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + AUTH_JWT_SECRET: ${JWT_SECRET} + + LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN} + LOGFLARE_URL: http://analytics:4000 + NEXT_PUBLIC_ENABLE_LOGS: true + # Comment to use Big Query backend for analytics + NEXT_ANALYTICS_BACKEND_PROVIDER: postgres + # Uncomment to use Big Query backend for analytics + # NEXT_ANALYTICS_BACKEND_PROVIDER: bigquery + + kong: + image: kong:2.8.1 + restart: unless-stopped + volumes: + # https://github.com/supabase/supabase/issues/12661 + - ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z + depends_on: + analytics: + condition: service_healthy + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + # https://github.com/supabase/cli/issues/14 + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + # https://unix.stackexchange.com/a/294837 + entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && + /docker-entrypoint.sh kong docker-start' + + auth: + image: supabase/gotrue:v2.177.0 + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:9999/health" + ] + timeout: 5s + interval: 5s + retries: 3 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + environment: + GOTRUE_API_HOST: 0.0.0.0 + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: ${API_EXTERNAL_URL} + + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + + GOTRUE_SITE_URL: ${SITE_URL} + GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS} + GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP} + + GOTRUE_JWT_ADMIN_ROLES: service_role + GOTRUE_JWT_AUD: authenticated + GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated + GOTRUE_JWT_EXP: ${JWT_EXPIRY} + GOTRUE_JWT_SECRET: ${JWT_SECRET} + + GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} + GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM} + + # Uncomment to bypass nonce check in ID Token flow. Commonly set to true when using Google Sign In on mobile. + # GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true + + # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true + # GOTRUE_SMTP_MAX_FREQUENCY: 1s + GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} + GOTRUE_SMTP_HOST: ${SMTP_HOST} + GOTRUE_SMTP_PORT: ${SMTP_PORT} + GOTRUE_SMTP_USER: ${SMTP_USER} + GOTRUE_SMTP_PASS: ${SMTP_PASS} + GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME} + GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE} + GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION} + GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY} + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE} + + GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP} + GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM} + # Uncomment to enable custom access token hook. Please see: https://supabase.com/docs/guides/auth/auth-hooks for full list of hooks and additional details about custom_access_token_hook + + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: "true" + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: "pg-functions://postgres/public/custom_access_token_hook" + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS: "" + + # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED: "true" + # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI: "pg-functions://postgres/public/mfa_verification_attempt" + + # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED: "true" + # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI: "pg-functions://postgres/public/password_verification_attempt" + + # GOTRUE_HOOK_SEND_SMS_ENABLED: "false" + # GOTRUE_HOOK_SEND_SMS_URI: "pg-functions://postgres/public/custom_access_token_hook" + # GOTRUE_HOOK_SEND_SMS_SECRETS: "v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n" + + # GOTRUE_HOOK_SEND_EMAIL_ENABLED: "false" + # GOTRUE_HOOK_SEND_EMAIL_URI: "http://host.docker.internal:54321/functions/v1/email_sender" + # GOTRUE_HOOK_SEND_EMAIL_SECRETS: "v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n" + + rest: + image: postgrest/postgrest:v12.2.12 + restart: unless-stopped + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + environment: + PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} + PGRST_DB_ANON_ROLE: anon + PGRST_JWT_SECRET: ${JWT_SECRET} + PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} + PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY} + command: [ "postgrest" ] + + realtime: + # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain + image: supabase/realtime:v2.34.47 + restart: unless-stopped + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "-H", + "Authorization: Bearer ${ANON_KEY}", + "http://localhost:4000/api/tenants/realtime-dev/health" + ] + timeout: 5s + interval: 5s + retries: 3 + environment: + PORT: 4000 + DB_HOST: ${POSTGRES_HOST} + DB_PORT: ${POSTGRES_PORT} + DB_USER: supabase_admin + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_NAME: ${POSTGRES_DB} + DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime' + DB_ENC_KEY: supabaserealtime + API_JWT_SECRET: ${JWT_SECRET} + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + ERL_AFLAGS: -proto_dist inet_tcp + DNS_NODES: "''" + RLIMIT_NOFILE: "10000" + APP_NAME: realtime + SEED_SELF_HOST: true + RUN_JANITOR: true + + # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up + storage: + image: supabase/storage-api:v1.25.7 + restart: unless-stopped + volumes: + - ./volumes/storage:/var/lib/storage:z + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://storage:5000/status" + ] + timeout: 5s + interval: 5s + retries: 3 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + rest: + condition: service_started + imgproxy: + condition: service_started + environment: + ANON_KEY: ${ANON_KEY} + SERVICE_KEY: ${SERVICE_ROLE_KEY} + POSTGREST_URL: http://rest:3000 + PGRST_JWT_SECRET: ${JWT_SECRET} + DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + FILE_SIZE_LIMIT: 52428800 + STORAGE_BACKEND: file + FILE_STORAGE_BACKEND_PATH: /var/lib/storage + TENANT_ID: stub + # TODO: https://github.com/supabase/storage-api/issues/55 + REGION: stub + GLOBAL_S3_BUCKET: stub + ENABLE_IMAGE_TRANSFORMATION: "true" + IMGPROXY_URL: http://imgproxy:5001 + + imgproxy: + image: darthsim/imgproxy:v3.8.0 + restart: unless-stopped + volumes: + - ./volumes/storage:/var/lib/storage:z + healthcheck: + test: [ "CMD", "imgproxy", "health" ] + timeout: 5s + interval: 5s + retries: 3 + environment: + IMGPROXY_BIND: ":5001" + IMGPROXY_LOCAL_FILESYSTEM_ROOT: / + IMGPROXY_USE_ETAG: "true" + IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} + + meta: + image: supabase/postgres-meta:v0.91.0 + restart: unless-stopped + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + environment: + PG_META_PORT: 8080 + PG_META_DB_HOST: ${POSTGRES_HOST} + PG_META_DB_PORT: ${POSTGRES_PORT} + PG_META_DB_NAME: ${POSTGRES_DB} + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + + functions: + image: supabase/edge-runtime:v1.67.4 + restart: unless-stopped + volumes: + - ./volumes/functions:/home/deno/functions:Z + depends_on: + analytics: + condition: service_healthy + environment: + JWT_SECRET: ${JWT_SECRET} + SUPABASE_URL: http://kong:8000 + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} + SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786 + VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}" + command: [ "start", "--main-service", "/home/deno/functions/main" ] + + analytics: + image: supabase/logflare:1.14.2 + restart: unless-stopped + # Uncomment to use Big Query backend for analytics + # volumes: + # - type: bind + # source: ${PWD}/gcloud.json + # target: /opt/app/rel/logflare/bin/gcloud.json + # read_only: true + healthcheck: + test: [ "CMD", "curl", "http://localhost:4000/health" ] + timeout: 5s + interval: 5s + retries: 10 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + environment: + LOGFLARE_NODE_HOST: 127.0.0.1 + DB_USERNAME: supabase_admin + DB_DATABASE: _supabase + DB_HOSTNAME: ${POSTGRES_HOST} + DB_PORT: ${POSTGRES_PORT} + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_SCHEMA: _analytics + LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} + LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN} + LOGFLARE_SINGLE_TENANT: true + LOGFLARE_SUPABASE_MODE: true + LOGFLARE_MIN_CLUSTER_SIZE: 1 + + # Comment variables to use Big Query backend for analytics + POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase + POSTGRES_BACKEND_SCHEMA: _analytics + LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true + # Uncomment to use Big Query backend for analytics + # GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID} + # GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER} + + # Comment out everything below this point if you are using an external Postgres database + db: + image: supabase/postgres:15.8.1.060 + restart: unless-stopped + volumes: + - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z + # Must be superuser to create event trigger + - ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z + # Must be superuser to alter reserved role + - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z + # Initialize the database settings with JWT_SECRET and JWT_EXP + - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z + # PGDATA directory is persisted between restarts + - ./volumes/db/data:/var/lib/postgresql/data:Z + # Changes required for internal supabase data such as _analytics + - ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z + # Changes required for Analytics support + - ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z + # Changes required for Pooler support + - ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z + # Use named volume to persist pgsodium decryption key between restarts + - db-config:/etc/postgresql-custom + healthcheck: + test: [ "CMD", "pg_isready", "-U", "postgres", "-h", "localhost" ] + interval: 5s + timeout: 5s + retries: 10 + depends_on: + vector: + condition: service_healthy + environment: + POSTGRES_HOST: /var/run/postgresql + PGPORT: ${POSTGRES_PORT} + POSTGRES_PORT: ${POSTGRES_PORT} + PGPASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATABASE: ${POSTGRES_DB} + POSTGRES_DB: ${POSTGRES_DB} + JWT_SECRET: ${JWT_SECRET} + JWT_EXP: ${JWT_EXPIRY} + command: + [ + "postgres", + "-c", + "config_file=/etc/postgresql/postgresql.conf", + "-c", + "log_min_messages=fatal" # prevents Realtime polling queries from appearing in logs + ] + + vector: + image: timberio/vector:0.28.1-alpine + restart: unless-stopped + volumes: + - ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z + - ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro,z + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://vector:9001/health" + ] + timeout: 5s + interval: 5s + retries: 3 + environment: + LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} + command: [ "--config", "/etc/vector/vector.yml" ] + security_opt: + - "label=disable" + + # Update the DATABASE_URL if you are using an external Postgres database + supavisor: + image: supabase/supavisor:2.5.7 + restart: unless-stopped + volumes: + - ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z + healthcheck: + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "http://127.0.0.1:4000/api/health" + ] + interval: 10s + timeout: 5s + retries: 5 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + environment: + PORT: 4000 + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase + CLUSTER_POSTGRES: true + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + VAULT_ENC_KEY: ${VAULT_ENC_KEY} + API_JWT_SECRET: ${JWT_SECRET} + METRICS_JWT_SECRET: ${JWT_SECRET} + REGION: local + ERL_AFLAGS: -proto_dist inet_tcp + POOLER_TENANT_ID: ${POOLER_TENANT_ID} + POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE} + POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN} + POOLER_POOL_MODE: transaction + DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE} + command: + [ + "/bin/sh", + "-c", + "/app/bin/migrate && /app/bin/supavisor eval \"$$(cat + /etc/pooler/pooler.exs)\" && /app/bin/server" + ] + +volumes: + db-config: