Three Dokku Plugins That Built Each Other
I've been writing Dokku plugins for a while now. The latest three - dokku-snapshot, dokkufile, and dokku-library - weren't designed as a suite. They emerged from two separate problems and converged into something more useful than any of them would be alone.
It Started with Backups
dokku-snapshot started as the obvious missing piece in my homelab. I had 20+ apps running on Dokku, each with databases and storage volumes. Dokku's service plugins give you postgres:export and postgres:import, but there's no unified way to snapshot an entire app's data layer - databases, volumes, everything - in one shot.
So I started building that. snapshot:create myapp would find every linked service, export each one using its native mechanism, tar up the storage volumes, and bundle it all into a timestamped snapshot. snapshot:restore myapp <id> would reverse the process.
But you can't just restore the data. You need to restore the configuration too. The domains, the port mappings, the environment variables, the storage mount paths, the service links. If you're restoring to a fresh server - the disaster recovery case - you need to recreate the entire app before you can pour the data back in.
So I started writing code to capture and replay Dokku configuration. Read the current domains with dokku domains:report, store them, replay them with dokku domains:set. Same for ports, env vars, storage mounts, SSL certs, Docker options, process scaling - everything.
At some point I looked at what I'd built and realized: this isn't a backup feature. This is infrastructure as code.
Extracting Dokkufile
The configuration capture and replay logic in snapshot had grown into its own thing. It could read the live state of any Dokku app, represent it as structured data, diff it against a desired state, and apply the changes. That's not a backup tool. That's Terraform for Dokku. I spend my days working on infrastructure as code at HashiCorp, so maybe it was inevitable that it would fall out of my side projects too.
So I extracted it. dokkufile became its own plugin - a standalone, declarative infrastructure-as-code tool. You write a dokkufile.yml:
apps:
my-app:
image: ghcr.io/org/my-app:latest
domains:
- my-app.example.com
services:
postgres:
link: my-app-db
letsencrypt: true
env:
NODE_ENV: production
storage:
/var/lib/dokku/data/storage/my-app:/app/data
services:
my-app-db:
type: postgres
Push it to your Dokku server, and a post-extract hook diffs the desired state against reality and converges. Add a domain? Add a line. Remove a port mapping? Delete a line. The hook runs before the build, so even build-time settings like Docker args and buildpack selection take effect immediately.
The key design decision: omitted fields are left untouched. Dokkufile only manages what you explicitly declare. This makes it safe to introduce incrementally - you don't have to describe your entire app on day one.
Once dokkufile existed as its own tool, I retrofitted snapshot around it. Instead of snapshot having its own configuration capture logic, it delegates to dokkufile for the config layer and focuses purely on what it's good at: data. snapshot:create captures database exports and volume tarballs. The config is already version-controlled in your dokkufile.yml. Clean separation.
Meanwhile, a Test Harness Wanted to Be More
While snapshot and dokkufile were evolving together, something completely unrelated was happening with dokku-sso.
dokku-sso manages LDAP directories and SSO for Dokku apps. Testing it meant spinning up real applications - Gitea, Grafana, Nextcloud, Radarr - configuring them, linking them to auth services, and verifying that login actually works through a browser. The test harness had to deploy and configure these apps from scratch every CI run.
That test harness was essentially a deployment automation tool. Each app had a JSON manifest describing what image to pull, what databases to provision, what env vars to set, what domains to configure. The CI would read the manifest and run the corresponding Dokku commands.
After a few apps, I realized: this is useful outside of testing. If my test harness can deploy Gitea in one command, why can't anyone deploy Gitea in one command? The deployment logic was already written. It just needed a better interface than "run the auth test suite."
So I pulled it out into dokku-library - a curated app catalog for Dokku. dokku library:checkout gitea and you get a running Gitea instance with a database, SSL, and proper configuration.
The original manifests were JSON - verbose, with a lot of deployment logic baked in. They described not just what the app should look like, but how to deploy it step by step. When dokkufile matured, the manifests could be dramatically simplified. Instead of encoding deployment procedures, each manifest just describes the desired state in dokkufile's YAML format. The library plugin resolves template variables (domain, secrets, app name), writes a temporary dokkufile, and calls dokkufile:apply. All the heavy lifting - service provisioning, env var management, domain configuration, SSL - is handled by dokkufile.
The manifest for a complex app like Outline (which needs Postgres, Redis, and a dozen env vars) went from a sprawling JSON blob to a clean YAML file that reads like documentation.
How They Fit Together
The three plugins solve three different problems but share a common foundation:
dokkufile is the base layer. Declarative config management for Dokku apps. It answers: "what should this app look like?"
dokku-library builds on dokkufile. It provides curated app manifests that resolve to dokkufile YAML. It answers: "I want to run Gitea/Ghost/Nextcloud - just set it up."
dokku-snapshot complements dokkufile. It handles the data that dokkufile can't - database contents and volume files. It answers: "if this server dies, can I get everything back?"
Together, they cover the full lifecycle:
- Deploy a new app from the library (
library:checkout grafana) - Manage its configuration as code (
dokkufile.ymlin your repo) - Back up its data (
snapshot:create grafana) - Restore everything to a new server (apply the dokkufile, restore the snapshot)
None of this was planned. Snapshot needed config management, so dokkufile got extracted. Auth needed app deployment for testing, so library got extracted. Library needed a deployment engine, and dokkufile was already there. Three plugins that grew toward each other from opposite directions.
And to bring it full circle: dokku-sso's test harness - the thing that became dokku-library - now uses dokku-library. Instead of each test setting up Gitea or Grafana from scratch with custom deployment scripts, the CI just runs dokku library:checkout gitea and gets a working instance. The test code that spawned the library was replaced by the library itself.
What I Learned
Extract ruthlessly. When snapshot's config layer started looking like its own tool, extracting it felt like a detour. It wasn't. It made snapshot simpler, and it made library possible. The extraction paid for itself twice.
Test harnesses are products. The dokku-sso test harness was solving a real deployment problem - it just happened to be solving it in CI instead of production. Recognizing that the test tooling had standalone value turned throwaway test code into a real plugin.
Shared foundations compound. Once dokkufile existed, every subsequent tool got simpler. Library didn't need its own deployment logic. Snapshot didn't need its own config capture. Each new plugin that builds on dokkufile starts further ahead.
The three plugins are on GitHub: dokkufile, dokku-snapshot, and dokku-library.
Comments
Leave a Comment