Atlas expedition 7 angles ↓

Self-Hosted PaaS for Homelab PR Previews: Decision Framework 2026

Only Coolify and Dokploy deploy PR previews natively; both bundle Traefik and demand dedicated VM isolation from your existing proxy — the full decision framework across six options on ten criteria.

7 succeeded 116 sources ~32 min read #200

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 exec or API call to create preview_<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.com at the VM IP, or skip DNS entirely and use the built-in preview-${appName}-${id}.traefik.me free 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), run docker compose -p pr-$N -f docker-compose.yml -f docker-compose.preview.yml up -d --build.
  • scripts/teardown.shdocker compose -p pr-$N down -v --remove-orphans && rm -rf /opt/previews/pr-$N.
  • cron-cleanup.shfind /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 named pr-{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-$N on 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 deploy on pull_request opened/synchronize — with a per-PR config/deploy.yml that sets a unique app: myapp-pr-$N and destination host.
  • Registry push step in CI (Kamal requires a Docker registry; images must exist before deploy).
  • kamal remove on pull_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.

Sub-topics