Add gitea_app_uuid to the allowed fields for application create and
update endpoints, with string validation on update, so Gitea App
sources can be set through the API.
Add a test checking that a closed GitLab merge request on the manual
webhook closes the matching preview and loads only applications of
that repository.
Allow validated host file and directory mounts outside resource directories
while protecting external paths from deletion.
Clean up cancelled deployment containers and clear tracked process IDs.
Preserve legacy KeyDB and Dragonfly password parsing, accept server backup
paths without extension restrictions, and allow Compose variable proxy ports.
- Manual webhooks (GitHub, GitLab, Gitea, Bitbucket) now reject all
deliveries for a locked-out scope before reading secrets or verifying
signatures, including correctly signed ones, until the window expires.
- ProvisionGithubRunnerJob checks whether the execution has already
finished before and after each provisioning step, and only sets
ready_at or Failed while it is still occupying/provisioning. A runner
that is cancelled, timed out or completed mid-provisioning is no longer
started, retried or overwritten.
- VolumeCloneJob creates bind-volume clones as plain local volumes
without the source driver options, so the clone never mounts the
source host directory.
- Image pulls for additional servers no longer ignore docker pull errors.
- Add tests for each change.
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.
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.
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>
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>
- 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>
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>
- 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
Centralize scp-style Git URL parsing so user@host:path (including custom
usernames and embedded ports) is accepted and converted to HTTPS for
public clones, API create, webhooks, validation, and commit/branch links.
Strip leading port segments from scp-style GitLab repository URLs so manual webhook matching compares the repository path consistently. Cover both ported and unported SSH URL forms.
Git hosts treat owner/repo names case-insensitively, but the exact
repository match used a case-sensitive comparison, so a payload whose
casing differed from the stored git remote would fail to match and
skip a legitimate deployment.
Lowercase both canonical repository paths before comparing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The manual webhook handlers selected target applications with a
`git_repository LIKE %full_name%` substring query, so a payload
repository name could match unintended applications when repository
names overlap.
Add a `MatchesManualWebhookApplications` trait that validates the
incoming `owner/repo` value and matches `Application.git_repository`
by exact normalized path. Github, Gitlab, Gitea and Bitbucket manual
handlers now use it, reject invalid repository input early, and return
a consistent generic webhook failure payload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Auto-generate a 40-char random secret for each manual_webhook_secret_* column on Application creation so new apps are never left with an empty secret.
- Add encrypted cast for the four webhook-secret columns; backfill migration re-encrypts existing plaintext values and fills missing ones.
- Reject webhook deliveries when the stored secret is empty (GitHub, GitLab, Bitbucket, Gitea manual endpoints).
- Bitbucket: require the sha256 algorithm prefix on X-Hub-Signature instead of trusting the client-supplied algo.
- GitLab: drop the ?? '' fallback on the token comparison.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>