Building Hostship: A Lightweight Dokku Alternative

I wanted a hassle-free way for users to deploy Plark—a self-hosted website builder—on any VPS.

Think Dokku’s spirit (originally under 100 lines of Bash) … I want it lightweight, for a single application, and simple for users.

I was inspired by ONCE’s model, where you buy a license, self-host on your own server, and start everything with a single command.

Looking closer, I saw that ONCE protects their images with pull authorization—you buy a license, get a token, and only then can you pull the docker image. My initial plan was to follow that same approach.

But mid-build I asked: Is that the right direction?

Reach Over Control

It’s hard to sell a product without letting people try it firsthand. And with a self-hosted product, I don’t incur per-user infrastructure costs—so why not open it up? I chose a simpler path:

  • Open installs for everyone.

  • Gate features behind a license.

This approach keeps things simple and accessible. Of course, someone could try to reverse-engineer and unlock features (violating the terms of use), but with frequent updates and the value of stability, most users would stick with the official builds.

In return, I avoid the complexity of running a private registry and auth stack, and simply push images to Docker Hub. That not only reduces moving parts, but also makes it trivial to distribute Plark on platforms like Railway, Render, Dokploy, and others—anywhere that can pull public Docker images.

The result: less friction, wider distribution, and fewer moving parts..

Paid features can be toggled in-app using a license key which is verified against a lightweight License endpoint—completely decoupled from deployment.

Objective

  • One-liner install. No prerequisites beyond a fresh VPS. 

  • Single compose file. Defines the services needed for your application. 

  • Pull-and-run updates. A tiny listener that can trigger updates on request.

Compose metadata

The application is defined by a single compose.json file hosted in an S3 bucket, which always points to the latest version. When an update is triggered, the CLI fetches the latest compose file and overrides the local one.

The CLI

hostship setup <compose-url>
  • Installs Docker (if missing).

  • downloads compose.json

  • Writes a .env that includes an DEPLOY_URL. Triggering this url will update your compose to the latest version.

    • DEPLOY_URL=http://172.17.0.1:8080/update/<KEY>

      The server listener validates the key before updating.

hostship start
  • Starts the application.

hostship hotreload
  • A tiny HTTP listener, that when triggered fetches the latest compose.json and restart the application.

hostship systemd install
  • To ensure the service listener runs in the background and persists across reboots, this configures a systemd service.

Domain Management

One aspect of tools like Dokku is domain management—proxying requests, generating SSL certificates, and wiring everything up automatically. ONCE CLI as well, for example, asks for the domain name right during the setup. I initially considered following that pattern but decided to take a different path.

Decoupling Networking from the CLI

Domain and proxy requirements may vary widely across users and applications, For example:

  • Some need simple redirects (e.g. www → apex domain or vice versa).

  • Custom cache headers.

  • Multiple domains for different services.

  • Multiple domains for the same service.

Baking domain and proxy management into the CLI would make it either bloated or too opinionated. Instead, I chose not to handle these concerns at all, and leaned on Caddy’s API—letting the Docker services themselves handle domains, SSL, and related configuration.

Domain Handling

If the compose file is responsible for spinning up containers, Hostship is responsible for fetching and updating the compose.json file.

From there, the Docker services themselves take over. On startup, Plark makes an initial call to Caddy’s API to check the current configuration.

  • If the configuration is empty, it initializes Caddy, so the application is exposed on the server’s IP address. 

  • Once initialized, Plark also provides access for admin users to update the domain name of the service.

Another upside of this approach: unlike ONCE, this doesn’t force domain entry through the CLI. It also means a server can be fully provisioned without the user ever needing to SSH into it manually—since many providers (like Hetzner and DigitalOcean) support running an init script via cloud-init on first boot. Therefore, you can pass in the install script, and the server comes online with your application already running.

Auto-Generating Domains: A Failed Experiment

I briefly experimented with auto-generating domains by combining the service name and the server’s IP address, for example: service.12.123.22.123.plark.com.

This could be paired with services like sslip.io or a self-hosted custom DNS, to resolve IP-based domains and issue SSL certificates automatically. But the drawbacks piled up quickly:

  • Let’s Encrypt rate limits: issuing certificates repeatedly under the same apex domain quickly hit caps.

  • Extra dependencies: relying on external DNS services adds fragility.

  • User confusion: it is unexpected to install software on your own server and see it exposed on a domain name that you have no control over.

Why IPs are good enough (and now secure)

Exposing services on an IP address is perfectly fine for the initial setup. Users can always attach their own domains right after.

Even better: Let’s Encrypt has announced that, later this year (2025), it will begin issuing certificates for IP addresses. This means services exposed by IP will soon be able to run securely.

Last Thoughts

What I took away from building Hostship is that cutting complexity pays off. A small CLI, a single compose file, and a lightweight update flow can go a long way without dragging in the weight of a full PaaS. By focusing only on the essentials, I ended up with a setup that’s easy to ship, easy to maintain, and easy for others to run.

If you’re working on building self-hosted product, this pattern can save you weeks of engineering time — and spare users a lot of unnecessary friction.

Want to see how it works under the hood? The code is open source and available on GitHub: plark-inc/hostship

Built with Plark