Skip to content

Ship to servers you own.

A self hosted deploy platform for solo developers and small teams. One command puts it on your VPS or homelab box, and each release is checked before it takes traffic.

Install now

curl -fsSL https://nebulactrl.com/install.sh | sudo bash
  • v0.1.1
  • Free to use
  • amd64 and arm64
  • Debian 12 and 13
  • Ubuntu 22.04 to 26.04
  • Rocky and Alma 9
  • 1 GB RAM

Uninstalling leaves your containers running. Read the script first if you like, or read the docs.

An alternative to Railway, Coolify and Dokploy. Compared row by row.

Recorded on Debian 12Debian GNU/Linux 12 (bookworm), arm64
$ curl -fsSL https://nebulactrl.com/install.sh | sudo bash
==> Checking the system
    ok  Debian GNU/Linux 12 (bookworm), arm64, systemd
    ok  16040 MB RAM
==> Base packages
    ok  installed: gnupg
==> Docker Engine
        installing Docker Engine from Docker's official repository ...
    ok  Docker Engine installed
    ok  Docker 29.9.2, Compose 5.6.1
==> Plan
    warn 203.0.113.10 is a private address: the dashboard will only be reachable from your network, and servers outside it cannot connect (use --public-ip or a domain).
    ok  mode: ip, dashboard: http://203.0.113.10:8000
        no domain given: the dashboard is served over plain HTTP (see the notice at the end)
==> Docker daemon defaults
    ok  wrote /etc/docker/daemon.json
    ok  Docker restarted with the new defaults
==> Edge network
    ok  created nebulactrl-edge
==> Installation files in /opt/nebulactrl
    ok  generated secrets in /opt/nebulactrl/.env (0600)
    ok  backup directory /var/backups/nebulactrl
==> NebulaCtrl image
    ok  loaded nebulactrl:instlocal from /root/image.tar
… 7 more steps and 35 lines, skipped in this replay
================================================================
 NebulaCtrl is installed
================================================================
 Dashboard : http://203.0.113.10:8000
 Setup URL : http://203.0.113.10:8000/setup?token=<token>
             open it to create the admin account (the token works once, until setup is done)
 Files     : /opt/nebulactrl  (.env holds every secret, mode 0600)
 Backups   : /var/backups/nebulactrl/control-plane (daily 03:30 UTC)

 MUST DO NOW: back up the secret key
   sudo grep NEBULACTRL_SECRET_KEY /opt/nebulactrl/.env
   Store it in a password manager, apart from the backups. It exists nowhere else;
   without it the database and its backups cannot be read.
… 14 more lines of notices, skipped in this replay
NebulaCtrl, Acme Shop, three services
The NebulaCtrl canvas for the Acme Shop project, production environment. An Internet node with the domain shop.localhost is joined by a cable to a service named web. Below it a service named api is cabled to a PostgreSQL 18 database named postgres. Each card has a Live badge, the Build, Release and Verify lights, its server agent1, its replica count and its memory.

Every line is the installer's own output. The replay skips the long middle; the real run took 1 min 4 s.Installed. This is the canvas it opens on: Acme Shop with three services, each live.

  • 0of 378 requests dropped

    A rolling deploy under load, v1 replaced by v2

    Measured 27 October 2026

  • 64s

    Install on a clean Debian 12 machine

    Measured 27 October 2026

  • 5.7s

    One image deploy, pull to old release retired

    Measured 27 October 2026

  • 283MiB

    Idle memory: control plane, Postgres and proxy

    Measured 27 October 2026

  • 27MB

    Agent binary

    Measured 27 October 2026

How each figure was measured
  • A rolling deploy under load, v1 replaced by v2. Integration test TestZeroDowntimeRolling: one GET every 20 ms (50 a second) through the proxy while the new release replaced the old one. 378 requests, no response other than 2xx, no connection errors, and v1 never answered after v2 took over. Measured on 2026-10-27, MacBook Pro (Apple M1 Pro, 32 GB), macOS 27.0.1, Docker 29.8.2 in OrbStack (15.7 GiB VM).
  • Install on a clean Debian 12 machine. One run of deploy/install/install.sh in IP mode, with no domain and the image preloaded, timed by its own clock: 64.44 s from the first line to the last. content/install-recording.json is that run. Measured on 2026-10-27, Debian 12 (bookworm) arm64, 16 GB RAM, clean machine.
  • One image deploy, pull to old release retired. The deployment_events timestamps of one rolling deploy of traefik/whoami (image already on the host), read from GET /deployments/{id}/events: 5.657 s in total, with the route moving at 3.39 s. content/deploy-recording.json is that run. Measured on 2026-10-27, MacBook Pro (Apple M1 Pro, 32 GB), macOS 27.0.1, Docker 29.8.2 in OrbStack (15.7 GiB VM).
  • Idle memory: control plane, Postgres and proxy. The installer's own compose file with the image built from the root Dockerfile, one project and three services, idle. docker stats, median of five samples: control plane 209.7 and Postgres 52.7, plus the proxy container of a running agent at 20.1. Measured on 2026-10-27, MacBook Pro (Apple M1 Pro, 32 GB), macOS 27.0.1, Docker 29.8.2 in OrbStack (15.7 GiB VM).
  • Agent binary. Built the way the release builds it: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" ./cmd/agent. 27,234,464 bytes (arm64: 24,772,768). Measured on 2026-10-27, MacBook Pro (Apple M1 Pro, 32 GB), macOS 27.0.1, Docker 29.8.2 in OrbStack (15.7 GiB VM).

Press Deploy, then break it.

Replay a healthy release, then a deployment whose health check never passes. The new container starts beside the old one and the agent checks it from outside, so your image needs no curl. Until that check passes, the old release keeps serving. How deployments work

Which recorded deployment to replay

A replay of a recorded deployment of web, 5.7 s in all, shown at the pace it ran. A rolling deploy: the new container starts beside the old one.

Internet

shop.localhost

web
traefik/whoami:latest
Live
Live
agent11/1 replica
Memory, idle6.7 MB
api
traefik/whoami:latest
Live
Single container, recreated on deployLive
agent11/1 replica
Memory, idle2.1 MB
postgres
postgres:18
Live
Single container, recreated on deployLive
agent11/1 replica
Memory, idle21.6 MB
TrafficCurrent release
  • Current release100
  • New release0
DeploymentNot running

Serving the current release.

Events of this deploymentLast run, press Deploy to replay it
  1. 0.1Pull image startedPulling traefik/whoami:latest
  2. 0.4Pull image succeeded
  3. 0.5Create container startedCreating container nc-web-f9db9596-0
  4. 0.9Create container succeeded
  5. 0.9Start container started
  6. 1.0Start container succeeded
  7. 1.0Health check startedChecking health (http)
  8. 3.1Health check succeeded
  9. 3.2Move the route startedUpdating proxy routes
  10. 3.4Move the route succeeded
  11. 3.4Verify through the proxy startedVerifying http://shop.localhost/ through the proxy
  12. 3.4Verify through the proxy succeeded
  13. 3.4Retire previous release startedRetiring previous release
  14. 5.6Retire previous release succeeded

Traffic moves only after the new container passes its checks.

The Debug panel of one deployment of web. Status Succeeded, trigger Deploy, duration 6.2 s, server agent1. The Timeline tab lists every event with its clock time, the time since the start and since the previous event: status created, queued and preparing, pull started and succeeded with the image digest, create, releasing, start, and health started, then verifying.
Every deployment keeps its own timeline to the millisecond. This one pulled the image, created and started the container and began its health check in 0.66 s.
  • Failed

    The health check does not pass

    The new container is removed and the old release keeps serving. The deployment names the step and keeps the last 50 log lines.

  • Rolled back

    The check through the proxy fails

    The previous route is put back and the new container is removed. Cancel works at any step until this check passes.

  • Succeeded

    Every check passes

    The old container gets its stop grace period, then is removed.

A rollback redeploys an earlier release's image and configuration. Databases, volumes and external services are not rolled back. Deployments in the docs

Changes wait as a diff until you deploy them.

Variables, domains and settings stage on the canvas. Review the diff, discard one change, or deploy them together in dependency order. Click a service and the inspector holds its releases, logs, variables and metrics. Staged changes in the docs

The Review changes sheet for the web service in production, headed 4 changes across 1 service, saved, not deployed. Four added lines: config.memoryMb 512, variables.AUTH_SECRET generated from secret(32), variables.DATABASE_URL set to a reference to the postgres service, and variables.LOG_LEVEL info. Each has a remove button. An optional deploy message field sits below.
Staged changes. Four edits to web are saved but not running. Drop any line, or deploy them together with a message.
The inspector for the web service on its Overview tab. Tabs: Overview, Deployments, Variables, Metrics, Analytics, Logs, Settings. Health reads Healthy, an HTTP GET on port 80 every 2 seconds. Replicas 1 of 1, uptime, 0 restarts. CPU and memory tiles show current use with a small chart each.
The inspector for one service: its health check, replicas, restarts, CPU and memory, with its deployments, variables, logs and settings one tab away.

Servers dial out. Nothing dials in.

Each server runs a small agent, 27.2 MB, that opens a WebSocket to the control plane and keeps it open. A host behind a home router works like a VPS, because the control plane never has to reach it. If the control plane goes down, containers and proxies keep running.

Each agent reports Docker, CPU, memory and disk. Revoke it and its connection closes at once. Adding servers

The Fleet view of the Servers page. Four tiles: 1 server, 3 services, 2.7 GiB of memory used of 15.7 GiB, and 0 B of memory limits promised. Below, the server agent1 is online with 4 vCPU. It shows bars for CPU, memory 17 percent and disk 46 percent, and lists its three services api, postgres and web, all running, with their memory use.
The fleet view: what each server has, what the services on it are promised, and what they use now.

Add the next server with one command

Servers, then Add server, shows this command with a one time token. The agent enrolls, then reports Docker, CPU, memory and disk.

curl -fsSL https://<your-host>/install-agent.sh | sudo sh -s -- --token <token>

What is in the box

350 one click templates, a CLI, an API and everything below, with nothing held back for a paid plan.

Each template pins an image digest and states its licence. Templates in the docs

The New from template dialog, searching 350 apps with the word dashboard and showing 29 matches. Category chips with counts sit above a grid of cards for Dashy, Glance, Heimdall, Homarr, Homepage and LaraPaper, each with its service count, memory estimate and licence.
New from template, searched for dashboards. Each of the 350 apps becomes ordinary services on the canvas, staged until you deploy.
  • The command palette with the web service in focus. Rows: Open fleet, Create service, New from template, Import Docker Compose, New Compose stack, web, Deploy web (review, then confirm), Restart web, Run command in web and Open shell in web.

    Command palette

    Deploy, restart or open a shell from the keyboard.

  • A server card for agent1, online. Agent and Docker versions, CPU, memory and disk readings, each with a bar drawn in dither, and a mark on the disk bar. Below: proxy ok, 3 services, seen just now.

    Capacity per server

    A server card: CPU, memory and disk from its agent, and the state of its proxy.

  • The Create menu with a search field. Choices: Git repository, Docker image, Database, Templates, Import Docker Compose, Compose stack, External service and TCP/UDP port.

    Create menu

    Eight ways to add a service.

  • CLI, API and MCP

    A command and an endpoint for everything in the dashboard, an OpenAPI 3.1 document and an optional MCP server.

    nebulactrl services deploy web --waitnebulactrl services logs web -f

Ship

Git push and previews
Signed webhooks, monorepo watch paths and a preview environment per pull request.
Compose stacks
A docker-compose.yml runs as one stack under a safe allow list.

Run

Databases
PostgreSQL, MySQL, MariaDB, Valkey, Redis, MongoDB and ClickHouse with backups to S3.
Volume backups
restic backups for any service's volumes, restored into a new volume.
Replicas and autoscaling
One to ten replicas behind the proxy, with a CPU target.
Tasks, logs and shell
Scheduled tasks, stored logs with drains to Loki or syslog, and a shell in any container.

Reach

Cloudflare and Tailscale
A managed Cloudflare Tunnel and DNS per server, and a Tailscale or Headscale sidecar per service.
Isolated environments
One network per environment on a host, and outbound traffic blocked per service.
Traffic analytics
Requests, status classes and origins, counted by the agent. Raw log lines never leave the host.

Trust

Teams and sign in
Passkeys, TOTP, any OIDC provider and GitHub login, 4 roles and an audit log. All free.
Hardened hosts
A Lynis audit per server with reversible safe fixes.
Notifications
Email, Slack, Discord, Telegram and signed webhooks.

One command, then a dashboard.

The installer prints every step it takes. It adds Docker only if it is missing, keeps the daemon settings you already have, puts Postgres on a private network and schedules a daily backup. It ends by telling you to copy the key it generated, because without that key the backups cannot be read.

Add --domain and the dashboard comes up over HTTPS with a Let's Encrypt certificate. Without it you get plain HTTP on port 8000, with a notice that says why that is only for trying things out. Every step and every flag

NebulaCtrl sends no telemetry and does not check for updates. We keep it that way.

  1. Install

    Run the command above on a Debian, Ubuntu, Rocky or Alma host. Every secret is generated and the dashboard comes up.

  2. Add a server

    Optional. Skip it to deploy on the first machine.

  3. Deploy

    Point a service at a Git repository, a Compose file, a template or an image and press Deploy, or run it from CI. The quickstart adds a domain and a database.

nebulactrl services deploy web --wait

The 15 steps, as recorded on a clean Debian 12 machine. Times are the recording's clock, where long waits are capped, so they add up to 33 s; the real run took 1 min 4 s.

Prepare the host

Docker, its daemon defaults and the edge network.

  1. Checking the system

    Debian 12, arm64, systemd, 16 GB of RAM.

    under 0.1 s

  2. Base packages

    Installs gnupg, the one package it needs first.

    2.6 s

  3. Docker Engine

    Installs Docker Engine and Compose from Docker's own repository.

    5.9 s

  4. Plan

    Prints the mode and the dashboard address before it changes anything else.

    under 0.1 s

  5. Docker daemon defaults

    Writes /etc/docker/daemon.json and restarts Docker.

    0.6 s

  6. Edge network

    Creates the nebulactrl-edge network.

    under 0.1 s

Start the control plane

Secrets, the image, the control plane and a daily backup.

  1. Installation files in /opt/nebulactrl

    Generates the secrets into a 0600 .env and a backup directory.

    under 0.1 s

  2. NebulaCtrl image

    Loads the control plane image.

    1.0 s

  3. Starting the control plane

    Starts the control plane and waits until it answers.

    3.0 s, capped

  4. Daily control-plane backup

    Schedules a daily local backup and keeps 14.

    under 0.1 s

Enroll the first server

The agent on this same host.

  1. Agent on this host (the first server)

    Installs the agent as a systemd unit and enrolls it as the first server.

    3.3 s, capped

Harden the host

Firewall, fail2ban, security updates and a Lynis audit.

  1. Firewall

    Enables ufw with SSH, 80, 443 and the dashboard port.

    3.7 s

  2. fail2ban (SSH)

    Starts fail2ban on sshd.

    3.4 s

  3. Automatic security updates

    Enables unattended security updates, no automatic reboot.

    1.9 s

  4. Lynis hardening audit

    Audits the host with Lynis and applies the fixes it can undo.

    7.7 s

Where it stands against Coolify, Dokploy and Railway

  • Behind on

    Point in time database recovery, database failover, host bind mounts and Nixpacks, and only a partial answer on private networking across hosts.

  • Ahead on

    Checking a release through the proxy, staged changes with a diff, a network per environment and blocking outbound traffic per service.

  • Younger

    Coolify and Dokploy have larger communities and more years in production.

The full comparison, row by row

92of 100

capabilities NebulaCtrl supports, counted from its own code.

  • Coolify62 of 100
  • Dokploy53 of 100
  • Railway49 of 100
Checked against each vendor's documentation on 28 October 2026. A row that does not apply to a product, such as Railway hosting your servers, is left empty.

Before you install

The objections that stop most installs, answered first.

All questionsGo to the install command

Is it ready for production?
It is young, so start with something you can afford to move. The control plane is a single instance without high availability, the Cloudflare and Tailscale paths have been tested against simulated APIs and not a live account, and the limitations page lists everything it does not do. Your apps keep running if the control plane goes down. The agent runs as root, because the Docker socket is root equivalent, and NebulaCtrl assumes a trusted team: roles limit what people can do in the product, not what a compromised host can do. The threat model has the rest.
How is it different from Coolify or Dokploy?
The three are close cousins. NebulaCtrl puts your services on a canvas with staged changes, checks health from outside the container, verifies through the proxy and puts the old route back if that fails, and isolates environments from each other on a host. Coolify and Dokploy are ahead on Nixpacks, host bind mounts, a raw proxy config editor, community size and years in production. The comparison lists both directions.
What do I need?
One Linux machine with systemd and root access: Debian 12 or 13, Ubuntu 22.04, 24.04 or 26.04 LTS, or RHEL, Rocky or AlmaLinux 9, on amd64 or arm64, with 1 GB of RAM for the control plane. The installer adds Docker 24 or newer if it is missing. Ports 80 and 443 should be free. A domain is optional: without one the dashboard is plain HTTP on port 8000 until you add one. Requirements
What does the installer change?
It installs Docker if needed, applies safe daemon defaults (log rotation, live restore), starts Postgres on a private network and the control plane, sets up ufw, fail2ban and unattended security updates, and schedules a daily backup. Every step has a flag to skip it, it keeps your existing Docker settings, and it never changes SSH settings. To upgrade, run it again, with --version to pin a release; to go back, run it with the previous version. The uninstaller keeps your apps and data unless you ask otherwise. Read the script before you run it, or see what each step does and how to upgrade and roll back.

Try it on a machine you can afford to wipe.

Nothing to sign up for and nothing to pay. Your containers stay plain Docker, your data stays on your disks, and the uninstaller leaves your apps running unless you ask it not to.

curl -fsSL https://nebulactrl.com/install.sh | sudo bash
  • v0.1.1
  • Free to use
  • amd64 and arm64
  • Debian 12 and 13
  • Ubuntu 22.04 to 26.04
  • Rocky and Alma 9
  • 1 GB RAM

Follow the quickstart