- 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>
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.
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.
An all-databases MySQL/MariaDB restore also restored the mysql system
database, so after the next restart the root and app passwords were the
source ones and the MariaDB health check failed. The restore now skips mysql,
sys, performance_schema and information_schema by default. The new option
"Restore users and privileges" (restore_mysql_users in the API) keeps the
old behaviour.
GitLab, Authentik, Zitadel, Clerk and Azure identities were keyed by the
provider name only, so a user with the same subject on another instance
could sign in as the linked user after the base URL or tenant changed.
The issuer column now stores the normalized instance, existing rows are
backfilled from the current settings, and oauth:rekey handles host renames.
- 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
GET /applications/{uuid}/logs ignored service_name and always returned
logs from the first container, so 'coolify app logs --service' could show
logs from the wrong Compose service. When service_name is present, select
the container whose labels contain the matching
com.docker.compose.service entry and 404 when nothing matches.
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>
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.
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.
- 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>
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>
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>
The duplicate check used the signed-in user's current team, or all teams
when there was none. Check the team the key belongs to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub, Gitea and Bitbucket manual webhooks accepted any signature when
APP_ENV=local. GitLab and the GitHub App webhook already did not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Configuration diff: report all settings when an app has no earlier
snapshot, so older apps show "configuration changed" again (#11430).
- DigitalOcean/Vultr: do not repeat the "API error:" prefix.
- HasSecretManager: the deleting listener returns nothing, so later
deleting listeners still run.
- GitLab webhook: use new_public_id() like the other webhooks.
- Team danger zone: show the link and open the page only for the team owner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security:
- Settings email: authorize the SMTP and Resend toggles.
- Server navbar: show Sentinel status only to users who can view Sentinel.
- GitHub source: permission gates on Sync name and Delete.
Broken features:
- Profile: avatar upload (missing import and file uploads trait).
- API tokens: keep the "Never" expiry choice.
- Environment variables: show validation errors instead of a generic error.
- Servers API: read the server uuid from the route in domains_by_server,
so ?uuid=<application> filters instead of returning 404.
- Cloud servers: Power On for stopped Hetzner, DigitalOcean and Vultr servers.
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>
- TrustHosts: validate the Host header also when the FQDN cache is cold.
- Settings email: authorize the SMTP and Resend toggles.
- Server navbar: show Sentinel status only to users who can view Sentinel.
- GitHub source: restore permission gates on Sync name and Delete, and the
Test connection button.
- Profile: make avatar upload work (missing import and file uploads trait).
- API tokens: keep the "Never" expiry choice.
- Environment variables: show validation errors instead of a generic error.
- Servers API: read the server uuid from the route in domains_by_server.
- Configuration diff: report all settings when no earlier snapshot exists.
- Cloud servers: restore Power On for stopped servers; do not repeat the
provider error prefix.
- HasSecretManager: do not stop later deleting listeners.
- GitLab webhook: use new_public_id() like the other webhooks.
- Dev: show the Lima VM start command on the localhost server page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Editing a domain (UI application, service and preview domains, the
General form, API application, preview, service, and service
application updates) now releases the hostnames that the edit
removed, like removing a domain does. Records that another live
resource still uses are kept.
- A per-hostname lock (managed-dns:{team}:{hostname}) now covers the
release (re-check, Cloudflare delete, row delete) and the create
paths. A release can no longer delete a record that is being added
again; a busy lock makes the release retry and the create wait or
retry, and a create after a release makes the record again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Gitea handler read the pull request commit from a top-level
head.sha that Gitea does not send, so preview deployments always used
HEAD. It now reads pull_request.head.sha, with the top-level after of
newer Gitea versions as a fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every webhook handler (GitHub, GitLab, Gitea, Bitbucket, GitHub App,
GitLab App) now reads the fields it uses through typed readers: refs,
titles, and actions must be strings, ids positive integers, commit
SHAs 7-64 hex characters, and URLs http(s). An invalid value returns
"Nothing to do. Invalid '<field>' in the request." without a
deployment. Signature checks and the failure lockout are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A Compose bind volume with a Coolify `content:` block could make
Coolify write any host file (for example /root/.ssh/authorized_keys)
when the file was loaded. Content sources must now be inside the
resource directory (./ paths); other sources get a clear validation
error for services and applications (UI, API, load, deployment),
and every content write is confined again on the server. Bind
mounts without content are unchanged. All 94 template content
volumes use ./ paths and still work.
- Quote container names in scheduled tasks and in the stop actions for
applications, previews, and services.
- Fix a development-only crash when a preview with a bind mount was
parsed (the preview suffix returned a plain string).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GitLab, GitHub, and Gitea crashed with a 500 on push payloads without
a commit list, and other malformed payloads (missing repository,
project, ref, Bitbucket changes, non-string values, unsupported
Gitea events) also returned 500. They now get a clean response; a
push with an unknown file list still deploys, and a branch deletion
does not deploy.
- The manual webhook lockout counts only distinct failed attempts: the
same wrong GitLab token, or an identical HMAC redelivery, counts
once, so a misconfigured hook no longer locks out a valid one. Every
new guess still counts (30 per scope and minute). Attempts are
stored only as HMAC hashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Git-based Docker Compose applications skipped the Compose injection
validation that services use. It now runs when the file is loaded or
reloaded, when the raw Compose is saved (UI and API create), and at
deployment before any command uses the file; an unsafe file is not
saved, and a deployment stops with a clear log line.
- All 371 service templates and realistic Compose files still pass.
- Network names may now mix text with $VAR, ${VAR}, ${VAR:-default},
or ${VAR-default} (for example ${COMPOSE_PROJECT_NAME}_default), so
such existing applications keep deploying; command substitution and
unsafe defaults stay rejected.
- Quote the preserved-repository path in a stat command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Imports store their cleanup data (names and paths, no credentials)
on the activity. When Coolify fails an import after a restart or as
stale, it queues a stop of the restore inside the database container
(only processes of that operation and their children, bottom-up so
the database server is not affected) and the normal cleanup.
- The cleanup runs once per import, and a stopped CoolifyTask does
not run again when the queue retries it.
- The stop message stays on the activity: RunRemoteProcess saves the
process id at start from the stored properties and keeps a stop
status at the end, and CoolifyTask::failed() keeps the message.
- Regenerate the OpenAPI spec (409 for database start, restart, and
import, plus earlier API changes that were missing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two parallel start requests both passed the in-progress check and got
200, because the start activity is created later by the queued action.
A per-database reservation (Cache::add, 600 s) is now taken before the
check, so the second request gets 409. The action releases it when it
creates its activity, skips, or fails. Restart, import, the deploy
API, MCP, and Livewire use the same reservation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A Coolify restart now fails queued or running database imports, so
they no longer block all later imports with 409.
- An import without progress for the SSH command timeout plus 30
minutes (at least 2 hours) is stale and no longer blocks.
- The import sets its operation property when the activity is created,
so a worker cannot save the activity without it.
- Start and restart refuse a second operation while a start, restart,
or import is in progress, in the UI, the API (409), and actions.
- DatabaseStartJob runs only while its activity is still queued and
the newest one, and holds a lock per database during the commands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Failed manual webhook deliveries were counted per provider and source
IP. On Coolify Cloud many customers share the Git provider IP, so one
repository with a wrong secret locked out valid deliveries of all
other repositories for 60 seconds.
The failure key now also contains the repository and branch. Guessing
the secret of one application stays limited to 30 tries per minute,
and a lockout rejects all deliveries in that scope, also correct ones.
A GitLab request without a token no longer counts as a failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since GitHub App and GitLab API calls use the outbound URL guard,
GitHub Enterprise or GitLab on a private network failed with "Webhook
URL resolved to an unsafe IP address" unless an admin allow-listed it.
On self-hosted instances, Git source URLs and requests now allow
private (RFC 1918), CGNAT (100.64/10, Tailscale), and IPv6 unique local
addresses, plus internal hostnames such as .internal, .local, and
container names. Loopback, localhost, link-local (cloud metadata),
0.0.0.0, and other reserved targets stay blocked. Redirects stay off
and DNS stays pinned. Coolify Cloud and all other outbound URLs keep
the strict rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Proxy: list and delete Traefik ACME certificates from the server proxy
page via new TraefikAcmeService and Get/DeleteTraefikCertificate actions
- DNS: track ownership and cross-resource references for managed DNS
records so records are only deleted when no longer referenced; release
records asynchronously on resource deletion via ReleaseManagedDnsRecordsJob
and ManagedDnsRecordCleanup; harden Cloudflare provider deletion results
- Databases: fail closed on start when prerequisites or the CA certificate
are missing (DatabaseStartException, Server::ensureCaCertificate) and
clean up stale start activities via ResourceStartActivity
- Webhooks: throttle repeated manual webhook signature failures for
GitHub, GitLab, Gitea and Bitbucket
- Deployments: improve compose build-context handling and compose file
load error reporting
- Install scripts: rework terminal UI output in install.sh (stable and
nightly)
- Misc: settings sidebar accordion fixes, log drain toggle rollback,
add Serverside to README sponsors
- Add migrations and tests covering the above
The server role migration gives every normal server the combined role, and
the build server queries counted combined servers as build servers.
"Use a build server" then built on a random production server, the resource
picker listed each server twice, and "Deployments only" did not stop builds.
- Build server selection and the picker use only "Builds only" servers.
A null role falls back to the legacy is_build_server flag.
- Without a dedicated build server, builds fall back to the deployment
server, never to another combined server.
- A "Deployments only" server always builds on a build server and needs a
Docker image name. It never builds itself, except for restarts. Docker
image and Compose applications are not affected.
- Setting "Deployments only" requires a dedicated build server.
- The API keeps is_build_server in sync with the role for downgrades.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>