Deploy with Docker or Podman
The recommended path for operators who run containers. The repository ships:
- a
Dockerfile— the coordinator binary plus its host-side runtime dependencies (gitand a pinnedactrelease). Job containers are created byactthrough the Docker-compatible daemon atDOCKER_HOST, never inside this image, so the image contains no container runtime, docker-compose.yml— coordinator + a dedicated docker-in-docker (dind) sidecar (the default, recommended layout),docker-compose.host-socket.yml— coordinator against a daemon socket mounted from the host (no dind, nothing privileged).
The image is build-it-yourself for now. We plan to build and publish it via CI produced using this runner.
Quick start (dind sidecar)
bash
git clone https://gitnostr.com/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-ci.git
cd ngit-ci
NGIT_CI_REPOS='npub1.../ngit#NGIT' docker compose up --build -dThe two main operator settings are:
NGIT_CI_REPOS— the only required setting: comma-separated repos to watch, eachnpub1...(all repos by that author) ornpub1.../identifier(one repo). Append#ALIAS(for examplenpub1.../ngit#NGIT) when that repo needs secrets.NGIT_CI_SECRET_<ALIAS>__<NAME>— optional per-repo secrets exposed to trusted workflows as${{ secrets.NAME }}. The alias must match a#ALIASinNGIT_CI_REPOS; note the double underscore before the secret name. Put these in the shell environment or a locked-down.envfile, not on argv.NGIT_CI_ACT_CACHE_ENABLED=false— disable the default persistent, trust-scoped build cache; see cross-run build cache.
Example with a secret:
bash
cat > .env <<'EOF'
NGIT_CI_REPOS='npub1.../ngit#NGIT'
NGIT_CI_SECRET_NGIT__DEPLOY_TOKEN=...
EOF
chmod 600 .env
docker compose up --build -dSecrets are injected only for maintainer-authored triggers and never cross repo boundaries. See per-repo secrets for the full trust model and naming rules.
Maintainers can also provision secrets themselves without operator involvement. Set NGIT_CI_NOSTR_SECRET_RELAYS to one or more coordinator inbox relays (the compose file passes it through; empty, the default, disables intake) and repository maintainers can submit encrypted Repository Secret Updates from GitWorkshop for every watched repository. Values are stored encrypted on the coordinator-data volume and an operator value wins a same-name collision. See receiving secrets over Nostr and, for the optional NGIT_CI_OPERATOR_BUNKER=nbunksec1... line in ngit-ci-secrets.env, bunker-sealed secret values.
Set NGIT_CI_BLOSSOM_SERVERS to optionally upload full job logs and workflow artifacts to one or more Blossom servers; empty (the default) disables those uploads. When the upload endpoint is private, set NGIT_CI_BLOSSOM_PUBLIC_URL to the public origin published in result tags.
Everything else is tunable via NGIT_CI_* environment variables — see configuration.md — with workable defaults in the compose file, including per-job container resource caps (NGIT_CI_ACT_CONTAINER_OPTIONS, default --memory=4g --memory-swap=4g --cpus=2 --pids-limit=2048).
In this layout act's job containers run inside the dind sidecar, so untrusted job containers never touch the host's Docker daemon. The dind container is privileged (dockerd requires it); its unauthenticated TCP port is reachable only on the compose-internal network — do not add a ports: mapping to it. The compose file sets NGIT_CI_ACT_BIND_WORKSPACE=false because the sidecar daemon cannot resolve paths inside the coordinator container; act copies the checkout through the Docker API at each actions/checkout step instead.
Variant: host daemon socket (no dind)
Where privileged containers are unavailable or unwanted — e.g. inside an unprivileged Proxmox LXC container, or on hosts that already run a suitable daemon:
bash
DOCKER_GROUP_ID=$(stat -c %g /var/run/docker.sock) \
NGIT_CI_REPOS='npub1.../ngit#NGIT' \
docker compose -f docker-compose.host-socket.yml up --build -dThe trade-off is the shape of the risk, not strictly more or less of it:
- default dind: the dind sidecar is privileged (its escape would be host root), but job containers can never see or control the host's daemon and leave no state on it.
- host socket: nothing is privileged, but the mounted socket makes the coordinator container root-equivalent on a rootful Docker host, and CI job containers run as siblings on your host daemon. Job containers themselves still get no daemon socket (the coordinator's default
--act-container-daemon-socket -).
Middle ground: point DOCKER_SOCKET at a rootless Podman socket (typically /run/user/<uid>/podman/podman.sock, served by podman system service or its systemd socket unit). Nothing is privileged and the socket is not root-equivalent; set DOCKER_GROUP_ID to the gid owning that socket.
The host-socket compose file also sets NGIT_CI_ACT_BIND_WORKSPACE=false: the daemon runs outside the coordinator container's mount namespace and cannot bind its /data/work paths directly.
State and persistence
Everything the coordinator persists lives in the /data volume (coordinator-data):
.coordinator.nsec— the coordinator's generated Nostr identity key. CI results are signed by this key, so clients' trust attaches to it: back it up, keep the volume. To pin an existing identity, place the<COORDINATOR_SECRET_KEY>value in/data/.coordinator.nsecinside the coordinator data volume before first start rather than passing it on the command line. If/data/.coordinator.nsecis missing, the coordinator generates it on startup and persists it incoordinator-data. If you do deliberately provide a key through the file orNGIT_CI_COORDINATOR_NSEC, an invalid or empty value is a startup error; ngit-ci will not silently generate a replacement identity for a bad configured key.processed-ids.json— processed-event-id dedupe state, preventing duplicate runs across restarts.queued-jobs.json— unstarted jobs frozen during a graceful stop and recovered after a short restart.work/— clones and per-job state.
The dind variant additionally persists pulled job images in dind-storage (the catthehacker act platform image is several hundred MB; keep the volume to avoid re-pulling).
Operating
bash
docker compose logs -f coordinator # follow coordinator logs
git pull && docker compose up --build -d # upgrade
docker compose down # stop (volumes/state retained)Set NGIT_CI_LOG_LEVEL=debug for more coordinator tracing.
Compose gives the coordinator 33 minutes to stop: the default 30-minute job timeout, two-minute post-run upload budget, and one minute of overhead. A stop freezes queued starts immediately but waits for active jobs to publish their results before checkpointing the queue. If you raise either job/upload timeout, raise NGIT_CI_STOP_GRACE_PERIOD by the same amount so Docker does not send SIGKILL before the checkpoint is written.