Provider errors now return 502 with a safe message, the provider status, and
the provider error code instead of a bare 500. Hetzner no longer sends a create
request again after a 5xx, and a VPS that Coolify did not save is deleted.
Phased Ubuntu updates, apt-mark holds, and apk version pins showed as
available updates that no update could install.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Retry CDN requests only for transient errors and honor Retry-After
(Cloudflare error 1200 sends 60 s), with jitter.
- Read helper and Sentinel versions from the local versions.json, which
the hourly update check refreshes, instead of fetching per job/server.
- Replace the random 0-600 s delay with a stable per-instance offset
inside the cron interval (max 1 h).
- Manual update checks and upgrades make one CDN attempt only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turn on used the saved time and the confirmation showed the time from
page load, so an unsaved new time was ignored. Turn on now validates
and saves the time in the field, and the confirmation refers to the
field instead of a fixed time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Automatic security updates now have their own "Turn on" / "Turn off"
action with a confirmation, instead of a field that the Save bar
applied. The confirmation explains what can stop:
- Docker from the operating system packages (docker.io, moby-engine)
restarts on a security update, so all containers stop and start.
- Docker CE from the Docker repository is not updated: its repository
has no security origin (apt) and no security advisories (dnf).
Checked on Debian 12 (unattended-upgrade --dry-run skips docker-ce)
and CentOS Stream 9.
- Services that use an updated library can restart; a new kernel is
installed without a reboot.
The on/off state is locked, so only the confirmed action can change it.
A new maintenance window is applied to the server on Save only when the
updates are on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check frequency, automatic security updates, and maintenance window
are now fields of the first section, in one row, instead of two extra
boxes. The "not supported" note moved into the existing callout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ui.server.packages_update_requested: user, server, packages (or all),
package manager, and the activity of the update.
- ui.server.packages_update_finished: written when the remote process
finishes (also when it fails), with the result and the exit code of
the activity. The update gets a fresh type_uuid (an indexed activity
property), so the listener finds exactly this activity. The actor
comes from the request, because the listener runs in the queue.
- ui.server.reboot_requested with result started or failed.
- ui.server.auto_security_updates_updated (enabled, time, package
manager, result) and ui.server.patch_settings_updated for a changed
check frequency.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ServerSetting::patchStatus() summarizes the saved patch check
(updates, security updates, reboot required, error). Results saved
before security and reboot detection count as 0 and false.
- Grid cards show "N security updates", "N updates", "Patch check
failed", and "Reboot required" badges; the table gets a Patches
column from lg. The badge text is searchable.
- The toolbar shows a one-line team summary, for example "Patches: 2
servers with updates · 1 with security updates · 1 needs a reboot".
- Badges and the summary are shown only for servers where the user can
view the security pages (viewSecurity on the server), so members do
not see them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New server settings: is_auto_security_updates_enabled and
auto_security_updates_time (maintenance window, HH:MM in the time zone
of the server). Both move with a server transfer.
- Debian/Ubuntu: install unattended-upgrades and write
52coolify-unattended-upgrades. `#clear` drops the distro origin lists,
so only the security origins stay (Debian <codename>-security, Ubuntu
<codename>-security, and the Ubuntu Pro ESM security origins). A
drop-in for apt-daily-upgrade.timer sets the time.
https://manpages.debian.org/bookworm/apt/apt.conf.5.en.htmlhttps://github.com/mvo5/unattended-upgrades/blob/master/README.md
- RHEL family: install dnf-automatic (dnf5-plugin-automatic on dnf5),
set upgrade_type = security and apply_updates = yes in
/etc/dnf/automatic.conf, and add a drop-in for dnf-automatic.timer
(dnf4) or dnf5-automatic.timer (dnf5).
https://dnf.readthedocs.io/en/latest/automatic.htmlhttps://dnf5.readthedocs.io/en/latest/dnf5_plugins/automatic.8.html
- Off removes the Coolify files, so the operating system default comes
back. Other package managers show "not supported".
- The schedule and the automatic updates share one form and one
unsaved bar. The remote commands run only when the automatic update
settings change; a failure keeps the old setting.
- Commands are written for non-root SSH users (tee and single sh -c
scripts) and were run in Debian 12, Ubuntu 24.04, Rocky 9, and Fedora
containers as root and as a sudo user.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Security updates: apt marks a package from a "-security" suite (Debian
12 now reports oldstable-security). dnf reads `dnf updateinfo list
--security` (dnf5 keeps updateinfo as an alias of advisory). zypper
uses `zypper -x patch --dry-run --category security`, because
`zypper -x list-patches` ignores --category and has no package names.
pacman and apk cannot tell, so they show no security count.
https://dnf5.readthedocs.io/en/latest/commands/advisory.8.htmlhttps://github.com/openSUSE/zypper/blob/master/doc/zypper.8.txt
- Reboot required: /var/run/reboot-required, `needrestart -b` (KSTA 2
or 3), `dnf needs-restarting -r` (matched by text, because a missing
plugin also exits 1), and `zypper needs-rebooting` (exit 102).
https://github.com/liske/needrestart/blob/master/README.batch.mdhttps://dnf-plugins-core.readthedocs.io/en/latest/needs_restarting.html
- An explicit risk list per package manager (Docker engine, kernels,
libc/glibc/musl, systemd, dbus, openssh) replaces the "name contains
docker/kernel" guess on the page and in every notification channel.
- The page shows security and reboot badges, a security count, and the
risk of each package. "Reboot server" (with confirmation, update
policy on the loaded server) schedules the reboot with systemd-run and
checks the server again five minutes later. Archived (transferred)
servers are never touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- server_settings gets patch_check_result (JSON), patch_check_error,
patch_checked_at, and patch_check_frequency.
- The patches page shows the saved result and "Last checked" on load.
"Check for updates" queues ServerPatchCheckJob (manual) instead of
running SSH in the Livewire request. The job saves the result and
broadcasts ServerPatchCheckFinished on the server team channel; the
page reloads the saved result (with a 10 s poll as a fallback).
- A finished package update (callEventOnFinish) queues a new check on
the server side, so the page no longer depends on the browser.
ServerPackageUpdated is removed.
- The check frequency is a server setting (cron or preset, at most once
per hour). Empty keeps the staggered weekly default.
- The failure notification is sent only when the state changes from ok
to error, not on every check.
- Page access needs viewSecurity and every action needs update on the
loaded server; a member gets 403. Updates accept only a package from
the saved result.
- Server transfer: the frequency moves, the last result stays on the
source (runtime).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Runtime logs: add a compact text view, move display options and log level filters into a View menu, move copy and download into an Export menu, and remove the extra top margin.
- Replace spinner loading cards with skeletons on the logs, dynamic configurations, proxy, TLS certificates, Docker images, environment variables, preview deployments, rollback, subscription, cloud provider, and new resource pages.
Store server IPs in one form (no brackets, short lower-case IPv6) and
write each address in the form its context needs:
- URLs and host:port (Sentinel, database URLs, SFTP backup download,
DNS servers, SMTP, webhook base URLs, links, health checks) get
brackets through formatHostForUrl().
- Traefik Host() rules use the address without brackets, so IPv6
domains no longer return 404; saved labels are fixed at deploy.
Caddy keeps the brackets.
- SSH keeps the plain address.
- Hetzner gives an IPv6 /64 network; IPv6-only servers now get the
server address (prefix::1) instead of the unusable prefix::64.
- sslip labels, registry image names with an IPv6 host, the API domain
list, allowlist matching and duplicate checks handle IPv6.
A migration normalizes stored IPv6 server addresses, repairs Hetzner
servers saved as ::64 that are not reachable, and adds brackets to
saved Sentinel URLs.
Replace datalist and searchable-listbox components with the shared listbox and migrate form fields.
Deduplicate superseded Livewire updates to prevent repeated array update hooks, with regression coverage.
The per-server schedules reused the cache keys of the old schedule, so
every server ran these checks at once on the first run after the
upgrade. New keys start fresh, so each server runs at its own slot.
Require usage to fall five percentage points below the threshold before releasing the notification throttle. Reset it after Sentinel reports recovery so a new spike alerts immediately.
Add regression coverage for recovery and usage fluctuating near the threshold.
The high disk usage throttle stayed for 24 hours after usage dropped,
so a new spike in that time sent no alert.
- Release the throttle when disk usage is at or below the threshold
- Restart a running Sentinel that has not pushed for 10 minutes, at most
once per hour. The restart applies the current URL and token.
- Restart an unhealthy Sentinel. Do not restart while its health check
starts or while its API does not answer yet, which caused restart loops.
- Remember why pushes fail: the reason Coolify rejects a push, and the last
push error from Sentinel's /api/push-status. Show it on the Sentinel page
while Sentinel is out of sync.
- When the instance URL or public IP changes, move remote servers that use
the generated Sentinel URL to the new URL. Custom URLs stay.
- ServerCheckJob uses the stored reachability state instead of a second SSH
probe. One failed probe no longer marks the server unreachable without
UNREACHABLE_THRESHOLD or marks all its resources exited.
- Do not dispatch ServerCheckJob for build servers and Swarm workers.
- Check cloud provider state every 5 minutes instead of every minute.
- ServerPatchCheckJob uses isFunctional() and drops the unused retries.
- Delete the unused ServerCleanupMux job.
- Disable SSH multiplexing per command. The global config change stayed in
the queue worker, so later jobs (also deployments) ran without mux.
- CheckAndStartSentinelJob returns early when the server is not
functional. It now reads the container state with `docker ps --filter`,
so an SSH failure throws and is no longer treated as a missing
container that needs a restart.
- ServerManagerJob only dispatches the Sentinel version check for
functional servers.
- On cloud, ServerPatchCheckJob runs on the maintenance queue and
ServerLimitCheckJob runs on the webhooks queue.
- The default number of cloud maintenance workers goes from 10 to 60.
- excludeCertainErrors drops its unused ignore list and is typed as
`never`.
- Docs and the Horizon config comments are updated, and tests cover the
new behavior.
Servers no longer all connect at minute 0. The hourly Sentinel check uses
a per-server minute, and the weekly patch check uses a per-server time on
Sunday between 04:00 and 23:59.
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.
- Delete Blade components, component classes, Livewire components and
views that nothing uses after the redesign.
- Delete tests that only compare Blade/CSS source text; keep the source
checks for authorization gates, stable wire:key values and escaping.
- Update stale tests to the current UI and behaviour.
- Update DESIGN.md to the current navigation and layout.
- Add a lesson: test behaviour, not source markup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a per-server Analytics page scoped to that host, move traffic
settings out of Sentinel, and put metrics settings on Charts.
Surface 4xx/5xx on path rows and restyle deployment logs.
- Mark Analytics, Dashboard\TrafficAnalytics and Project\Application\Analytics
Livewire components #[Lazy] with skeleton placeholder() views so pages render
instantly while Sentinel data loads in a deferred request
- Add SentinelTrafficClient::prefetchServerWide() to warm all server-wide
endpoints in a single batched docker exec instead of ~14-15 serial SSH
round-trips, and refactor overview/paths/breakdown URL building into shared
helpers
- Cap per-app leaderboard overviews fetched via MAX_LEADERBOARD_APPS to bound
requests on servers with many apps
- Add hasRequestSeries() to detect empty request charts and render a no-data
state instead of a blank chart
- Gate ServerSetting's automatic Sentinel restart on is_sentinel_enabled so
tuning geoip/traffic settings no longer silently enables Sentinel
- Add reusable skeleton UI components (skeleton, skeleton/table, skeleton/tiles)
for the new placeholder views
Persist DigitalOcean, Hetzner, and Vultr servers before public IP
assignment, then backfill placeholder addresses from provider state.
Treat partial Sentinel snapshots as non-authoritative and document the
destinations API with OpenAPI schemas.
Add dedicated show/edit pages for cloud provider tokens and cloud-init
scripts, including descriptions and UUID routes.
Generate private keys directly from the index and surface cloud provider
API loading errors in server creation flows.
Authorize stored Hetzner cloud provider tokens before fetching
firewalls or networks, hide provider error details, and keep server
creation intact if backup activation fails.
Collapse advanced Hetzner server options by default while preserving
visibility when advanced values are selected.