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.
Server certificates now use a prime256v1 (P-256) EC key. The CA keeps
secp521r1. BoringSSL-based Electron clients such as MongoDB Compass
cannot use a P-521 server key.
Add a feature test that checks the CA stays on P-521 and the server
certificate uses P-256. It also checks that the server key matches its
certificate and that the CA signature verifies.
GetContainersStatus now resolves all unknown preview owners with one query,
like the Sentinel push. The lookup helper also no longer fails on an
application without a uuid.
Force start now uses the locked start path. It still skips the concurrency
limits, but a deployment that is no longer queued is not started again, and
the deployment page says so.
Domain Connect used the unfiltered server IPs, so a private address could be
published. It now uses the first public IP and refuses with the no public IP
message when there is none, like automatic DNS.
The release workflow only accepts vX.Y.Z tags that match the config version.
The nightly versions.json now matches versions.json, as the workflow test
requires.
- Regenerate SSL Certificates now writes server.crt/server.key for every
engine except MongoDB, which keeps the combined server.pem. Before, the
button always wrote server.pem, so Postgres, MySQL, MariaDB, Redis, KeyDB
and Dragonfly failed to start after a regenerate.
- The certificate renewal job uses the same rule, so MongoDB no longer
switches to server.crt/server.key on renewal and fails on restart.
- Starting an SSL database writes the CA that signs its certificate to
/data/coolify/ssl/coolify-ca.crt, so a stale CA file (for example one
written by another server entry for the same host) no longer causes
"certificate signature failure".
- Saving a custom CA certificate that does not match the stored CA
private key is rejected.
- The CA file write commands live in SslHelper::caCertificateFileCommands().
Fixes#10388Fixes#6771Fixes#11926
- The certificate delete dialog no longer lists a proxy restart it does not
perform, and its warning mentions the backup.
- Confirmation dialogs list actions with a dash instead of a trash icon.
- Error buttons get a red focus ring instead of the accent ring.
- Use existing icons for the TLS empty state and built-in shared variables.
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.
PUT /user/profile-information changed the email with no SSO check and no
verification. The profile page has its own verified email change, so the
Fortify feature, its action and its binding are removed.
- The S3 revalidation job stops within its time budget and continues from a
cursor on the next run.
- An hourly sweep deletes throttle rows whose subject no longer exists.
- Resolve relative LocalFileVolume paths against the resource directory.
- Create cloned volumes with the source driver and options.
- Stage clone archives for non-root users in a mktemp directory (mode 700)
instead of a predictable path in /var/tmp.
- Align the traffic window to the minute so the 60 s cache is used.
- Skip only an unsafe key instead of hiding the whole server.
- Look up unknown container owners in one query per push.
A queued start or restart whose reservation expired no longer runs. Failed
queued starts are recorded as failed activities and release the reservation.
- Decide whether to start a queued deployment inside the application and
server lock, so two requests cannot start the same app twice.
- Send the multi-server summary when the last child is cancelled.
- Show the registry message when an inline Dockerfile image is missing on a
deployments-only additional server.
Entries such as ${HTTP_PORT:-80}:80 blocked proxy start, save and restart.
Defaults are used for the conflict check and unresolved entries are skipped.
Coolify copies acme.json to a timestamped backup before every change and
keeps the last 10. The proxy page lists the backups with Restore and Delete
for team admins, and the restore dialog can restart the proxy right away.
Changing the Vault base URL or namespace, or the Infisical base URL or
client ID, sent the stored secret to the new settings. The secret must now
be entered again, and base URL changes are audit logged.
A user who accepted another team's invitation but had not set a password
yet was deleted, with their team data. Now only users in no team except
their own personal team are deleted.
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.
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
Accept Compose variables ($NAME, ${NAME}, ${NAME-default},
${NAME:-default}) in the `name:` of an external volume, and in the old
`external: {name: x}` syntax. Docker Compose resolves them from the
deployment .env. Coolify never removes external volumes, so it does not
need the resolved name.
The volume key must still be a literal Docker volume name, because
Compose does not resolve variables in keys. Names that are unsafe or use
unsupported syntax are still rejected, for example command substitution,
spaces, escaped dollars, and ${VAR:?err}.
Add tests for application and service parsing, legacy volume rows, the
StackForm save, legacy Compose parsing, and the rejected cases.
Pushes to main published three tags per commit (sha-<sha>,
sha-<sha>-amd64, sha-<sha>-aarch64). Publish the per-arch images under
rolling main-amd64/main-aarch64 tags instead, and keep one sha-<sha>
manifest per commit next to a rolling main tag.
Serialize runs so concurrent pushes cannot mix architectures, and label
images with the source revision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Git source HTTP clients previously disabled redirects entirely, so API
calls for renamed repositories (e.g. GitHub 301 to /repositories/{id})
failed. Add SafeExternalUrl::sameOriginRedirectOptions() and use it in
the GitSource HTTP macro to follow up to five strict redirects that stay
on the same scheme, host and port.
A redirect to a different origin gets a RequestException, because the
DNS pin covers only the original host and port. This blocks SSRF via
redirects to metadata or attacker hosts. Authorization headers and the
original request method and body are kept on same-origin redirects.
Tests cover following same-host redirects, keeping POST on a redirect,
refusing cross-host/scheme/port redirects and the five-redirect cap.
Hardcoded variables parsed from docker-compose are now redacted in the
environment variable list unless the user can update the resource. The
value is nulled server-side and flagged with is_value_hidden, so it no
longer reaches the Livewire snapshot or rendered HTML.
ShowHardcoded gets a locked isValueHidden property. When it is set, the
copy button is hidden and the details modal shows "Hidden (only admins
can view)" in place of the value. Previously the view checked member
role only.
Add tests covering redaction for members and visibility for owners.
SSH: sshd runs the remote command with the user's login shell, which
can be fish or csh/tcsh. Those shells cannot parse POSIX `if ... fi`, so
the bash/sh fallback wrapper now runs inside `sh -c "..."`. Unit tests
run the wrapper under every login shell that is installed and check that
it stays one simple sh command.
Secret managers: the SecretManagerLinks component now lists and resolves
tokens by the resource's team instead of the session team. A team switch
in another tab can no longer link a token from a different team.
SecretManagerLink::fetchSecrets() also throws when the linked token does
not belong to the resource's team, so no request is sent with it.
Also adds a lesson note: remove the SSH mux file before live SSH or
login-shell tests.
Mark DNS proposals, conflicts and server IP properties as #[Locked] so
clients cannot tamper with them. Re-resolve the zone per hostname,
lowercase hostnames, validate record content server-side and use the
resource's team id for DNS lookups and jobs.
Move modal-confirmation submitAction parsing into a tested JS helper
that strips quotes and keeps commas, parentheses and escaped quotes
inside quoted arguments. Pass the hostname to replaceManagedDnsRecord
through it instead of Js::from.
Rework member-permission tests to open the real DNS dialog rather than
setting locked properties directly.
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.