DeleteResourceJob no longer restores a service and rethrows when remote
cleanup fails. The resource is always removed from Coolify, and the team
gets a notification that Docker resources may remain on the server. No
notification is sent when the server itself was already deleted.
removeSubresourceContainer now returns false instead of throwing when the
server is not functional. Deleting a service application or database then
shows a warning that the container is removed on the next service start.
StopService handles a missing destination server, environment, project,
or team without throwing. The danger zone warning now shows for any
resource whose server is unreachable, not only services.
A plain pipe into the compressor only reported the compressor's exit
status, so a failed dump still produced a small, valid archive. Add
pipeToFileKeepingExitStatus() to propagate the source's exit status in
POSIX sh and use it for all compressed dump commands.
- Delete the partial local backup file on any failed backup, not only
for ClickHouse, since untracked files are never removed by retention
- Wait up to 10s for locks when running VACUUM INTO for SQLite backups
- Refuse SQLite restores unless the file has a SQLite header, so an
empty backup no longer wipes the database
- Add tests for dump exit status handling and SQLite restore validation
Numeric coolify.{type}Id labels can collide with resources on another
Coolify instance managing the same server, so they no longer select or
resolve container owners. Drop the legacyId fallback from container
lookups in DeleteService and ComplexStatusCheck.
Legacy application containers without a UUID label are now resolved by
their compose service name, which starts with the application UUID.
Socialite's GitLab provider fetches the user from /api/v3/user, which
GitLab removed in 11.0 (410 Gone). Add App\Auth\GitlabProvider that
reads /api/v4/user with a bearer token and register it in the
socialite helper, for both gitlab.com and self-hosted base URLs.
Environment variable copyValue now checks the 'update' policy on the
variable instead of the member role of the current team, so members
can no longer copy values after switching to a team they own. Remove
the redundant member check from ShowHardcoded.
Add tests for the GitLab v4 user lookup and the team-switch copy case.
Before OAuth identities existed, OAuth sign-in matched users by email
only. Add a created_before_oauth_identities flag, set for every user at
upgrade time, so their first OAuth identity links without a provider
email verification claim. The flag is cleared once an identity links.
Also trust Microsoft Entra ID user principal names and GitLab confirmed
primary emails as verified, and pass the email to hasVerifiedEmail.
Add tests using real provider payloads.
A non-root SSH user cannot open redirects, expand globs, or cd into
/data/coolify (9999:root 0700). Several remote commands relied on that
and failed on non-root servers.
- Write README files with `tee` instead of shell redirects in database
starts and application deployments
- Clear Postgres init scripts with `find` instead of a glob
- Split the proxy config backfill if/else so each branch gets sudo
- Start the log drain with `docker compose -f` instead of `cd`
- Wrap the preserved-repository compose start in `sh -c` for non-root
- Stage clone archives outside /data/coolify via StagesCloneArchives
- Extend the sudo parser and add non-root command tests
The proxy directory is 9999:root 0700 on the Coolify host, so a
non-root SSH user cannot cd into it, open redirects in it, or expand
globs there (#4255).
- Start/restart proxy with absolute `docker compose -f` and
`docker stack deploy -c` paths instead of `cd $proxy_path`
- Write the Caddyfile via `tee` instead of a shell redirect
- Check for and prune config backups with `find` instead of globs
- Read the config size with `stat -c %s` instead of `wc -c <`
- Add feature tests for non-root proxy commands and record the lesson
Delete the accidentally committed .claude/worktrees/agent-a1f84dd00720572e0
gitlink, which pointed to a local agent worktree rather than a real submodule.
Use auth_rate_limit_ip() rather than $request->ip() for the api, 5 and
feedback rate limiters. On cloud, each client behind Cloudflare now gets
its own bucket instead of sharing the proxy's bucket. Self-hosted
instances ignore the CF-Connecting-IP header.
Add feature tests for per-client sentinel push buckets on cloud and for
ignoring the header on self-hosted.
Add scripts/restore-backup-vm.sh, which restores an instance backup into
an offline KVM VM to test database migrations. Bump the v4 version in
other/nightly/versions.json to sha-1e9add3.
High disk usage notifications were sent every ~10 minutes on Coolify Cloud
because the cache rate limiter did not hold across Sentinel-triggered checks.
- Add a notification_throttles table and NotificationThrottle helper that
atomically records when a notification was sent for a resource.
- Add the ThrottledNotification contract; Team::notify() skips a throttled
notification that was already sent within its interval.
- High disk usage: configurable per-server interval
(server_disk_usage_notification_interval_hours, default 24h) in
Server > Advanced and the servers API.
- Container restarted (coolify-proxy, database TCP proxy): at most one
notification per hour per server/database during a restart loop.
- Move the unreachable and missing-backup markers into the new table and
migrate existing data (servers.unreachable_notification_sent and
scheduled_database_backups.missing_backup_notification_sent_at).
- Regenerate OpenAPI files.
Import now always claims the server and rebinds Sentinel. Remove the
SSH-written ownership file, the write_remote/rebind_sentinel/claim
options, and the POST /servers/{uuid}/claim endpoint. The migrator now
fails when the target cannot take management. Disabling management
records transfer.management_disabled so the server shows as
transferable, and the transfer and import pages are simplified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the shared team id and team channel logic of 15 broadcast events
into a BroadcastsToTeam trait. Events no longer fall back to the session
team: the session team can differ from the resource team, and queued
jobs have no session.
Fix StopDatabaseProxy, which dispatched DatabaseProxyStopped without a
team, so the event was never broadcast from queued jobs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install.sh ran chown -R 9999:root and chmod -R 700 on all of
/data/coolify on every run. This changed bind-mounted data of services,
applications, and databases, and reset the ownership that a non-root
SSH user needs.
Set owner and mode on /data/coolify and the resource directories only
when the installer creates them. Change only source, ssh, and images
recursively, because the coolify container uses them.
Also wait until the Docker daemon accepts connections after a restart,
because OpenRC returns before the daemon is ready.
Fixes#11945
Livewire rebuilds listener names from the session team on every event
call. After a team switch in another tab, open pages still listen on the
old team channel, so events failed with EventHandlerDoesNotExist (500),
or ran handlers that load resources scoped to the new team (404).
Store the team at mount and build team channel listeners from it. Events
for a team that is no longer the session team are ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without SMTP or Resend configured, the Help form posts to the Cloud
feedback API but ignored the response, so it always reported success.
Check the response and show an error on failure. The Cloud endpoint now
returns 502 when the Discord webhook fails.
Re-test S3 storages marked unusable every hour so a storage that becomes
reachable again is usable without manual action. Failures keep the
storage unusable and reuse the existing once-per-outage email.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scheduled job manager checked every enabled schedule each minute and
evaluated its cron expression in PHP. On Coolify Cloud one run took more than
60 seconds, so runs overlapped and minutes were skipped.
Each schedule now stores its next due time:
- next_run_at on scheduled database backups, scheduled tasks, and volume
backups, and docker_cleanup_next_run_at on server settings (UTC, indexed)
- model hooks calculate it when a schedule is created or its frequency or
enabled flag changes; a server timezone change or a move to another server
calculates it again for the affected schedules
The `scheduled:dispatch` command runs the manager every minute from the
scheduler, not from a queue. It selects only due rows and claims each
occurrence with an atomic update of next_run_at, together with its delivery
record, so parallel runs on several nodes never dispatch it twice.
- database and volume backups run once when they are late; tasks and Docker
cleanups more than 10 minutes late are logged as missed
- a local time that occurs two times when the clocks go back runs once
- the migration continues each schedule after its last recorded occurrence and
drops scheduled_job_states
Show the next run, in the server timezone, on the database backup, scheduled
task, volume backup, and Docker cleanup pages, or "Invalid frequency" when the
frequency cannot be calculated. scheduled:diagnostics now lists overdue
schedules and open deliveries.
Remove workarounds that are no longer needed: the queue overlap lock and its
stale-lock repair, the catch-up window, and debug-only logs.
af983a29d changed getCurrentApplicationContainerStatus() to take the
application model, but missed this caller. Scheduled tasks of an
application failed with a TypeError.
The floating bottom-left pill and its expanded list covered page content.
- Desktop: show a compact 'N deployments' row in the sticky sidebar footer
(spinner with count badge when the sidebar is collapsed); the list opens
to the right of the sidebar
- Mobile: show a spinner with the count in the top bar; the list opens
below the top bar
- Poll only the copy visible at the current breakpoint (wire:poll.visible)
- Show the indicator on the dashboard too, and drop the path-based
visibility logic and its navigate round trip
Container ownership labels now use UUIDs: coolify.applicationUuid,
coolify.serviceUuid, coolify.service.subUuid and coolify.databaseUuid.
Numeric ids differ between Coolify instances, so after a server transfer
the target could not find existing containers: redeploys left the old
container running and service statuses were wrong.
Containers are matched by UUID label first, then by the compose project
(Swarm: stack namespace), which is the resource UUID since July 2024, and
last by the older numeric id label. Existing containers keep working
without a redeploy and get the UUID labels on the next deploy or restart.
Drop the laravel/nightwatch package and its agent service from the
development and production s6-overlay configs. Remove the related
environment variables, constants, phpunit setting, Boost skill
registration, and configure-nightwatch agent skills. Update the
container role script test to stop expecting the nightwatch-agent
service.
Call write_missing_content_files() at the start of start_by_compose_file()
so bind-mounted content files exist before `docker compose up` runs, which
would otherwise create them as empty directories. This also covers
Dockerfile deployments to additional servers that lack the files.
Add a feature test for a Dockerfile deployment to an additional server,
and note in lessons that long-lived Horizon workers must flush static
caches in Queue::before.
Horizon workers kept Server::findCached() results between jobs, so scheduled
database backups used stale reachability and subscription state. A backup that
was skipped with the stale state counted as done for that day. Flush the
Server identity map before each queue job.
Add debug logs for the full path of a scheduled run, so a multi-node cloud
instance shows where a run is lost:
- manager start delay, gap since the previous start, host, pid, memory, and
a warning when the start is later than the catch-up window
- skip logs with the server state that was used and the current database state
- occurrence enqueued, claimed, rejected, completed and failed, with host and
the delay since the due time; a warning when the previous run is still open
- count of occurrences still open after 15 minutes on each manager run
- database backup runs that end without a backup
- server connection state changes with the checking node and the SSH error
Also keep publishing pending occurrences when one of them fails.
- Show the status summary next to the server name on the primary and additional server rows.
- Move Deploy, Make primary, Stop and Remove into one Actions dropdown per server row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Stop the application on every server, also when one server is not functional.
- Write file mounts on every server of the application, not only on the main server.
- Deploy the exact commit, rollback flag and image tag of the main deployment to the
additional servers, so a rollback reverts all servers.
- Pull the pushed image on additional servers for inline Dockerfile applications
instead of building it again on each server.
- Fail multi-server deployments without a Docker image name, also from webhooks and the API.
- Send one deployment notification after all servers finish (parent_deployment_uuid).
- Block persistent volumes on multi-server applications (API, Livewire, SQLite connect)
and warn in the deployment log for existing applications that have both.
- Remove dead code in push_to_docker_registry().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use the Coolify confirmation modal instead of wire:confirm for server
management, server transfer and import, resource migration, and S3
backup disabling. Show a toast instead of alert() for a missing GitHub
App webhook endpoint. Add a test that blocks native dialogs in views.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the dev-only management ownership section with an Enable/Disable
management button in the Connection section header, and place the
Connection section above the Server overview.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dashboard redesign added an IP address column to the servers list
and put each server IP into the Alpine data, so the IP showed in the
page source. Remove the column and the value, and render the component
in the test instead of matching template strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the browser confirm on the Delete button with a two-step
confirmation modal. To delete, users now type the image reference.
The modal is shown only to users who can update the server. delete()
now accepts an optional password argument so it works with the modal's
submit action.
Adds an Images entry to the server sidebar that lists the Docker images
stored on that server: repository, tag, image ID, age, size and the
containers using each one. Images no container references are labelled
Unused and can be deleted individually; Docker's own reported total and
reclaimable size are shown above the table.
Container logs and scheduled-task commands and output are now shown only to
users who can edit the resource. The API returns them only to tokens with
the read:sensitive or root ability, the same as the MCP tools.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a Management ownership switch to the server page, shown only in
development, that lets a user with update permission release or claim
control of a server from this Coolify instance. Claiming is blocked for
localhost and when the team is over its server limit. Both actions are
recorded in the audit log.
When ServerTransferClaimer claims a server, it now clears force_disabled
if the team is within its server limit, and it always saves settings,
not only when the sentinel is rebound.
TrustHosts skipped its check while the cache key instance_settings_fqdn_host
was empty, and only the skipped check filled that key. So since e1fe58639
(October 2025) no release restricted the Host header. Turning it on now would
lock out users who reach Coolify by server IP or another name when the domain
or proxy is broken.
Links in emails and notifications do not use the Host header: base_url()
uses the configured FQDN or the server IP (see ResetPasswordUrlTest). So
remove the middleware, its cache key and its tests. TrustProxies is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove tests that copy app logic into the test, grep PHP source as
text, only check literals, constants, class_exists or PHP built-ins,
duplicate other tests, or are always skipped. Keep source checks that
guard security and authorization rules.
Rename a duplicate Pest helper so the full suite loads, drop the
missing v5/Browser path, and remove the CLAUDE.md symlink in favor of
AGENTS.md. Correct AGENTS.md: Dusk is not installed and the upgrade
script is scripts/upgrade.sh.
cleanup:database used orderBy()->skip(10)->delete(). Laravel drops the offset
in a DELETE, so it deleted all old rows anyway, and the count query with the
offset printed 0 on PostgreSQL. Count without the offset, delete in batches of
1000 rows so a large table is not locked by one long DELETE, and write the
output through the command so it is captured.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reject delete_from_provider values that are not booleans, and reject the
request when the team has no token for a linked cloud provider. Before,
both cases removed the server from Coolify but kept the cloud VM running.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With APP_ENV=local the deployment log showed every build-time value (raw,
stripped and escaped) and the old and new value of overrides. Print only the
key, the type and the value length.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>