- Add a main-menu row that reopens the resource menu, with a placeholder row
- Show only the other view as an icon in the view switcher
- Remove the active tile glow and the collapsed Search frame
- Tint the dark command palette purple and whiten focused-row details
- Soften the toast edge and give the toast icon tile 3D depth
- Document the decisions in DESIGN.md
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>
The repository URL rule rejected ssh://user@host:port/path, so deploy-key
resources on servers with a custom SSH port needed a manual URL edit after
creation. Accept ssh:// URLs and keep them unchanged for deploy keys so the
port reaches the clone. Rename scpStyleGitUrlToHttps to sshGitUrlToHttps and
let it convert ssh:// URLs too, so public flows, repository links and GitHub
App repository slugs handle them.
Closes#12161
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Environments without resources now appear as placeholder cards on the
project, team projects and server canvases. They sit in the zone that
holds most of their project's cards, or in a new "Empty environments"
zone when there is none. A new EmptyCard component renders them.
- Project canvas: admins can move groups and cards, and the moves are
saved on their environment canvas. Environments are ordered by id.
- Server canvas: admins can move single cards as well as groups.
- Shared variable canvas: built-in COOLIFY_SERVER_UUID/NAME variables
and their links are hidden on server scope.
- Main nav: resource menus register through registerMainNavContext()
with depth levels (#main-nav-context-{depth}). Back goes one level
lower, and Back at depth 0 returns to the main menu.
- The deploy canvas state, styles and controls are simplified, and the
related sidebar, navbar and dashboard views are updated.
Tests are added or updated for the canvases and the main nav resource
menus.
Applications and service applications can now use the server resolver,
no resolver ("none"), or a custom resolver name. This allows servers
that mix Let's Encrypt domains with domains that use their own
certificates, such as Cloudflare Origin CA. The setting is available in
the UI and the API.
Closes#12138
Coolify always added tls.certresolver=letsencrypt to generated HTTPS
routers, so Traefik logged errors when the proxy used its own
certificates (for example Cloudflare Origin CA) without that resolver.
Add a server setting (default letsencrypt) in the proxy page and API.
An empty value omits the resolver from resource labels, the Coolify
dashboard routes, and the catch-all router.
Closes#12138
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.
Accept HTTP(S), git:// and SCP-style SSH repository URLs whose host is an
IP address, including bracketed IPv6 such as git@[2a01:4f8::1]:org/repo.git.
The clone runs on the user's deployment server, so the IP block did not
protect Coolify, and SSH URLs with IPv4 already passed.
Loopback, link-local and other reserved addresses are still rejected as
internal hosts. Brackets are allowed only around a valid IPv6 host.
Sentry self-hosted (getsentry/self-hosted 26.8.0) could not be deployed
as a Docker Compose application. Three separate problems blocked it:
- YAML collection aliases: the first fix raised the limit to 256 only in
Application::oldRawParser(). The safety check added in v4.4.0
(validateDockerComposeForInjection) and all other Compose parse calls
still used the Symfony default of 128, so the Sentry file (133 aliases)
failed with "Maximum number of collection aliases (128) exceeded".
Add parseDockerComposeYaml() and use it for every Compose parse.
- Argument list too long: the deployment job sent the generated Compose
file as base64 inside the SSH command. Linux limits one argument to
128 KiB, and the Sentry file is about 370 KB. execute_remote_command()
now accepts an 'input' key that streams content over SSH stdin, and
the job writes the Compose file with it.
- A list-style build arg without a value (build.args: [SENTRY_IMAGE])
merged into a map environment kept its numeric key, so Docker Compose
rejected the file with "non-string key in ...environment: 0". It now
becomes NAME with no value, as in Compose.
Closes#11725
Count running scheduled tasks, database backups, and volume backups,
excluding runs older than their job timeout. Report scheduled job counts
and keep the progress check active while any scheduled jobs are running.
Keep reserved jobs active and bound missing Horizon records by the server's
deployment timeout. Add regression coverage for active and finished jobs.
Use UTC PostgreSQL sessions and bump the release version to 4.4.1.
Since aabd12737 a Compose bind volume with a content block had to stay
inside the resource directory. Services and Compose applications saved
in v4.3.23 with other paths, and Git Compose files with them, no longer
started or deployed.
Only admins and owners can edit Compose files, and they can already
write any host file with a plain bind mount, so the rule added no
security.
- Write content files to their host path again, as in v4.3.23
- Keep shell escaping and the safe-character checks on volume sources,
so injection attempts are still rejected
- On cloud, RestartService and RestartServiceApplication now run on the
deployments queue instead of high.
- StartProxy and RestartProxyJob, and the remote processes they start,
now run on the deployments queue.
- Self-hosted still uses the high queue.
- Tests cover the new routing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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.
On cloud, GitHub pull request webhooks and Stripe events now run on a
dedicated `webhooks` queue so a busy `high` queue cannot delay them.
Self-hosted instances keep these jobs on `high`.
- Add webhooks_queue() helper and use it in ProcessGithubPullRequestWebhook
and StripeProcessJob
- Provision a `webhooks` Horizon pool in cloud production, sized by
HORIZON_WEBHOOKS_PROCESSES (default 10)
- Document the new pool, rollout and rollback steps
- Cover pool provisioning and job queue routing in tests
Cloud production now runs one supervisor per queue (deployments, crons,
high, default, maintenance), so a busy queue cannot starve the others.
Add maintenance_queue() for slow Docker cleanups and a shared
horizon.worker_timeout config value. Self-hosted keeps the s6 supervisor.
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>
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.
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.
Before Coolify kept `driver`, `driver_opts` and `labels` of renamed
Compose volumes, Docker created those volumes without them. Applying the
options now would make Docker Compose ask to recreate the volumes on
every deployment.
- Add `ignores_compose_driver_options` column to local_persistent_volumes
and mark all volumes that exist at upgrade time
- Parsers now use composeRenamedVolumeDeclarationFor() so flagged volumes
keep the old name-only top-level declaration
- Legacy application parsers (v1/v2) always emit name-only declarations
- Reset the flag when a volume is replicated, since a clone is a new
Docker volume
- Include the flag in server transfer export/import; bundles without it
default to true
- Storages UI shows which volumes ignore driver options and allows
deleting them so the options apply on recreation
- Add tests for parsers, cloning and server transfer
- database import: add keep_owners (restore PostgreSQL owners and
privileges) and sqlite_database (pick the target SQLite file) to the
API, OpenAPI spec, and import form
- oauth: require password confirmation and support team selection on
OAuth login
- remote process: send team broadcast events to the team of the server
that ran the process instead of the causer's user id
- servers API: handle the legacy build server setting
- compose: support parent bind paths and volume driver options
- dns: improve record hints and provider management
- notifications: fix throttling and missing backup notifications
- ui: rework the confirmation modal
- add feature and unit tests for the new behavior
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.
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.
A 200 response with an empty body or an error page replaced the local
templates and the shared cache, so the service list became empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revert the app code changes of d666a0a95, 69eeb2f62, 1e66c9f9b and
a32082b03, so login, registration, OAuth, team danger zone, service
templates, and the other touched files are back to their earlier state.
Keep the removal of unused components and markup-only tests (d666a0a95
also contained those deletions by mistake).
StartupExecutionCleanupTest now fakes the template CDN with the current
file content, so app:init in the test no longer empties
templates/service-templates-latest.json.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 200 response with an empty body or an error page replaced the local
templates and the shared cache, so the service list became empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Legacy Compose applications keep the old name of an external volume. The
deployment log now tells the user why and how to move to the external
volume. The storage page shows external volumes as a read-only list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Switching an external volume from the old <uuid>_<name> volume to the
declared external volume on redeploy could make an app look as if it
lost its data (for example a database that initializes a new, empty
data directory).
- If a resource already has the storage entry with the old generated
name, the parsers keep using that volume and show a warning in the
deployment log and the service start log, with the steps to switch.
- Without that entry (new resources, or external volumes added later)
the external volume is used as declared.
- Deleting the old storage entry (storage page) switches to the
external volume on the next deployment; the storage entry shows a
note about it.
- Legacy Compose applications (parser versions 1 and 2) never stored
their volumes, so they keep the old volume names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compose volumes declared with external: true (or external: {name: x})
were renamed to <uuid>_<name>, so containers got a new empty volume
instead of the existing one. All Compose parsers (applications,
previews, services, and the legacy parsers) now use them as written:
no uuid prefix, no preview suffix, the top-level declaration kept, and
no tracking row, so Coolify never removes a shared external volume.
External volume names must be literal; ${VAR} names are rejected.
Existing <uuid>_<name> volumes and their rows are kept on redeploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Development mode rewrote /data/coolify paths to the dev Docker volume
for every server. That is only right for the testing-host backend,
which uses the host Docker daemon. The KVM localhost and other dev VMs
have their own /data/coolify, so bind mounts, Compose content files,
the Traefik and Caddy traffic mounts, Sentinel, and database proxies
pointed to wrong paths there.
- Server::sharesDevHostDocker() and devHostDockerPath() decide per
server; only the testing-host server (ip coolify-testing-host) gets
the volume path. Production is unchanged.
- The testing-host volume names come from the Compose project
(DEV_COOLIFY_DATA_VOLUME, DEV_COOLIFY_BACKUPS_VOLUME), with the
legacy names as fallback.
- In development, saved proxy configurations with the old path are
fixed when they load and ask for a proxy restart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>