From 53abea63f7a4ec5854ad3e9bdf96a8966bbc7386 Mon Sep 17 00:00:00 2001 From: Matt <63170914+matas0@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:35:46 +0400 Subject: [PATCH 1/3] feat: Dify template --- templates/compose/dify.yaml | 645 ++++++++++++++++++++++++++++++++++++ 1 file changed, 645 insertions(+) create mode 100644 templates/compose/dify.yaml diff --git a/templates/compose/dify.yaml b/templates/compose/dify.yaml new file mode 100644 index 000000000..350f93198 --- /dev/null +++ b/templates/compose/dify.yaml @@ -0,0 +1,645 @@ +# documentation: https://docs.dify.ai +# slogan: Dify is an open-source LLM app development platform. Dify's intuitive interface combines AI workflow, RAG pipeline, agent capabilities, model management, observability features and more, letting you quickly go from prototype to production. +# tags: ai, weaviate, openai, gpt, llm, lmops, dify, redis, postgres, qdrant, RAG, agent +# logo: svgs/dify.png +# port: 80 + + +x-shared-env: &shared-api-worker-env + LOG_LEVEL: ${LOG_LEVEL:-INFO} + DEBUG: ${DEBUG:-false} + FLASK_DEBUG: ${FLASK_DEBUG:-false} + CONSOLE_WEB_URL: ${CONSOLE_WEB_URL:-} + CONSOLE_API_URL: ${CONSOLE_API_URL:-} + SERVICE_API_URL: ${SERVICE_API_URL} + APP_WEB_URL: ${APP_WEB_URL:-} + CHECK_UPDATE_URL: ${CHECK_UPDATE_URL:-https://updates.dify.ai} + OPENAI_API_BASE: ${OPENAI_API_BASE:-https://api.openai.com/v1} + FILES_URL: ${FILES_URL:-} + FILES_ACCESS_TIMEOUT: ${FILES_ACCESS_TIMEOUT:-300} + APP_MAX_ACTIVE_REQUESTS: ${APP_MAX_ACTIVE_REQUESTS:-0} + MIGRATION_ENABLED: ${MIGRATION_ENABLED:-true} + DEPLOY_ENV: ${DEPLOY_ENV:-PRODUCTION} + DIFY_BIND_ADDRESS: ${DIFY_BIND_ADDRESS:-0.0.0.0} + DIFY_PORT: ${DIFY_PORT:-5001} + SERVER_WORKER_AMOUNT: ${SERVER_WORKER_AMOUNT:-} + SERVER_WORKER_CLASS: ${SERVER_WORKER_CLASS:-} + CELERY_WORKER_CLASS: ${CELERY_WORKER_CLASS:-} + GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-360} + CELERY_WORKER_AMOUNT: ${CELERY_WORKER_AMOUNT:-} + CELERY_AUTO_SCALE: ${CELERY_AUTO_SCALE:-false} + CELERY_MAX_WORKERS: ${CELERY_MAX_WORKERS:-} + CELERY_MIN_WORKERS: ${CELERY_MIN_WORKERS:-} + API_TOOL_DEFAULT_CONNECT_TIMEOUT: ${API_TOOL_DEFAULT_CONNECT_TIMEOUT:-10} + API_TOOL_DEFAULT_READ_TIMEOUT: ${API_TOOL_DEFAULT_READ_TIMEOUT:-60} + DB_USERNAME: ${DB_USERNAME:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-difyai123456} + DB_HOST: ${DB_HOST:-db} + DB_PORT: ${DB_PORT:-5432} + DB_DATABASE: ${DB_DATABASE:-dify} + SQLALCHEMY_POOL_SIZE: ${SQLALCHEMY_POOL_SIZE:-30} + SQLALCHEMY_POOL_RECYCLE: ${SQLALCHEMY_POOL_RECYCLE:-3600} + SQLALCHEMY_ECHO: ${SQLALCHEMY_ECHO:-false} + POSTGRES_MAX_CONNECTIONS: ${POSTGRES_MAX_CONNECTIONS:-100} + POSTGRES_SHARED_BUFFERS: ${POSTGRES_SHARED_BUFFERS:-128MB} + POSTGRES_WORK_MEM: ${POSTGRES_WORK_MEM:-4MB} + POSTGRES_MAINTENANCE_WORK_MEM: ${POSTGRES_MAINTENANCE_WORK_MEM:-64MB} + POSTGRES_EFFECTIVE_CACHE_SIZE: ${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB} + REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_USERNAME: ${REDIS_USERNAME:-} + REDIS_PASSWORD: $SERVICE_PASSWORD_REDIS + REDIS_USE_SSL: ${REDIS_USE_SSL:-false} + REDIS_DB: 0 + CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://:$SERVICE_PASSWORD_REDIS@redis:6379/1} + BROKER_USE_SSL: ${BROKER_USE_SSL:-false} + WEB_API_CORS_ALLOW_ORIGINS: ${WEB_API_CORS_ALLOW_ORIGINS:-*} + CONSOLE_CORS_ALLOW_ORIGINS: ${CONSOLE_CORS_ALLOW_ORIGINS:-*} + STORAGE_TYPE: ${STORAGE_TYPE:-local} + STORAGE_LOCAL_PATH: storage + S3_USE_AWS_MANAGED_IAM: ${S3_USE_AWS_MANAGED_IAM:-false} + S3_ENDPOINT: ${S3_ENDPOINT:-} + S3_BUCKET_NAME: ${S3_BUCKET_NAME:-} + S3_ACCESS_KEY: ${S3_ACCESS_KEY:-} + S3_SECRET_KEY: ${S3_SECRET_KEY:-} + S3_REGION: ${S3_REGION:-us-east-1} + AZURE_BLOB_ACCOUNT_NAME: ${AZURE_BLOB_ACCOUNT_NAME:-} + AZURE_BLOB_ACCOUNT_KEY: ${AZURE_BLOB_ACCOUNT_KEY:-} + AZURE_BLOB_CONTAINER_NAME: ${AZURE_BLOB_CONTAINER_NAME:-} + AZURE_BLOB_ACCOUNT_URL: ${AZURE_BLOB_ACCOUNT_URL:-} + GOOGLE_STORAGE_BUCKET_NAME: ${GOOGLE_STORAGE_BUCKET_NAME:-} + GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64: ${GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64:-} + ALIYUN_OSS_BUCKET_NAME: ${ALIYUN_OSS_BUCKET_NAME:-} + ALIYUN_OSS_ACCESS_KEY: ${ALIYUN_OSS_ACCESS_KEY:-} + ALIYUN_OSS_SECRET_KEY: ${ALIYUN_OSS_SECRET_KEY:-} + ALIYUN_OSS_ENDPOINT: ${ALIYUN_OSS_ENDPOINT:-} + ALIYUN_OSS_REGION: ${ALIYUN_OSS_REGION:-} + ALIYUN_OSS_AUTH_VERSION: ${ALIYUN_OSS_AUTH_VERSION:-v4} + TENCENT_COS_BUCKET_NAME: ${TENCENT_COS_BUCKET_NAME:-} + TENCENT_COS_SECRET_KEY: ${TENCENT_COS_SECRET_KEY:-} + TENCENT_COS_SECRET_ID: ${TENCENT_COS_SECRET_ID:-} + TENCENT_COS_REGION: ${TENCENT_COS_REGION:-} + TENCENT_COS_SCHEME: ${TENCENT_COS_SCHEME:-} + OCI_ENDPOINT: ${OCI_ENDPOINT:-} + OCI_BUCKET_NAME: ${OCI_BUCKET_NAME:-} + OCI_ACCESS_KEY: ${OCI_ACCESS_KEY:-} + OCI_SECRET_KEY: ${OCI_SECRET_KEY:-} + OCI_REGION: ${OCI_REGION:-} + VECTOR_STORE: ${VECTOR_STORE:-weaviate} + WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080} + WEAVIATE_API_KEY: $SERVICE_PASSWORD_WEAVIATE + RELYT_HOST: ${RELYT_HOST:-db} + RELYT_PORT: ${RELYT_PORT:-5432} + RELYT_USER: $SERVICE_USER_RELYT + RELYT_PASSWORD: $SERVICE_PASSWORD_RELYT + RELYT_DATABASE: ${RELYT_DATABASE:-postgres} + TIDB_VECTOR_HOST: ${TIDB_VECTOR_HOST:-tidb} + TIDB_VECTOR_PORT: ${TIDB_VECTOR_PORT:-4000} + TIDB_VECTOR_USER: $SERVICE_USER_TIDB + TIDB_VECTOR_PASSWORD: $SERVICE_PASSWORD_TIDB + TIDB_VECTOR_DATABASE: ${TIDB_VECTOR_DATABASE:-dify} + # AnalyticDB configuration + ANALYTICDB_KEY_ID: ${ANALYTICDB_KEY_ID:-} + ANALYTICDB_KEY_SECRET: ${ANALYTICDB_KEY_SECRET:-} + ANALYTICDB_REGION_ID: ${ANALYTICDB_REGION_ID:-} + ANALYTICDB_INSTANCE_ID: ${ANALYTICDB_INSTANCE_ID:-} + ANALYTICDB_ACCOUNT: ${ANALYTICDB_ACCOUNT:-} + ANALYTICDB_PASSWORD: ${ANALYTICDB_PASSWORD:-} + ANALYTICDB_NAMESPACE: ${ANALYTICDB_NAMESPACE:-dify} + ANALYTICDB_NAMESPACE_PASSWORD: ${ANALYTICDB_NAMESPACE_PASSWORD:-} + TENCENT_VECTOR_DB_URL: ${TENCENT_VECTOR_DB_URL:-http://127.0.0.1} + TENCENT_VECTOR_DB_API_KEY: ${TENCENT_VECTOR_DB_API_KEY:-dify} + TENCENT_VECTOR_DB_TIMEOUT: ${TENCENT_VECTOR_DB_TIMEOUT:-30} + TENCENT_VECTOR_DB_USERNAME: ${TENCENT_VECTOR_DB_USERNAME:-dify} + TENCENT_VECTOR_DB_DATABASE: ${TENCENT_VECTOR_DB_DATABASE:-dify} + TENCENT_VECTOR_DB_SHARD: ${TENCENT_VECTOR_DB_SHARD:-1} + TENCENT_VECTOR_DB_REPLICAS: ${TENCENT_VECTOR_DB_REPLICAS:-2} + UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15} + UPLOAD_FILE_BATCH_LIMIT: ${UPLOAD_FILE_BATCH_LIMIT:-5} + ETL_TYPE: ${ETL_TYPE:-dify} + MULTIMODAL_SEND_IMAGE_FORMAT: ${MULTIMODAL_SEND_IMAGE_FORMAT:-base64} + UPLOAD_IMAGE_FILE_SIZE_LIMIT: ${UPLOAD_IMAGE_FILE_SIZE_LIMIT:-10} + SENTRY_DSN: ${API_SENTRY_DSN:-} + SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0} + SENTRY_PROFILES_SAMPLE_RATE: ${API_SENTRY_PROFILES_SAMPLE_RATE:-1.0} + NOTION_INTEGRATION_TYPE: ${NOTION_INTEGRATION_TYPE:-public} + NOTION_CLIENT_SECRET: ${NOTION_CLIENT_SECRET:-} + NOTION_CLIENT_ID: ${NOTION_CLIENT_ID:-} + NOTION_INTERNAL_SECRET: ${NOTION_INTERNAL_SECRET:-} + MAIL_TYPE: ${MAIL_TYPE:-resend} + MAIL_DEFAULT_SEND_FROM: ${MAIL_DEFAULT_SEND_FROM:-} + SMTP_SERVER: ${SMTP_SERVER:-} + SMTP_PORT: ${SMTP_PORT:-465} + SMTP_USERNAME: ${SMTP_USERNAME:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_USE_TLS: ${SMTP_USE_TLS:-true} + SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false} + RESEND_API_KEY: ${RESEND_API_KEY:-your-resend-api-key} + RESEND_API_URL: https://api.resend.com + INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-1000} + INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72} + RESET_PASSWORD_TOKEN_EXPIRY_HOURS: ${RESET_PASSWORD_TOKEN_EXPIRY_HOURS:-24} + CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194} + CODE_EXECUTION_API_KEY: ${SANDBOX_API_KEY:-dify-sandbox} + CODE_MAX_NUMBER: ${CODE_MAX_NUMBER:-9223372036854775807} + CODE_MIN_NUMBER: ${CODE_MIN_NUMBER:--9223372036854775808} + CODE_MAX_STRING_LENGTH: ${CODE_MAX_STRING_LENGTH:-80000} + TEMPLATE_TRANSFORM_MAX_LENGTH: ${TEMPLATE_TRANSFORM_MAX_LENGTH:-80000} + CODE_MAX_STRING_ARRAY_LENGTH: ${CODE_MAX_STRING_ARRAY_LENGTH:-30} + CODE_MAX_OBJECT_ARRAY_LENGTH: ${CODE_MAX_OBJECT_ARRAY_LENGTH:-30} + CODE_MAX_NUMBER_ARRAY_LENGTH: ${CODE_MAX_NUMBER_ARRAY_LENGTH:-1000} + SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128} + SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128} + +services: + # API service + api: + image: langgenius/dify-api:0.6.16 + restart: always + environment: + SECRET_KEY: $SERVICE_PASSWORD_64_SECRETKEY + INIT_PASSWORD: $SERVICE_USER_INITPASSWORD + # Use the shared environment variables. + <<: *shared-api-worker-env + # Startup mode, 'api' starts the API server. + MODE: api + depends_on: + - db + - redis + volumes: + # Mount the storage directory to the container, for storing user files. + - './volumes/app/storage:/app/api/storage' + networks: + - ssrf_proxy_network + - default + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5001/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # worker service + # The Celery worker for processing the queue. + worker: + image: langgenius/dify-api:0.6.16 + restart: always + environment: + # Use the shared environment variables. + <<: *shared-api-worker-env + # Startup mode, 'worker' starts the Celery worker for processing the queue. + MODE: worker + depends_on: + - db + - redis + volumes: + # Mount the storage directory to the container, for storing user files. + - './volumes/app/storage:/app/api/storage' + networks: + - ssrf_proxy_network + - default + healthcheck: + test: ["CMD-SHELL", "celery -A app.celery_app inspect ping -d celery@$HOSTNAME"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Frontend web application. + web: + image: langgenius/dify-web:0.6.16 + restart: always + environment: + CONSOLE_API_URL: ${CONSOLE_API_URL:-} + APP_API_URL: ${APP_API_URL:-} + SENTRY_DSN: ${WEB_SENTRY_DSN:-} + NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0} + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # The postgres database. + db: + image: postgres:15-alpine + restart: always + environment: + PGUSER: ${PGUSER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456} + POSTGRES_DB: ${POSTGRES_DB:-dify} + PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} + command: > + postgres -c 'max_connections=${POSTGRES_MAX_CONNECTIONS:-100}' + -c 'shared_buffers=${POSTGRES_SHARED_BUFFERS:-128MB}' + -c 'work_mem=${POSTGRES_WORK_MEM:-4MB}' + -c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}' + -c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}' + volumes: + - './volumes/db/data:/var/lib/postgresql/data' + healthcheck: + test: [ "CMD", "pg_isready" ] + interval: 1s + timeout: 3s + retries: 30 + + # The redis cache. + redis: + image: redis:6-alpine + restart: always + volumes: + # Mount the redis data directory to the container. + - './volumes/redis/data:/data' + # Set the redis password when startup redis server. + command: redis-server --requirepass $SERVICE_PASSWORD_REDIS + healthcheck: + test: [ "CMD", "redis-cli", "-a", "$SERVICE_PASSWORD_REDIS", "ping" ] + + # The DifySandbox + sandbox: + image: langgenius/dify-sandbox:0.2.1 + restart: always + environment: + # The DifySandbox configurations + # Make sure you are changing this key for your deployment with a strong key. + # You can generate a strong key using `openssl rand -base64 42`. + API_KEY: ${SANDBOX_API_KEY:-dify-sandbox} + GIN_MODE: ${SANDBOX_GIN_MODE:-release} + WORKER_TIMEOUT: ${SANDBOX_WORKER_TIMEOUT:-15} + ENABLE_NETWORK: ${SANDBOX_ENABLE_NETWORK:-true} + HTTP_PROXY: ${SANDBOX_HTTP_PROXY:-http://ssrf_proxy:3128} + HTTPS_PROXY: ${SANDBOX_HTTPS_PROXY:-http://ssrf_proxy:3128} + SANDBOX_PORT: ${SANDBOX_PORT:-8194} + volumes: + - './volumes/sandbox/dependencies:/dependencies' + networks: + - ssrf_proxy_network + - default + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8194/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # ssrf_proxy server + # for more information, please refer to + # https://docs.dify.ai/learn-more/faq/self-host-faq#id-18.-why-is-ssrf_proxy-needed + ssrf_proxy: + image: ubuntu/squid:latest + restart: always + volumes: + - type: bind + source: ./ssrf_proxy/squid.conf.template + target: /etc/squid/squid.conf.template + read_only: true + content: | + acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN) + acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN) + acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN) + acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines + acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN) + acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN) + acl localnet src fc00::/7 # RFC 4193 local private network range + acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines + acl SSL_ports port 443 + acl Safe_ports port 80 # http + acl Safe_ports port 21 # ftp + acl Safe_ports port 443 # https + acl Safe_ports port 70 # gopher + acl Safe_ports port 210 # wais + acl Safe_ports port 1025-65535 # unregistered ports + acl Safe_ports port 280 # http-mgmt + acl Safe_ports port 488 # gss-http + acl Safe_ports port 591 # filemaker + acl Safe_ports port 777 # multiling http + acl CONNECT method CONNECT + http_access deny !Safe_ports + http_access deny CONNECT !SSL_ports + http_access allow localhost manager + http_access deny manager + http_access allow localhost + include /etc/squid/conf.d/*.conf + http_access deny all + + ################################## Proxy Server ################################ + http_port 3128 + coredump_dir ${COREDUMP_DIR} + refresh_pattern ^ftp: 1440 20% 10080 + refresh_pattern ^gopher: 1440 0% 1440 + refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 + refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims + refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims + refresh_pattern \/InRelease$ 0 0% 0 refresh-ims + refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims + refresh_pattern . 0 20% 4320 + + + # cache_dir ufs /var/spool/squid 100 16 256 + # upstream proxy, set to your own upstream proxy IP to avoid SSRF attacks + # cache_peer 172.1.1.1 parent 3128 0 no-query no-digest no-netdb-exchange default + + ################################## Reverse Proxy To Sandbox ################################ + http_port 3129 accel vhost + cache_peer ${SANDBOX_HOST} parent ${SANDBOX_PORT} 0 no-query originserver + acl src_all src all + http_access allow src_all + - type: bind + source: ./ssrf_proxy/docker-entrypoint.sh + target: /docker-entrypoint.sh + read_only: true + content: | + #!/bin/bash + + # Modified based on Squid OCI image entrypoint + + # This entrypoint aims to forward the squid logs to stdout to assist users of + # common container related tooling (e.g., kubernetes, docker-compose, etc) to + # access the service logs. + + # Moreover, it invokes the squid binary, leaving all the desired parameters to + # be provided by the "command" passed to the spawned container. If no command + # is provided by the user, the default behavior (as per the CMD statement in + # the Dockerfile) will be to use Ubuntu's default configuration [1] and run + # squid with the "-NYC" options to mimic the behavior of the Ubuntu provided + # systemd unit. + + # [1] The default configuration is changed in the Dockerfile to allow local + # network connections. See the Dockerfile for further information. + + echo "[ENTRYPOINT] re-create snakeoil self-signed certificate removed in the build process" + if [ ! -f /etc/ssl/private/ssl-cert-snakeoil.key ]; then + /usr/sbin/make-ssl-cert generate-default-snakeoil --force-overwrite > /dev/null 2>&1 + fi + + tail -F /var/log/squid/access.log 2>/dev/null & + tail -F /var/log/squid/error.log 2>/dev/null & + tail -F /var/log/squid/store.log 2>/dev/null & + tail -F /var/log/squid/cache.log 2>/dev/null & + + # Replace environment variables in the template and output to the squid.conf + echo "[ENTRYPOINT] replacing environment variables in the template" + awk '{ + while(match($0, /\${[A-Za-z_][A-Za-z_0-9]*}/)) { + var = substr($0, RSTART+2, RLENGTH-3) + val = ENVIRON[var] + $0 = substr($0, 1, RSTART-1) val substr($0, RSTART+RLENGTH) + } + print + }' /etc/squid/squid.conf.template > /etc/squid/squid.conf + + /usr/sbin/squid -Nz + echo "[ENTRYPOINT] starting squid" + /usr/sbin/squid -f /etc/squid/squid.conf -NYC 1 + - ssrf_proxy_var_log_squid:/var/log/squid + - ssrf_proxy_var_spool_squid:/var/spool/squid + entrypoint: ["/bin/sh", "/docker-entrypoint.sh"] + environment: + # pls clearly modify the squid env vars to fit your network environment. + HTTP_PORT: ${SSRF_HTTP_PORT:-3128} + COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid} + REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194} + SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox} + SANDBOX_PORT: ${SANDBOX_PORT:-8194} + networks: + - ssrf_proxy_network + - default + healthcheck: + test: ["CMD", "squid", "-k", "check"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # The nginx reverse proxy. + # used for reverse proxying the API service and Web service. + nginx: + image: nginx:latest + restart: always + volumes: + - type: bind + source: ./nginx/nginx.conf.template + target: /etc/nginx/nginx.conf.template + read_only: true + content: | + # Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + + user nginx; + worker_processes ${NGINX_WORKER_PROCESSES}; + + error_log /var/log/nginx/error.log notice; + pid /var/run/nginx.pid; + + + events { + worker_connections 1024; + } + + + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout ${NGINX_KEEPALIVE_TIMEOUT}; + + #gzip on; + client_max_body_size ${NGINX_CLIENT_MAX_BODY_SIZE}; + + include /etc/nginx/conf.d/*.conf; + } + - type: bind + source: ./nginx/proxy.conf.template + target: /etc/nginx/proxy.conf.template + read_only: true + content: | + # Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_read_timeout ${NGINX_PROXY_READ_TIMEOUT}; + proxy_send_timeout ${NGINX_PROXY_SEND_TIMEOUT}; + - type: bind + source: ./nginx/https.conf.template + target: /etc/nginx/https.conf.template + read_only: true + content: | + # Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + + listen ${NGINX_SSL_PORT} ssl; + ssl_certificate ${SSL_CERTIFICATE_PATH}; + ssl_certificate_key ${SSL_CERTIFICATE_KEY_PATH}; + ssl_protocols ${NGINX_SSL_PROTOCOLS}; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + - type: bind + source: ./nginx/docker-entrypoint.sh + target: /docker-entrypoint-mount.sh + read_only: true + content: | + #!/bin/bash + + if [ "${NGINX_HTTPS_ENABLED}" = "true" ]; then + # Check if the certificate and key files for the specified domain exist + if [ -n "${CERTBOT_DOMAIN}" ] && \ + [ -f "/etc/letsencrypt/live/${CERTBOT_DOMAIN}/${NGINX_SSL_CERT_FILENAME}" ] && \ + [ -f "/etc/letsencrypt/live/${CERTBOT_DOMAIN}/${NGINX_SSL_CERT_KEY_FILENAME}" ]; then + SSL_CERTIFICATE_PATH="/etc/letsencrypt/live/${CERTBOT_DOMAIN}/${NGINX_SSL_CERT_FILENAME}" + SSL_CERTIFICATE_KEY_PATH="/etc/letsencrypt/live/${CERTBOT_DOMAIN}/${NGINX_SSL_CERT_KEY_FILENAME}" + else + SSL_CERTIFICATE_PATH="/etc/ssl/${NGINX_SSL_CERT_FILENAME}" + SSL_CERTIFICATE_KEY_PATH="/etc/ssl/${NGINX_SSL_CERT_KEY_FILENAME}" + fi + export SSL_CERTIFICATE_PATH + export SSL_CERTIFICATE_KEY_PATH + + # set the HTTPS_CONFIG environment variable to the content of the https.conf.template + HTTPS_CONFIG=$(envsubst < /etc/nginx/https.conf.template) + export HTTPS_CONFIG + # Substitute the HTTPS_CONFIG in the default.conf.template with content from https.conf.template + envsubst '${HTTPS_CONFIG}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf + fi + + if [ "${NGINX_ENABLE_CERTBOT_CHALLENGE}" = "true" ]; then + ACME_CHALLENGE_LOCATION='location /.well-known/acme-challenge/ { root /var/www/html; }' + else + ACME_CHALLENGE_LOCATION='' + fi + export ACME_CHALLENGE_LOCATION + + env_vars=$(printenv | cut -d= -f1 | sed 's/^/$/g' | paste -sd, -) + + envsubst "$env_vars" < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf + envsubst "$env_vars" < /etc/nginx/proxy.conf.template > /etc/nginx/proxy.conf + + envsubst < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf + + # Start Nginx using the default entrypoint + exec nginx -g 'daemon off;' + - type: bind + source: ./nginx/default.conf.template + target: /etc/nginx/conf.d/default.conf.template + read_only: true + content: | + # Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + + server { + listen ${NGINX_PORT}; + server_name ${NGINX_SERVER_NAME}; + + location /console/api { + proxy_pass http://api:5001; + include proxy.conf; + } + + location /api { + proxy_pass http://api:5001; + include proxy.conf; + } + + location /v1 { + proxy_pass http://api:5001; + include proxy.conf; + } + + location /files { + proxy_pass http://api:5001; + include proxy.conf; + } + + location / { + proxy_pass http://web:3000; + include proxy.conf; + } + + # placeholder for acme challenge location + ${ACME_CHALLENGE_LOCATION} + + # placeholder for https config defined in https.conf.template + ${HTTPS_CONFIG} + } + - './nginx/ssl:/etc/ssl' + - './volumes/certbot/conf/live:/etc/letsencrypt/live' + - './volumes/certbot/conf:/etc/letsencrypt' + - './volumes/certbot/www:/var/www/html' + entrypoint: [ "sh", "-c", "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" ] + environment: + NGINX_SERVER_NAME: $SERVICE_FQDN_NGINX + NGINX_HTTPS_ENABLED: ${NGINX_HTTPS_ENABLED:-false} + NGINX_SSL_PORT: ${NGINX_SSL_PORT:-443} + NGINX_PORT: ${NGINX_PORT:-80} + # You're required to add your own SSL certificates/keys to the `./nginx/ssl` directory + # and modify the env vars below in .env if HTTPS_ENABLED is true. + NGINX_SSL_CERT_FILENAME: ${NGINX_SSL_CERT_FILENAME:-dify.crt} + 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_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65} + NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} + NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} + NGINX_ENABLE_CERTBOT_CHALLENGE: ${NGINX_ENABLE_CERTBOT_CHALLENGE:-false} + CERTBOT_DOMAIN: ${CERTBOT_DOMAIN:-} + depends_on: + - api + - web + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + + # The Weaviate vector store. + weaviate: + image: semitechnologies/weaviate:1.19.0 + profiles: + - '' + - weaviate + restart: always + volumes: + # Mount the Weaviate data directory to the con tainer. + - ./volumes/weaviate:/var/lib/weaviate + environment: + # The Weaviate configurations + # You can refer to the [Weaviate](https://weaviate.io/developers/weaviate/config-refs/env-vars) documentation for more information. + PERSISTENCE_DATA_PATH: ${WEAVIATE_PERSISTENCE_DATA_PATH:-/var/lib/weaviate} + QUERY_DEFAULTS_LIMIT: ${WEAVIATE_QUERY_DEFAULTS_LIMIT:-25} + AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: ${WEAVIATE_AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED:-false} + DEFAULT_VECTORIZER_MODULE: ${WEAVIATE_DEFAULT_VECTORIZER_MODULE:-none} + CLUSTER_HOSTNAME: ${WEAVIATE_CLUSTER_HOSTNAME:-node1} + AUTHENTICATION_APIKEY_ENABLED: ${WEAVIATE_AUTHENTICATION_APIKEY_ENABLED:-true} + AUTHENTICATION_APIKEY_ALLOWED_KEYS: $SERVICE_PASSWORD_WEAVIATE + AUTHENTICATION_APIKEY_USERS: $SERVICE_USER_WEAVIATE + AUTHORIZATION_ADMINLIST_ENABLED: ${WEAVIATE_AUTHORIZATION_ADMINLIST_ENABLED:-true} + AUTHORIZATION_ADMINLIST_USERS: $SERVICE_USER_WEAVIATE + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/v1/.well-known/live"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: + # create a network between sandbox, api and ssrf_proxy, and can not access outside. + ssrf_proxy_network: + driver: bridge + internal: true + +volumes: + ssrf_proxy_var_log_squid: + ssrf_proxy_var_spool_squid: From b8cf64dd2a13c580359189f117da15de024cdf87 Mon Sep 17 00:00:00 2001 From: Matt <63170914+OhThatMatt@users.noreply.github.com> Date: Wed, 14 Aug 2024 22:39:10 +0400 Subject: [PATCH 2/3] Dify template improvements --- templates/compose/dify.yaml | 48 ++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/templates/compose/dify.yaml b/templates/compose/dify.yaml index 350f93198..95690b226 100644 --- a/templates/compose/dify.yaml +++ b/templates/compose/dify.yaml @@ -11,7 +11,7 @@ x-shared-env: &shared-api-worker-env FLASK_DEBUG: ${FLASK_DEBUG:-false} CONSOLE_WEB_URL: ${CONSOLE_WEB_URL:-} CONSOLE_API_URL: ${CONSOLE_API_URL:-} - SERVICE_API_URL: ${SERVICE_API_URL} + SERVICE_API_URL: APP_WEB_URL: ${APP_WEB_URL:-} CHECK_UPDATE_URL: ${CHECK_UPDATE_URL:-https://updates.dify.ai} OPENAI_API_BASE: ${OPENAI_API_BASE:-https://api.openai.com/v1} @@ -32,11 +32,11 @@ x-shared-env: &shared-api-worker-env CELERY_MIN_WORKERS: ${CELERY_MIN_WORKERS:-} API_TOOL_DEFAULT_CONNECT_TIMEOUT: ${API_TOOL_DEFAULT_CONNECT_TIMEOUT:-10} API_TOOL_DEFAULT_READ_TIMEOUT: ${API_TOOL_DEFAULT_READ_TIMEOUT:-60} - DB_USERNAME: ${DB_USERNAME:-postgres} - DB_PASSWORD: ${DB_PASSWORD:-difyai123456} + DB_USERNAME: $SERVICE_USER_POSTGRES + DB_PASSWORD: $SERVICE_PASSWORD_POSTGRES DB_HOST: ${DB_HOST:-db} DB_PORT: ${DB_PORT:-5432} - DB_DATABASE: ${DB_DATABASE:-dify} + DB_DATABASE: dify SQLALCHEMY_POOL_SIZE: ${SQLALCHEMY_POOL_SIZE:-30} SQLALCHEMY_POOL_RECYCLE: ${SQLALCHEMY_POOL_RECYCLE:-3600} SQLALCHEMY_ECHO: ${SQLALCHEMY_ECHO:-false} @@ -51,7 +51,7 @@ x-shared-env: &shared-api-worker-env REDIS_PASSWORD: $SERVICE_PASSWORD_REDIS REDIS_USE_SSL: ${REDIS_USE_SSL:-false} REDIS_DB: 0 - CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://:$SERVICE_PASSWORD_REDIS@redis:6379/1} + CELERY_BROKER_URL: redis://:$SERVICE_PASSWORD_REDIS@redis:6379/1 BROKER_USE_SSL: ${BROKER_USE_SSL:-false} WEB_API_CORS_ALLOW_ORIGINS: ${WEB_API_CORS_ALLOW_ORIGINS:-*} CONSOLE_CORS_ALLOW_ORIGINS: ${CONSOLE_CORS_ALLOW_ORIGINS:-*} @@ -154,7 +154,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:0.6.16 + image: langgenius/dify-api:latest restart: always environment: SECRET_KEY: $SERVICE_PASSWORD_64_SECRETKEY @@ -182,7 +182,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.6.16 + image: langgenius/dify-api:latest restart: always environment: # Use the shared environment variables. @@ -199,7 +199,7 @@ services: - ssrf_proxy_network - default healthcheck: - test: ["CMD-SHELL", "celery -A app.celery_app inspect ping -d celery@$HOSTNAME"] + test: ["CMD-SHELL", "celery inspect ping"] interval: 30s timeout: 10s retries: 3 @@ -207,7 +207,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.6.16 + image: langgenius/dify-web:latest restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -215,7 +215,7 @@ services: SENTRY_DSN: ${WEB_SENTRY_DSN:-} NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0} healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000"] + test: ["CMD", "wget", "--spider", "-q", "http://web:3000"] interval: 30s timeout: 10s retries: 3 @@ -226,10 +226,10 @@ services: image: postgres:15-alpine restart: always environment: - PGUSER: ${PGUSER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456} - POSTGRES_DB: ${POSTGRES_DB:-dify} - PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} + POSTGRES_USER: $SERVICE_USER_POSTGRES + POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES + POSTGRES_DB: dify + PGDATA: /var/lib/postgresql/data/pgdata command: > postgres -c 'max_connections=${POSTGRES_MAX_CONNECTIONS:-100}' -c 'shared_buffers=${POSTGRES_SHARED_BUFFERS:-128MB}' @@ -239,10 +239,10 @@ services: volumes: - './volumes/db/data:/var/lib/postgresql/data' healthcheck: - test: [ "CMD", "pg_isready" ] - interval: 1s - timeout: 3s - retries: 30 + test: ["CMD", "pg_isready", "-U", "$SERVICE_USER_POSTGRES", "-d", "dify"] + interval: 10s + timeout: 5s + retries: 5 # The redis cache. redis: @@ -252,13 +252,13 @@ services: # Mount the redis data directory to the container. - './volumes/redis/data:/data' # Set the redis password when startup redis server. - command: redis-server --requirepass $SERVICE_PASSWORD_REDIS + command: redis-server --requirepass "$SERVICE_PASSWORD_REDIS" healthcheck: test: [ "CMD", "redis-cli", "-a", "$SERVICE_PASSWORD_REDIS", "ping" ] # The DifySandbox sandbox: - image: langgenius/dify-sandbox:0.2.1 + image: langgenius/dify-sandbox:latest restart: always environment: # The DifySandbox configurations @@ -276,12 +276,6 @@ services: networks: - ssrf_proxy_network - default - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8194/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s # ssrf_proxy server # for more information, please refer to @@ -628,7 +622,7 @@ services: AUTHORIZATION_ADMINLIST_ENABLED: ${WEAVIATE_AUTHORIZATION_ADMINLIST_ENABLED:-true} AUTHORIZATION_ADMINLIST_USERS: $SERVICE_USER_WEAVIATE healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/v1/.well-known/live"] + test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/v1/.well-known/live"] interval: 30s timeout: 10s retries: 3 From 6faf80fd797f0e395598dd2781c63d7f7d754834 Mon Sep 17 00:00:00 2001 From: Matt <63170914+matas0@users.noreply.github.com> Date: Wed, 14 Aug 2024 22:55:11 +0400 Subject: [PATCH 3/3] Added dify logo --- public/svgs/dify.png | Bin 0 -> 7324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/svgs/dify.png diff --git a/public/svgs/dify.png b/public/svgs/dify.png new file mode 100644 index 0000000000000000000000000000000000000000..326acf789cfbacfbe2a749fa99cebabbf998eb7d GIT binary patch literal 7324 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 literal 0 HcmV?d00001