Webhook Endpoint Architecture#
Dokploy exposes three webhook handler endpoints under apps/dokploy/pages/api/deploy/ :
| Endpoint | File | Targets |
|---|---|---|
POST /api/deploy/github | github.ts | All GitHub-connected applications & compose apps |
POST /api/deploy/[refreshToken] | [refreshToken].ts | Single application (GitLab, Bitbucket, Gitea, Docker, Git) |
POST /api/deploy/compose/[refreshToken] | compose/[refreshToken].ts | Single compose deployment |
These two patterns are architecturally opposite:
- GitHub: one shared URL for every application in a GitHub App installation — the endpoint identifies which applications to deploy via database queries.
- RefreshToken: a unique URL per application/compose — the token in the URL directly addresses a single record.
GitHub: Single Shared Endpoint#
Every GitHub App installation registers the same callback URL, <host>/api/deploy/github, during provider setup . All push and pull_request events from that installation land here regardless of which repository or application is involved.
Request validation flow :
- Reject if
x-hub-signature-256header is missing. - Look up the GitHub provider record by
installation.idfrom the payload body. - Verify the HMAC-SHA-256 signature against that provider's
githubWebhookSecret.
Application matching is done with broad db.query.applications.findMany() calls filtered at the DB level — never narrowed by any token in the URL itself :
push+ branch events: filter bysourceType=github,autoDeploy=true,triggerType=push,branch,repository,owner,githubId.push+ tag events: same filters buttriggerType=tag.pull_requestevents (preview deployments): filter bysourceType=github,repository,branch,isPreviewDeploymentsActive=true,owner,githubId.
One webhook event can therefore trigger deployments for multiple applications in a single request .
Cross-Application Conflict Implications#
Because the GitHub endpoint matches all applications that share the same GitHub provider and repository/branch combination, a single push can trigger simultaneous deploys across unrelated Dokploy applications. The key implications:
- Fan-out by design: a push to
mainon repoacme/apiwill trigger every application in that installation that is configured foracme/api@mainwithautoDeploy=true. There is no per-application filter key in the webhook URL. watchPathsas a narrowing filter: theshouldDeploy()helper gates each application independently on its configured glob patterns before enqueuing. This is the only per-application opt-out for push events.- No webhook secret per application: validation happens at the provider level (one secret per GitHub App installation), not per application. A compromised provider secret exposes all applications under that installation.
- Preview deployment conflict surface: for
pull_requestevents, every matching application fires a preview build. Label filters (previewLabels) and collaborator permission checks (previewRequireCollaboratorPermissions) are the per-application gates .
RefreshToken Pattern (Non-GitHub Providers)#
For GitLab, Bitbucket, Gitea, and Docker registry webhooks, each application gets a unique webhook URL (/api/deploy/<refreshToken>). The handler fetches the single application record matching that token and rejects immediately if none is found . The provider type is detected at runtime from request headers (x-gitlab-event, x-event-key, x-gitea-event).
This means a non-GitHub webhook event is inherently scoped to one application — no fan-out, no cross-application ambiguity.
Key Source Files#
| File | Role |
|---|---|
pages/api/deploy/github.ts | GitHub webhook handler — installation lookup, multi-app fan-out, push/tag/PR dispatch |
pages/api/deploy/[refreshToken].ts | Per-app webhook for GitLab, Bitbucket, Gitea, Docker |
pages/api/deploy/compose/[refreshToken].ts | Per-compose webhook |
components/dashboard/settings/git/github/add-github-provider.tsx | Registers /api/deploy/github as the GitHub App webhook URL |