Swarrow: The Deployment Capability I Actually Needed
In my colophon, I mention that I run this site on a dedicated Hetzner box that I self-manage, and that everything is containerised with Docker and orchestrated with Docker Swarm. I chose it because I think it is perfect for this use case. I definitely don't need Kubernetes.
Docker Swarm does what I need without asking me to turn operating a server into a second job. The server is intended to host several of my projects, and I keep the configuration for that server in one central infrastructure repository. Each application lives elsewhere, in its own repository, with its own build and release cycle.
That arrangement felt clean at first, but I became increasingly uncomfortable with the handoff between the two.
An application repository can build a container image whenever I merge a change. But how should its GitHub Actions workflow put that image onto the server? The usual answers are straightforward: give the workflow SSH access, let it run Docker commands remotely or expose some part of the Docker API.
I looked at tools and approaches built around remote command execution: Kamal, Capistrano, Ansible over SSH and Docker over SSH. They differ considerably and can be constrained, but their basic unit of access was still broader than I wanted. My workflow needed to select one image for one existing service, not obtain a general-purpose path for running commands on the host.
The workflow does not need to administer the server. It does not need to create services, change networks, inspect secrets, alter mounts or decide where workloads should run. It needs to say one thing: there is a new image for this application, and I would like the existing service to use it.
That smaller sentence is what eventually became Swarrow.
The awkward authority in a simple deployment
SSH is appealing because it is universal. Once a workflow can reach the machine, the deployment can be a script containing whatever commands happen to be useful.
On a server shared by several projects, SSH access granted for one application is not naturally limited to that application. The same is true of membership in the docker group or access to the Docker socket. Docker can start privileged containers, mount the host filesystem, change other services and reach much further than replacing one image. Access to it is effectively authority over the host and the wider swarm.
I could try to constrain an SSH key to a particular command, then make that command parse a carefully restricted set of arguments. But at that point I would already be building a protocol and a policy boundary. Hiding it behind SSH would not make the boundary simpler; it would only make it less explicit.
And so the question changed for me from “how can GitHub Actions run the deployment command?” to “what is the smallest deployment capability this repository should have?”
The answer was not “access to Docker”. It was not even “access to this service” in the general sense. It was permission to select one immutable image for one service that had already been configured by the infrastructure operator.
Infrastructure and releases belong to different places
There was another boundary I wanted to preserve.
It is common for an application repository to contain a Compose or stack file alongside the source. Sometimes that is exactly the right arrangement. In my case, though, the stack file increasingly felt like infrastructure definition rather than application definition.
The application knows how to build itself. The server knows how that application is allowed to run.
Networks, storage, secrets, placement constraints, resource limits, replica counts and rollout behaviour describe the server and its operational policy. They may also describe relationships between several projects. Keeping those decisions in each application repository would either scatter the server’s desired state across repositories or require copying parts of it between them.
The fact that an application might be open source made the discomfort easier to notice, although secrecy was not the main issue. A public stack file does not grant anyone access by itself, and infrastructure should not depend on its topology remaining secret. The more important problem is ownership. Publishing an application should not mean publishing or maintaining a partial definition of the server on which I happen to run it.
So I wanted a clean division:
- The infrastructure repository defines the services, networks, storage, secrets, resources and rollout policy.
- The application repository builds images and decides when to release them.
- Docker Swarm owns the live runtime state.
- Something small carries the release decision from the application to the existing service.
Swarrow is that last part. It does not read an application’s Compose file, create a stack or reconcile infrastructure. It leaves the topology where it already belongs.
A deliberately small deployment handoff
A Swarrow deployment begins in a GitHub Actions workflow. The workflow builds and publishes an image, then resolves it to an immutable digest. Instead of sending a tag such as latest or main, it sends the SHA-256 digest identifying the exact image it intends to run.
The request contains three meaningful pieces of information:
- A short-lived identity token issued by GitHub for the running job
- The name of a deployment already configured in Swarrow
- The immutable image digest
The request does not contain a Docker service name or image repository. Swarrow already knows both from its local configuration. It verifies the GitHub token, checks that the workflow may use the named deployment, combines the approved repository with the supplied digest and changes only the image of the approved service.

This is the separation I was after. The application chooses its release. The infrastructure operator chooses what that release is allowed to affect.
There is still quite a lot hiding behind that small exchange. Swarrow has to make retries safe, prevent a captured request from being reused, avoid overwriting simultaneous infrastructure changes and distinguish Docker accepting an update from Swarm successfully rolling it out. It keeps the request open while it observes the rollout and reports what it actually saw.
Narrow does not mean harmless
It's worth highlighting that Swarrow is still a privileged component. It talks to the Docker API on a Swarm manager, so compromising Swarrow or its host can compromise the swarm. The value is not that Swarrow makes Docker access harmless. It is that the public interface is much smaller than Docker’s interface and local policy is checked before any Docker operation occurs.
It also does not sandbox the application being deployed.
An authorised repository chooses code that will run as its configured service. That code receives whatever secrets, networks, storage, Linux capabilities and other privileges the infrastructure operator already assigned to the service. If the application repository is compromised, the attacker may gain all of those application-level capabilities.
Swarrow limits control-plane authority. It prevents that workflow from selecting another service, rewriting topology or making arbitrary Docker calls. It cannot make an over-privileged workload safe, and it does not claim to provide isolation between mutually hostile applications.
This means that “least privilege” in Swarrow's sense is always relative to the work being delegated. Choosing an application image is already meaningful production authority. Swarrow simply avoids attaching authority over the rest of the server to it.
The first release
Swarrow v0.1.0 is now available under the Apache License 2.0.
The release includes standalone Linux binaries for AMD64 and ARM64, as well as a multi-platform container image. The release workflow publishes checksums, provenance, an SBOM, attestations and a text file containing the image’s immutable digest. It does not publish a mutable latest tag.
This is intentionally a pre-1.0 release. The interfaces may still change as I use it and learn where the operational edges are. The scope is also deliberately small: GitHub Actions, one file-based policy, direct workflows, immutable image digests and existing Docker Swarm services.
The design, threat model, configuration reference and HTTP API contain the detail I have left out here. Oh, and the source is public on GitHub.
The thing I wanted was not automation powerful enough to deploy anything. I wanted a way for a project to release a new image without giving it authority over the server around it. Swarrow is my answer to that.