diff --git a/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml index 42df4785e..f0c77577e 100644 --- a/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml +++ b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml @@ -9,9 +9,6 @@ body: > [!IMPORTANT] > **Please ensure you are using the latest version of Coolify before submitting an issue, as the bug may have already been fixed in a recent update.** (Of course, if you're experiencing an issue on the latest version that wasn't present in a previous version, please let us know.) - # ๐Ÿ’Ž Bounty Program (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new)) - - If you would like to prioritize the issue resolution, consider adding a bounty to this issue through our [Bounty Program](https://console.algora.io/org/coollabsio/bounties/new). - - type: textarea attributes: label: Error Message and Logs diff --git a/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml b/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml deleted file mode 100644 index ef26125e0..000000000 --- a/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: ๐Ÿ’Ž Enhancement Bounty -description: "Propose a new feature, service, or improvement with an attached bounty." -title: "[Enhancement]: " -labels: ["โœจ Enhancement", "๐Ÿ” Triage"] -body: - - type: markdown - attributes: - value: | - > [!IMPORTANT] - > **This issue template is exclusively for proposing new features, services, or improvements with an attached bounty.** Enhancements without a bounty can be discussed in the appropriate category of [Github Discussions](https://github.com/coollabsio/coolify/discussions). - - # ๐Ÿ’Ž Add a Bounty (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new)) - - [Click here to add the required bounty](https://console.algora.io/org/coollabsio/bounties/new) - - - type: dropdown - attributes: - label: Request Type - description: Select the type of request you are making. - options: - - New Feature - - New Service - - Improvement - validations: - required: true - - - type: textarea - attributes: - label: Description - description: Provide a detailed description of the feature, improvement, or service you are proposing. - validations: - required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7fd2c358e..e1286eb22 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -22,7 +22,7 @@ ## Category ## Preview - + ## AI Assistance diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9aec08420..85fceb28f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -212,7 +212,7 @@ #### Review Process - Duplicate or superseded work - Security or quality concerns -#### Code Quality, Testing, and Bounty Submissions +#### Code Quality and Testing All contributions must adhere to the highest standards of code quality and testing: - **Testing Required**: Every PR must include steps to test your changes. Untested code will not be reviewed or merged. @@ -220,15 +220,6 @@ #### Code Quality, Testing, and Bounty Submissions - **Code Standards**: Follow the existing code style, conventions, and patterns in the codebase. - **No AI-Generated Code**: Do not submit code generated by AI tools without fully understanding and verifying it. AI-generated submissions that are untested or incorrect will be rejected immediately. -**For PRs that claim bounties:** - -- **Eligibility**: Bounty PRs must strictly follow all guidelines above. Untested, poorly described, or non-compliant PRs will not qualify for bounty rewards. -- **Original Work**: Bounties are for genuine contributions. Submitting AI-generated or copied code solely for bounty claims will result in disqualification and potential removal from contributing. -- **Quality Standards**: Bounty submissions are held to even higher standards. Ensure comprehensive testing, clear documentation, and alignment with project goals. When maintainers review the changes, they should work as expected (the things mentioned in the PR description plus what the bounty issuer needs). -- **Claim Process**: Only successfully merged PRs that pass all reviews (core maintainers + bounty issuer) and meet bounty criteria will be awarded. Follow the issue's bounty guidelines precisely. -- **Prioritization**: Contributor PRs are prioritized over first-time or new contributors. -- **Developer Experience**: We highly advise beginners to avoid participating in bug bounties for our codebase. Most of the time, they don't know what they are changing, how it affects other parts of the system, or if their changes are even correct. -- **Review Comments**: When maintainers ask questions, you should be able to respond properly without generic or AI-generated fluff. ## Development Notes diff --git a/README.md b/README.md index a5aa69343..9a5feff4e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Coolify An open-source & self-hostable Heroku / Netlify / Vercel alternative. ![Latest Release Version](https://img.shields.io/badge/dynamic/json?labelColor=grey&color=6366f1&label=Latest%20released%20version&url=https%3A%2F%2Fcdn.coollabs.io%2Fcoolify%2Fversions.json&query=coolify.v4.version&style=for-the-badge -) [![Bounty Issues](https://img.shields.io/static/v1?labelColor=grey&color=6366f1&label=Algora&message=%F0%9F%92%8E+Bounty+issues&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties/new) +) ## About the Project @@ -65,7 +65,6 @@ ### Huge Sponsors ### Big Sponsors * [23M](https://23m.com?ref=coolify.io) - Your experts for high-availability hosting solutions! -* [Algora](https://algora.io?ref=coolify.io) - Open source contribution platform * [American Cloud](https://americancloud.com?ref=coolify.io) - US-based cloud infrastructure services * [Arcjet](https://arcjet.com?ref=coolify.io) - Advanced web security and performance solutions * [BC Direct](https://bc.direct?ref=coolify.io) - Your trusted technology consulting partner diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index 011c58639..e37a39c3d 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Models\ScheduledDatabaseBackup; use App\Models\TeamInvitation; use App\Models\User; use Illuminate\Bus\Queueable; @@ -12,6 +13,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue @@ -32,6 +34,7 @@ public function handle(): void try { $this->cleanupInvitationLink(); $this->cleanupExpiredEmailChangeRequests(); + $this->enforceBackupRetention(); } catch (\Throwable $e) { Log::error('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); } @@ -55,4 +58,25 @@ private function cleanupExpiredEmailChangeRequests() 'email_change_code_expires_at' => null, ]); } + + private function enforceBackupRetention(): void + { + if (! Cache::add('backup-retention-enforcement', true, 1800)) { + return; + } + + try { + $backups = ScheduledDatabaseBackup::where('enabled', true)->get(); + foreach ($backups as $backup) { + try { + removeOldBackups($backup); + } catch (\Throwable $e) { + Log::warning('Failed to enforce retention for backup '.$backup->id.': '.$e->getMessage()); + } + } + } catch (\Throwable $e) { + Log::error('Failed to enforce backup retention: '.$e->getMessage()); + Cache::forget('backup-retention-enforcement'); + } + } } diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index a2d08e1e8..207191cbd 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -22,6 +22,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; @@ -80,6 +81,13 @@ public function __construct(public ScheduledDatabaseBackup $backup) $this->timeout = $backup->timeout ?? 3600; } + public function middleware(): array + { + $expireAfter = ($this->backup->timeout ?? 3600) + 300; + + return [(new WithoutOverlapping('database-backup-'.$this->backup->id))->expireAfter($expireAfter)->dontRelease()]; + } + public function handle(): void { try { @@ -107,6 +115,8 @@ public function handle(): void throw new \Exception('Database not found?!'); } + $this->markStaleExecutionsAsFailed(); + BackupCreated::dispatch($this->team->id); $status = str(data_get($this->database, 'status')); @@ -727,6 +737,31 @@ private function getFullImageName(): string return "{$helperImage}:{$latestVersion}"; } + private function markStaleExecutionsAsFailed(): void + { + try { + $timeoutSeconds = ($this->backup->timeout ?? 3600) * 2; + + $staleExecutions = $this->backup->executions() + ->where('status', 'running') + ->where('created_at', '<', now()->subSeconds($timeoutSeconds)) + ->get(); + + foreach ($staleExecutions as $execution) { + $execution->update([ + 'status' => 'failed', + 'message' => 'Marked as failed - backup execution exceeded maximum allowed time', + 'finished_at' => now(), + ]); + } + } catch (Throwable $e) { + Log::channel('scheduled-errors')->warning('Failed to clean up stale backup executions', [ + 'backup_id' => $this->backup->uuid, + 'error' => $e->getMessage(), + ]); + } + } + public function failed(?Throwable $exception): void { Log::channel('scheduled-errors')->error('DatabaseBackup permanently failed', [ diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 62ac7ec0d..dbfa15a55 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -208,13 +208,8 @@ private function getGitSource() if ($this->repository_url_parsed->getSegment(3) === 'tree') { $path = str($this->repository_url_parsed->getPath())->trim('/'); - $this->git_branch = str($path)->after('tree/')->before('/')->value(); - $this->base_directory = str($path)->after($this->git_branch)->after('/')->value(); - if (filled($this->base_directory)) { - $this->base_directory = '/'.$this->base_directory; - } else { - $this->base_directory = '/'; - } + $this->git_branch = str($path)->after('tree/')->value(); + $this->base_directory = '/'; } else { $this->git_branch = 'main'; } @@ -235,9 +230,32 @@ private function getBranch() return; } if ($this->git_source->getMorphClass() === GithubApp::class) { - ['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}"); - $this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s'); - $this->branchFound = true; + $originalBranch = $this->git_branch; + $branchToTry = $originalBranch; + + while (true) { + try { + $encodedBranch = urlencode($branchToTry); + ['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$encodedBranch}"); + $this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s'); + $this->git_branch = $branchToTry; + + $remaining = str($originalBranch)->after($branchToTry)->trim('/')->value(); + $this->base_directory = filled($remaining) ? '/'.$remaining : '/'; + + $this->branchFound = true; + + return; + } catch (\Throwable $e) { + if (str_contains($branchToTry, '/')) { + $branchToTry = str($branchToTry)->beforeLast('/')->value(); + + continue; + } + + throw $e; + } + } } } diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 6308bae8b..4038c6288 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -8,6 +8,14 @@ class ScheduledDatabaseBackup extends BaseModel { + protected function casts(): array + { + return [ + 'database_backup_retention_max_storage_locally' => 'float', + 'database_backup_retention_max_storage_s3' => 'float', + ]; + } + protected $fillable = [ 'uuid', 'team_id', diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php index 6f38e5444..15d0f19e0 100644 --- a/app/Support/ValidationPatterns.php +++ b/app/Support/ValidationPatterns.php @@ -40,10 +40,11 @@ class ValidationPatterns * Blocks dangerous shell metacharacters: ; | ` $ ( ) > < newlines and carriage returns * Allows & for command chaining (&&) which is common in multi-step build commands * Allows double quotes for build args with spaces (e.g. --build-arg KEY="value") - * Blocks backslashes and single quotes to prevent escape-sequence attacks + * Blocks backslashes to prevent escape-sequence attacks + * Allows single and double quotes for quoted arguments (e.g. --entrypoint "sh -c 'npm start'") * Uses [ \t] instead of \s to explicitly exclude \n and \r (which act as command separators) */ - public const SHELL_SAFE_COMMAND_PATTERN = '/^[a-zA-Z0-9 \t._\-\/=:@,+\[\]{}#%^~&"]+$/'; + public const SHELL_SAFE_COMMAND_PATTERN = '/^[a-zA-Z0-9 \t._\-\/=:@,+\[\]{}#%^~&"\']+$/'; /** * Pattern for Docker volume names diff --git a/config/constants.php b/config/constants.php index 828493208..d0ae9be65 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,9 +2,9 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.471', - 'helper_version' => '1.0.12', - 'realtime_version' => '1.0.11', + 'version' => '4.0.0-beta.472', + 'helper_version' => '1.0.13', + 'realtime_version' => '1.0.12', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0bd4ae2dd..e6d2bce54 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -60,7 +60,7 @@ services: retries: 10 timeout: 2s soketi: - image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.11' + image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.12' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index ca233356a..00734fb0e 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -96,7 +96,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.10' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.12' pull_policy: always container_name: coolify-realtime restart: always diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 14879eb96..9c984a5ee 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -28,7 +28,8 @@ ARG NIXPACKS_VERSION USER root WORKDIR /artifacts -RUN apk add --no-cache bash curl git git-lfs openssh-client tar tini +RUN apk upgrade --no-cache && \ + apk add --no-cache bash curl git git-lfs openssh-client tar tini RUN mkdir -p ~/.docker/cli-plugins RUN if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ curl -sSL https://github.com/docker/buildx/releases/download/v${DOCKER_BUILDX_VERSION}/buildx-v${DOCKER_BUILDX_VERSION}.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx && \ diff --git a/docker/coolify-realtime/Dockerfile b/docker/coolify-realtime/Dockerfile index 99157268b..325a30dcc 100644 --- a/docker/coolify-realtime/Dockerfile +++ b/docker/coolify-realtime/Dockerfile @@ -10,7 +10,8 @@ ARG TARGETPLATFORM ARG CLOUDFLARED_VERSION WORKDIR /terminal -RUN apk add --no-cache openssh-client make g++ python3 curl +RUN apk upgrade --no-cache && \ + apk add --no-cache openssh-client make g++ python3 curl COPY docker/coolify-realtime/package.json ./ RUN npm i RUN npm rebuild node-pty --update-binary diff --git a/docker/development/Dockerfile b/docker/development/Dockerfile index 98b4d2006..77013e1b9 100644 --- a/docker/development/Dockerfile +++ b/docker/development/Dockerfile @@ -33,7 +33,8 @@ RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID && \ docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID --service nginx # Install PostgreSQL repository and keys -RUN apk add --no-cache gnupg && \ +RUN apk upgrade --no-cache && \ + apk add --no-cache gnupg && \ mkdir -p /usr/share/keyrings && \ curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /usr/share/keyrings/postgresql.gpg diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml index 0bd4ae2dd..e6d2bce54 100644 --- a/other/nightly/docker-compose.prod.yml +++ b/other/nightly/docker-compose.prod.yml @@ -60,7 +60,7 @@ services: retries: 10 timeout: 2s soketi: - image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.11' + image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.12' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/other/nightly/docker-compose.windows.yml b/other/nightly/docker-compose.windows.yml index ca233356a..00734fb0e 100644 --- a/other/nightly/docker-compose.windows.yml +++ b/other/nightly/docker-compose.windows.yml @@ -96,7 +96,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.10' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.12' pull_policy: always container_name: coolify-realtime restart: always diff --git a/other/nightly/versions.json b/other/nightly/versions.json index af11ef4d3..26d755967 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,16 +1,16 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.471" + "version": "4.0.0-beta.472" }, "nightly": { "version": "4.0.0" }, "helper": { - "version": "1.0.12" + "version": "1.0.13" }, "realtime": { - "version": "1.0.11" + "version": "1.0.12" }, "sentinel": { "version": "0.0.21" diff --git a/package-lock.json b/package-lock.json index 6959704a1..0af80f950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "pusher-js": "8.4.0", "tailwind-scrollbar": "4.0.2", "tailwindcss": "4.1.18", - "vite": "7.3.0", + "vite": "7.3.2", "vue": "3.5.26" } }, @@ -2709,9 +2709,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 81cd8c9a4..661b13e4c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "pusher-js": "8.4.0", "tailwind-scrollbar": "4.0.2", "tailwindcss": "4.1.18", - "vite": "7.3.0", + "vite": "7.3.2", "vue": "3.5.26" }, "dependencies": { diff --git a/public/svgs/grimmory.svg b/public/svgs/grimmory.svg new file mode 100644 index 000000000..cd8230fa2 --- /dev/null +++ b/public/svgs/grimmory.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index 28c67c5b4..2ae3ad166 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -84,21 +84,21 @@ Inline comments with space before # (e.g., KEY=value #comment) are stripped. - @if ($showPreview) - @endif Save All Environment Variables @else - @if ($showPreview) - @endif @endcan diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index 6e93d296b..b873a6f05 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -155,7 +155,7 @@ @else -
+
~/kong.yml && /docker-entrypoint.sh kong docker-start' + image: kong/kong:3.9.1 + entrypoint: /home/kong/kong-entrypoint.sh depends_on: supabase-analytics: condition: service_healthy + healthcheck: + test: ["CMD", "kong", "health"] + interval: 5s + timeout: 5s + retries: 5 environment: - SERVICE_URL_SUPABASEKONG_8000 - KONG_PORT_MAPS=443:8000 - JWT_SECRET=${SERVICE_PASSWORD_JWT} - KONG_DATABASE=off - - KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml + - KONG_DECLARATIVE_CONFIG=/usr/local/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_DNS_NOT_FOUND_TTL=1 + - KONG_PLUGINS=request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function - KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k - KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k + - 'KONG_PROXY_ACCESS_LOG=/dev/stdout combined' - SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} - SUPABASE_SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY} + - SUPABASE_PUBLISHABLE_KEY=${SUPABASE_PUBLISHABLE_KEY:-} + - SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY:-} + - ANON_KEY_ASYMMETRIC=${ANON_KEY_ASYMMETRIC:-} + - SERVICE_ROLE_KEY_ASYMMETRIC=${SERVICE_ROLE_KEY_ASYMMETRIC:-} - DASHBOARD_USERNAME=${SERVICE_USER_ADMIN} - DASHBOARD_PASSWORD=${SERVICE_PASSWORD_ADMIN} - 'KONG_STORAGE_CONNECT_TIMEOUT=${KONG_STORAGE_CONNECT_TIMEOUT:-60}' - 'KONG_STORAGE_WRITE_TIMEOUT=${KONG_STORAGE_WRITE_TIMEOUT:-3600}' - 'KONG_STORAGE_READ_TIMEOUT=${KONG_STORAGE_READ_TIMEOUT:-3600}' - 'KONG_STORAGE_REQUEST_BUFFERING=${KONG_STORAGE_REQUEST_BUFFERING:-false}' - - 'KONG_STORAGE_RESPONSE_BUFFERING=${KONG_STORAGE_RESPONSE_BUFFERING:-false}' + - 'KONG_STORAGE_RESPONSE_BUFFERING=${KONG_STORAGE_RESPONSE_BUFFERING:-false}' volumes: + - type: bind + source: ./volumes/api/kong-entrypoint.sh + target: /home/kong/kong-entrypoint.sh + content: | + #!/bin/bash + # Custom entrypoint for Kong that builds Lua expressions for request-transformer + # and performs environment variable substitution in the declarative config. + + if [ -n "$SUPABASE_SECRET_KEY" ] && [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then + export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or (headers.apikey == '$SUPABASE_SECRET_KEY' and 'Bearer $SERVICE_ROLE_KEY_ASYMMETRIC') or (headers.apikey == '$SUPABASE_PUBLISHABLE_KEY' and 'Bearer $ANON_KEY_ASYMMETRIC') or headers.apikey)" + export LUA_RT_WS_EXPR="\$((query_params.apikey == '$SUPABASE_SECRET_KEY' and '$SERVICE_ROLE_KEY_ASYMMETRIC') or (query_params.apikey == '$SUPABASE_PUBLISHABLE_KEY' and '$ANON_KEY_ASYMMETRIC') or query_params.apikey)" + else + export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or headers.apikey)" + export LUA_RT_WS_EXPR="\$(query_params.apikey)" + fi + + awk '{ + result = "" + rest = $0 + while (match(rest, /\$[A-Za-z_][A-Za-z_0-9]*/)) { + varname = substr(rest, RSTART + 1, RLENGTH - 1) + if (varname in ENVIRON) { + result = result substr(rest, 1, RSTART - 1) ENVIRON[varname] + } else { + result = result substr(rest, 1, RSTART + RLENGTH - 1) + } + rest = substr(rest, RSTART + RLENGTH) + } + print result rest + }' /home/kong/temp.yml > "$KONG_DECLARATIVE_CONFIG" + + sed -i '/^[[:space:]]*- key:[[:space:]]*$/d' "$KONG_DECLARATIVE_CONFIG" + + exec /entrypoint.sh kong docker-start # https://github.com/supabase/supabase/issues/12661 - type: bind source: ./volumes/api/kong.yml @@ -51,9 +95,11 @@ services: - username: anon keyauth_credentials: - key: $SUPABASE_ANON_KEY + - key: $SUPABASE_PUBLISHABLE_KEY - username: service_role keyauth_credentials: - key: $SUPABASE_SERVICE_KEY + - key: $SUPABASE_SECRET_KEY ### ### Access Control List @@ -69,8 +115,8 @@ services: ### basicauth_credentials: - consumer: DASHBOARD - username: $DASHBOARD_USERNAME - password: $DASHBOARD_PASSWORD + username: '$DASHBOARD_USERNAME' + password: '$DASHBOARD_PASSWORD' ### @@ -106,6 +152,36 @@ services: - /auth/v1/authorize plugins: - name: cors + - name: auth-v1-open-jwks + _comment: 'Auth: /auth/v1/.well-known/jwks.json -> http://supabase-auth:9999/.well-known/jwks.json' + url: http://supabase-auth:9999/.well-known/jwks.json + routes: + - name: auth-v1-open-jwks + strip_path: true + paths: + - /auth/v1/.well-known/jwks.json + plugins: + - name: cors + + - name: auth-v1-open-sso-acs + url: "http://supabase-auth:9999/sso/saml/acs" + routes: + - name: auth-v1-open-sso-acs + strip_path: true + paths: + - /sso/saml/acs + plugins: + - name: cors + + - name: auth-v1-open-sso-metadata + url: "http://supabase-auth:9999/sso/saml/metadata" + routes: + - name: auth-v1-open-sso-metadata + strip_path: true + paths: + - /sso/saml/metadata + plugins: + - name: cors ## Secure Auth routes - name: auth-v1 @@ -121,6 +197,14 @@ services: - name: key-auth config: hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -141,7 +225,15 @@ services: - name: cors - name: key-auth config: - hide_credentials: true + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -162,12 +254,17 @@ services: - name: cors - name: key-auth config: - hide_credentials: true + hide_credentials: false - name: request-transformer config: add: headers: - - Content-Profile:graphql_public + - "Content-Profile: graphql_public" + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Content-Profile: graphql_public" + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -190,6 +287,14 @@ services: - name: key-auth config: hide_credentials: false + - name: request-transformer + config: + add: + querystring: + - "apikey: $LUA_RT_WS_EXPR" + replace: + querystring: + - "apikey: $LUA_RT_WS_EXPR" - name: acl config: hide_groups_header: true @@ -197,7 +302,7 @@ services: - admin - anon - name: realtime-v1-rest - _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + _comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*' url: http://realtime-dev:4000/api protocol: http routes: @@ -210,6 +315,14 @@ services: - name: key-auth config: hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -217,7 +330,8 @@ services: - admin - anon - ## Storage routes: the storage server manages its own auth + ## Storage API endpoint + ## No key-auth - S3 protocol requests don't carry an apikey header. - name: storage-v1 _comment: 'Storage: /storage/v1/* -> http://supabase-storage:5000/*' connect_timeout: $KONG_STORAGE_CONNECT_TIMEOUT @@ -233,11 +347,20 @@ services: response_buffering: $KONG_STORAGE_RESPONSE_BUFFERING plugins: - name: cors + - name: post-function + config: + access: + - | + local auth = kong.request.get_header("authorization") + if auth == nil or auth == "" or auth:find("^%s*$") then + kong.service.request.clear_header("authorization") + end ## Edge Functions routes - name: functions-v1 _comment: 'Edge Functions: /functions/v1/* -> http://supabase-edge-functions:9000/*' url: http://supabase-edge-functions:9000/ + read_timeout: 150000 routes: - name: functions-v1-all strip_path: true @@ -246,15 +369,28 @@ services: plugins: - name: cors - ## Analytics routes - - name: analytics-v1 - _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' - url: http://supabase-analytics:4000/ + ## OAuth 2.0 Authorization Server Metadata (RFC 8414) + - name: well-known-oauth + _comment: 'Auth: /.well-known/oauth-authorization-server -> http://supabase-auth:9999/.well-known/oauth-authorization-server' + url: http://supabase-auth:9999/.well-known/oauth-authorization-server routes: - - name: analytics-v1-all + - name: well-known-oauth strip_path: true paths: - - /analytics/v1/ + - /.well-known/oauth-authorization-server + plugins: + - name: cors + + ## Analytics routes + ## Not used - Studio and Vector talk directly to analytics via Docker networking. + # - name: analytics-v1 + # _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' + # url: http://supabase-analytics:4000/ + # routes: + # - name: analytics-v1-all + # strip_path: true + # paths: + # - /analytics/v1/ ## Secure Database routes - name: meta @@ -275,6 +411,48 @@ services: allow: - admin + ## Block access to /api/mcp + - name: mcp-blocker + _comment: 'Block direct access to /api/mcp' + url: http://supabase-studio:3000/api/mcp + routes: + - name: mcp-blocker-route + strip_path: true + paths: + - /api/mcp + plugins: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + + ## MCP endpoint - local access + - name: mcp + _comment: 'MCP: /mcp -> http://supabase-studio:3000/api/mcp (local access)' + url: http://supabase-studio:3000/api/mcp + routes: + - name: mcp + strip_path: true + paths: + - /mcp + plugins: + # Block access to /mcp by default + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + # Enable local access (danger zone!) + # 1. Comment out the 'request-termination' section above + # 2. Uncomment the entire section below, including 'deny' + # 3. Add your local IPs to the 'allow' list + #- name: cors + #- name: ip-restriction + # config: + # allow: + # - 127.0.0.1 + # - ::1 + # deny: [] + ## Protected Dashboard - catch all remaining routes - name: dashboard _comment: 'Studio: /* -> http://studio:3000/*' @@ -290,7 +468,7 @@ services: config: hide_credentials: true supabase-studio: - image: supabase/studio:2026.01.07-sha-037e5f9 + image: supabase/studio:2026.03.16-sha-5528817 healthcheck: test: [ @@ -310,7 +488,11 @@ services: - STUDIO_PG_META_URL=http://supabase-meta:8080 - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - POSTGRES_HOST=${POSTGRES_HOST:-supabase-db} - - CURRENT_CLI_VERSION=2.67.1 + - POSTGRES_PORT=${POSTGRES_PORT:-5432} + - POSTGRES_DB=${POSTGRES_DB:-postgres} + - 'PGRST_DB_SCHEMAS=${PGRST_DB_SCHEMAS:-public,storage,graphql_public}' + - PGRST_DB_MAX_ROWS=${PGRST_DB_MAX_ROWS:-1000} + - PGRST_DB_EXTRA_SEARCH_PATH=${PGRST_DB_EXTRA_SEARCH_PATH:-public} - DEFAULT_ORGANIZATION_NAME=${STUDIO_DEFAULT_ORGANIZATION:-Default Organization} - DEFAULT_PROJECT_NAME=${STUDIO_DEFAULT_PROJECT:-Default Project} @@ -320,10 +502,12 @@ services: - SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} - SUPABASE_SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY} - AUTH_JWT_SECRET=${SERVICE_PASSWORD_JWT} + - PG_META_CRYPTO_KEY=${SERVICE_PASSWORD_PGMETACRYPTO} - LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PUBLIC_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PRIVATE_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLAREPRIVATE} - LOGFLARE_URL=http://supabase-analytics:4000 - - 'SUPABASE_PUBLIC_API=${SERVICE_URL_SUPABASEKONG}' # Next.js client-side environment variables (required for browser access) - 'NEXT_PUBLIC_SUPABASE_URL=${SERVICE_URL_SUPABASEKONG}' - NEXT_PUBLIC_SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} @@ -333,8 +517,13 @@ services: # Uncomment to use Big Query backend for analytics # NEXT_ANALYTICS_BACKEND_PROVIDER=bigquery - 'OPENAI_API_KEY=${OPENAI_API_KEY}' + - SNIPPETS_MANAGEMENT_FOLDER=/app/snippets + - EDGE_FUNCTIONS_MANAGEMENT_FOLDER=/app/edge-functions + volumes: + - ./volumes/snippets:/app/snippets + - ./volumes/functions:/app/edge-functions supabase-db: - image: supabase/postgres:15.8.1.048 + image: supabase/postgres:15.8.1.085 healthcheck: test: pg_isready -U postgres -h 127.0.0.1 interval: 5s @@ -365,7 +554,7 @@ services: source: ./volumes/db/realtime.sql target: /docker-entrypoint-initdb.d/migrations/99-realtime.sql content: | - \set pguser `echo "supabase_admin"` + \set pguser `echo "$POSTGRES_USER"` create schema if not exists _realtime; alter schema _realtime owner to :pguser; @@ -380,7 +569,7 @@ services: source: ./volumes/db/pooler.sql target: /docker-entrypoint-initdb.d/migrations/99-pooler.sql content: | - \set pguser `echo "supabase_admin"` + \set pguser `echo "$POSTGRES_USER"` \c _supabase create schema if not exists _supavisor; alter schema _supavisor owner to :pguser; @@ -624,7 +813,7 @@ services: source: ./volumes/db/logs.sql target: /docker-entrypoint-initdb.d/migrations/99-logs.sql content: | - \set pguser `echo "supabase_admin"` + \set pguser `echo "$POSTGRES_USER"` \c _supabase create schema if not exists _analytics; alter schema _analytics owner to :pguser; @@ -633,7 +822,7 @@ services: - supabase-db-config:/etc/postgresql-custom supabase-analytics: - image: supabase/logflare:1.4.0 + image: supabase/logflare:1.31.2 healthcheck: test: ["CMD", "curl", "http://127.0.0.1:4000/health"] timeout: 5s @@ -655,11 +844,10 @@ services: - DB_PORT=${POSTGRES_PORT:-5432} - DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - DB_SCHEMA=_analytics - - LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PUBLIC_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PRIVATE_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLAREPRIVATE} - LOGFLARE_SINGLE_TENANT=true - - LOGFLARE_SINGLE_TENANT_MODE=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:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOSTNAME:-supabase-db}:${POSTGRES_PORT:-5432}/_supabase @@ -670,7 +858,7 @@ services: # GOOGLE_PROJECT_ID=${GOOGLE_PROJECT_ID} # GOOGLE_PROJECT_NUMBER=${GOOGLE_PROJECT_NUMBER} supabase-vector: - image: timberio/vector:0.28.1-alpine + image: timberio/vector:0.53.0-alpine healthcheck: test: [ @@ -722,13 +910,13 @@ services: inputs: - project_logs route: - kong: 'starts_with(string!(.appname), "supabase-kong")' - auth: 'starts_with(string!(.appname), "supabase-auth")' - rest: 'starts_with(string!(.appname), "supabase-rest")' - realtime: 'starts_with(string!(.appname), "realtime-dev")' - storage: 'starts_with(string!(.appname), "supabase-storage")' - functions: 'starts_with(string!(.appname), "supabase-functions")' - db: 'starts_with(string!(.appname), "supabase-db")' + kong: 'contains(string!(.appname), "supabase-kong")' + auth: 'contains(string!(.appname), "supabase-auth")' + rest: 'contains(string!(.appname), "supabase-rest")' + realtime: 'contains(string!(.appname), "supabase-realtime")' + storage: 'contains(string!(.appname), "supabase-storage")' + functions: 'contains(string!(.appname), "supabase-edge-functions")' + db: 'contains(string!(.appname), "supabase-db")' # Ignores non nginx errors since they are related with kong booting up kong_logs: type: remap @@ -741,10 +929,13 @@ services: .metadata.request.headers.referer = req.referer .metadata.request.headers.user_agent = req.agent .metadata.request.headers.cf_connecting_ip = req.client - .metadata.request.method = req.method - .metadata.request.path = req.path - .metadata.request.protocol = req.protocol .metadata.response.status_code = req.status + url, split_err = split(req.request, " ") + if split_err == null { + .metadata.request.method = url[0] + .metadata.request.path = url[1] + .metadata.request.protocol = url[2] + } } if err != null { abort @@ -793,14 +984,20 @@ services: parsed, err = parse_regex(.event_message, r'^(?P