The sharpest divide across all six options is not feature richness or resource cost — it is whether PR preview automation is a platform feature or something you write. Only Coolify [1] and Dokploy [2] cross that line: both tear previews down automatically on PR close, post status comments via GitHub App, and isolate preview env vars from production secrets — without a line of custom shell. CapRover, Dokku, and Kamal all converge on the same GitHub Actions glue pattern once you bolt on PR preview support; at that point the PaaS layer stops earning its overhead relative to DIY.
The Traefik conflict is the second major gate. Your existing Traefik reverse proxy cannot peacefully coexist with either native-preview option on the same host: Coolify bundles its own Traefik/Caddy and hardcodes a port-80 validation check that prevents it starting alongside any pre-existing proxy [3]; Dokploy installs Traefik at setup time. The resolution — a dedicated KVM VM that the PaaS fully controls [4] — adds VM sprawl but eliminates all port contention. DIY bash, Dokku, and Kamal compose cleanly with an existing Traefik via Docker labels, no port negotiation, no second proxy.
Coolify vs Dokploy is the only matchup that matters once you decide to use a PaaS. Coolify leads on catalog depth (280+ one-click services [5]), ecosystem size (⭐ 57k, 325k users [6]), and built-in secrets quality (encrypted-at-rest + Docker Build Secrets [7]). Dokploy leads on backup (S3 volume backups and a DB restore UI that Coolify lacks [8]), a configurable max-previews cap (Coolify still has no per-app TTL or expiry [9]), and lighter idle RAM (~630 MB vs 800 MB–1.2 GB [10][11]). For many homelabbers the deciding input will be Coolify’s January 2026 disclosure of 11 critical CVEs (CVSS up to 10.0, including RCE-as-root and SSH key leakage to low-privileged members [12][13]) — patched in beta.445+ / v4 GA, but a meaningful signal about the platform’s security posture. The dashboard must not be internet-exposed without a VPN or Cloudflare tunnel regardless of option.
The GITHUB_TOKEN footgun documented in the 2026-04-27 Synology expedition applies to every path that uses GitHub Actions self-hosted runners for deployment: GITHUB_TOKEN cannot trigger downstream GitHub Actions workflows on the same repository [14], so CI jobs that depend on pull_request events emitted from the runner silently never fire. This affects DIY bash, CapRover, Dokku, and Kamal — all of which rely on runner-initiated token calls. The fix (a GitHub App installation token generated in the workflow) is one-time setup overhead that Coolify and Dokploy bypass entirely: they use webhook-based GitHub App flows that GitHub recognizes as distinct from runner events and honors for downstream CI triggers.
Secrets is the quietest cross-cutting differentiator. Coolify’s encrypted storage + Docker BuildKit secrets is the best built-in story [7]. DIY bash, Dokku, and Kamal default to plaintext .env files on disk — fine for a solo homelab, a meaningful gap the moment a second person gets shell access. Layering Infisical [15] on any of these closes the gap but adds operational surface. CapRover and Dokku lack role-based access entirely, so even encrypted env vars are visible to all deployers.
r/selfhosted 2026 consensus: Coolify is the default recommendation (⭐ 57k, 325k users [6]); Dokploy is the rising lightweight alternative, specifically praised for native PR previews [16]; CapRover is respected for 9-year stability but in slow-burn mode [17]; Dokku retains a terminal-native following for its 95 MB idle footprint [18].
The open question this expedition doesn’t close: whether Dokploy’s v0.27+ memory regression — idle RAM doubled, suspected cause identified but issue closed without a confirmed fix [11] — gets resolved before it forces a de facto 4 GB minimum on an already headroom-constrained Proxmox VM running two apps and five to ten concurrent previews.
Comparison Table
| Option | PR preview trigger | Custom glue remaining | Traefik coexist | GH integration | Idle RAM | Secrets | Backup / state | Self-update | Multi-app/env | Maturity / consensus |
|---|---|---|---|---|---|---|---|---|---|---|
| Coolify ⭐ 57k | Native — GitHub App [t1] | DB-per-preview script; TTL/expiry cron [t2] | ❌ owns 80/443 [t3] | Excellent — PR comments [t1] | 800 MB–1.2 GB [t4] | Encrypted + BuildKit [t5] | Partial (no vols) [t6] | 3 modes; snapshot first | ✓ full, 280 templates | v4 GA Apr 2026; 11 CVEs Jan 2026 [t7][t8] |
| Dokploy ⭐ 35k | Native — webhook/GH App [t9] | Wildcard DNS; max-preview config [t9] | ❌ bundles Traefik | Good — 5 git providers | ~630 MB+ [t10] | Env vars | S3 vols + DB UI [t11] | Docker-based | ✓ Swarm-native | Apr 2024; memory regression v0.27+ [t10] |
| DIY bash | GH Actions self-hosted runner [t12] | ~200 lines bash (deploy+teardown+cron) [t12] | ✓ labels only | GH Actions + HMAC webhook | ~0 MB platform | Manual .env / Docker secrets | Manual cron | n/a | Manual per-repo | Community-proven; full control [t12] |
| CapRover ⭐ 15k | REST API + GH Actions [t13] | Full lifecycle: create/build/post/delete app | ❌ owns nginx | Webhook — one branch/app | ~350 MB [t14] | Basic env vars | Manual | UI-driven, quarterly | Single-tenant; Swarm clustering | 2017; slow-burn since 2024 [t15] |
| Dokku ⭐ 32k | Community plugin (fragile) [t16] | SSH wiring + GH Actions + plugin config | ✓ nginx compat | Git-push SSH | ~95 MB [t17] | Env vars only | Plugin-based | bootstrap.sh |
Single-server; plugin ecosystem | 2013; active MIT; 339 releases [t17] |
| Kamal 2 ⭐ 14k | Custom GH Actions + scripts | Full preview lifecycle + registry push step | ✓ kamal-proxy | GH Actions + Docker registry | ~0 MB platform | .kamal/secrets file |
None built-in | gem update kamal |
Multi-server SSH | 37signals-backed; powers HEY.com; no UI [t18] |
Remaining Custom Glue per Option
Coolify
One-time setup: configure the GitHub App and grant it read on administration + code, read/write on pull requests [1]. After that, no deployment scripts. What remains:
- Per-preview DB script — Coolify does not provision isolated databases per preview; wire a
docker compose execor API call to createpreview_<pr_number>schemas and seed from a sanitized dump. - TTL/expiry cron — Coolify has no auto-expiry for running previews [9]; add a cron job that calls the Coolify API to list and delete previews older than N days.
- Wildcard TLS template — use
-(dash-separated), not.(dot-separated); the latter produces multi-level subdomains that break single-level wildcard certificates [19].
Dokploy
One-time setup: link a git provider, enable preview deployments, configure the max-preview cap (default: 3). What remains:
- Wildcard DNS — point
*.yourdomain.comat the VM IP, or skip DNS entirely and use the built-inpreview-${appName}-${id}.traefik.mefree subdomain (no DNS change required) [2]. - S3 backup destination — configure once in settings; Dokploy handles scheduling automatically.
- Nothing else. Dokploy’s built-in max-preview cap and auto-cleanup on PR close mean no expiry cron is needed.
DIY bash/compose
Estimated 200 lines across three files [20]:
scripts/deploy.sh— clone or fast-forward the PR branch, write per-PR.env.preview(project name, port formula, preview URL), rundocker compose -p pr-$N -f docker-compose.yml -f docker-compose.preview.yml up -d --build.scripts/teardown.sh—docker compose -p pr-$N down -v --remove-orphans && rm -rf /opt/previews/pr-$N.cron-cleanup.sh—find /opt/previews -maxdepth 1 -mtime +7 -exec teardown.sh {} \;as a daily safety net.- GitHub Actions workflow:
pull_request: [opened, synchronize, reopened, closed]→ dispatch to the self-hosted runner. docker-compose.preview.yml— Traefik labels (traefik.http.routers.pr-$N.rule=Host(...)) and resource limits (memory: 512m,cpus: 0.5).- HMAC validation if using adnanh/webhook [21] instead of the self-hosted runner.
- GitHub App installation token in the workflow if downstream CI pipelines must trigger from PR events.
CapRover
No native PR concept. The minimum viable glue:
- GitHub Actions job on
pull_request opened/synchronize: call CapRover REST API to create an app namedpr-{number}, set the branch, trigger a build, post the preview URL as a PR comment via the GitHub API. - GitHub Actions job on
pull_request closed: call CapRover REST API to delete the app. - Estimated: 80–120 lines of YAML + shell (or a JavaScript Action) for the REST lifecycle [22].
- Wildcard DNS and SSL still require the same one-time setup as any other option.
- GitHub App installation token required for downstream CI to trigger.
Dokku
No native PR concept. Two community paths, both incomplete [23]:
- dokku-pr-action: GitHub Action that creates a Dokku app on open, redeploys on push, destroys on close. Known failure: concurrent pushes hit Dokku’s deploy lock; per-preview databases unimplemented.
- Custom lifecycle (~150 lines): SSH into the Dokku host from GitHub Actions, run
dokku apps:create pr-$N,dokku git:sync,dokku ps:scale web=1,dokku apps:destroy pr-$Non close. - In both cases: SSH key wired into GitHub Actions secrets, GitHub App installation token for downstream CI, and Dokku’s Let’s Encrypt plugin for per-app TLS.
Kamal 2
No platform awareness of PRs at all. Full lifecycle is custom CI:
kamal deployonpull_request opened/synchronize— with a per-PRconfig/deploy.ymlthat sets a uniqueapp: myapp-pr-$Nand destination host.- Registry push step in CI (Kamal requires a Docker registry; images must exist before deploy).
kamal removeonpull_request closed.- No management UI — all status comes from CI logs or the Docker daemon on the target server.
- Effectively equivalent scope to DIY bash, with a cleaner deployment primitive but less homelab community documentation for this pattern.
Decision Rubric
Pick Coolify if you want the richest self-hosted PaaS (UI, monitoring, 280+ one-click services, native PR previews out of the box) and can afford to: dedicate a 4 GB+ KVM VM to it, keep it patched to the latest release, and place the dashboard behind a VPN. The DSM-installer limitation from the 2026-04-27 Synology expedition is gone — a Debian VM is the correct host and Coolify installs cleanly.
Pick Dokploy if native PR previews and better backup hygiene (S3 volume backups, DB restore UI) matter more than catalog depth, and if Coolify’s January 2026 CVE disclosure makes you want the less-audited-but-less-targeted alternative. Verify the v0.27+ memory regression is resolved, or provision 6 GB+ to absorb it safely.
Pick DIY bash/compose if your existing Traefik lives on the shared Docker host and you cannot (or will not) provision a second VM, you want every layer of the preview lifecycle to be code you own and understand, and you can spend 1–2 days on initial wiring. The ongoing maintenance cost is low once the scripts stabilise.
Pick Dokku if absolute minimum platform RAM (95 MB) is the constraint, you have one or two small apps, and a CLI-native git-push workflow is sufficient. Use dokku-pr-action for previews accepting its fragility, or skip previews entirely.
Pick CapRover if you deploy only single-container apps, need the lowest idle RAM among GUI-having PaaSes (~350 MB), and PR preview is not a primary workflow — the full scripted lifecycle via the REST API is functional but is table stakes you write yourself.
Pick Kamal 2 if you ship pre-built images from a Docker registry, want zero platform RAM overhead, prefer infrastructure-as-code YAML over a dashboard, and are comfortable writing the entire PR preview lifecycle in CI — there is no PaaS layer helping you here, but there is also nothing in the way.