Over the last couple of years I've tried most of the ways there are to run apps on servers I own: Kubernetes, Coolify, Dokploy, and Dokku, the one I keep coming back to.
Look through my projects folder and you'll find a lot of git remotes that start with dokku@. Client sites, side projects, this website. Some point at servers out on the internet, and a couple point at machines sitting on my home network.
So I built the version of Dokku I wanted next. It's called Jokku. It keeps Dokku's commands and its git push, then runs every instance of your app in its own Firecracker microVM, on as many servers as you give it.
Kubernetes. In 2023 I built a k3s cluster on machines at home: a highly available control plane on an external MySQL datastore, nginx ingress, cert-manager, MetalLB, Prometheus, the Kubernetes dashboard, and Flux to deploy from git. It was a real cluster. It also meant that putting one small website online took a Deployment, a Service, an Ingress and a certificate issuer, plus a kubeconfig in GitHub's secrets so CI could apply them. Kubernetes is built for companies with a platform team. I'm one person with a lot of small apps.
Coolify. A self-hosted Heroku with a web dashboard in front. Connect a repo, and it builds and runs your app in Docker behind Traefik, with a long catalog of one-click services next to it. It does a lot, and almost all of it happens in the dashboard. My notes for setting up a site on Dokku are a dozen commands I can paste into a terminal. On a dashboard, they turn into a list of things to click.
Dokploy. Same idea as Coolify, with Docker Swarm when you want more than one server. It's what this website deploys through today, and it's the one I pushed hardest, so it gets its own section.
My trouble with Dokploy came from running it on a cluster, across several servers.
The whole time, running on more than one server felt like something Dokploy allowed rather than something it was built for.
None of these are bad software. But through all of it, I kept a Dokku box around.
If you haven't used it, Dokku is a Heroku you install on your own server. You git push, it builds your app, runs it, and puts your domain in front of it. config:set for environment variables, domains:add for a domain, letsencrypt:enable for HTTPS. That's pretty much the whole thing, and it's the right size for most of what I build.
It also has two limits that come from how it's designed:
And the home network boxes have a problem of their own: getting the internet to them means opening ports on the router, or running a tunnel from a public server.
Firecracker is the virtual machine monitor AWS built to run Lambda and Fargate. It boots a real VM, with its own kernel, in a fraction of a second. If you've deployed to Fly.io, your app was already running in one.
That's where every Jokku app runs. Each instance (web.1, web.2, worker.1) is its own microVM:
USER, and gets out of the way.Each instance runs in its own systemd unit and not as a child of Jokku itself, so restarting or updating Jokku leaves every app running.
The first rule in Jokku's design doc is "Dokku's UX, kept as-is." The command names, the argument order, even the -----> output. If you know Dokku, you already know Jokku:
curl -fsSL https://raw.githubusercontent.com/wes/jokku/main/install.sh | sudo sh
git remote add jokku jokku@your-server:myapp
git push jokku main
jokku domains:add myapp myapp.com
jokku letsencrypt:enable myapp
That's a bare Ubuntu server to an app on HTTPS. Jokku builds the Dockerfile with BuildKit, turns the image into a disk, boots it, waits for it to accept connections, and only then moves traffic over. If the new version doesn't start, the push is rejected, its instances are torn down, the failing one's logs print right there in your terminal, and the old version keeps serving. If it does start, the old instances are stopped a minute after traffic moves. A deploy either finishes or leaves things exactly as they were.
Here's a push and then a scale-up to three servers, from the animation on jokku.dev:
That's how jokku.dev ships, too. The site runs on Jokku, and a change to it is a git push.
Your laptop needs git and ssh, and nothing else. Every command is an SSH call, and behind that, an HTTP call to Jokku's API. Nothing in the CLI touches the database or the disk directly, so a GUI could sit on that API later without a rewrite.
Jokku is one Go binary. It's the CLI, the API server, the agent on every server, the SSH command, the git hook, the HTTP proxy (Caddy, built in as a library), and the init process inside every microVM. The only other things it installs are Firecracker and BuildKit, both pinned.
State lives in SQLite on the first server. The others look at what should be running, compare it to what is, and fix the difference, so a restart or a crash doesn't turn into an adventure.
One server is a complete Jokku, and a cluster runs the same code with more servers in it. There's no multi-server mode and no settings for it. When you want another server, ask the first for a join command and run what it prints on the new box:
jokku cluster:join-command
The servers connect over a WireGuard mesh, apps spread across all of them, and every server can take web traffic, so DNS can point at as many as you like. Apps find each other by name: cache.internal reaches the cache app on whichever server it's on.
If a server dies, its apps start on the others a minute later. nodes:drain moves everything off one before maintenance.
This is the part for those boxes on my home network.
For a while now, the way I've gotten websites from a public VPS to servers at home, without opening a port on my home network, is Pangolin. A small public server takes the traffic, and your network at home connects out to it over WireGuard. I love it. It's great software, and if you want to put things at home on the internet, go take a look at it.
But it meant every domain lived in two places. A new site got its domain set up where the app was deployed, and then again in Pangolin, so the public server knew where to send it. Two systems to keep in sync, for every domain. I didn't want to do that anymore.
So Jokku has edges. An edge is a small public server, like a cheap droplet, that receives your domains' traffic and sends it home over Jokku's encrypted mesh. Your servers dial out to the edge, so nothing at home needs an open port. And because the edge is part of the cluster, it already knows every app's domains. domains:add and letsencrypt:enable are the whole job: the edge picks up the route and serves the certificate, with nothing to set up anywhere else.
jokku edge:add root@203.0.113.7 # once, over ssh
jokku external:create ha http://192.168.1.50:8123
jokku domains:add ha ha.example.com
jokku http-auth:enable ha --password
external:create makes an app out of something Jokku doesn't even run, like Home Assistant or a NAS. From there it's like any other app: give it a domain and the edge serves it. http-auth:enable puts a login page in front of it, with a shared password or with users and authenticator codes.
Here's the whole trip from a browser to your app, with an edge in front of a cluster at home.
web instance of every app, and it picks one.10.210.2.x for server-2, so an instance's address says which server it's on. WireGuard sends the request there, encrypted. If the instance is on the server that took the request, it skips the mesh entirely.wg0, crosses the server's jokku0 bridge, and goes through a tap device into the microVM's eth0, where your app is listening on $PORT.Apps talking to each other use the same mesh. Every server runs a small DNS server, jokku-dns, on its bridge, so when a worker on server-1 asks for cache.internal, it gets back the address of the cache app's instance on server-2. The answer only lives for five seconds, so the name follows the app through deploys and moves. Inside the VM it's just a hostname.
This is the part Dokploy taught me to care about. A database never moves without its disk.
A volume is a disk attached to one instance, and the app runs on whichever server has the disk. When you move it, with nodes:drain or storage:move, the disk is copied while the app keeps running, then there's a short pause for the last few changes. If its server dies, an app with a volume waits for that server to come back instead of starting somewhere else on an empty disk.
Volumes back up to any S3-compatible bucket: Tigris, S3, R2, B2, anything that speaks S3. Backups run every 15 minutes by default, they're encrypted with a key you keep, and they only upload the blocks that changed, so frequent backups cost very little. Backups also change what happens when a server dies: once it's been gone for five minutes, its backed-up volumes are restored onto another server from their latest backup, and their apps start there. A server that's only cut off from the rest stops those apps first, so you never end up with two copies of a database each thinking it's the real one.
Postgres, MySQL and Redis are built in too, each on a volume of its own and backed up from the moment it's created:
jokku db:postgres:create shopdb
jokku db:postgres:link shopdb myapp # sets DATABASE_URL and restarts myapp
jokku top is a live view of the whole cluster in your terminal: servers, apps, instances, CPU and memory. Press 6 and every request flies across its app's lane as a dot, green for fine, yellow for a 4xx and red for a 5xx, and lands on the instance and server that answered it. It's a ridiculous amount of fun for what is, technically, a list of HTTP requests.
Jokku has been my nights-and-weekends project for the last couple of months.
The first release was a big one. It was the foundation I'd been working on for a while: the control plane, the API every command goes through, Dokku's commands over SSH, and the installer, with git push into Firecracker microVMs right behind it. Everything since has been built on top of that: clusters, volumes, compose files, backups that restore themselves, databases, and most recently edges and logins. It's on v0.8.2 now, and what's new lists every release.
sudo jokku update updates a server in place. It backs up Jokku's own database first, puts the old version back if the new one won't start, and your apps keep serving the whole time.
Jokku says "early development" right at the top of its README, and it means it. A few things worth knowing before you point it at anything important:
releases:rollback is planned.The command reference marks every Dokku command as works, planned, or not planned, so you can check before you switch.
Jokku is free. Everything is at jokku.dev: the install command, the docs, a page for coming from Dokku, and what's new in every release. The code is on GitHub.
If you already have a Dokku box, you already know how to use it. And if something breaks, open an issue.