Skip to content

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 (git and a pinned act release). Job containers are created by act through the Docker-compatible daemon at DOCKER_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 -d

The two main operator settings are:

  • NGIT_CI_REPOS — the only required setting: comma-separated repos to watch, each npub1... (all repos by that author) or npub1.../identifier (one repo). Append #ALIAS (for example npub1.../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 #ALIAS in NGIT_CI_REPOS; note the double underscore before the secret name. Put these in the shell environment or a locked-down .env file, 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 -d

Secrets 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 -d

The 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.nsec inside the coordinator data volume before first start rather than passing it on the command line. If /data/.coordinator.nsec is missing, the coordinator generates it on startup and persists it in coordinator-data. If you do deliberately provide a key through the file or NGIT_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.

Git collaboration, without the platform.