GitOps with SwarmCLI charts
August 13, 2026 · View on GitHub
Renovate opens a PR when a chart you pin gets a new version. swarmcli charts apply converges the swarm to whatever the file says. This guide is the joint
between them: a CI job that runs the apply after the PR merges, so the merge
is the deployment.
Everything here is one repository containing your swarmcli-release.yaml, its
values files, and a CI workflow. Nothing needs to run on the swarm itself.
The file
# swarmcli-release.yaml
repositories:
- name: swarmcli-charts
url: https://eldara-tech.github.io/swarmcli-charts
releases:
- name: edge
chart: swarmcli-charts/traefik
version: "0.1.1"
values: [./values/traefik.yaml]
- name: hello
chart: swarmcli-charts/whoami
version: "0.1.8"
Quote the version. Renovate's built-in helmfile manager reads this file
because its key names are Helmfile's, and that schema accepts a number — so an
unquoted 1.0 is parsed as a float and written back as "1", which resolves to
nothing. "0.1.8" is a string and stays one.
Chart versions are plain SemVer. The leading v belongs to the release git tag,
not to the version in this file.
Reaching the swarm from CI
swarmcli deploys by running docker --context <name> stack deploy, so the CI
environment needs two things: the docker CLI on PATH, and a named Docker
context pointing at the swarm. DOCKER_HOST alone is not the supported path —
create a context and select it.
Selecting it means either docker context use <name>, or setting
DOCKER_CONTEXT=<name> in the environment, which takes precedence and leaves
the runner's Docker config untouched. The examples below use context use
because it is the more familiar of the two; in a job that does anything else
with Docker, prefer the variable.
Over SSH (recommended)
Nothing is exposed: the daemon keeps listening only on its local socket, and the
connection is an ordinary SSH session running docker system dial-stdio on the
far end.
docker context create swarm --docker "host=ssh://deploy@manager.example.com"
docker context use swarm
Three things to get right, and each fails in a way that is not obvious:
- The remote user must be able to talk to the daemon — in the
dockergroup, or whatever your host's equivalent is.ssh deploy@manager docker versionis the check; if that works, so will this. - Pin the host key. Write
known_hostsfrom a CI secret rather than disabling checking. An unknown host key fails the connection closed, which is the behaviour you want, but it fails at the deploy step with a message about ssh rather than about your chart. - OpenSSH finds
~/.ssh/configthroughgetpwuid(), not$HOME. If your job runs as a UID with no matching/etc/passwdentry — common in container executors — the config you wrote is silently ignored: no port, no identity, no host-key policy. Either run as a user that exists, or put the settings in the context URL and pass-Fexplicitly.
Over mTLS
If you already run a CA and expose the daemon on a TCP port, that works too and needs no SSH access to the host:
docker context create swarm \
--docker "host=tcp://manager.example.com:2376,ca=$PWD/ca.pem,cert=$PWD/cert.pem,key=$PWD/key.pem"
docker context use swarm
The three PEM files come from CI secrets. This is more moving parts than the SSH path and exposes a port that must then be firewalled; prefer SSH unless you have the CA already.
Getting the binary
GitHub Actions
ubuntu-latest already has the docker CLI and an SSH client, so the only thing
to fetch is swarmcli itself. Download the release asset and check it — the
assets are public, so this needs no token:
- name: Install swarmcli
env:
SWARMCLI_VERSION: v1.13.0
run: |
base="https://github.com/Eldara-Tech/swarmcli/releases/download/$SWARMCLI_VERSION"
curl -fsSL -O "$base/swarmcli_Linux_x86_64.tar.gz"
curl -fsSL -O "$base/checksums.txt"
grep ' swarmcli_Linux_x86_64.tar.gz$' checksums.txt | sha256sum -c -
tar xzf swarmcli_Linux_x86_64.tar.gz swarmcli
sudo install swarmcli /usr/local/bin/
Which artefact. From the release after v1.13.0, one swarmcli tag publishes two builds.
swarmcli_<OS>_<ARCH>.tar.gzandeldaratech/swarmcli:<tag>are the Business Edition build, carrying licensed code that is inert without a licence;swarmcli_<OS>_<ARCH>_oss.tar.gzandeldaratech/swarmcli:<tag>-ossare wholly Apache-2.0, withchecksums-oss.txtcovering them. Rendering and applying charts needs nothing licensed, so a CI job should take theossone — substitute both names in the snippets below when you bump the pin past v1.13.0. The executable inside isswarmclieither way, so nothing else in these examples changes. See editions.
Not the container image, here.
eldaratech/swarmcliis builtFROM docker, so it is Alpine-based. Alpine is not among the distributions the Actions runner supports (actions/runner#801), and JavaScript actions such asactions/checkoutare not guaranteed to run inside one. Use the image in adocker run, or on a CI system that does not inject actions into the container — see below.
GitLab CI, Gitea Actions, Drone
Here the image is exactly right: it carries swarmcli, the docker CLI and an
SSH client already, and nothing is injected into it.
image:
name: eldaratech/swarmcli:v1.13.0
entrypoint: [""] # the image's entrypoint is swarmcli itself
The workflow
Two jobs: one that shows the operator what a PR would do, one that does it after the merge.
# .github/workflows/deploy.yml
name: Deploy
on:
pull_request:
paths: ['swarmcli-release.yaml', 'values/**']
push:
branches: [main]
paths: ['swarmcli-release.yaml', 'values/**']
# One convergence at a time. Two merges applying to the same swarm concurrently
# would race, and cancelling a half-finished apply is worse than queueing.
concurrency:
group: swarm-deploy
cancel-in-progress: false
jobs:
apply:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install swarmcli
env:
SWARMCLI_VERSION: v1.13.0
run: |
base="https://github.com/Eldara-Tech/swarmcli/releases/download/$SWARMCLI_VERSION"
curl -fsSL -O "$base/swarmcli_Linux_x86_64.tar.gz"
curl -fsSL -O "$base/checksums.txt"
grep ' swarmcli_Linux_x86_64.tar.gz$' checksums.txt | sha256sum -c -
tar xzf swarmcli_Linux_x86_64.tar.gz swarmcli
sudo install swarmcli /usr/local/bin/
- name: Point at the swarm
env:
SSH_KEY: ${{ secrets.SWARM_SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.SWARM_KNOWN_HOSTS }}
run: |
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/id_ed25519
docker context create swarm --docker "host=ssh://deploy@manager.example.com"
docker context use swarm
# No `charts repo add` step: apply reads the `repositories` block and adds
# what is missing, so a fresh runner needs no prior state. It refuses to
# silently repoint a name that is already configured at another URL.
# On a PR: show the diff and stop. --diff implies --dry-run, so this
# cannot deploy even by accident.
- name: Plan
if: github.event_name == 'pull_request'
run: swarmcli charts apply -f swarmcli-release.yaml --diff
# On main: converge, and wait for it rather than reporting success the
# moment the API accepts the spec.
- name: Apply
if: github.event_name == 'push'
run: swarmcli charts apply -f swarmcli-release.yaml --wait --timeout 10m
The GitLab equivalent runs the same two apply invocations; the image replaces
the install step, and resource_group replaces concurrency:
# .gitlab-ci.yml
default:
image:
name: eldaratech/swarmcli:v1.13.0
entrypoint: [""]
before_script:
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- printf '%s\n' "$SWARM_SSH_KEY" > ~/.ssh/id_ed25519
- printf '%s\n' "$SWARM_KNOWN_HOSTS" > ~/.ssh/known_hosts
- chmod 600 ~/.ssh/id_ed25519
- docker context create swarm --docker "host=ssh://deploy@manager.example.com"
- docker context use swarm
plan:
rules: [{ if: $CI_PIPELINE_SOURCE == "merge_request_event" }]
script:
- swarmcli charts apply -f swarmcli-release.yaml --diff
apply:
resource_group: swarm-deploy # the concurrency gate
rules: [{ if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH }]
script:
- swarmcli charts apply -f swarmcli-release.yaml --wait --timeout 10m
Renovate
One line of configuration, because this repository ships the preset:
{ "extends": ["github>Eldara-Tech/swarmcli-charts"] }
It teaches Renovate's built-in helmfile manager to read
swarmcli-release.yaml, and resolves each chart against the repositories
block in the file itself — so it works for any chart repository you add, not
only this one.
Set GITHUB_COM_TOKEN if you self-host Renovate
Renovate fetches release notes from github.com, and it fetches any
github>-hosted preset from there too — including this one. Unauthenticated,
those calls are rate-limited by IP, which on a shared runner is exhausted
quickly. The symptoms are two, and the second is the one that wastes an
afternoon:
- changelogs stop appearing in the PRs, and
- the preset itself fails to resolve, so Renovate behaves as though it had no configuration at all.
A GitHub personal access token with no scopes — read-only public access is all that is needed — fixes both:
env:
GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
This applies to self-hosted Renovate on GitLab, Gitea, Bitbucket, or the renovate chart in this repository. The hosted GitHub App does not need it.
Reviewing the PR
The plan job's output is the artifact to review. --diff prints each changed
release's manifest diff — the actual before-and-after of what will run — and
--dry-run on its own prints just the plan. Both refuse to deploy.
$ swarmcli charts apply -f swarmcli-release.yaml --diff
RELEASE CHART FROM TO ACTION
edge swarmcli-charts/traefik 0.1.1 0.1.1 unchanged
hello swarmcli-charts/whoami 0.1.7 0.1.8 upgrade
--- hello (upgrade) ---
services:
whoami:
- image: traefik/whoami:v1.10.1
+ image: traefik/whoami:v1.10.2
deploy:
replicas: 1
0 to install, 1 to upgrade, 1 unchanged
dry-run: nothing was deployed
It also names anything the swarm holds that the file no longer claims — a release this file installed and has since dropped, and any release nothing here owns. Neither is removed; a dry-run is where you want to find out.
Paste that into the PR and the review is a code review: the chart bump is the diff, and merging is what applies it.
To ask the same question outside a PR — "is anything I am running behind?" —
swarmcli charts outdated compares every deployed release against the
repository indexes.
Pulling instead of pushing
Everything above pushes: CI holds the credential, reaches the swarm, and applies.
The other shape is a controller running on the swarm that watches the
repositories itself — nothing outside needs swarm access, and drift a
docker service update introduced behind your back is noticed rather than
waited out until the next merge. That is
swarmcli-cd, and the
swarmcli-cd chart deploys it.