Ansible Galaxy & Git Mirror Daemon

ORBITRON

Self-hosted mirror for Ansible roles & collections β€” from Ansible Galaxy or straight from GitHub. A single static Go binary that speaks native Galaxy V1 + V3 APIs β€” soansible-galaxy just works against it, with zero client changes, even when there is no internet.

CLIENTansible.cfgini
[galaxy]
server_list = orbitron

[galaxy_server.orbitron]
url = https://orbitron.internal/api/
  • 1static binary
  • 0runtime deps
  • V1+V3Galaxy APIs
Capabilities

Built for isolated, pinned, repeatable automation

Orbitron caches whatever your pipelines need β€” roles, collections, git-backed sources β€” pulled from Ansible Galaxy or cached straight from GitHub & GitLab, and serves them as an authentic Galaxy endpoint. Everything ships in one dependency-free binary.

Native Galaxy V1 + V3 API

Drop-in for ansible-galaxy role install and collection install. Point ansible.cfg at it β€” no client changes, no retraining. Roles are cached as unpacked git checkouts and streamed as on-the-fly .tar.gz; collections are stored as .tar.gz artifacts served with real Galaxy V3 metadata. Pull from Ansible Galaxy β€” or cache straight from GitHub.

Official Ansible collection

Manage the daemon purely with Ansible: chrisvanmeer.orbitron ships seven HTTP modules (info, token, manifest, sync, purge, prune, dump) and two roles β€” orbitron for install & lifecycle, orbitron_mirror for declarative mirroring. One playbook run, from empty host to serving cache.
Manage it from Ansible β€” read the guide β†’

One static binary

Compiled with CGO_ENABLED=0 for linux/amd64 and linux/arm64. Zero runtime dependencies β€” drop it on a bare-metal box, a container, or a Nomad allocation and it runs.

Parallel background sync

Ingest YAML requirements manifests once. Roles and collections are cloned and downloaded by concurrent workers, stored under content-addressed SHA-256 hashes, and replayed on every full re-sync.

Ansible-compatible version specifiers

latest, all, =1.4.5, >=1.0.0, ~=1.4.5, <2.0 and wildcards β€” a zero-dependency PEP 440-subset engine with proper semantic ordering. Never re-downloads what it already has on disk.

Access-based pruning

--prune --days N removes versions never served inside the retention window, backed by an atomic per-storage access index. Preview candidates with POST /api/v1/prune with {"dry_run": true}. Never-accessed versions are always kept.

Galaxy-themed web dashboard

The /ui cache matrix: sortable storage inventory with block-level disk usage, tail-f log streaming, and system telemetry. Embedded HTMX, no CDN calls β€” air-gap ready. Type the mirror's name and see what happens.

Authentication & SSO that fits your stack

Bearer tokens and HTTP Basic by default, plus optional OpenID Connect SSO (Keycloak) for the dashboard β€” PKCE S256, group gating, HTTP-only session cookies. Protect client pulls with require_auth_pull or keep them open.

One-command cache backup

GET /api/v1/dump streams the entire mirror as one gzip tar archive β€” every role, collection artifact, git-sourced checkout, stored manifest and the access index β€” rooted at orbitron/ and closed by a dump.json manifest with a SHA-256 per file. Grab it from the dashboard's metrics drawer or with the orbitron_dump Ansible module. The token store is never included, so a content backup can't leak admin credentials into an offline vault.

Observability out of the box

Prometheus metrics on /metrics β€” uptime, cache counts, version totals, namespaces, and real block-allocated byte usage at item and version granularity β€” paired with a ready-made Grafana dashboard (orbitron_v13_current.json) that tracks multiple daemons behind one Prometheus via an Orbitron instance template variable. Uptime panels are based on a process start-time gauge, so a config reload never resets them.
Architecture

Ingest once. Sync. Serve forever.

A tightly modelled pipeline β€” one daemon, three jobs: pull from the upstream world, cache locally, and hand artifacts back over authentic Galaxy endpoints.

orbitron coreAPI + metrics in one binary
orbitron uiself-hosted HTMX dashboard
content cachededuped artifacts on disk
outbound gatewayGalaxy Β· GitHub Β· GitLab
Orbitron architecture diagram
orbitron core
  • Management API + Galaxy V1+V3 serving on one port
  • Bearer / Basic auth on every admin endpoint
  • Concurrent sync workers, SIGHUP config hot-reload
orbitron ui
  • HTMX dashboard, zero build step
  • Optional SSO via OIDC
  • Live storage inventory, sync status & cache bytes
content cache
  • SHA-256 content-addressed, deduplicated
  • .access.json tracks every read for pruning & telemetry
  • Roles as checkout trees, collections as artifacts
outbound gateway
  • Galaxy V1/V3 + GitHub/GitLab HTTPS upstreams
  • Optional forward proxy for restricted networks
  • Private CA bundles for outbound TLS
  1. 01

    Ingest manifests

    POST YAML role & collection requirements to the management API. Each manifest is content-addressed by SHA-256 and deduplicated on identical re-submission.

  2. 02

    Sync in parallel

    Concurrent workers resolve versions against Galaxy V1/V3 or a git remote, download collection .tar.gz artifacts and shallow-clone sources straight from GitHub/GitLab (--depth 1). Versions already on disk are never re-fetched.

  3. 03

    Store on disk

    Roles keep unpacked checkout trees; collections are stored as artifacts β€” both tracked through an atomic access index (.access.json) for pruning and telemetry.

  4. 04

    Serve like Galaxy

    Roles β€” cached as unpacked git checkouts β€” stream out as on-the-fly .tar.gz; collections are served from their stored .tar.gz with full V3 metadata. ansible-galaxy sees a normal Galaxy server.

The /ui dashboard

Operations on the bridge of a spaceship

Full-screen, zero-dependency monitoring built into the binary and styled like the Orbitron site itself β€” a soft galaxy backdrop with drifting orbital rings, from the login screen to the cache matrix.

orbitron // cache matrix↗ click for full size
Orbitron web dashboard β€” cache matrix
  • 01

    Cache Matrix

    Every cached role and collection with sortable TYPE / NAME / VERSION / LAST ACCESS / DISK USAGE columns, live search, filters, LATEST pills, fresh-access pulses and collapsed multi-version rows that unfold on click.

  • 02

    Tail-f log stream

    A sliding β–² LOG STREAM drawer behaves like tail -f β€” pinned to new lines, releasing when you scroll up, with colorized levels, a text filter, and one-click download to orbitron-logs.txt.

  • 03

    System telemetry

    The β—„ SYS METRICS sidebar reports uplink status, cache activity, cache disk usage, mount free space, OS, architecture and last boot, plus a 14-day access sparkline. Auto-refreshing.

  • 04

    Air-gap friendly

    HTMX is embedded in memory β€” no external CDN calls. Cookie-based auth, or SSO via Keycloak. Type the mirror’s name for a surprise.

Observability

Ship a mirror, not dashboards

A ready-made Grafana dashboard accompanies the Prometheus endpoint β€” import it and point it at the scrape targets. Both variants (orbitron_v13_current.json andorbitron_v12_legacy.json) bind to the data source you pick on import.

grafana // orbitron overview↗ click for full size
Orbitron Grafana dashboard screenshot
  • 01

    Multi-daemon

    A single dashboard follows every Orbitron instance behind the same Prometheus via the Orbitron instance template variable.

  • 02

    Storage & versions

    Disk usage, role/collection counts and blocked storage bytes over time β€” per item and per cached version.

  • 03

    Overview stats

    Uptime, namespaces, tokens and more in stat panels β€” two visual variants ship in the repo.

  • 04

    Heavy hitters

    A ranked table of the largest cached versions by on-disk footprint β€” the items that actually drive your capacity plan, at a glance.

Quickstart

Up and mirroring in minutes

Five ways to run a mirror β€” bare metal, container, Nomad, Kubernetes, or fully managed by Ansible. Same superpower underneath: a Galaxy endpoint your playbooks already know how to talk to.

Install as a systemd service

Fetch a static binary from the releases page, then --install bootstraps the orbitron user, directories, systemd unit and logrotate in one shot. systemctl reload = hot SIGHUP re-config.

DEPLOYterminalbash
$ sudo ./bin/orbitron --install
$ sudo systemctl status orbitron

# create an admin token (script-friendly: -q)
$ sudo orbitron --generate-token -q

# hot-reload config.yml, no downtime
$ sudo systemctl reload orbitron

Run the GHCR image

Multi-arch image (amd64 + arm64), non-root, git and openssh-client bundled. Config is rendered from ORBITRON_* env vars β€” no file to mount. Persist your cache on a volume. Clone the repo first: the docker-compose.yml in the repo root wires up the image, ports, env and the named cache volume.

DEPLOYterminalbash
$ git clone https://github.com/chrisvanmeer/orbitron.git
$ cd orbitron
$ docker compose up -d
# uses docker-compose.yml from the repo root
[+] Running 1/1
 βœ” Container orbitron  Started

$ docker compose logs orbitron | grep "ADMIN TOKEN"
orbitron  | [2026-09-23 08:12:04] ADMIN TOKEN: obt_xxxx...
# also available via ghcr.io/chrisvanmeer/orbitron:latest

Run the bundled jobspec

orbitron.nomad.hcl ships in the repo root (service + /healthz check, persistent host volume, secrets via env). Clone the repo and run the jobspec from the clone; register a scratch host volume on the Nomad client before the first run.

DEPLOYterminalbash
$ git clone https://github.com/chrisvanmeer/orbitron.git
$ cd orbitron
$ nomad run orbitron.nomad.hcl
$ nomad status orbitron

$ nomad alloc logs <alloc-id> | grep "ADMIN TOKEN"
# host volume: see the "HashiCorp Nomad" README section

Deploy with the bundled Helm chart

A StatefulSet with a persistent volume for the cache and admin token, /healthz probes and an optional ingress. Clone the repo first: helm/orbitron in the repo root is a ready-made chart β€” configure it straight or via your own values.yaml.

DEPLOYterminalbash
$ git clone https://github.com/chrisvanmeer/orbitron.git
$ cd orbitron
$ helm install orbitron ./helm/orbitron

$ kubectl logs orbitron-0 | grep "ADMIN TOKEN"
orbitron-0  | [2026-09-25 09:12:04] ADMIN TOKEN: obt_xxxx...

# behind a reverse proxy:
$ helm install orbitron ./helm/orbitron \
  --set ingress.enabled=true \
  --set ingress.hosts[0].host=mirror.internal

Declarative, with the official collection

chrisvanmeer.orbitron covers the full lifecycle from Ansible: install, configure, bootstrap tokens, mirror wait-for-sync and day-two pruning β€” seven HTTP modules plus two roles.Ansible collection docs β†’

DEPLOYorbitron_daemon.ymlbash
$ ansible-galaxy collection install chrisvanmeer.orbitron
$ ansible-playbook -i hosts orbitron_daemon.yml

# roles:
#   chrisvanmeer.orbitron.orbitron        # install + operate daemon
#   chrisvanmeer.orbitron.orbitron_mirror # declare roles/collections

1 Β· Feed it

Post a YAML requirements manifest β€” roles, collections or git-backed sources.

INGESTroles_requirements.ymlyaml
roles:
  - name: geerlingguy.nginx
    version: ">=2.0.0"
  - name: RHEL9-CIS
    src: https://github.com/ansible-lockdown/RHEL9-CIS.git
    version: 2.0.2
APIcurlbash
curl -X POST https://orbitron.internal/api/v1/requirements/roles \
  -H "Authorization: Bearer $ORBITRON_TOKEN" \
  -H "Content-Type: text/yaml" \
  --data-binary @roles_requirements.yml

2 Β· Consume it

Point ansible-galaxy at the mirror. That is the whole setup.

CLIENTansible.cfgini
[galaxy]
server_list = orbitron

[galaxy_server.orbitron]
url = https://orbitron.internal/api/
# token = YOUR_ORBITRON_TOKEN  # if require_auth_pull
FETCHterminalbash
# install the roles
ansible-galaxy role install -r roles_requirements.yml
# install the collections
ansible-galaxy collection install -r collections_requirements.yml

Put it behind a reverse proxy. Terminate TLS with Nginx,Traefik or HAProxy in front of the daemon β€” Orbitron honorsX-Forwarded-Proto / X-Forwarded-For, and ansible.cfgabove talks native HTTPS. Point load balancer health checks at the unauthenticatedGET /healthz (200 when storage is writable). This keeps API calls, tokens and any git credentials in mirrored src URLs encrypted in transit.

Reverse proxy one-pagers

Terminate TLS in front of the daemon β€” same config as the docs, copy-paste ready.

APACHE/etc/httpd/conf.d/orbitron.confconf
<VirtualHost *:443>
    ServerName orbitron.example.com

    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/orbitron.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/orbitron.example.com/privkey.pem

    ProxyPreserveHost On
    ProxyRequests Off
    ProxyTimeout 300

    ProxyPass        / http://127.0.0.1:8080/ retry=0
    ProxyPassReverse / http://127.0.0.1:8080/

    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set X-Forwarded-For  "%{REMOTE_ADDR}s"
</VirtualHost>
CADDYCaddyfileconf
orbitron.example.com {
    # Caddy sets X-Forwarded-Proto / X-Forwarded-For automatically
    reverse_proxy 127.0.0.1:8080
}
HAPROXY/etc/haproxy/haproxy.cfgconf
frontend orbitron
    bind :443 ssl crt /etc/haproxy/certs/orbitron.pem
    default_backend orbitron_backend

backend orbitron_backend
    option forwardfor
    option httpchk GET /healthz
    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    timeout server 5m
    server orbitron 127.0.0.1:8080 check inter 10s
NGINX/etc/nginx/sites-available/orbitronconf
server {
    listen 443 ssl;
    http2 on;
    server_name orbitron.example.com;

    ssl_certificate     /etc/letsencrypt/live/orbitron.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/orbitron.example.com/privkey.pem;

    proxy_read_timeout 300s;
    proxy_send_timeout 300s;

    location / {
        proxy_pass         http://127.0.0.1:8080;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    }

    location = /healthz {
        proxy_pass http://127.0.0.1:8080/healthz;
    }
}
TRAEFIKtraefik.ymlconf
http:
  routers:
    orbitron:
      rule: "Host(`orbitron.example.com`)"
      service: orbitron
      tls:
        certResolver: letsencrypt
  services:
    orbitron:
      loadBalancer:
        servers:
          - url: http://127.0.0.1:8080
        # Traefik sets X-Forwarded-* automatically
        healthCheck:
          path: /healthz
          interval: 10s
HTTP API

Management endpoints that play nice

A small, pragmatic API surface β€” ingest content, watch syncs, inspect storage and run day-two operations. The Galaxy V1 & V3 serving endpoints live on the same tree.

  • GET/healthz

    Unauthenticated liveness for load balancers & orchestrators β€” 200 when the storage path is writable, 503 when degraded.

  • POST/api/v1/requirements/roles Β· /collections

    Ingest role & collection YAML manifests. Content-addressed by SHA-256 and deduplicated; parallel workers mirror on the spot.

  • POSTGET/api/v1/sync Β· /sync/status

    Re-run every stored manifest, or poll progress and a bounded history of finished syncs.

  • GETDELETE/api/v1/storage

    Full cached-version inventory. DELETE /storage/{type}/{name}/{version} purges a single cached version.

  • POST/api/v1/prune

    Access-based cleanup with retention window β€” preview exactly what would be deleted with {"dry_run": true}.

  • GETPOSTDELETE/api/v1/tokens

    Token lifecycle: create, list, revoke and rotate admin tokens, with optional per-token TTLs.

  • GET/api/v1/dump

    Stream the whole cache as a .tar.gz backup, with a per-file SHA-256 manifest. The token store is never included.

  • GET/metrics

    Prometheus exposition β€” uptime, cache counts, version totals and block-level storage bytes.

Every endpoint above except /healthz requires a Bearer token or HTTP Basic auth.

self-host Β· cache Β· serve

Keep automation moving, even offline

Grab the binary, feed it a requirements file, and let your fleet install roles and collections at LAN speed β€” pinned, cached and yours forever.