- Do not provision a runner for a queued pull request job when no matching
runner configuration allows pull request jobs (the run event is read from
GitHub; the job-started hook still refuses it if the lookup fails).
- Do not open an existing runner group to all or public repositories, so
restrictions an organization admin set on GitHub stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Different database types can share an id, so the database list showed
backup configs of other databases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- References to locked (shown once) shared variables keep their reference
text in the UI, copy action, and API instead of the locked value.
- Remove the saved proxy configuration and validation logs from nested
servers (for example destination.server) in API responses unless the
token can read sensitive data.
- The services list API now applies removeSensitiveData to every service.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A renamed template keeps its old key as an alias, so API callers and
services stored with the old key still find the template.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Try to remove every volume and always delete the configuration
directory (it holds the .env with secrets), then report the volumes that
failed.
- Delete volume backup schedules also when a service is removed from
Coolify only.
- The delete API no longer claims the server is unreachable when the
caller asked to delete from Coolify only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Parse and dump the proxy Compose file with custom tags, so `!reset` and
`!override` survive the traffic analytics update and `{}`/`[]` keep
their type.
- Remove the Traefik log rotation sidecar when the proxy stops or another
proxy type starts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Older versions accepted any static image through the API. Such a stored
value now stops the deployment with a message that lists the supported
web servers instead of a ValueError.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The start command now quotes the password. Databases created before this
release keep the unquoted v4.3.23 argument when quoting would change the
password the server receives, until their password changes. Remote secret
passwords are always quoted exactly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Write remote secret values as exact dotenv literals in the service .env
file and escape "$" where they go directly into a generated compose file
(environment, start commands, healthchecks).
- Resolve remote secrets before a service or database restart stops the
running resource, and show the secret manager error instead of a generic
start failure (new RemoteSecretException).
- Cache fetched key names for the autocomplete and rate limit how often a
user can contact the secret manager.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Uploading and starting a database import overwrites data, so a deploy
token is no longer enough. Import activity IDs must be numeric.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add an hourly cleanup:database-import-uploads command that removes staged
import uploads older than a day, while holding the import lock of the
database so a running import keeps its file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An import task can now run until the SSH command timeout (capped below the
Horizon worker timeout). When the CoolifyTask fails, times out, or its SSH
command ends with 124/255, Coolify stops the restore in the database
container and keeps the import in progress until the stop has run, so the
database stays blocked. The restore reports its own 124/255 as 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a --purge option that purges the synced CDN files (and the legacy
/coolify-nightly/ paths for nightly) from the Cloudflare cache, and remind
the operator to run it after the CDN pull request is deployed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CDN files are synced through the coollabs-cdn GitHub repository only.
Remove the --bunny direct upload path, the BUNNY_API_KEY and
BUNNY_STORAGE_API_KEY settings, and rename the command to sync:cdn.
Return database import output from the API only when the token has
read:sensitive and the user can update the resource. Otherwise the
output field is null.
Give the temporary PostgreSQL restore databases random suffixes. Before
restoring, check that neither name is already taken instead of dropping
any database that already uses it. The old database is no longer
dropped before the swap, so unrelated databases are left alone.
Add API tests for output visibility by role and token ability. Add
restore script tests that run against stateful PostgreSQL client stubs.
- Start Sentinel with --restart unless-stopped so Docker restarts it
after a crash or reboot.
- Make CheckAndStartSentinelJob unique per server so copies cannot
collect in a slow queue.
- Read X-Sentinel-Version on push and update an outdated Sentinel
without SSH; skip the hourly SSH version check for servers that
report their version.
The scheduler dispatched VolumeBackupRecoveryJob every minute for each
pending execution, and each job retried 20 times with a 60 second backoff.
A stuck execution kept about 20 failing jobs active at a time.
- Run each recovery job once; the scheduler is the only retry path.
- Make the job unique per execution so duplicates do not pile up.
- Dispatch recovery for an execution at most once every 5 minutes.
- Clear the pending flag and record the reason when recovery cannot
succeed (server, resource, S3 storage, or filename no longer exists),
so it does not fail forever and block new backups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scheduler ran one dispatcher for database backups, tasks, volume backups,
and Docker cleanups after each other. A slow type, for example many tasks due
at the same minute, made the whole run longer, and the shared overlap lock
then made all types skip the next minute.
Run `scheduled:dispatch --type=<type>` every minute for backups, tasks,
volume-backups, and docker-cleanups. Each type runs in its own background
process with its own scheduler and job overlap locks, and publishes and
recovers only the deliveries of its own job type. Running the command without
a type still processes all types.
The command called shouldRunCronNow() with the dedup keys, and that helper
writes the key it checks. For server-check, server-storage-check, and
server-patch-check these are the keys of ServerManagerJob, so running the
command in a minute when a check was due made the real check skip that run.
It also read cache keys for backups, tasks, and Docker cleanups that the
scheduled job manager no longer uses. Read scheduled_job_states instead, and
list open and failed deliveries, where an old "enqueued" row shows a queued
job that was lost.
The missed daily backups on Coolify Cloud came from Redis evicting queue keys
(allkeys-lfu on a full instance), not from the scan of all schedules. The
next_run_at design (452259aac, 5c5740533) did not prevent that, and it added
stored state that must be kept in sync on every frequency, timezone, and server
change. It was not released, so remove it:
- revert next_run_at, its model hooks, the "Next run" UI, and the diagnostics
changes; restore scheduled_job_states and the queue overlap lock
- a new migration removes the next_run_at columns and creates
scheduled_job_states again where the removed migration already ran
Keep the changes that fix real problems:
- run the manager every minute from the scheduler with `scheduled:dispatch`,
so a busy queue cannot delay it past the catch-up window
- load the server chain of scheduled backups with each chunk, which removes
about 7,000 queries per run on Coolify Cloud
- catch Throwable for each schedule, so one broken schedule does not stop the
others
- publish database and volume backups again when their queued job was not
started after 60 minutes (for example, lost in Redis); log lost tasks and
Docker cleanups as missed. A lost backup is not run again when a newer
occurrence of the same schedule exists.
New or changed manual webhook secrets (GitHub, GitLab, Bitbucket,
Gitea) must have at least 16 characters, in the Webhooks page and in
the application API. Stored secrets keep working: an unchanged value
passes, and an empty value still disables the webhook. Each secret
field gets a Generate button that fills a random 40-character secret;
the user saves it after adding it to the Git provider.
Homarr set SECRET_ENCRYPTION_KEY twice; only the
SERVICE_HEX_64_HOMARR line was ever used, so the duplicate is removed
and existing keys do not change. Bulwark quoted list-syntax values, so
the quotes reached the container and JMAP_SERVER_URL was never created
as a required variable. Existing services keep their stored compose.
- refreshEnvs is now sent only to the environment variable list, and
rows no longer listen to it, so adding or removing variables cannot
queue an update for a removed row ("Snapshot missing"). Locking a row
refreshes that row directly; the compose editor gets its own event.
- saveCustomName() no longer saves a custom container name that another
application on the server already uses; it restores the stored value.
- Cancelling a GitHub runner uses the Coolify confirmation modal instead
of a native browser dialog.
ImportForm::checkFile() and checkS3File() ran without authorization, so
a member with view access could check whether paths exist on the server
or probe S3. They now require update access, like the restore actions.
Server\Show::checkVultrInstanceStatus() now requires view access, like
the Hetzner and DigitalOcean checks.
New SQLite backup schedules copied the database's file list at creation,
so files added later were never backed up. Schedules created in the UI
or the API without an explicit file list now store none, and the backup
job resolves the current files at run time. An explicit choice is still
stored. Existing schedules are not changed.
A schedule with a frequency that no longer parses stayed due and logged
"invalid frequency" every minute. Its next run is now cleared (shown as
invalid frequency in the UI), the warning is logged at most once a day,
and fixing the frequency makes it run again. The enabled flag is not
changed.
A delivery stuck in "enqueued" because its queued job was lost was never
run or cleaned up. After 60 minutes, database and volume backups are
published again (late backups run once), and tasks and Docker cleanups
are marked missed with a log entry. The claim token makes a duplicate
job a no-op.
A queued deployment whose commit no longer validates (for example one
queued before an upgrade) made the job dispatch throw. The row stayed
in_progress, blocked every later deployment of the application, and
stopped the queue loop. Such a row is now marked failed with a log
entry and a failure notification, and the next queued deployment
starts.
An exception after the new container passed its health check (for
example in additional destinations, the next-deployment loop, or
notifications) made failed() remove the new container, which could be
the only one running. failed() now keeps a healthy new container, and
errors in the steps after a status change are logged without flipping
a finished deployment to failed.
- Match the Compose service label with format_docker_labels_to_json()
and skip containers without labels.
- Document service_name on the preview logs endpoint and remove the
incorrect "ignored for non-Compose applications" claim. Every
application runs as a Compose service, so a mismatch returns 404.
- Add regression tests for service selection, the default container,
and the exact-match 404.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add optional per-option description support to the listbox component,
rendered as muted text below the option label.
Use it on the server role selector (new server and server settings) to
explain each role, and note that GitHub Actions runners require the
"Builds only" role in helper texts and the runners empty state.
Update Laravel Boost and regenerate the agent skills for .agents, .claude
and .cursor:
- Add infer-conventions and testing-best-practices skills
- Add MCP app reference to mcp-development
- Remove the pest-testing and Ray debugging skills, plus the
laravel-best-practices testing rule
- Refresh laravel-best-practices rules, livewire, and tailwindcss skills
- Update AGENTS.md, boost.json, and composer dependencies
- Extend MCP endpoint feature tests
Deployments failed with "Server is not functional" when the server was
still flagged unreachable after a single failed scheduled check. Add
Server::isFunctionalAfterRecheck(), which runs a live SSH and Docker
connection check when the server is marked unreachable. It skips the
check for disabled servers and servers with placeholder IPs, then
re-evaluates isFunctional(). ApplicationDeploymentJob now uses it.
ServerManagerJob no longer skips the SSH connection check for servers
with a live Sentinel when the server is flagged unreachable or unusable.
The Sentinel heartbeat does not restore these flags, so without the
check they would never recover.
Add feature tests for the recheck behavior and unit tests covering the
Sentinel skip condition.
A single failed SSH attempt or any unrelated exception in
ServerConnectionCheckJob marked the server unreachable and unusable, so
deployments failed with "Server is not functional" while the server was
online.
- Retry a failed SSH attempt once after 3 seconds within the same check.
- Mark the server unreachable only after UNREACHABLE_THRESHOLD (2)
consecutive failed checks; notifications use the same threshold.
- Keep the stored state when the job fails for a reason other than SSH or
Docker (database, cache, bugs).
- Raise the job timeout to 60 seconds and the overlap lock above it. The
old 15 second timeout killed checks before they wrote a result.
- Run the Docker and Compose version commands once with a 15 second
timeout. A failed Compose check no longer marks the server unusable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runner group was limited to private repositories, so jobs from
public repositories never got a runner: Coolify still started a runner
for each matching job, which then waited idle until its timeout.
The runner group now allows every repository of the organization,
public ones too; existing private groups are updated. GitHub gives a
runner any queued job with matching labels, so pull request jobs are
refused inside the runner: a job-started hook fails jobs from
pull_request, pull_request_target and review events before any step
runs, and fails closed when the event is unknown. A new runner setting,
off by default, allows pull request jobs; the settings page then warns
to require approval for fork pull request workflows on GitHub.
Cloning an application, database, service or environment did not copy
the secret manager link, so variables with remote secret references
failed on the next deployment with "no secret manager source is
configured".
Clones now get a copy of the link when the integration token belongs to
the clone's team. Server transfer does not export integration tokens or
links; the export now warns which resources need the token and link
again on the target instance.
Import all stopped at the first remote secret whose name is not a valid
environment variable key (for example db-password in Vault or
Infisical). The key setter threw, so the import was partial, no audit
entry was written, the variable list did not refresh, and the user saw
only a generic error.
Invalid names are now skipped and listed in the result toast and the
audit entry, and the rest are imported. Browse marks invalid names and
offers no add action for them, and adding one shows a clear error. The
refreshEnvs event now goes only to the environment variable list.
The server transfer bundle did not include the standalone_sqlite_id of
persistent volumes, so after an import an application's mount of a
SQLite data volume was no longer linked to the database. The shared
volume checks then failed: deleting the SQLite database, or the
application with its volumes, could remove the SQLite data.
The exporter now sends the SQLite database uuid with each volume, and
the importer links the volume again after all databases are imported
(with a warning when the database is not in the bundle). As a backstop,
StandaloneSqlite::deleteVolumes() skips volumes that another resource
uses, like Application already does.
The MongoDB health check passed only on the writable primary
(db.hello().isWritablePrimary or isMaster().ismaster). A replica set
that was never initiated, which is common for Prisma setups, and any
non-primary member were reported unhealthy after a restart.
The check now runs db.adminCommand({ ping: 1 }) with mongosh or the
legacy mongo shell. Ping needs no authentication, passes on every
running member, and still fails when mongod is not reachable.
The failed-authentication lockout for manual webhooks was checked before
the signature, and the bucket used the client IP from X-Forwarded-For.
An unauthenticated client could send failed attempts with the Git host's
address for a public repository and branch, and correctly signed
deliveries then got 429, so deployments were lost.
The lockout now applies only to deliveries that fail authentication; a
delivery with a valid secret for a matched application is always
processed. Failures are counted per connection address (REMOTE_ADDR, or
CF-Connecting-IP on cloud), so rotating X-Forwarded-For no longer gives
a new bucket.
Proxy port validation parsed the configuration with plain Symfony YAML,
which rejects Docker Compose tags such as !reset and !override. Proxy
start, restart and save then failed for these valid configurations, and
automatic start after a reboot stopped silently. v4.3.23 did not block
on such configurations.
The parser now reads custom tags and validates the values inside them,
so malformed ports are still rejected. CheckProxy refreshes the status
of a running proxy before it reads the configuration, as in v4.3.23, so
an unreadable configuration no longer hides a running proxy.
An additional server of a multi-server application must run the image
that the main server built and pushed. On servers with the build or
both role, a disabled build cache still forced a local rebuild, and a
failed registry pull fell back to a local build. That ignored the build
server and the build-server fallback setting, and the servers could run
different images.
Additional servers now always pull the main server's image (a forced
rebuild is reset, except for Docker Compose applications, which build
on each server by design). When the image is not in the registry, the
deployment fails with a clear message instead of building it.
Since rolling updates also run for applications with a custom internal
name (when consistent container names are off), the new container gets
a generated name. failed() still kept the new container whenever a
custom internal name was set, so an exception after the new container
started (for example an SSH or docker inspect error during the health
check) left it running next to the old version with the same proxy
labels.
failed() now keeps the container only for consistent container names
and pull request deployments, the same cases that skip rolling updates.
Reword the legacy-volume notice to say the resource must be stopped and
data backed up before deleting the entry and Docker volume.
In the delete modal, explain what happens with and without deleting
the Docker volume. Add a warning that the resource must be stopped
first, because Docker cannot remove a volume that a running container
uses, and suggest a backup before deleting.
Drop the OAuth confirm/callback flow that let users confirm destructive
actions by re-authenticating through a linked provider. This removes the
confirm route and session handling in OauthController, the middleware
bypass for those routes, OauthSetting::providerName(),
User::oauthConfirmationProviders(),
OauthLoginService::identityBelongsToUser() and the
socialite_reauthentication_parameters() helper.
Users with a linked OAuth identity no longer need a password step and
only use the dialog's typed confirmation. Password confirmation is now
required only for users who have a password and no SSO identity. The
confirmation modal and tests are updated to match.
The health check from #11730 picked mongosh or the legacy mongo shell
from the image name (mongo:4*). Images without mongosh whose name does
not start with mongo:4, such as mongo:3.6 or registry mirrors of
mongo:4.4, ran a missing mongosh (exit 127) and stayed unhealthy.
The check now uses mongosh when the container has it and falls back
to the legacy mongo shell otherwise.
Replace the test that only checked the source text with tests that run
the generated script with fake shells.