Preview Deployment Access Control#
Preview deployments in Dokploy are triggered by pull_request webhook events from GitHub. Because preview builds execute arbitrary code and run with access to server environment variables, the webhook handler at /api/deploy/github enforces three stacked access-control mechanisms before creating a deployment: PR author validation, collaborator permission gating, and label-based filtering.
All three checks happen inside the pull_request event branch, applied per-application after the HMAC-SHA-256 webhook signature is verified .
Collaborator Permission Gate#
Default behavior: enabled. When app.previewRequireCollaboratorPermissions !== false, the handler calls checkUserRepositoryPermissions for the PR author before allowing a deployment .
checkUserRepositoryPermissions calls octokit.rest.repos.getCollaboratorPermissionLevel and allows only three of GitHub's five permission levels: write, maintain, or admin . Users with read or triage — including all external fork contributors — are denied.
Fail-safe design: if GitHub returns a 404 (user is not a collaborator at all), the function returns { hasWriteAccess: false, permission: null } rather than throwing, so permission errors default to deny . Errors during permission checks also block the app .
This feature was introduced by PR #2192 and added the previewRequireCollaboratorPermissions boolean column to the application table (default true). The setting can be toggled off in the application's preview settings UI — but disabling it on public repositories is explicitly discouraged .
Security Notification Comment#
When any app blocks a PR author, the handler calls createSecurityBlockedComment to post an explanatory comment to the PR . The comment identifies the blocked user, their actual permission level, the required level, and instructions for resolution.
To avoid duplicate comments on subsequent pushes to the same PR, hasExistingSecurityComment scans the PR's existing comments for the marker string "🚨 Preview Deployment Blocked - Security Protection" before posting .
Label-Based Filtering#
If an application sets previewLabels (a text[] column added by PR #2231), only PRs carrying at least one matching label will trigger a preview build. PRs without a matching label are silently skipped for that app .
When previewLabels is empty, the label check is bypassed entirely — all PRs qualify .
The labeled PR action is treated as a deployment trigger (shouldCreateDeployment = true), which means adding a qualifying label to an already-open PR creates a new preview deployment if one doesn't exist yet . This was a deliberate fix in PR #3960 to handle the case where a PR was opened before its required label was added. The unlabeled action is recognized but does not trigger a new deployment .
Preview Limit#
After label filtering, the handler enforces a per-app previewLimit. If the current number of preview deployments for an app equals or exceeds the limit, the PR is skipped . A previewLimit of 0 means no limit.
Decision Flow#
pull_request event (opened / synchronize / reopened / labeled / unlabeled)
│
├── Missing prAuthor? → 400 error
│
├── For each app with isPreviewDeploymentsActive = true:
│ ├── previewRequireCollaboratorPermissions !== false?
│ │ ├── checkUserRepositoryPermissions()
│ │ │ ├── write / maintain / admin → proceed
│ │ │ └── read / triage / none → block + security comment (deduplicated)
│ │
│ ├── previewLabels configured?
│ │ ├── PR has matching label → proceed
│ │ └── No match → skip
│ │
│ ├── previewDeployments.length > previewLimit? → skip
│ │
│ └── shouldCreateDeployment (opened/sync/reopened/labeled)?
│ ├── No existing preview → createPreviewDeployment()
│ └── Existing preview → re-queue deploy job
│
└── unlabeled → no new deployment created
Key Files#
| File | Purpose |
|---|---|
apps/dokploy/pages/api/deploy/github.ts | Webhook handler — all three access-control checks |
packages/server/src/utils/providers/github.ts | checkUserRepositoryPermissions — GitHub API permission check |
packages/server/src/services/github.ts | createSecurityBlockedComment, hasExistingSecurityComment, getSecurityBlockedMessage |
packages/server/src/db/schema/application.ts | previewRequireCollaboratorPermissions (bool, default true), previewLabels (text[]), previewLimit (int) columns |
apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx | UI toggle for previewRequireCollaboratorPermissions and previewLabels |