Skip to content

Comparison

Positioning, not a ranking. All of these projects are worth using. This page exists to make the actual technical differences legible, sourced from direct competitor research (cloned source, changelogs, and issue trackers for each project, read at the level of detail this project's own research already produced).

At a glance

ProjectNode controlOrchestrationObservabilityIngressLicenseBuild engine
CoolifySSH plus CLI-shelled docker/docker compose via instant_remote_process(), every remote operation is a heredoc of shell commands over SSHDocker Compose per app, cutover via Traefik label discovery (rolling_update())Bolted-on agent ("Sentinel"), mandatory as of a 2026-08/09 change (previously opt-in), still a separately-lifecycled processTraefik, separate long-lived container, auto-discovers containers via Docker labelsNot stated in researchNixpacks or Dockerfile (per-app config column)
DokploySSH-tunneled Docker Engine API (via dockerode) for the core deploy call; CLI shelled over an ssh2 exec channel for lifecycle ops, cleanup, and the build pipelineDocker Swarm services: service.update()/createService(), Swarm's native UpdateConfig/RollbackConfig/FailureAction: rollbackSeparate Go binary (apps/monitoring), polling ticker, container stats default every 60sTraefik, run as a Swarm service on the shared overlay networkNot stated in researchNot stated in research
CapRoverDocker Swarm API via dockerode; DockerApi.initSwarm() runs even on a single nodeDocker Swarm services, AUTO update order (stop-first if volumes mounted, start-first otherwise)Optional sibling containers (NetData, GoAccess), not built into corenginx, sibling Swarm service; config written to a .fut file, validated with nginx -t, then SIGHUP-reloadedNot stated in researchNot stated in research
DokkuSSH/git push triggers the local dokku bash entrypoint directly on the host; no persistent daemon, no separate node agentCustom Bash scheduler (scheduler-docker-local) driving plain docker containers, no SwarmNot stated in researchnginx by default (pluggable per-app: Caddy, HAProxy, Traefik, OpenResty), config rendered via sigil and reloaded with nginx -s reloadNot stated in researchHerokuish buildpacks (Heroku-style app.json/Procfile) or a Dockerfile
KamalOne-shot SSH CLI session per command, no daemon, no agent, state re-derived live from docker ps/docker inspect every invocationNone: no scheduler or daemon. CLI runs docker run per host; kamal-proxy performs an in-memory atomic target swap after its own HTTP health probeNone built in; kamal-proxy exposes a bare Prometheus metrics port to scrape yourself, no query APIkamal-proxy, one standalone Go container per server, atomic in-process routing table updateMITDockerfile-based build step (buildpacks requested via community PR, not shipped)
LevelrailReverse-dialed gRPC agent with mTLS; no CLI shelling anywhere, Docker Engine API wrapper onlyCustom Go reconciler over the Docker Engine API, level-triggered, blue-green default strategy, rolling/recreate also supportedNode-local metrics (15s resolution) and full-text log store, federated query API, threshold alerting with multi-channel notifications, crashloop detection, Prometheus remote-read, all shippedEmbedded Caddy, driven in-process via its admin API; automatic internal-issuer TLS works today, a public ACME issuer and settings toggle are built but unverified against a live domainApache 2.0 (this project's own settled decision)BuildKit-based Dockerfile builds with local cache, Railpack auto-detection (Node.js/Go), static-site serving

Levelrail vs Coolify

Control path: Coolify drives every managed node over SSH, shelling out docker and docker compose commands, then parsing the text output. Levelrail never shells a CLI command against a node; instead, it wraps the Docker Engine API directly.

Deploy health checks: Coolify's per-deploy health check is off by default, so a broken deploy can be marked successful while the previously-working container is deleted underneath it. Levelrail only reports a cutover complete once the new container's readiness probe has actually passed.

Current architecture: An earlier pass of this page noted an in-progress v5.x rewrite (a Rust per-node agent talking gRPC, Caddy replacing Traefik). This was re-verified directly against upstream on 2026-09-17 and is no longer accurate. The v5.x branch has been stale for six months and never merged. What was tracked as v4.x was folded into main (a naming consolidation, not a fork). There is no live rearchitecture in flight today: the SSH-and-shell-out architecture is Coolify's current and only shipping design.

Levelrail vs Dokploy

Orchestration model: Dokploy is a thin control surface over Docker Swarm. Its rolling update, rollback, and cross-node routing are all Swarm's own mechanisms. A deployment is marked "done" the instant the Swarm API call returns, before there's any evidence the new task is actually healthy.

Levelrail's reconciler owns cutover, rollback, and node placement directly. A status condition is only written once the new container's readiness probe passes.

Common pain points: Dokploy's most-discussed GitHub issues cluster around networking and ingress fragility on Swarm's routing mesh, plus a separately-configured Traefik container. Issues include Traefik breaking on restart and gateway timeouts on worker-node replicas.

Levelrail's embedded, in-process Caddy plus its own WireGuard mesh are built to avoid this exact failure class.

Levelrail vs CapRover

Swarm dependency: CapRover standardizes on Docker Swarm even for single-node installs.

Deploy health checks: CapRover has no per-app deployment health check. A deploy is considered successful once the image builds and the Swarm API call resolves. A crashlooping app after deploy gets reported as success, and the operator finds out from a 502 error.

Rollback: CapRover has no rollback feature in its backend. The only path back is manually re-tagging and re-deploying an old image by hand.

Levelrail gates cutover on the app spec's readiness and liveness probes, and ships rollback with pinned prior images as a first-class, always-available action.

Levelrail vs Dokku

Deploy ordering: Dokku gets the deploy ordering right: new container up, health checks pass, proxy config regenerated and validated, then the old container is stopped.

Idle cost: Dokku has zero idle cost since there is no persistent daemon. Every command is a fresh SSH or git-triggered bash invocation.

Tradeoffs: The cost of no daemon is that Dokku cannot do anything proactive between commands. No event-stream consumption, no background reconciliation. Its boot-time container recovery script is an admitted "temporary hack" the maintainers have carried since a years-old issue.

Rollback: Dokku's default scheduler has no rollback command. Rollback only exists on the newer, opt-in Kubernetes-backed scheduler.

Levelrail's agent streams Docker events continuously rather than triggering only on command, and rollback with pinned images is available from Phase 1, not deferred to an alternate scheduler.

Levelrail vs Kamal

Design philosophy: Kamal is the deliberate outlier: no daemon, no agent, no database. Just a one-shot SSH CLI plus a standalone Go reverse-proxy container (kamal-proxy) that performs an in-memory atomic target swap once its own HTTP health probe passes.

Strengths: It is the lightest-weight design here. Its two-stage health gate (container state, then an HTTP probe) is a pattern worth keeping.

Limitations: Kamal is not a control plane. There is no persistent agent, no event-driven observed state, and no queryable metrics store. (kamal-proxy exposes a bare Prometheus port to scrape yourself, nothing federated.)

Critical issue: Its single most-commented issue in the whole tracker (76 comments) is the SSH transport itself disconnecting mid-command. This failure category doesn't exist without a reachable SSH session driving every deploy.

Levelrail's approach: We keep Kamal's health-gate discipline but replace the SSH transport with a persistent, reverse-dialed gRPC agent. We add the federated, node-local observability Kamal has no path to.

Beyond the reconciler: operational surface

The sections above cover how each project talks to a node and cuts over a deploy. That's the architectural core, but an operator running this in production day to day also needs access control, alerting, and managed data stores that don't fall over.

This section describes what Levelrail itself has shipped. It is deliberately scoped to Levelrail only, not a line-by-line claim about competitors. That would need the same level of sourced research the architecture table above got, which this project hasn't done yet.

Access control

An IAM-style policy engine (internal/api/iam.go) with AWS-IAM-shaped Allow/Deny statements:

  • Attachable to a user or an API token
  • Scoped to a specific resource (app:myapp, database:mydb, or *)
  • Additive on top of a flat abilities list
  • Three curated role presets (admin, operator, viewer) apply a full ability set in one action instead of hand-picking abilities
  • Every request (CLI, dashboard, MCP server, or raw API) runs through the same ability-check hook and lands in a queryable audit log
  • CSV export and configurable retention included

Feature flags

Boolean flags plus an optional gradual rollout percentage:

  • Scoped to an app
  • Read live at runtime via GET /api/v1/flags/evaluate/{key} with a read-scoped API token
  • No redeploy or restart required, since the value is never baked into a container image

Alerting

Nine rule kinds: threshold, crashloop, certificate expiry, OS patch status, scheduled-task failure, node disk space, node resource usage, domain health, missing backup.

Seventeen notification channel kinds: webhook, Slack, Discord, email, Telegram, Pushover, PagerDuty, Microsoft Teams, Resend, ntfy, Gotify, Mattermost, Lark, Rocket.Chat, Opsgenie, Webex, Google Chat.

Each channel is independently queryable for delivery history, with retry-with-backoff on every HTTP-based kind.

Managed databases

Eight engines: Postgres, Redis, MySQL, MongoDB, MariaDB, KeyDB, Dragonfly, ClickHouse.

All accessed through one dynamic engine registry, not eight separate implementations. Features apply generically across all eight:

  • Scheduled backups with retention
  • Restore and restore-into-a-new-resource
  • Automatic post-backup verification (re-download, re-hash, compare against what was recorded at backup time)

Trust beyond the reconciler

None of this changes the answer to "why build a new one instead of using an existing platform." That answer is still the architecture. A reconciler that never SSHes into a box is not, by itself, a reason to trust a tool with production secrets and access control. This section is the evidence for the second half of that trust.

What Levelrail doesn't do (yet)

Grounded in this project's own current feature and build status, not the original phase plan, since the actual build is further along in places than that plan suggests (multi-node and the WireGuard mesh are shipped, not "not started"). See docs/roadmap.md for the full current status; the real gaps as of today:

  • Real public ACME certificates, verified. TLS today defaults to an internal, self-signed issuer. A real Caddy ACME issuer, a settings toggle, and form validation are built and wired end to end, but issuance against a live domain has only been unit-tested at the config level, not spot-checked against a real domain.

Feature matrix

The "Beyond the reconciler" section above described only Levelrail, not a line-by-line comparison of competitors. This table closes that gap using the same sourcing method as the architecture table at the top of this page.

Sourcing methodology:

Every competitor claim comes from reading the cloned source directly:

  • Coolify: Laravel/Livewire app
  • Dokploy: TypeScript monorepo
  • CapRover: Node.js/TypeScript app
  • Dokku: Bash plugin tree
  • Kamal: Ruby gem

This is from the project's own private competitor research, not general knowledge. Where a clone didn't contain enough evidence to answer confidently, the cell says "not stated in research" rather than guessing.

Levelrail's column is sourced from docs/roadmap.md (current as of 2026-09-01), cross-checked against the file paths cited on main.

FeatureLevelrailCoolifyDokployCapRoverDokkuKamal
Multi-user accounts / teams, invite modelYes, real accounts (email or OAuth), plus a self-service email invite flow (internal/api/invites.go): any caller holding write ability can invite, capped server-side to abilities they themselves already hold (an operator can invite another operator or a viewer, never an admin), the platform mints and emails a one-time link (or returns it directly if SMTP isn't configured)Yes, team-scoped, but invite is admin/owner-initiated, not open signup: InviteLink::generateInviteLink() pre-creates the invited User (app/Livewire/Team/InviteLink.php:77-87)Yes, and genuinely self-service among permitted members: any member holding member.create can invite (organization.ts:300 inviteMember)No. Single-tenant by design: UserManagerProvider.get() throws for any namespace but root (src/user/UserManagerProvider.ts:14-19)No accounts, only admin-added SSH keys (ssh-keys:add); no self-service pathNot applicable, no user/team concept at all (local CLI over the operator's own SSH access)
RBAC granularityFine-grained, resource-scoped: IAM-style Allow/Deny policies attachable to a user or token, scoped to app:name/database:name/* (internal/api/iam.go, internal/store/iam_policy.go), additive on top of 3 role presetsCoarse: one team-wide role column (owner/admin/member), no per-app scoping (Team::members() pivot, app/Models/Team.php:258-260; policies check isAdminOfTeam)Fine-grained on paper, roughly 30 resources with distinct actions (access-control.ts:14-52), but custom per-resource roles require a paid license; the free tier gets 3 fixed rolesNone, moot with one account: UserJwt carries only namespace/tokenVersion, no role field (src/models/UserJwt.ts)None in core: binary admin-vs-not by SSH key naming convention; per-app ACL is a user-auth trigger stub only community plugins implementNot applicable, no identity system to scope
One-click template catalog (count)123 hand-authored templates (internal/catalog/catalog.go), served over GET /api/v1/service-templates371 files in templates/compose/Fetched at runtime from templates.dokploy.com (packages/server/src/templates/github.ts), not bundled; count not verifiable from this cloneMechanism exists (OneClickAppDeployManager.ts) but the manifest lives in a separate, uncloned repo; count not stated in researchNone bundled; roughly 80+ separate official plugin repos, each installed individually, no in-product browse-and-install catalogNone; kamal init only scaffolds one app's own deploy.yml, not a service catalog
Docker Compose as a deploy targetYes (internal/compose, multi-service DeploySpec fan-out)Yes, first-class build_pack option (app/Models/Application.php:47)Yes, first-class, dedicated compose table/router (packages/server/src/db/schema/compose.ts:33-58)No for user apps: ICaptainDefinition accepts a Dockerfile/image/template, one of four, never compose (src/models/ICaptainDefinition.ts); compose syntax is reused only inside the one-click template DSLNo; strictly single-container-per-process-type via Dockerfile/Procfile across every builder-* pluginNo; own deploy.yml schema only, no compose parser found
Managed database engines8: Postgres, Redis, MySQL, MongoDB, MariaDB, KeyDB, Dragonfly, ClickHouse, one dynamic engine registry8: Postgres, MySQL, MariaDB, MongoDB, Redis, KeyDB, Dragonfly, ClickHouse (app/Models/Standalone*.php), essentially the same list as Levelrail5: Postgres, MySQL, MariaDB, MongoDB, RedisNone first-class; databases run as ordinary one-click apps with no dedicated backup/credential/resource-limit wiringNone in core; separate official plugins per engine (dokku-postgres, dokku-mysql, dokku-mongo, and similar), opt-in installsNone; "accessories" are generic sidecar containers the operator configures by hand, no managed backups or credentials
Scheduled backups with automated restore verificationYes: scheduled/cron backups across all 8 engines plus app volumes, with automatic post-backup verification (re-download, re-hash, compare checksum/size/format against what was recorded) after every run, and restore/restore-as-newScheduled, but not verified: only a non-zero-byte-size check (DatabaseBackupJob::calculate_size()), no checksum or test-restore; no restore job found anywhere in the codebaseScheduled (BullMQ cron workers), but no automated verification step; restore is a separate, manually-triggered actionOn-demand only, no scheduling in core (BackupManager.ts has no cron); a real restore path exists, but zero automated verificationNone in core; the backup plugin was deprecated in 0.4.x, current guidance is a manual tar of the data directory with manual integrity checkingNone; no backup feature exists anywhere in the codebase
Preview environments per pull requestYes, opt-in per app. GitHub, GitLab, and Bitbucket webhook lifecycles are all live end-to-end tested (open/update/close, including that closing one actually stops and removes the running container). Scheduled TTL sweep, dashboard and CLI wiringYes (app/Models/ApplicationPreview.php, webhook handling across GitHub/GitLab/Gitea)Yes, full CRUD router plus GitHub webhook handling for opened/synchronize/reopened/closed (preview-deployment.ts)No; zero matches for preview/pull_request in sourceNo in core; the only "preview" hit is an unrelated k3s-scheduler manifest-diff command, not a per-PR ephemeral environment. The documented review-apps pattern is an external CI recipeNo; zero matches for preview in source
Feature flags / runtime config without redeployYes: boolean plus gradual rollout percentage, scoped to an app, read live via GET /api/v1/flags/evaluate/{key}, never baked into the container imageNo dedicated flag system found, only ordinary DB-backed settings columnsNo; no feature-flag concept foundNo operator-facing flag system; the only flag found is an internal vendor kill-switch gating the paid Pro tierPartial in name only: config:set --no-restart skips the container restart, but the running container then simply never sees the new value until next recreate, no live-read pathNo; every env change requires a fresh kamal deploy since containers read env once at boot
Alerting rule kinds and notification channel kinds9 rule kinds (threshold, crashloop, certificate expiry, OS patch status, scheduled-task failure, node disk space, node resource usage, domain health, missing backup) and 17 channel kinds (webhook, Slack, Discord, email, Telegram, Pushover, PagerDuty, Microsoft Teams, Resend, Gotify, Ntfy, Mattermost, Lark, Rocket.Chat, Opsgenie, Webex, Google Chat)Mostly fixed lifecycle events (deployment failed/success, restart-limit-reached, backup failed, server unreachable) plus one real threshold rule (disk usage percent); 6 channels: Discord, Slack, Telegram, Pushover, email, generic webhookFixed lifecycle booleans (deploy, build error, backups, restart, cleanup) plus one threshold rule (server CPU/memory, via a separate Go monitoring binary); 12 channels: Slack, Telegram, Discord, email, Resend, Gotify, Ntfy, Mattermost, Pushover, custom webhook, Lark, TeamsSplit across two disconnected systems: node-resource alerts fully delegated to a sibling NetData container (no rules of CapRover's own), plus a subscription-gated Pro event system (login, build success/fail) with only email/webhook channelsNone in core; no alert/notification plugin foundNone built in; only a lifecycle-hook mechanism (pre-deploy, post-deploy, and similar) a user can wire to their own notification script by hand
CLI tool maturity--output json|table|text, --query (JMESPath), shell completion for bash/zsh/fish, and an RFC-8628-shaped device-login flow (auth login --device)No standalone CLI exists at allNo standalone CLI exists at allA separate caprover CLI package exists but its source isn't in this clone; not stated in research at the code levelThe CLI is the whole product: 22+ subcommands support --format json, bash completion ships (contrib/bash-completion); no device-login flow, access is SSH-key based by designMature Thor-based CLI (kamal config, kamal docs) but no --json/structured output and no shell-completion script found; SSH-key based, no device-login
MCP server / AI-agent-facing API layerYes, cmd/levelrail-mcp, 68 tools, same bearer-token/ability model as the REST API and CLIYes, roughly 45 tools: app/Mcp/Servers/CoolifyServer.php, gated per-team by a settings flagNo; zero matches for mcp in the repoNoNo, and no REST API at all by designNo; no HTTP API of any kind
Audit logging with per-request attributionYes: every request through the single ability-check hook lands in a queryable audit log, attributed to a client kind (cli/dashboard/mcp/api) via the User-Agent header, CSV export, configurable retention with a periodic sweeperNo general audit trail; only narrower activity/deployment command-output logs (spatie/laravel-activitylog), no actor-attribution for arbitrary mutationsYes, but narrower and paid: an auditLog table records user/action/resource (packages/server/src/db/schema/audit-log.ts), no IP/user-agent/client-kind, and the whole feature is gated behind an enterprise licenseNo; only anonymous usage telemetry sent to CapRover's own servers, explicitly excluding sensitive eventsPartial, opt-in, unstructured: an events log attributes each plugin-trigger call to the SSH key's name/fingerprint, but it's off by default and unqueryable (dokku events:on)Partial but decentralized: a per-host, per-service log file via auditor.rb, attributed to a git-email or shell username, not a central request-attributed log
API token scopingFine-grained abilities (read, read:sensitive, write, write:sensitive, deploy, root) plus resource-scoped IAM policies attachable to a tokenNamed ability strings (read, read:sensitive, write, write:sensitive, deploy) via Sanctum, no resource scoping beyond thatAll-or-nothing in practice: an apikey.permissions column exists in the schema but token-creation code never sets it, so a key just inherits its user's full roleNo scoping; purpose-tagged JWTs (cookie vs webhook vs download) but no capability distinction within the admin sessionNot applicable, no REST API or token conceptNot applicable, no REST API or token concept; SSH-key access only

What this table shows

Current gap: The template catalog. Levelrail ships 123 curated entries versus Coolify's 371, an intentional curation-over-count bet per ADR 015. This is a real breadth gap.

Gaps that have closed:

  • MCP tool count - Levelrail now ships 68 tools against Coolify's roughly 45
  • Notification channel breadth - Levelrail ships 17 channel kinds against Dokploy's 12
  • Invite model - Invites are now self-service for any write-ability caller, capped server-side so nobody can invite someone more privileged than themselves, the same shape as Dokploy's

Worth naming plainly: Dokploy's fine-grained RBAC and per-request audit log are real, working functionality. Both sit behind a paid enterprise license upstream rather than Dokploy's free tier, which matters for an apples-to-apples read.

Backup verification: This row is the sharpest example of Levelrail's differentiation. None of the other five projects researched here run any automated check on a backup after taking it:

  • Coolify checks only that the dump file is non-empty
  • Dokploy and CapRover do no check at all
  • Dokku and Kamal have no built-in backup feature at all

Everywhere else in this table, Levelrail matches or leads.

Released under the Apache 2.0 License.