Detects, installs, validates, repairs and upgrades Docker stacks — without ever breaking what's already running.
🇺🇸 English · 🇧🇷 Português
You have EasyPanel in production and you want to run Dokploy on the same server — without migrating anything, without tearing anything down, without a port fight.
The problem: both use Docker Swarm, both want Traefik, both compete for :80,
:443 and :3000. Installing the second one the standard way breaks the
first.
DeployKit solves that surgically and idempotently:
┌──────────────────── same host · same swarm ──────────────────────┐
│ │
│ EasyPanel ──▶ :80 :443 :3000 (untouched, owns its Traefik)│
│ │
│ Dokploy ──▶ :3001 · dokploy-traefik :8080 / :8443 │
│ dokploy-postgres · dokploy-network │
│ │
│ Cloudflare Tunnel ──▶ dokploy-traefik:80 (no host port open) │
│ │
└──────────────────────────────────────────────────────────────────┘
Golden rule: never break an existing installation. Every operation is idempotent, reversible, validated and explicit. When in doubt, DeployKit picks the safest action — not the fastest one.
- 🔍
doctor— read-only host diagnostic (Docker, Swarm, ports, IPAM, networks, providers). Changes nothing. - 📦
install— resolves dependencies, shows the plan, asks for confirmation and installs only what's missing. - ↩️ Automatic rollback — failed halfway? it reverts (LIFO) only what this run created. Never touches pre-existing infrastructure.
- ❤️ Blocking health checks — install only returns once the service actually converges (replicas running / container up).
- 🔁
upgrade— image re-pull + controlled redeploy. - 🩹
repair— detects what's unhealthy and offers a fix, always with confirmation. - 🧩 Idempotent — run it as many times as you like; existing resources are reused, never recreated for nothing.
- 🐝 Swarm-aware — detects the active swarm and reuses it; never runs
initorleave.
# get it and build (pure Go, cross-compiles to Linux with no toolchain)
git clone https://github.com/pablorobert/deploykit.git && cd deploykit
scripts/build.sh
scp dist/deploykit-linux-arm64 root@SERVER:/tmp/deploykit
# diagnostic — changes nothing
ssh root@SERVER '/tmp/deploykit doctor'
# install the whole Dokploy stack (postgres → dokploy → traefik)
ssh -t root@SERVER '/tmp/deploykit install traefik'
# expose it through a Cloudflare Tunnel (no host port opened)
ssh -t root@SERVER 'CF_TUNNEL_TOKEN=<token> /tmp/deploykit install cloudflared'📋 Sample doctor output
Docker .............. OK
Docker Swarm ........ Active (manager)
EasyPanel ........... Installed
Dokploy ............. Installed
Traefik ............. EasyPanel
Bridge Network ...... 172.30.0.0/24
Overlay Networks .... OK (6)
IPAM ................ OK
Port 80 ............. Occupied
Port 443 ............ Occupied
Port 3000 ........... Occupied
Port 3001 ........... Occupied
Port 8080 ........... Available
Port 8443 ........... Available
Summary
System is compatible.
| Command | What it does |
|---|---|
deploykit doctor |
Full diagnostic, read-only. Never modifies the system. |
deploykit install <provider> |
Resolves deps, shows the plan, confirms and installs (with rollback). |
deploykit remove <provider> |
Removes only that provider's own resources. |
deploykit upgrade <provider> |
Image re-pull + redeploy. |
deploykit repair |
Detects problems and offers fixes. |
install/remove/upgrade/repairask for confirmation. Use-yto skip.
| Provider | Installs | Depends on |
|---|---|---|
postgres |
PostgreSQL — dokploy-postgres |
— |
dokploy |
Dokploy control plane — dokploy (UI :3001) |
postgres |
traefik |
Dokploy's Traefik — dokploy-traefik (:8080/:8443) |
dokploy |
cloudflared |
Cloudflare Tunnel connector (swarm service) | traefik |
Dependencies are resolved in topological order (with cycle detection).
install cloudflared pulls the whole chain:
postgres ─▶ dokploy ─▶ traefik ─▶ cloudflared
No Redis. Self-hosted Dokploy dropped its internal
dokploy-rediscontainer in v0.29.9; a healthy instance is justdokploy,dokploy-postgresanddokploy-traefik. DeployKit targetsdokploy/dokploy:latest, so it never installs Redis.
The postgres provider comes up with the default password deploykit
(internal/providers/postgres/postgres.go), passed as an environment variable —
not as a Docker secret. It's a lab default: change it before exposing
anything. Dokploy also logs a warning about BETTER_AUTH_SECRET being unset
and falling back to an insecure default. Both are on the roadmap
and neither is a blocker on a closed host, but know that they're there.
CF_TUNNEL_TOKEN=<token> deploykit install cloudflaredThe token goes into the container as TUNNEL_TOKEN — never on the command
line.
Traffic flows like this:
your-domain/* → Cloudflare (terminates TLS) → Tunnel → dokploy-traefik:80 → app
On the Cloudflare side: create the tunnel in Zero Trust, copy the token, and map
each public hostname to dokploy-traefik:80. Since TLS terminates at the edge,
Dokploy's own HTTPS stays off for domains going through the tunnel — two
certificates on the same route get in each other's way. No port is opened on the
host.
Careful with wildcards: a
*.domainCNAME is convenient, but an exact record beats a wildcard. If some subdomain needs to stay outside the tunnel, create its specific record pointing at the old destination.
cmd/ cobra commands: root · doctor · install · remove · upgrade · repair
internal/
├── core/ reusable building blocks (core knows no provider)
│ ├── docker official SDK client (github.com/moby/moby)
│ ├── swarm swarm detection (never mutates)
│ ├── ports port scanning (via dial, no privileges)
│ ├── network idempotent overlay networks
│ ├── ipam overlapping subnet validation
│ ├── health convergence waiting (services and containers)
│ ├── service idempotent swarm services + spec builder
│ ├── container idempotent plain containers (+ on-demand pull)
│ ├── volume idempotent volumes (+ release waiting on remove)
│ └── rollback transaction with LIFO undo
├── providers/ Provider contract + Registry (deps) + Installer
│ └── postgres · dokploy · traefik · cloudflared · svcprovider
├── doctor/ orchestrates the diagnostic
└── ui/ console + confirmations
The primary interface to Docker is the official SDK, not shell scripts. Everything provider-specific lives in its own subpackage — adding a provider means creating a new package and registering it in the CLI. The core never changes.
If you're reusing this repo for a different stack, this is the part you'll
touch. A provider is one type implementing internal/providers.Provider:
type Provider interface {
Name() string // stable CLI identifier
Dependencies() []string// provider names installed before this one
Detect(ctx, c) (bool, error) // already installed? (idempotency)
Validate(ctx, c) error // check prerequisites, mutate nothing
Install(ctx, c, tx) error // create resources, record undo on tx
Health(ctx, c) error // block until converged
Upgrade(ctx, c) error
Remove(ctx, c) error // only this provider's own resources
}- Create
internal/providers/<name>/and implement the interface. Build oninternal/core/*(service,container,volume,network) — those helpers are already idempotent and register their own rollback undo. - Register it in
registry()incmd/install.go. - Declare
Dependencies(); ordering, cycle detection and the install plan come for free.
For a plain swarm service, internal/providers/svcprovider already implements
most of the contract — see cloudflared for how thin a provider can get.
Requires Go 1.24+. The project is pure Go (no cgo) → it cross-compiles to Linux with no external toolchain.
scripts/build.sh [version] # builds dist/deploykit-linux-{amd64,arm64} (static, stripped)
go test ./... # unit tests
go vet ./...Validated in production (Linux arm64 host) with EasyPanel live next to it:
- ✅ Full Dokploy stack running alongside EasyPanel, no conflict
- ✅ Automatic rollback proven (failure → reverts everything, zero orphans)
- ✅ Blocking health checks (install waits for 1/1)
- ✅ Cloudflare Tunnel live (own domain, no host port opened)
The scenario that motivated DeployKit (EasyPanel + Dokploy sharing one host) is
no longer the author's setup, so the project is published as a reference, not
as actively maintained software. It works as documented and the code is MIT —
fork it, lift the internal/core blocks, or write your own providers on top.
Issues may sit unanswered; PRs are welcome but not guaranteed a review.
Known gaps, kept here as a map for whoever picks this up — not a commitment:
- Move secrets to Docker secrets (Postgres password,
BETTER_AUTH_SECRET, tunnel token) - Network scope on the swarm provider of the Traefik config Dokploy generates
-
doctorwarning (without fixing) about firewall-blocked ports - Integration tests against a real daemon
- New providers: Portainer, MinIO, Grafana, Prometheus, N8N…
MIT — see LICENSE.
Built with Go and enough care to never break production. 🐝