Compare commits

...

40 commits

Author SHA1 Message Date
matfire
97897db82f
Merge 7c4f8f37a3 into cb759b2846 2026-02-24 19:02:15 +00:00
Andras Bacsai
cb759b2846
fix(api): correct permission requirements for POST endpoints (#8600)
Some checks are pending
Staging Build / build-push (aarch64, linux/aarch64, ubuntu-24.04-arm) (push) Waiting to run
Staging Build / build-push (amd64, linux/amd64, ubuntu-24.04) (push) Waiting to run
Staging Build / merge-manifest (push) Blocked by required conditions
2026-02-24 14:57:51 +01:00
Andras Bacsai
d8419fad93 chore: prepare for PR 2026-02-24 14:57:32 +01:00
Andras Bacsai
279322d50f
fix(input): prevent eye icon flash on password fields before Alpine.js loads (#8599) 2026-02-24 12:57:22 +01:00
Andras Bacsai
f39a1da7be
fix(auth): prevent CSRF redirect loop during 2FA challenge (#8596) 2026-02-24 12:57:10 +01:00
Andras Bacsai
448e922e6c chore: prepare for PR 2026-02-24 12:56:54 +01:00
Andras Bacsai
78e584a136
feat(service): upgrade beszel and beszel-agent to v0.18 (#8513) 2026-02-24 12:56:36 +01:00
Andras Bacsai
912e5f6db2
feat(service): disable pterodactyl panel and pterodactyl wings (#8512) 2026-02-24 12:55:52 +01:00
Andras Bacsai
f8de374f77
feat(service): disable plane (#8580) 2026-02-24 12:55:29 +01:00
Andras Bacsai
2986d7604e chore: prepare for PR 2026-02-24 10:17:16 +01:00
ShadowArcanist
b36d67288b feat(service): disable plane
The latest version of plane v1.2.2 have security fixed but our template is using v1.0.0 which is 5 months behind the current latest. New version v1.2.2 doesn't work with our existing template so disabling it for now to prevent users from deploying a vulnerable version of plane
2026-02-24 02:34:35 +05:30
Andras Bacsai
021605dbf0
fix(deploy): split BuildKit and secrets detection (#8565)
Some checks are pending
Staging Build / build-push (aarch64, linux/aarch64, ubuntu-24.04-arm) (push) Waiting to run
Staging Build / build-push (amd64, linux/amd64, ubuntu-24.04) (push) Waiting to run
Staging Build / merge-manifest (push) Blocked by required conditions
2026-02-23 15:20:25 +01:00
Andras Bacsai
ec14b55f0a chore: prepare for PR 2026-02-23 14:28:28 +01:00
Andras Bacsai
2310ad5f7f
chore(ui): widen project heading nav spacing (#8564) 2026-02-23 14:17:38 +01:00
Andras Bacsai
6cacd2f0ff chore: prepare for PR 2026-02-23 14:17:15 +01:00
Andras Bacsai
46923f7e77
fix(applications): treat zero private_key_id as deploy key (#8563) 2026-02-23 14:16:11 +01:00
Andras Bacsai
620da191b1 chore: prepare for PR 2026-02-23 14:15:13 +01:00
Andras Bacsai
d71d91d63e fix(version): update coolify version to 4.0.0-beta.464 and nightly version to 4.0.0-beta.465 2026-02-23 13:47:26 +01:00
Andras Bacsai
1f3fca5f71
fix(database): chown redis/keydb configs when custom conf set (#8561) 2026-02-23 13:26:58 +01:00
Andras Bacsai
76a6960f44 chore: prepare for PR 2026-02-23 13:26:01 +01:00
Andras Bacsai
f68d60a373
chore(horizon): make max time configurable (#8560) 2026-02-23 13:25:13 +01:00
Andras Bacsai
b7b0dfeddd chore: prepare for PR 2026-02-23 13:24:49 +01:00
Andras Bacsai
133241bac1
fix(service): resolve team lookup via service relationship (#8559) 2026-02-23 13:24:01 +01:00
Andras Bacsai
61a54afe2b fix(service): resolve team lookup via service relationship
Update service application/database team accessors to traverse the service relation chain and add coverage to prevent null team regressions.
2026-02-23 13:23:12 +01:00
Andras Bacsai
58acdccfc9
fix(team): include webhook notifications in enabled check (#8557) 2026-02-23 13:03:05 +01:00
Andras Bacsai
bf51ed905f chore: prepare for PR 2026-02-23 13:02:06 +01:00
Andras Bacsai
c30d94f089
chore(scheduler): fix scheduled job duration metric (#8551) 2026-02-23 12:20:15 +01:00
Andras Bacsai
cb0f5cc812 chore: prepare for PR 2026-02-23 12:19:57 +01:00
Andras Bacsai
ffb408f214 Create docker-compose-maxio.dev.yml 2026-02-23 12:18:50 +01:00
ShadowArcanist
76d3709163 feat(service): upgrade beszel and beszel-agent to v0.18 2026-02-21 23:17:23 +05:30
ShadowArcanist
c1951726c0 feat(service): disable pterodactyl panel and pterodactyl wings
The template is using latest version of pterodactyl and the issue is the db migration fails for new users but works fine for existing deployments. We cannot revert the template to previous version because the current latest version addresses few CVEs so it's better to disable this template for now
2026-02-21 21:42:28 +05:30
github-actions[bot]
7c4f8f37a3 docs: update changelog 2026-02-17 11:24:33 +00:00
matfire
2cd1082fbd
Merge branch 'next' into v4.x 2026-02-17 12:23:58 +01:00
github-actions[bot]
17f5259f32 docs: update changelog 2026-02-16 08:32:31 +00:00
matfire
331493a0b0
Merge branch 'next' into v4.x 2026-02-16 09:32:01 +01:00
github-actions[bot]
346b2b8bd8 docs: update changelog 2026-02-14 13:11:23 +00:00
matfire
4045c7267e
Merge branch 'next' into v4.x 2026-02-14 14:10:52 +01:00
github-actions[bot]
c180947d6b docs: update changelog 2026-02-07 19:36:24 +00:00
Matteo Gassend
32e1fd97ae feat(templates): add ElectricSQL docker compose template 2026-02-07 20:35:51 +01:00
github-actions[bot]
960ceddf15 docs: update changelog 2026-02-07 18:52:40 +00:00
35 changed files with 953 additions and 279 deletions

View file

@ -1190,7 +1190,41 @@ All notable changes to this project will be documented in this file.
- *(service)* Update autobase to version 2.5 (#7923)
- *(service)* Add chibisafe template (#5808)
- *(ui)* Improve sidebar menu items styling (#7928)
- *(service)* Improve open-archiver
- *(template)* Add open archiver template (#6593)
- *(service)* Add linkding template (#6651)
- *(service)* Add glip template (#7937)
- *(templates)* Add Sessy docker compose template (#7951)
- *(api)* Add update urls support to services api
- *(api)* Improve service urls update
- *(api)* Add url update support to services api (#7929)
- *(api)* Improve docker_compose_domains
- *(api)* Add more allowed fields
- *(notifications)* Add mattermost notifications (#7963)
- *(templates)* Add ElectricSQL docker compose template
- *(service)* Add back soketi-app-manager
- *(service)* Upgrade checkmate to v3 (#7995)
- *(service)* Update pterodactyl version (#7981)
- *(service)* Add langflow template (#8006)
- *(service)* Upgrade listmonk to v6
- *(service)* Add alexandrie template (#8021)
- *(service)* Upgrade formbricks to v4 (#8022)
- *(service)* Add goatcounter template (#8029)
- *(installer)* Add tencentos as a supported os
- *(installer)* Update nightly install script
- Update pr template to remove unnecessary quote blocks
- *(service)* Add satisfactory game server (#8056)
- *(service)* Disable mautic (#8088)
- *(service)* Add bento-pdf (#8095)
- *(ui)* Add official postgres 18 support
- *(database)* Add official postgres 18 support
- *(ui)* Use 2 column layout
- *(database)* Add official postgres 18 and pgvector 18 support (#8143)
- *(ui)* Improve global search with uuid and pr support (#7901)
- *(openclaw)* Add Openclaw service with environment variables and health checks
- *(service)* Disable maybe
- *(service)* Disable maybe (#8167)
- *(service)* Add sure
- *(service)* Add sure (#8157)
### 🐛 Bug Fixes
@ -3773,6 +3807,7 @@ All notable changes to this project will be documented in this file.
- *(scheduling)* Change redis cleanup command frequency from hourly to weekly for better resource management
- *(versions)* Update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.5 and 4.0.0-beta.420.6
- *(database)* Ensure internal port defaults correctly for unsupported database types in StartDatabaseProxy
- *(git)* Tracking issue due to case sensitivity
- *(versions)* Update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.6 and 4.0.0-beta.420.7
- *(scheduling)* Remove unnecessary padding from scheduled task form layout for improved UI consistency
- *(horizon)* Update queue configuration to use environment variable for dynamic queue management
@ -3798,7 +3833,6 @@ All notable changes to this project will be documented in this file.
- *(application)* Add option to suppress toast notifications when loading compose file
- *(git)* Tracking issue due to case sensitivity
- *(git)* Tracking issue due to case sensitivity
- *(git)* Tracking issue due to case sensitivity
- *(ui)* Delete button width on small screens (#6308)
- *(service)* Matrix entrypoint
- *(ui)* Add flex-wrap to prevent overflow on small screens (#6307)
@ -4422,6 +4456,67 @@ All notable changes to this project will be documented in this file.
- *(api)* Deprecate applications compose endpoint
- *(api)* Applications post and patch endpoints
- *(api)* Applications create and patch endpoints (#7917)
- *(service)* Sftpgo port
- *(env)* Only cat .env file in dev
- *(api)* Encoding checks (#7944)
- *(env)* Only show nixpacks plan variables section in dev
- Switch custom labels check to UTF-8
- *(api)* One click service name and description cannot be set during creation
- *(ui)* Improve volume mount warning for compose applications (#7947)
- *(api)* Show an error if the same 2 urls are provided
- *(preview)* Docker compose preview URLs (#7959)
- *(api)* Check domain conflicts within the request
- *(api)* Include docker_compose_domains in domain conflict check
- *(api)* Is_static and docker network missing
- *(api)* If domains field is empty clear the fqdn column
- *(api)* Application endpoint issues part 2 (#7948)
- Optimize queries and caching for projects and environments
- *(perf)* Eliminate N+1 queries from InstanceSettings and Server lookups (#7966)
- Update version numbers to 4.0.0-beta.462 and 4.0.0-beta.463
- *(service)* Update seaweedfs logo (#7971)
- *(service)* Soju svg
- *(service)* Autobase database is not persisted correctly (#7978)
- *(ui)* Make tooltips a bit wider
- *(ui)* Modal issues
- *(validation)* Add @, / and & support to names and descriptions
- *(backup)* Postgres restore arithmetic syntax error (#7997)
- *(service)* Users unable to create their first ente account without SMTP (#7986)
- *(ui)* Horizontal overflow on application and service headings (#7970)
- *(service)* Supabase studio settings redirect loop (#7828)
- *(env)* Skip escaping for valid JSON in environment variables (#6160)
- *(service)* Disable kong response buffering and increase timeouts (#7864)
- *(service)* Rocketchat fails to start due to database version incompatibility (#7999)
- *(service)* N8n v2 with worker timeout error
- *(service)* Elasticsearch-with-kibana not generating account token
- *(service)* Elasticsearch-with-kibana not generating account token (#8067)
- *(service)* Kimai fails to start (#8027)
- *(service)* Reactive-resume template (#8048)
- *(api)* Infinite loop with github app with many repos (#8052)
- *(env)* Skip escaping for valid JSON in environment variables (#8080)
- *(docker)* Update PostgreSQL version to 16 in Dockerfile
- *(validation)* Enforce url validation for instance domain (#8078)
- *(service)* Bluesky pds invite code doesn't generate (#8081)
- *(service)* Bugsink login fails due to cors (#8083)
- *(service)* Strapi doesn't start (#8084)
- *(service)* Activepieces postgres 18 volume mount (#8098)
- *(service)* Forgejo login failure (#8145)
- *(database)* Pgvector 18 version is not parsed properly
- *(labels)* Make sure name is slugified
- *(parser)* Replace dashes and dots in auto generated envs
- Stop database proxy when is_public changes to false (#8138)
- *(docs)* Update documentation link for Openclaw service
- *(api-docs)* Use proper schema references for environment variable endpoints (#8239)
- *(ui)* Fix datalist border color and add repository selection watcher (#8240)
- *(server)* Improve IP uniqueness validation with team-specific error messages
- *(jobs)* Initialize status variable in checkHetznerStatus (#8359)
- *(jobs)* Handle queue timeouts gracefully in Horizon (#8360)
- *(push-server-job)* Skip containers with empty service subId (#8361)
- *(database)* Disable proxy on port allocation failure (#8362)
- *(sentry)* Use withScope for SSH retry event tracking (#8363)
- *(api)* Add a newline to openapi.json
- *(server)* Improve IP uniqueness validation with team-specific error messages
- *(service)* Glitchtip webdashboard doesn't load
- *(service)* Glitchtip webdashboard doesn't load (#8249)
### 💼 Other
@ -4886,6 +4981,7 @@ All notable changes to this project will be documented in this file.
- CVE-2025-55182 React2shell infected supabase/studio:2025.06.02-sha-8f2993d
- Bump superset to 6.0.0
- Trim whitespace from domain input in instance settings (#7837)
- Upgrade postgres client to fix build error
### 🚜 Refactor
@ -5510,6 +5606,10 @@ All notable changes to this project will be documented in this file.
- Move all env sorting to one place
- *(api)* Make docker_compose_raw description more clear
- *(api)* Update application create endpoints docs
- *(api)* Application urls validation
- *(services)* Improve some service slogans
- *(ssh-retry)* Remove Sentry tracking from retry logic
- *(ssh-retry)* Remove Sentry tracking from retry logic
### 📚 Documentation
@ -5616,7 +5716,6 @@ All notable changes to this project will be documented in this file.
- Update changelog
- *(tests)* Update testing guidelines for unit and feature tests
- *(sync)* Create AI Instructions Synchronization Guide and update CLAUDE.md references
- Update changelog
- *(database-patterns)* Add critical note on mass assignment protection for new columns
- Clarify cloud-init script compatibility
- Update changelog
@ -5647,6 +5746,15 @@ All notable changes to this project will be documented in this file.
- Update application architecture and database patterns for request-level caching best practices
- Remove git worktree symlink instructions from CLAUDE.md
- Remove git worktree symlink instructions from CLAUDE.md (#7908)
- Add transcript lol link and logo to readme (#7331)
- *(api)* Change domains to urls
- *(api)* Improve domains API docs
- Update changelog
- Update changelog
- *(api)* Improve app endpoint deprecation description
- Add Coolify design system reference
- Add Coolify design system reference (#8237)
- Update changelog
- Update changelog
### ⚡ Performance
@ -5686,6 +5794,9 @@ All notable changes to this project will be documented in this file.
- Add tests for shared environment variable spacing and resolution
- Add comprehensive preview deployment port and path tests
- Add comprehensive preview deployment port and path tests (#7677)
- Add Pest browser testing with SQLite :memory: schema
- Add dashboard test and improve browser test coverage
- Migrate to SQLite :memory: and add Pest browser testing (#8364)
### ⚙️ Miscellaneous Tasks
@ -6293,10 +6404,10 @@ All notable changes to this project will be documented in this file.
- *(versions)* Update Coolify versions to 4.0.0-beta.420.2 and 4.0.0-beta.420.3 in multiple files
- *(versions)* Bump coolify and nightly versions to 4.0.0-beta.420.3 and 4.0.0-beta.420.4 respectively
- *(versions)* Update coolify and nightly versions to 4.0.0-beta.420.4 and 4.0.0-beta.420.5 respectively
- *(service)* Update Nitropage template (#6181)
- *(versions)* Update all version
- *(bump)* Update composer deps
- *(version)* Bump Coolify version to 4.0.0-beta.420.6
- *(service)* Update Nitropage template (#6181)
- *(versions)* Update all version
- *(service)* Improve matrix service
- *(service)* Format runner service
- *(service)* Improve sequin
@ -6399,6 +6510,29 @@ All notable changes to this project will be documented in this file.
- *(services)* Upgrade service template json files
- *(api)* Update openapi json and yaml
- *(api)* Regenerate openapi docs
- Prepare for PR
- *(api)* Improve current request error message
- *(api)* Improve current request error message
- *(api)* Update openapi files
- *(service)* Update service templates json
- *(services)* Update service template json files
- *(service)* Use major version for openpanel (#8053)
- Prepare for PR
- *(services)* Update service template json files
- Bump coolify version
- Prepare for PR
- Prepare for PR
- Prepare for PR
- Prepare for PR
- Prepare for PR
- Prepare for PR
- Prepare for PR
- Prepare for PR
- Prepare for PR
- Prepare for PR
- Prepare for PR
- Prepare for PR
- Prepare for PR
### ◀️ Revert

View file

@ -207,6 +207,9 @@ class StartKeydb
if ($this->database->enable_ssl) {
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
}
if (! is_null($this->database->keydb_conf) && ! empty($this->database->keydb_conf)) {
$this->commands[] = "chown 999:999 $this->configuration_dir/keydb.conf";
}
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";

View file

@ -204,6 +204,9 @@ class StartRedis
if ($this->database->enable_ssl) {
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
}
if (! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf)) {
$this->commands[] = "chown 999:999 $this->configuration_dir/redis.conf";
}
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";

View file

@ -25,7 +25,7 @@ class CheckForcePasswordReset
}
$force_password_reset = auth()->user()->force_password_reset;
if ($force_password_reset) {
if ($request->routeIs('auth.force-password-reset') || $request->path() === 'force-password-reset' || $request->path() === 'livewire/update' || $request->path() === 'logout') {
if ($request->routeIs('auth.force-password-reset') || $request->path() === 'force-password-reset' || $request->path() === 'two-factor-challenge' || $request->path() === 'livewire/update' || $request->path() === 'logout') {
return $next($request);
}

View file

@ -171,6 +171,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private bool $dockerBuildkitSupported = false;
private bool $dockerSecretsSupported = false;
private bool $skip_build = false;
private Collection|string $build_secrets;
@ -381,13 +383,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function detectBuildKitCapabilities(): void
{
// If build secrets are not enabled, skip detection and use traditional args
if (! $this->application->settings->use_build_secrets) {
$this->dockerBuildkitSupported = false;
return;
}
$serverToCheck = $this->use_build_server ? $this->build_server : $this->server;
$serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})";
@ -403,53 +398,55 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($majorVersion < 18 || ($majorVersion == 18 && $minorVersion < 9)) {
$this->dockerBuildkitSupported = false;
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+). Build secrets feature disabled.");
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+).");
return;
}
$buildkitEnabled = instant_remote_process(
// Check buildx availability (always installed by Coolify on Docker 24.0+)
$buildxAvailable = instant_remote_process(
["docker buildx version >/dev/null 2>&1 && echo 'available' || echo 'not-available'"],
$serverToCheck
);
if (trim($buildkitEnabled) !== 'available') {
if (trim($buildxAvailable) === 'available') {
$this->dockerBuildkitSupported = true;
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit and Buildx detected on {$serverName}.");
} else {
// Fallback: test DOCKER_BUILDKIT=1 support via --progress flag
$buildkitTest = instant_remote_process(
["DOCKER_BUILDKIT=1 docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"],
["DOCKER_BUILDKIT=1 docker build --help 2>&1 | grep -q '\\-\\-progress' && echo 'supported' || echo 'not-supported'"],
$serverToCheck
);
if (trim($buildkitTest) === 'supported') {
$this->dockerBuildkitSupported = true;
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit secrets support detected on {$serverName}.");
$this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.');
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit support detected on {$serverName}.");
} else {
$this->dockerBuildkitSupported = false;
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not have BuildKit secrets support.");
$this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but not supported. Using traditional build arguments.');
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit. Build output progress will be limited.");
}
} else {
// Buildx is available, which means BuildKit is available
// Now specifically test for secrets support
}
// If build secrets are enabled and BuildKit is available, verify --secret flag support
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported) {
$secretsTest = instant_remote_process(
["docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"],
$serverToCheck
);
if (trim($secretsTest) === 'supported') {
$this->dockerBuildkitSupported = true;
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit and Buildx detected on {$serverName}.");
$this->dockerSecretsSupported = true;
$this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.');
} else {
$this->dockerBuildkitSupported = false;
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with Buildx on {$serverName}, but secrets not supported.");
$this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but not supported. Using traditional build arguments.');
$this->dockerSecretsSupported = false;
$this->application_deployment_queue->addLogEntry("Docker on {$serverName} does not support build secrets. Using traditional build arguments.");
}
}
} catch (\Exception $e) {
$this->dockerBuildkitSupported = false;
$this->dockerSecretsSupported = false;
$this->application_deployment_queue->addLogEntry("Could not detect BuildKit capabilities on {$serverName}: {$e->getMessage()}");
$this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but detection failed. Using traditional build arguments.');
}
}
@ -632,7 +629,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// For raw compose, we cannot automatically add secrets configuration
// User must define it manually in their docker-compose file
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
if ($this->dockerSecretsSupported && ! empty($this->build_secrets)) {
$this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.');
}
} else {
@ -653,7 +650,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
// Add build secrets to compose file if enabled and BuildKit is supported
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
if ($this->dockerSecretsSupported && ! empty($this->build_secrets)) {
$composeFile = $this->add_build_secrets_to_compose($composeFile);
}
@ -2817,7 +2814,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$nginx_config = base64_encode(defaultNginxConfiguration());
}
}
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile --progress plain -t {$this->production_image_name} {$this->workdir}";
if ($this->dockerBuildkitSupported) {
$build_command = "DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile --progress plain -t {$this->production_image_name} {$this->workdir}";
} else {
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile -t {$this->production_image_name} {$this->workdir}";
}
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
@ -2857,21 +2858,19 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function build_image()
{
// Add Coolify related variables to the build args/secrets
if ($this->dockerBuildkitSupported) {
// Coolify variables are already included in the secrets from generate_build_env_variables
// build_secrets is already a string at this point
} else {
if (! $this->dockerBuildkitSupported) {
// Traditional build args approach - generate COOLIFY_ variables locally
// Generate COOLIFY_ variables locally for build args
$coolify_envs = $this->generate_coolify_env_variables(forBuildTime: true);
$coolify_envs->each(function ($value, $key) {
$this->build_args->push("--build-arg '{$key}'");
});
$this->build_args = $this->build_args instanceof \Illuminate\Support\Collection
? $this->build_args->implode(' ')
: (string) $this->build_args;
}
// Always convert build_args Collection to string for command interpolation
$this->build_args = $this->build_args instanceof \Illuminate\Support\Collection
? $this->build_args->implode(' ')
: (string) $this->build_args;
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if ($this->disableBuildCache) {
$this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
@ -2899,7 +2898,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
'hidden' => true,
]);
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
if ($this->dockerSecretsSupported) {
// Modify the nixpacks Dockerfile to use build secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
@ -2907,9 +2906,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} elseif ($this->dockerBuildkitSupported) {
// BuildKit without secrets
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
ray($build_command);
} else {
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
}
} else {
$this->execute_remote_command([
@ -2919,18 +2917,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
'hidden' => true,
]);
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
if ($this->dockerSecretsSupported) {
// Modify the nixpacks Dockerfile to use build secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->workdir}");
} elseif ($this->dockerBuildkitSupported) {
// BuildKit without secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
}
}
@ -2952,7 +2948,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
} else {
// Dockerfile buildpack
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
if ($this->dockerSecretsSupported) {
// Modify the Dockerfile to use build secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
@ -2963,19 +2959,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
} elseif ($this->dockerBuildkitSupported) {
// BuildKit without secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
if ($this->force_rebuild) {
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
}
} else {
// Traditional build with args
if ($this->force_rebuild) {
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
}
}
$base64_build_command = base64_encode($build_command);
@ -3010,7 +3004,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$nginx_config = base64_encode(defaultNginxConfiguration());
}
}
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
if ($this->dockerBuildkitSupported) {
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
}
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
@ -3035,7 +3033,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} else {
// Pure Dockerfile based deployment
if ($this->application->dockerfile) {
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
if ($this->dockerSecretsSupported) {
// Modify the Dockerfile to use build secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
@ -3044,12 +3042,19 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} else {
$build_command = "DOCKER_BUILDKIT=1 docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}";
}
} else {
// Traditional build with args
} elseif ($this->dockerBuildkitSupported) {
// BuildKit without secrets
if ($this->force_rebuild) {
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
}
} else {
// Traditional build with args (no --progress for legacy builder compatibility)
if ($this->force_rebuild) {
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
}
}
$base64_build_command = base64_encode($build_command);
@ -3079,18 +3084,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
'hidden' => true,
]);
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
if ($this->dockerSecretsSupported) {
// Modify the nixpacks Dockerfile to use build secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}");
} elseif ($this->dockerBuildkitSupported) {
// BuildKit without secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
}
} else {
$this->execute_remote_command([
@ -3100,18 +3103,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
'hidden' => true,
]);
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
if ($this->dockerSecretsSupported) {
// Modify the nixpacks Dockerfile to use build secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}");
} elseif ($this->dockerBuildkitSupported) {
// BuildKit without secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
}
}
$base64_build_command = base64_encode($build_command);
@ -3132,7 +3133,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
} else {
// Dockerfile buildpack
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
if ($this->dockerSecretsSupported) {
// Modify the Dockerfile to use build secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
// Use BuildKit with secrets
@ -3144,19 +3145,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
} elseif ($this->dockerBuildkitSupported) {
// BuildKit without secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
if ($this->force_rebuild) {
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
}
} else {
// Traditional build with args
if ($this->force_rebuild) {
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
}
}
$base64_build_command = base64_encode($build_command);
@ -3332,7 +3331,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->analyzeBuildTimeVariables($variables);
}
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
if ($this->dockerSecretsSupported) {
$this->generate_build_secrets($variables);
$this->build_args = '';
} else {
@ -3819,7 +3818,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry("Service {$serviceName}: All required ARG declarations already exist.");
}
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
if ($this->dockerSecretsSupported && ! empty($this->build_secrets)) {
$fullDockerfilePath = "{$this->workdir}/{$dockerfilePath}";
$this->modify_dockerfile_for_secrets($fullDockerfilePath);
$this->application_deployment_queue->addLogEntry("Modified Dockerfile for service {$serviceName} to use build secrets.");

View file

@ -104,7 +104,7 @@ class ScheduledJobManager implements ShouldQueue
Log::channel('scheduled')->info('ScheduledJobManager completed', [
'execution_time' => $this->executionTime->toIso8601String(),
'duration_ms' => Carbon::now()->diffInMilliseconds($this->executionTime),
'duration_ms' => $this->executionTime->diffInMilliseconds(Carbon::now()),
'dispatched' => $this->dispatchedCount,
'skipped' => $this->skippedCount,
]);

View file

@ -990,7 +990,7 @@ class Application extends BaseModel
if (isDev() && data_get($this, 'private_key_id') === 0) {
return 'deploy_key';
}
if (data_get($this, 'private_key_id')) {
if (! is_null(data_get($this, 'private_key_id'))) {
return 'deploy_key';
} elseif (data_get($this, 'source')) {
return 'source';

View file

@ -88,7 +88,7 @@ class ServiceApplication extends BaseModel
public function team()
{
return data_get($this, 'environment.project.team');
return data_get($this, 'service.environment.project.team');
}
public function workdir()

View file

@ -124,7 +124,7 @@ class ServiceDatabase extends BaseModel
public function team()
{
return data_get($this, 'environment.project.team');
return data_get($this, 'service.environment.project.team');
}
public function workdir()

View file

@ -191,7 +191,8 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
$this->getNotificationSettings('discord')?->isEnabled() ||
$this->getNotificationSettings('slack')?->isEnabled() ||
$this->getNotificationSettings('telegram')?->isEnabled() ||
$this->getNotificationSettings('pushover')?->isEnabled();
$this->getNotificationSettings('pushover')?->isEnabled() ||
$this->getNotificationSettings('webhook')?->isEnabled();
}
public function subscriptionEnded()

View file

@ -77,6 +77,7 @@ function allowedPathsForUnsubscribedAccounts()
'login',
'logout',
'force-password-reset',
'two-factor-challenge',
'livewire/update',
'admin',
];
@ -95,6 +96,7 @@ function allowedPathsForInvalidAccounts()
'logout',
'verify',
'force-password-reset',
'two-factor-challenge',
'livewire/update',
];
}

View file

@ -2,7 +2,7 @@
return [
'coolify' => [
'version' => '4.0.0-beta.463',
'version' => '4.0.0-beta.464',
'helper_version' => '1.0.12',
'realtime_version' => '1.0.10',
'self_hosted' => env('SELF_HOSTED', true),

View file

@ -184,13 +184,13 @@ return [
'connection' => 'redis',
'balance' => env('HORIZON_BALANCE', 'false'),
'queue' => env('HORIZON_QUEUES', 'high,default'),
'maxTime' => 3600,
'maxTime' => env('HORIZON_MAX_TIME', 0),
'maxJobs' => 400,
'memory' => 128,
'tries' => 1,
'nice' => 0,
'sleep' => 3,
'timeout' => 3600,
'timeout' => env('HORIZON_TIMEOUT', 36000),
],
],

View file

@ -0,0 +1,209 @@
services:
coolify:
image: coolify:dev
pull_policy: never
build:
context: .
dockerfile: ./docker/development/Dockerfile
args:
- USER_ID=${USERID:-1000}
- GROUP_ID=${GROUPID:-1000}
ports:
- "${APP_PORT:-8000}:8080"
environment:
AUTORUN_ENABLED: false
PUSHER_HOST: "${PUSHER_HOST}"
PUSHER_PORT: "${PUSHER_PORT}"
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
PUSHER_APP_ID: "${PUSHER_APP_ID:-coolify}"
PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
PUSHER_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
healthcheck:
test: curl -sf http://127.0.0.1:8080/api/health || exit 1
interval: 5s
retries: 10
timeout: 2s
volumes:
- .:/var/www/html/:cached
- dev_backups_data:/var/www/html/storage/app/backups
networks:
- coolify
postgres:
pull_policy: always
ports:
- "${FORWARD_DB_PORT:-5432}:5432"
env_file:
- .env
environment:
POSTGRES_USER: "${DB_USERNAME:-coolify}"
POSTGRES_PASSWORD: "${DB_PASSWORD:-password}"
POSTGRES_DB: "${DB_DATABASE:-coolify}"
POSTGRES_HOST_AUTH_METHOD: "trust"
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ]
interval: 5s
retries: 10
timeout: 2s
volumes:
- dev_postgres_data:/var/lib/postgresql/data
redis:
pull_policy: always
ports:
- "${FORWARD_REDIS_PORT:-6379}:6379"
env_file:
- .env
healthcheck:
test: redis-cli ping
interval: 5s
retries: 10
timeout: 2s
volumes:
- dev_redis_data:/data
soketi:
image: coolify-realtime:dev
pull_policy: never
build:
context: .
dockerfile: ./docker/coolify-realtime/Dockerfile
env_file:
- .env
ports:
- "${FORWARD_SOKETI_PORT:-6001}:6001"
- "6002:6002"
volumes:
- ./storage:/var/www/html/storage
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
environment:
SOKETI_DEBUG: "false"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
healthcheck:
test: [ "CMD-SHELL", "curl -fsS http://127.0.0.1:6001/ready && curl -fsS http://127.0.0.1:6002/ready || exit 1" ]
interval: 5s
retries: 10
timeout: 2s
entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"]
vite:
image: node:24-alpine
pull_policy: always
container_name: coolify-vite
working_dir: /var/www/html
environment:
VITE_HOST: "${VITE_HOST:-localhost}"
VITE_PORT: "${VITE_PORT:-5173}"
ports:
- "${VITE_PORT:-5173}:${VITE_PORT:-5173}"
volumes:
- .:/var/www/html/:cached
command: sh -c "npm install && npm run dev"
networks:
- coolify
testing-host:
image: coolify-testing-host:dev
pull_policy: never
build:
context: .
dockerfile: ./docker/testing-host/Dockerfile
init: true
container_name: coolify-testing-host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- dev_coolify_data:/data/coolify
- dev_backups_data:/data/coolify/backups
- dev_postgres_data:/data/coolify/_volumes/database
- dev_redis_data:/data/coolify/_volumes/redis
- dev_minio_data:/data/coolify/_volumes/minio
networks:
- coolify
mailpit:
image: axllent/mailpit:latest
pull_policy: always
container_name: coolify-mail
ports:
- "${FORWARD_MAILPIT_PORT:-1025}:1025"
- "${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025"
networks:
- coolify
# maxio:
# image: ghcr.io/coollabsio/maxio
# pull_policy: always
# container_name: coolify-maxio
# ports:
# - "${FORWARD_MAXIO_PORT:-9000}:9000"
# environment:
# MAXIO_ACCESS_KEY: "${MAXIO_ACCESS_KEY:-maxioadmin}"
# MAXIO_SECRET_KEY: "${MAXIO_SECRET_KEY:-maxioadmin}"
# volumes:
# - dev_maxio_data:/data
# networks:
# - coolify
minio:
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z # Released on 15 October 2025
pull_policy: always
container_name: coolify-minio
command: server /data --console-address ":9001"
ports:
- "${FORWARD_MINIO_PORT:-9000}:9000"
- "${FORWARD_MINIO_PORT_CONSOLE:-9001}:9001"
environment:
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
volumes:
- dev_minio_data:/data
- dev_maxio_data:/data
networks:
- coolify
# maxio-init:
# image: minio/mc:latest
# pull_policy: always
# container_name: coolify-maxio-init
# restart: no
# depends_on:
# - maxio
# entrypoint: >
# /bin/sh -c "
# echo 'Waiting for MaxIO to be ready...';
# until mc alias set local http://coolify-maxio:9000 maxioadmin maxioadmin 2>/dev/null; do
# echo 'MaxIO not ready yet, waiting...';
# sleep 2;
# done;
# echo 'MaxIO is ready, creating bucket if needed...';
# mc mb local/local --ignore-existing;
# echo 'MaxIO initialization complete - bucket local is ready';
# "
# networks:
# - coolify
minio-init:
image: minio/mc:latest
pull_policy: always
container_name: coolify-minio-init
restart: no
depends_on:
- minio
entrypoint: >
/bin/sh -c "
echo 'Waiting for MinIO to be ready...';
until mc alias set local http://coolify-minio:9000 minioadmin minioadmin 2>/dev/null; do
echo 'MinIO not ready yet, waiting...';
sleep 2;
done;
echo 'MinIO is ready, creating bucket if needed...';
mc mb local/local --ignore-existing;
echo 'MinIO initialization complete - bucket local is ready';
"
networks:
- coolify
volumes:
dev_backups_data:
dev_postgres_data:
dev_redis_data:
dev_coolify_data:
dev_minio_data:
dev_maxio_data:
networks:
coolify:
name: coolify
external: false

View file

@ -0,0 +1,4 @@
<svg width="300" height="260" viewBox="0 0 300 260" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M126.822 31.3968C127.716 30.5005 128.923 30 130.176 30H205.093L98.2708 136.943C97.3776 137.84 96.1702 138.339 94.9168 138.339H20L126.822 31.3968Z" fill="#D0BCFF"/>
<path d="M113.684 140.714C113.684 139.399 114.752 138.328 116.063 138.328H205.093L113.684 230V140.714Z" fill="#D0BCFF"/>
</svg>

After

Width:  |  Height:  |  Size: 401 B

View file

@ -25,7 +25,7 @@
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
</svg>
{{-- Eye-off icon (shown when password is visible) --}}
<svg x-show="type === 'text'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
<svg x-cloak x-show="type === 'text'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />

View file

@ -3,15 +3,11 @@
<div>
<p class="font-mono font-semibold text-7xl dark:text-warning">419</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">This page is definitely old, not like you!</h1>
<p class="text-base leading-7 dark:text-neutral-300 text-black">Sorry, we couldn't find the page you're looking
for.
<p class="text-base leading-7 dark:text-neutral-300 text-black">Your session has expired. Please log in again to continue.
</p>
<div class="flex items-center mt-10 gap-x-2">
<a href="{{ url()->previous() }}">
<x-forms.button>Go back</x-forms.button>
</a>
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
<x-forms.button>Dashboard</x-forms.button>
<a href="/login">
<x-forms.button>Back to Login</x-forms.button>
</a>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support

View file

@ -1,7 +1,7 @@
<nav wire:poll.10000ms="checkStatus" class="pb-6">
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
<div class="navbar-main">
<nav class="flex shrink-0 gap-4 items-center whitespace-nowrap scrollbar min-h-10">
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.application.configuration', $parameters) }}">
Configuration

View file

@ -9,7 +9,7 @@
<h1>{{ $title }}</h1>
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
<div class="navbar-main" x-data">
<nav class="flex shrink-0 gap-4 items-center whitespace-nowrap scrollbar min-h-10">
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button>

View file

@ -55,7 +55,7 @@ Route::group([
Route::post('/projects/{uuid}/environments', [ProjectController::class, 'create_environment'])->middleware(['api.ability:write']);
Route::delete('/projects/{uuid}/environments/{environment_name_or_uuid}', [ProjectController::class, 'delete_environment'])->middleware(['api.ability:write']);
Route::post('/projects', [ProjectController::class, 'create_project'])->middleware(['api.ability:read']);
Route::post('/projects', [ProjectController::class, 'create_project'])->middleware(['api.ability:write']);
Route::patch('/projects/{uuid}', [ProjectController::class, 'update_project'])->middleware(['api.ability:write']);
Route::delete('/projects/{uuid}', [ProjectController::class, 'delete_project'])->middleware(['api.ability:write']);
@ -86,7 +86,7 @@ Route::group([
Route::get('/servers/{uuid}/validate', [ServersController::class, 'validate_server'])->middleware(['api.ability:read']);
Route::post('/servers', [ServersController::class, 'create_server'])->middleware(['api.ability:read']);
Route::post('/servers', [ServersController::class, 'create_server'])->middleware(['api.ability:write']);
Route::patch('/servers/{uuid}', [ServersController::class, 'update_server'])->middleware(['api.ability:write']);
Route::delete('/servers/{uuid}', [ServersController::class, 'delete_server'])->middleware(['api.ability:write']);

View file

@ -6,13 +6,26 @@
services:
beszel-agent:
image: 'henrygd/beszel-agent:0.16.1' # Released on 14 Nov 2025
image: 'henrygd/beszel-agent:0.18.4' # Released on 21 Feb 2026
network_mode: host # Network stats graphs won't work if agent cannot access host system network stack
environment:
# Required
- LISTEN=/beszel_socket/beszel.sock
- HUB_URL=${HUB_URL?}
- 'TOKEN=${TOKEN?}'
- 'KEY=${KEY?}'
- HUB_URL=$SERVICE_URL_BESZEL
- TOKEN=${TOKEN} # From hub token settings
- KEY=${KEY} # SSH public key(s) from hub
# Optional
- DISABLE_SSH=${DISABLE_SSH:-false} # Disable SSH
- LOG_LEVEL=${LOG_LEVEL:-warn} # Logging level
- SKIP_GPU=${SKIP_GPU:-false} # Skip GPU monitoring
- SYSTEM_NAME=${SYSTEM_NAME} # Custom system name
volumes:
- beszel_agent_data:/var/lib/beszel-agent
- beszel_socket:/beszel_socket
- '/var/run/docker.sock:/var/run/docker.sock:ro'
healthcheck:
test: ['CMD', '/agent', 'health']
interval: 60s
timeout: 20s
retries: 10
start_period: 5s

View file

@ -9,21 +9,41 @@
# Add the public Key in "Key" env variable and token in the "Token" variable below (These are obtained from Beszel UI)
services:
beszel:
image: 'henrygd/beszel:0.16.1' # Released on 14 Nov 2025
image: 'henrygd/beszel:0.18.4' # Released on 21 Feb 2026
environment:
- SERVICE_URL_BESZEL_8090
- CONTAINER_DETAILS=${CONTAINER_DETAILS:-true}
- SHARE_ALL_SYSTEMS=${SHARE_ALL_SYSTEMS:-false}
volumes:
- 'beszel_data:/beszel_data'
- 'beszel_socket:/beszel_socket'
healthcheck:
test: ['CMD', '/beszel', 'health', '--url', 'http://localhost:8090']
interval: 30s
timeout: 20s
retries: 10
start_period: 5s
beszel-agent:
image: 'henrygd/beszel-agent:0.16.1' # Released on 14 Nov 2025
image: 'henrygd/beszel-agent:0.18.4' # Released on 21 Feb 2026
network_mode: host # Network stats graphs won't work if agent cannot access host system network stack
environment:
# Required
- LISTEN=/beszel_socket/beszel.sock
- HUB_URL=http://beszel:8090
- 'TOKEN=${TOKEN}'
- 'KEY=${KEY}'
- HUB_URL=$SERVICE_URL_BESZEL
- TOKEN=${TOKEN} # From hub token settings
- KEY=${KEY} # SSH public key(s) from hub
# Optional
- DISABLE_SSH=${DISABLE_SSH:-false} # Disable SSH
- LOG_LEVEL=${LOG_LEVEL:-warn} # Logging level
- SKIP_GPU=${SKIP_GPU:-false} # Skip GPU monitoring
- SYSTEM_NAME=${SYSTEM_NAME} # Custom system name
volumes:
- beszel_agent_data:/var/lib/beszel-agent
- beszel_socket:/beszel_socket
- '/var/run/docker.sock:/var/run/docker.sock:ro'
healthcheck:
test: ['CMD', '/agent', 'health']
interval: 60s
timeout: 20s
retries: 10
start_period: 5s

View file

@ -0,0 +1,27 @@
# documentation: https://electric-sql.com/docs/guides/deployment
# slogan: Sync shape-based subsets of your Postgres data over HTTP.
# category: backend
# tags: electric,electricsql,realtime,sync,postgresql
# logo: svgs/electricsql.svg
# port: 3000
## This template intentionally does not deploy PostgreSQL.
## Set DATABASE_URL to an existing Postgres instance with logical replication enabled.
## If ELECTRIC_SECRET is set, your own backend/proxy must append it to shape requests.
services:
electric:
image: electricsql/electric:1.4.2
environment:
- SERVICE_URL_ELECTRIC_3000
- DATABASE_URL=${DATABASE_URL:?}
- ELECTRIC_SECRET=${SERVICE_PASSWORD_64_ELECTRIC}
- ELECTRIC_STORAGE_DIR=/app/persistent
- ELECTRIC_USAGE_REPORTING=${ELECTRIC_USAGE_REPORTING:-false}
volumes:
- electric_data:/app/persistent
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/v1/health"]
interval: 10s
timeout: 5s
retries: 5

View file

@ -1,3 +1,4 @@
# ignore: true
# documentation: https://docs.plane.so/self-hosting/methods/docker-compose
# slogan: The open source project management tool
# category: productivity

View file

@ -1,3 +1,4 @@
# ignore: true
# documentation: https://pterodactyl.io/
# slogan: Pterodactyl is a free, open-source game server management panel
# category: media
@ -102,4 +103,4 @@ services:
- MAIL_PORT=$MAIL_PORT
- MAIL_USERNAME=$MAIL_USERNAME
- MAIL_PASSWORD=$MAIL_PASSWORD
- MAIL_ENCRYPTION=$MAIL_ENCRYPTION
- MAIL_ENCRYPTION=$MAIL_ENCRYPTION

View file

@ -1,3 +1,4 @@
# ignore: true
# documentation: https://pterodactyl.io/
# slogan: Pterodactyl is a free, open-source game server management panel
# category: media

View file

@ -0,0 +1,75 @@
<?php
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
session(['currentTeam' => $this->team]);
});
describe('POST /api/v1/projects', function () {
test('read-only token cannot create a project', function () {
$token = $this->user->createToken('read-only', ['read']);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token->plainTextToken,
'Content-Type' => 'application/json',
])->postJson('/api/v1/projects', [
'name' => 'Test Project',
]);
$response->assertStatus(403);
});
test('write token can create a project', function () {
$token = $this->user->createToken('write-token', ['write']);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token->plainTextToken,
'Content-Type' => 'application/json',
])->postJson('/api/v1/projects', [
'name' => 'Test Project',
]);
$response->assertStatus(201);
$response->assertJsonStructure(['uuid']);
});
test('root token can create a project', function () {
$token = $this->user->createToken('root-token', ['root']);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token->plainTextToken,
'Content-Type' => 'application/json',
])->postJson('/api/v1/projects', [
'name' => 'Test Project',
]);
$response->assertStatus(201);
$response->assertJsonStructure(['uuid']);
});
});
describe('POST /api/v1/servers', function () {
test('read-only token cannot create a server', function () {
$token = $this->user->createToken('read-only', ['read']);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token->plainTextToken,
'Content-Type' => 'application/json',
])->postJson('/api/v1/servers', [
'name' => 'Test Server',
'ip' => '1.2.3.4',
'private_key_uuid' => 'fake-uuid',
]);
$response->assertStatus(403);
});
});

View file

@ -1,144 +1,59 @@
<?php
test('multiline environment variables are properly escaped for docker build args', function () {
$sshKey = '-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
-----END OPENSSH PRIVATE KEY-----';
test('generateDockerBuildArgs returns only keys without values', function () {
$variables = [
['key' => 'SSH_PRIVATE_KEY', 'value' => "'{$sshKey}'", 'is_multiline' => true],
['key' => 'SSH_PRIVATE_KEY', 'value' => "'some-ssh-key'", 'is_multiline' => true],
['key' => 'REGULAR_VAR', 'value' => 'simple value', 'is_multiline' => false],
];
$buildArgs = generateDockerBuildArgs($variables);
// SSH key should use double quotes and have proper escaping
$sshArg = $buildArgs->first();
expect($sshArg)->toStartWith('--build-arg SSH_PRIVATE_KEY="');
expect($sshArg)->toEndWith('"');
expect($sshArg)->toContain('BEGIN OPENSSH PRIVATE KEY');
expect($sshArg)->not->toContain("'BEGIN"); // Should not have the wrapper single quotes
// Regular var should use escapeshellarg (single quotes)
$regularArg = $buildArgs->last();
expect($regularArg)->toBe("--build-arg REGULAR_VAR='simple value'");
// Docker gets values from the environment, so only keys should be in build args
expect($buildArgs->first())->toBe('--build-arg SSH_PRIVATE_KEY');
expect($buildArgs->last())->toBe('--build-arg REGULAR_VAR');
});
test('multiline variables with special bash characters are escaped correctly', function () {
$valueWithSpecialChars = "line1\nline2 with \"quotes\"\nline3 with \$variables\nline4 with `backticks`";
test('generateDockerBuildArgs works with collection of objects', function () {
$variables = collect([
(object) ['key' => 'VAR1', 'value' => 'value1', 'is_multiline' => false],
(object) ['key' => 'VAR2', 'value' => "'multiline\nvalue'", 'is_multiline' => true],
]);
$buildArgs = generateDockerBuildArgs($variables);
expect($buildArgs)->toHaveCount(2);
expect($buildArgs->values()->toArray())->toBe([
'--build-arg VAR1',
'--build-arg VAR2',
]);
});
test('generateDockerBuildArgs collection can be imploded into valid command string', function () {
$variables = [
['key' => 'SPECIAL_VALUE', 'value' => "'{$valueWithSpecialChars}'", 'is_multiline' => true],
['key' => 'COOLIFY_URL', 'value' => 'http://example.com', 'is_multiline' => false],
['key' => 'COOLIFY_BRANCH', 'value' => 'main', 'is_multiline' => false],
];
$buildArgs = generateDockerBuildArgs($variables);
// The collection must be imploded to a string for command interpolation
// This was the bug: Collection was interpolated as JSON instead of a space-separated string
$argsString = $buildArgs->implode(' ');
expect($argsString)->toBe('--build-arg COOLIFY_URL --build-arg COOLIFY_BRANCH');
// Verify it does NOT produce JSON when cast to string
expect($argsString)->not->toContain('{');
expect($argsString)->not->toContain('}');
});
test('generateDockerBuildArgs handles variables without is_multiline', function () {
$variables = [
['key' => 'NO_FLAG_VAR', 'value' => 'some value'],
];
$buildArgs = generateDockerBuildArgs($variables);
$arg = $buildArgs->first();
// Verify double quotes are escaped
expect($arg)->toContain('\\"quotes\\"');
// Verify dollar signs are escaped
expect($arg)->toContain('\\$variables');
// Verify backticks are escaped
expect($arg)->toContain('\\`backticks\\`');
});
test('single-line environment variables use escapeshellarg', function () {
$variables = [
['key' => 'SIMPLE_VAR', 'value' => 'simple value with spaces', 'is_multiline' => false],
];
$buildArgs = generateDockerBuildArgs($variables);
$arg = $buildArgs->first();
// Should use single quotes from escapeshellarg
expect($arg)->toBe("--build-arg SIMPLE_VAR='simple value with spaces'");
});
test('multiline certificate with newlines is preserved', function () {
$certificate = '-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKL0UG+mRkSvMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTkwOTE3MDUzMzI5WhcNMjkwOTE0MDUzMzI5WjBF
-----END CERTIFICATE-----';
$variables = [
['key' => 'TLS_CERT', 'value' => "'{$certificate}'", 'is_multiline' => true],
];
$buildArgs = generateDockerBuildArgs($variables);
$arg = $buildArgs->first();
// Newlines should be preserved in the output
expect($arg)->toContain("\n");
expect($arg)->toContain('BEGIN CERTIFICATE');
expect($arg)->toContain('END CERTIFICATE');
expect(substr_count($arg, "\n"))->toBeGreaterThan(0);
});
test('multiline JSON configuration is properly escaped', function () {
$jsonConfig = '{
"key": "value",
"nested": {
"array": [1, 2, 3]
}
}';
$variables = [
['key' => 'JSON_CONFIG', 'value' => "'{$jsonConfig}'", 'is_multiline' => true],
];
$buildArgs = generateDockerBuildArgs($variables);
$arg = $buildArgs->first();
// All double quotes in JSON should be escaped
expect($arg)->toContain('\\"key\\"');
expect($arg)->toContain('\\"value\\"');
expect($arg)->toContain('\\"nested\\"');
});
test('empty multiline variable is handled correctly', function () {
$variables = [
['key' => 'EMPTY_VAR', 'value' => "''", 'is_multiline' => true],
];
$buildArgs = generateDockerBuildArgs($variables);
$arg = $buildArgs->first();
expect($arg)->toBe('--build-arg EMPTY_VAR=""');
});
test('multiline variable with only newlines', function () {
$onlyNewlines = "\n\n\n";
$variables = [
['key' => 'NEWLINES_ONLY', 'value' => "'{$onlyNewlines}'", 'is_multiline' => true],
];
$buildArgs = generateDockerBuildArgs($variables);
$arg = $buildArgs->first();
expect($arg)->toContain("\n");
// Should have 3 newlines preserved
expect(substr_count($arg, "\n"))->toBe(3);
});
test('multiline variable with backslashes is escaped correctly', function () {
$valueWithBackslashes = "path\\to\\file\nC:\\Windows\\System32";
$variables = [
['key' => 'PATH_VAR', 'value' => "'{$valueWithBackslashes}'", 'is_multiline' => true],
];
$buildArgs = generateDockerBuildArgs($variables);
$arg = $buildArgs->first();
// Backslashes should be doubled
expect($arg)->toContain('path\\\\to\\\\file');
expect($arg)->toContain('C:\\\\Windows\\\\System32');
expect($arg)->toBe('--build-arg NO_FLAG_VAR');
});
test('generateDockerEnvFlags produces correct format', function () {
@ -155,54 +70,14 @@ test('generateDockerEnvFlags produces correct format', function () {
expect($envFlags)->toContain('line2');
});
test('helper functions work with collection input', function () {
test('generateDockerEnvFlags works with collection input', function () {
$variables = collect([
(object) ['key' => 'VAR1', 'value' => 'value1', 'is_multiline' => false],
(object) ['key' => 'VAR2', 'value' => "'multiline\nvalue'", 'is_multiline' => true],
]);
$buildArgs = generateDockerBuildArgs($variables);
expect($buildArgs)->toHaveCount(2);
$envFlags = generateDockerEnvFlags($variables);
expect($envFlags)->toBeString();
expect($envFlags)->toContain('-e VAR1=');
expect($envFlags)->toContain('-e VAR2="');
});
test('variables without is_multiline default to false', function () {
$variables = [
['key' => 'NO_FLAG_VAR', 'value' => 'some value'],
];
$buildArgs = generateDockerBuildArgs($variables);
$arg = $buildArgs->first();
// Should use escapeshellarg (single quotes) since is_multiline defaults to false
expect($arg)->toBe("--build-arg NO_FLAG_VAR='some value'");
});
test('real world SSH key example', function () {
// Simulate what real_value returns (wrapped in single quotes)
$sshKey = "'-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
-----END OPENSSH PRIVATE KEY-----'";
$variables = [
['key' => 'KEY', 'value' => $sshKey, 'is_multiline' => true],
];
$buildArgs = generateDockerBuildArgs($variables);
$arg = $buildArgs->first();
// Should produce clean output without wrapper quotes
expect($arg)->toStartWith('--build-arg KEY="-----BEGIN OPENSSH PRIVATE KEY-----');
expect($arg)->toEndWith('-----END OPENSSH PRIVATE KEY-----"');
// Should NOT have the escaped quote sequence that was in the bug
expect($arg)->not->toContain("''");
expect($arg)->not->toContain("'\\''");
});

View file

@ -0,0 +1,77 @@
<?php
use App\Models\Environment;
use App\Models\Project;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('returns the correct team through the service relationship chain', function () {
$team = Team::factory()->create();
$project = Project::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
'name' => 'Test Project',
'team_id' => $team->id,
]);
$environment = Environment::create([
'name' => 'test-env-'.Illuminate\Support\Str::random(8),
'project_id' => $project->id,
]);
$service = Service::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
'name' => 'supabase',
'environment_id' => $environment->id,
'destination_id' => 1,
'destination_type' => 'App\Models\StandaloneDocker',
'docker_compose_raw' => 'version: "3"',
]);
$serviceDatabase = ServiceDatabase::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
'name' => 'supabase-db',
'service_id' => $service->id,
]);
expect($serviceDatabase->team())->not->toBeNull()
->and($serviceDatabase->team()->id)->toBe($team->id);
});
it('returns the correct team for ServiceApplication through the service relationship chain', function () {
$team = Team::factory()->create();
$project = Project::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
'name' => 'Test Project',
'team_id' => $team->id,
]);
$environment = Environment::create([
'name' => 'test-env-'.Illuminate\Support\Str::random(8),
'project_id' => $project->id,
]);
$service = Service::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
'name' => 'supabase',
'environment_id' => $environment->id,
'destination_id' => 1,
'destination_type' => 'App\Models\StandaloneDocker',
'docker_compose_raw' => 'version: "3"',
]);
$serviceApplication = ServiceApplication::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
'name' => 'supabase-studio',
'service_id' => $service->id,
]);
expect($serviceApplication->team())->not->toBeNull()
->and($serviceApplication->team()->id)->toBe($team->id);
});

View file

@ -0,0 +1,52 @@
<?php
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->team = Team::factory()->create();
});
describe('isAnyNotificationEnabled', function () {
test('returns false when no notifications are enabled', function () {
expect($this->team->isAnyNotificationEnabled())->toBeFalse();
});
test('returns true when email notifications are enabled', function () {
$this->team->emailNotificationSettings->update(['smtp_enabled' => true]);
expect($this->team->isAnyNotificationEnabled())->toBeTrue();
});
test('returns true when discord notifications are enabled', function () {
$this->team->discordNotificationSettings->update(['discord_enabled' => true]);
expect($this->team->isAnyNotificationEnabled())->toBeTrue();
});
test('returns true when slack notifications are enabled', function () {
$this->team->slackNotificationSettings->update(['slack_enabled' => true]);
expect($this->team->isAnyNotificationEnabled())->toBeTrue();
});
test('returns true when telegram notifications are enabled', function () {
$this->team->telegramNotificationSettings->update(['telegram_enabled' => true]);
expect($this->team->isAnyNotificationEnabled())->toBeTrue();
});
test('returns true when pushover notifications are enabled', function () {
$this->team->pushoverNotificationSettings->update(['pushover_enabled' => true]);
expect($this->team->isAnyNotificationEnabled())->toBeTrue();
});
test('returns true when webhook notifications are enabled', function () {
$this->team->webhookNotificationSettings->update(['webhook_enabled' => true]);
expect($this->team->isAnyNotificationEnabled())->toBeTrue();
});
});

View file

@ -0,0 +1,65 @@
<?php
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->team = Team::factory()->personal()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
session(['currentTeam' => $this->team]);
});
it('allows unauthenticated access to two-factor-challenge page', function () {
$response = $this->get('/two-factor-challenge');
// Fortify returns a redirect to /login if there's no login.id in session,
// but the important thing is it does NOT return a 419 or 500
expect($response->status())->toBeIn([200, 302]);
});
it('includes two-factor-challenge in allowed paths for unsubscribed accounts', function () {
$paths = allowedPathsForUnsubscribedAccounts();
expect($paths)->toContain('two-factor-challenge');
});
it('includes two-factor-challenge in allowed paths for invalid accounts', function () {
$paths = allowedPathsForInvalidAccounts();
expect($paths)->toContain('two-factor-challenge');
});
it('includes two-factor-challenge in allowed paths for boarding accounts', function () {
$paths = allowedPathsForBoardingAccounts();
expect($paths)->toContain('two-factor-challenge');
});
it('does not redirect authenticated user with force_password_reset from two-factor-challenge', function () {
$this->user->update(['force_password_reset' => true]);
$response = $this->actingAs($this->user)->get('/two-factor-challenge');
// Should NOT redirect to force-password-reset page
if ($response->isRedirect()) {
expect($response->headers->get('Location'))->not->toContain('force-password-reset');
}
});
it('renders 419 error page with login link instead of previous url', function () {
$response = $this->get('/two-factor-challenge', [
'X-CSRF-TOKEN' => 'invalid-token',
]);
// The 419 page should exist and contain a link to /login
$view = view('errors.419')->render();
expect($view)->toContain('/login');
expect($view)->toContain('Back to Login');
expect($view)->toContain('This page is definitely old, not like you!');
expect($view)->not->toContain('url()->previous()');
});

View file

@ -0,0 +1,11 @@
<?php
use App\Models\Application;
it('treats zero private key id as deploy key', function () {
$application = new Application();
$application->private_key_id = 0;
$application->source = null;
expect($application->deploymentType())->toBe('deploy_key');
});

View file

@ -0,0 +1,52 @@
<?php
use App\Actions\Database\StartKeydb;
use App\Models\StandaloneKeydb;
test('keydb config chown command is added when keydb_conf is set', function () {
$action = new StartKeydb;
$action->configuration_dir = '/data/coolify/databases/test-uuid';
$action->commands = [];
$database = Mockery::mock(StandaloneKeydb::class)->makePartial();
$database->shouldReceive('getAttribute')->with('keydb_conf')->andReturn('maxmemory 2gb');
$action->database = $database;
if (! is_null($action->database->keydb_conf) && ! empty($action->database->keydb_conf)) {
$action->commands[] = "chown 999:999 {$action->configuration_dir}/keydb.conf";
}
expect($action->commands)->toContain('chown 999:999 /data/coolify/databases/test-uuid/keydb.conf');
});
test('keydb config chown command is not added when keydb_conf is null', function () {
$action = new StartKeydb;
$action->configuration_dir = '/data/coolify/databases/test-uuid';
$action->commands = [];
$database = Mockery::mock(StandaloneKeydb::class)->makePartial();
$database->shouldReceive('getAttribute')->with('keydb_conf')->andReturn(null);
$action->database = $database;
if (! is_null($action->database->keydb_conf) && ! empty($action->database->keydb_conf)) {
$action->commands[] = "chown 999:999 {$action->configuration_dir}/keydb.conf";
}
expect($action->commands)->toBeEmpty();
});
test('keydb config chown command is not added when keydb_conf is empty', function () {
$action = new StartKeydb;
$action->configuration_dir = '/data/coolify/databases/test-uuid';
$action->commands = [];
$database = Mockery::mock(StandaloneKeydb::class)->makePartial();
$database->shouldReceive('getAttribute')->with('keydb_conf')->andReturn('');
$action->database = $database;
if (! is_null($action->database->keydb_conf) && ! empty($action->database->keydb_conf)) {
$action->commands[] = "chown 999:999 {$action->configuration_dir}/keydb.conf";
}
expect($action->commands)->toBeEmpty();
});

View file

@ -0,0 +1,53 @@
<?php
use App\Actions\Database\StartRedis;
use App\Models\StandaloneRedis;
test('redis config chown command is added when redis_conf is set', function () {
$action = new StartRedis;
$action->configuration_dir = '/data/coolify/databases/test-uuid';
$action->commands = [];
$database = Mockery::mock(StandaloneRedis::class)->makePartial();
$database->shouldReceive('getAttribute')->with('redis_conf')->andReturn('maxmemory 2gb');
$action->database = $database;
// Simulate the chown logic from handle()
if (! is_null($action->database->redis_conf) && ! empty($action->database->redis_conf)) {
$action->commands[] = "chown 999:999 {$action->configuration_dir}/redis.conf";
}
expect($action->commands)->toContain('chown 999:999 /data/coolify/databases/test-uuid/redis.conf');
});
test('redis config chown command is not added when redis_conf is null', function () {
$action = new StartRedis;
$action->configuration_dir = '/data/coolify/databases/test-uuid';
$action->commands = [];
$database = Mockery::mock(StandaloneRedis::class)->makePartial();
$database->shouldReceive('getAttribute')->with('redis_conf')->andReturn(null);
$action->database = $database;
if (! is_null($action->database->redis_conf) && ! empty($action->database->redis_conf)) {
$action->commands[] = "chown 999:999 {$action->configuration_dir}/redis.conf";
}
expect($action->commands)->toBeEmpty();
});
test('redis config chown command is not added when redis_conf is empty', function () {
$action = new StartRedis;
$action->configuration_dir = '/data/coolify/databases/test-uuid';
$action->commands = [];
$database = Mockery::mock(StandaloneRedis::class)->makePartial();
$database->shouldReceive('getAttribute')->with('redis_conf')->andReturn('');
$action->database = $database;
if (! is_null($action->database->redis_conf) && ! empty($action->database->redis_conf)) {
$action->commands[] = "chown 999:999 {$action->configuration_dir}/redis.conf";
}
expect($action->commands)->toBeEmpty();
});

View file

@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.463"
"version": "4.0.0-beta.464"
},
"nightly": {
"version": "4.0.0-beta.464"
"version": "4.0.0-beta.465"
},
"helper": {
"version": "1.0.12"