Skip to main content

Coolify vs CapRover vs Dokku: Self-Hosted PaaS Platforms (2026)

·PkgPulse Team

TL;DR

Coolify is the modern self-hosted PaaS — beautiful UI, one-click deployments, Docker and Docker Compose support, Git integration, automatic SSL, database management, built-in monitoring. CapRover is the lightweight self-hosted PaaS — one-click apps, Dockerfile and Docker Compose, cluster mode, web dashboard, Let's Encrypt SSL, simple and battle-tested. Dokku is the smallest PaaS — Heroku-like git push deployments, buildpacks, plugin ecosystem, CLI-first, the original self-hosted PaaS. In 2026: Coolify for the best UI and modern features, CapRover for simple Docker-based deployments, Dokku for Heroku-style CLI workflow.

Key Takeaways

  • Coolify: 25K+ GitHub stars — modern UI, Docker/Compose, Git webhooks, databases
  • CapRover: 13K+ GitHub stars — one-click apps, cluster mode, simple Docker deploys
  • Dokku: 29K+ GitHub stars — Heroku-like, buildpacks, plugin ecosystem, CLI-first
  • Coolify has the most polished web interface and widest deployment options
  • CapRover provides the easiest one-click app marketplace
  • Dokku offers the closest Heroku experience with git push workflow

Coolify

Coolify — modern self-hosted PaaS:

Installation

# One-line install on any VPS (Ubuntu/Debian):
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

# Access the dashboard:
# https://your-server-ip:8000

Docker Compose deployment

# docker-compose.yml — deployed via Coolify UI or API
version: "3.8"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://postgres:secret@db:5432/myapp
      NODE_ENV: production
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

API deployments

// Deploy via Coolify API:
const COOLIFY_URL = "https://coolify.example.com"
const COOLIFY_TOKEN = process.env.COOLIFY_API_TOKEN!

// List applications:
const apps = await fetch(`${COOLIFY_URL}/api/v1/applications`, {
  headers: { Authorization: `Bearer ${COOLIFY_TOKEN}` },
}).then((r) => r.json())

// Trigger deployment:
const deploy = await fetch(
  `${COOLIFY_URL}/api/v1/applications/${appId}/deploy`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${COOLIFY_TOKEN}`,
      "Content-Type": "application/json",
    },
  }
).then((r) => r.json())

// Create a new database:
const database = await fetch(`${COOLIFY_URL}/api/v1/databases`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${COOLIFY_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    type: "postgresql",
    name: "myapp-db",
    version: "16",
    server_id: serverId,
  }),
}).then((r) => r.json())

// Get deployment logs:
const logs = await fetch(
  `${COOLIFY_URL}/api/v1/deployments/${deployId}/logs`,
  {
    headers: { Authorization: `Bearer ${COOLIFY_TOKEN}` },
  }
).then((r) => r.json())

GitHub Actions integration

# .github/workflows/deploy.yml
name: Deploy to Coolify

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger Coolify deployment
        run: |
          curl -X POST \
            "${{ secrets.COOLIFY_URL }}/api/v1/applications/${{ secrets.COOLIFY_APP_ID }}/deploy" \
            -H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}" \
            -H "Content-Type: application/json"

Dockerfile (typical Node.js app)

# Dockerfile — Coolify auto-detects and builds
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

CapRover

CapRover — lightweight self-hosted PaaS:

Installation

# Install CapRover on a VPS:
docker run -p 80:80 -p 443:443 -p 3000:3000 \
  -e ACCEPTED_TERMS=true \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v captain-data:/captain \
  caprover/caprover

# Install CLI:
npm install -g caprover

# Setup (interactive):
caprover serversetup
# Enter IP, password, root domain, email for SSL

CLI deployments

# Login to CapRover:
caprover login
# Enter: URL, password

# Create an app:
caprover api --path "/user/apps/appDefinitions/register" \
  --method POST \
  --data '{"appName": "my-api", "hasPersistentData": false}'

# Deploy from current directory:
caprover deploy -a my-api

# Deploy with tarball:
tar -czf deploy.tar.gz --exclude node_modules --exclude .git .
caprover deploy -a my-api -t ./deploy.tar.gz

Captain Definition file

// captain-definition — CapRover deployment config
{
  "schemaVersion": 2,
  "dockerfileLines": [
    "FROM node:20-alpine AS builder",
    "WORKDIR /app",
    "COPY package*.json ./",
    "RUN npm ci",
    "COPY . .",
    "RUN npm run build",
    "",
    "FROM node:20-alpine",
    "WORKDIR /app",
    "COPY --from=builder /app/dist ./dist",
    "COPY --from=builder /app/node_modules ./node_modules",
    "COPY --from=builder /app/package.json ./",
    "EXPOSE 3000",
    "CMD [\"node\", \"dist/index.js\"]"
  ]
}
// captain-definition — using pre-built image
{
  "schemaVersion": 2,
  "imageName": "my-registry.com/my-api:latest"
}
// captain-definition — using Docker Compose
{
  "schemaVersion": 2,
  "dockerComposeFileContent": "version: '3.8'\nservices:\n  app:\n    build: .\n    ports:\n      - '80:3000'\n    environment:\n      NODE_ENV: production"
}

API automation

// CapRover API — automate deployments:
const CAPROVER_URL = "https://captain.example.com"
const CAPROVER_PASSWORD = process.env.CAPROVER_PASSWORD!

// Get auth token:
const { token } = await fetch(`${CAPROVER_URL}/api/v2/login`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ password: CAPROVER_PASSWORD }),
}).then((r) => r.json())

const headers = {
  "Content-Type": "application/json",
  "x-captain-auth": token,
  "x-namespace": "captain",
}

// List apps:
const { data } = await fetch(
  `${CAPROVER_URL}/api/v2/user/apps/appDefinitions`,
  { headers }
).then((r) => r.json())

console.log(data.appDefinitions.map((a: any) => a.appName))

// Create app:
await fetch(`${CAPROVER_URL}/api/v2/user/apps/appDefinitions/register`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    appName: "my-worker",
    hasPersistentData: false,
  }),
})

// Set environment variables:
await fetch(`${CAPROVER_URL}/api/v2/user/apps/appDefinitions/update`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    appName: "my-api",
    envVars: [
      { key: "DATABASE_URL", value: "postgresql://..." },
      { key: "REDIS_URL", value: "redis://..." },
      { key: "NODE_ENV", value: "production" },
    ],
  }),
})

// Enable SSL:
await fetch(`${CAPROVER_URL}/api/v2/user/apps/appDefinitions/enablessl`, {
  method: "POST",
  headers,
  body: JSON.stringify({ appName: "my-api" }),
})

// Deploy from Docker image:
await fetch(
  `${CAPROVER_URL}/api/v2/user/apps/appDefinitions/forcebuild`,
  {
    method: "POST",
    headers,
    body: JSON.stringify({
      appName: "my-api",
      captainDefinitionContent: JSON.stringify({
        schemaVersion: 2,
        imageName: "my-registry.com/my-api:v1.2.3",
      }),
    }),
  }
)

One-click apps

# CapRover one-click apps — deploy from marketplace:
# PostgreSQL, Redis, MongoDB, MySQL, RabbitMQ, MinIO,
# WordPress, Ghost, Plausible, Umami, n8n, Gitea, etc.

# Via CLI — list one-click apps:
caprover api --path "/user/oneclick/template/list"

# Deploy one-click app via API:
curl -X POST "${CAPROVER_URL}/api/v2/user/oneclick/template/deploy" \
  -H "x-captain-auth: ${TOKEN}" \
  -H "x-namespace: captain" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "postgres",
    "variables": {
      "$$cap_postgres_password": "supersecret",
      "$$cap_postgres_db": "myapp"
    }
  }'

Dokku

Dokku — the smallest PaaS:

Installation

# Install on Ubuntu 22.04+:
wget -NP . https://dokku.com/install/v0.34.x/bootstrap.sh
sudo DOKKU_TAG=v0.34.8 bash bootstrap.sh

# Set domain:
dokku domains:set-global example.com

# Add SSH key:
cat ~/.ssh/id_rsa.pub | dokku ssh-keys:add admin

Git push deployments

# Create app:
dokku apps:create my-api

# Set environment variables:
dokku config:set my-api \
  DATABASE_URL="postgresql://postgres:secret@db:5432/myapp" \
  NODE_ENV=production \
  PORT=3000

# Add git remote and deploy:
git remote add dokku dokku@example.com:my-api
git push dokku main

# Dokku auto-detects language via buildpacks:
# - package.json → Node.js buildpack
# - requirements.txt → Python buildpack
# - Dockerfile → Docker build

Procfile and app configuration

# Procfile — define process types:
web: node dist/index.js
worker: node dist/worker.js
// app.json — Dokku app configuration:
{
  "name": "my-api",
  "description": "Package comparison API",
  "scripts": {
    "dokku": {
      "predeploy": "npm run db:migrate",
      "postdeploy": "npm run db:seed"
    }
  },
  "formation": {
    "web": { "quantity": 2, "size": "standard-1x" },
    "worker": { "quantity": 1 }
  },
  "healthchecks": {
    "web": [
      {
        "type": "startup",
        "name": "web check",
        "path": "/health",
        "attempts": 10
      }
    ]
  }
}

Database plugins

# Install PostgreSQL plugin:
sudo dokku plugin:install https://github.com/dokku/dokku-postgres.git postgres

# Create database:
dokku postgres:create myapp-db

# Link to app (auto-sets DATABASE_URL):
dokku postgres:link myapp-db my-api

# Backup:
dokku postgres:export myapp-db > backup.sql

# Restore:
dokku postgres:import myapp-db < backup.sql

# Install Redis plugin:
sudo dokku plugin:install https://github.com/dokku/dokku-redis.git redis

# Create and link Redis:
dokku redis:create myapp-cache
dokku redis:link myapp-cache my-api

# Install Let's Encrypt:
sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
dokku letsencrypt:enable my-api
dokku letsencrypt:cron-job --add

Scaling and management

# Scale processes:
dokku ps:scale my-api web=3 worker=2

# View logs:
dokku logs my-api -t  # tail logs
dokku logs my-api -n 100  # last 100 lines

# App management:
dokku apps:list
dokku ps:report my-api
dokku domains:report my-api

# Rollback:
dokku ps:rollback my-api  # rollback to previous deploy

# Docker options:
dokku docker-options:add my-api deploy "--memory 512m"
dokku docker-options:add my-api deploy "--cpus 1.0"

# Proxy/port mapping:
dokku ports:add my-api http:80:3000
dokku ports:add my-api https:443:3000

# Maintenance mode:
dokku maintenance:enable my-api
dokku maintenance:disable my-api

# Custom domains:
dokku domains:add my-api api.example.com
dokku domains:add my-api www.example.com

Deployment automation

// Automate Dokku via SSH:
import { NodeSSH } from "node-ssh"

const ssh = new NodeSSH()
await ssh.connect({
  host: "dokku.example.com",
  username: "dokku",
  privateKeyPath: "~/.ssh/id_rsa",
})

// Run Dokku commands remotely:
async function dokku(command: string): Promise<string> {
  const result = await ssh.execCommand(command)
  if (result.stderr) console.error(result.stderr)
  return result.stdout
}

// Create app:
await dokku("apps:create staging-api")

// Set config:
await dokku('config:set staging-api NODE_ENV=staging DATABASE_URL="postgresql://..."')

// Check status:
const report = await dokku("ps:report staging-api")
console.log(report)

// Scale:
await dokku("ps:scale staging-api web=2")

// Cleanup:
await dokku("cleanup")  // Remove dangling images

ssh.dispose()
# .github/workflows/deploy-dokku.yml
name: Deploy to Dokku

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Deploy to Dokku
        uses: dokku/github-action@master
        with:
          git_remote_url: "ssh://dokku@example.com:22/my-api"
          ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
          branch: main

Feature Comparison

FeatureCoolifyCapRoverDokku
InterfaceWeb UI (modern)Web UI (functional)CLI only
Deploy methodGit, Docker, ComposeGit, Docker, Compose, imageGit push, Dockerfile
Buildpacks✅ (Nixpacks)❌ (Dockerfile only)✅ (Heroku buildpacks)
Docker ComposeVia plugin
One-click apps✅ (marketplace)Via plugins
SSL (Let's Encrypt)✅ (automatic)✅ (automatic)✅ (plugin)
Database management✅ (built-in)✅ (one-click)✅ (plugins)
Multi-server✅ (Docker Swarm)❌ (single server)
API✅ (REST)✅ (REST)SSH commands
Monitoring✅ (built-in)BasicVia plugins
Rollback
Persistent storage
Custom domains
Resource limits✅ (Docker options)
LanguagePHP/LaravelNode.jsGo/Bash
GitHub stars25K+13K+29K+

When to Use Each

Use Coolify if:

  • Want the most modern, polished web UI for managing deployments
  • Need built-in monitoring, database management, and multi-server support
  • Prefer Docker Compose and Nixpacks auto-detection
  • Building a team platform where non-CLI users need deployment access

Use CapRover if:

  • Want a simple, lightweight PaaS with a one-click app marketplace
  • Need Docker Swarm cluster support for multi-node setups
  • Prefer a functional web dashboard with straightforward Docker deployments
  • Want the easiest setup for common services (databases, caches, CMS)

Use Dokku if:

  • Want a Heroku-like git push deployment experience
  • Prefer CLI-first workflow with SSH-based management
  • Need Heroku buildpack compatibility for zero-config deploys
  • Want the most mature, battle-tested self-hosted PaaS

Methodology

GitHub stars as of March 2026. Feature comparison based on Coolify v4.x, CapRover v1.x, and Dokku v0.34.x.

Compare developer tools and deployment platforms on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.