Docker
docker run -d --name ghchronicle \ -v /etc/ghchronicle/config.yaml:/config.yaml:ro \ -e GITHUB_TOKEN -e INFLUX_TOKEN \ -p 9605:9605 \ ghcr.io/jmrplens/ghchronicle -config /config.yamlThe image
Section titled “The image”Built FROM gcr.io/distroless/static-debian13:nonroot over a static,
CGO_ENABLED=0 binary. There is no shell and no package manager in it, so code
execution inside the container has nothing to pivot with, and the nonroot tag
bakes in uid 65532, which keeps it off root even when the orchestrator sets
no securityContext of its own.
Two consequences worth knowing before you debug it:
docker exec ... shdoes not work. There is nosh. Read the logs instead.- Anything the container writes must be owned by, or writable by, uid 65532.
What has to be writable
Section titled “What has to be writable”The config file is mounted read-only. Four things are not:
| Path | Needed for |
|---|---|
state_ | Always. Without a persistent path the stargazer walk repeats on every restart |
sinks. | Always. The write ledger, which defaults to sitting beside the state file |
sinks. | Only with the file sink |
log. | Only with a log file configured |
Both of the first two live in the same directory by default, so one mounted volume covers them. Mounting only the state file loses the ledger on every restart, and every restart then costs a whole sweep of rewriting, which is the one thing the ledger exists to prevent: only what changed is written.
docker volume create ghchronicle-statedocker run -d --name ghchronicle \ -v /etc/ghchronicle/config.yaml:/config.yaml:ro \ -v ghchronicle-state:/var/lib/ghchronicle \ -e GITHUB_TOKEN \ ghcr.io/jmrplens/ghchronicle -config /config.yamlThe port
Section titled “The port”EXPOSE 9605 is the Prometheus exporter, and it is the only listener the
process ever opens. Publish it only if you enabled the prometheus sink; every
other sink is outbound.
With compose
Section titled “With compose”services: ghchronicle: image: ghcr.io/jmrplens/ghchronicle command: ["-config", "/config.yaml"] restart: unless-stopped environment: GITHUB_TOKEN: ${GITHUB_TOKEN} INFLUX_TOKEN: ${INFLUX_TOKEN} volumes: - ./config.yaml:/config.yaml:ro - state:/var/lib/ghchroniclevolumes: state:services: influxdb: image: influxdb:3-core volumes: - influx:/var/lib/influxdb3 ports: - "8181:8181"
ghchronicle: image: ghcr.io/jmrplens/ghchronicle command: ["-config", "/config.yaml"] restart: unless-stopped depends_on: - influxdb environment: GITHUB_TOKEN: ${GITHUB_TOKEN} INFLUX_TOKEN: ${INFLUX_TOKEN} volumes: - ./config.yaml:/config.yaml:ro - state:/var/lib/ghchronicle
volumes: influx: state:The collector reaches the database by service name, so
sinks.influxdb.url is http://influxdb:8181.
One sweep, then exit
Section titled “One sweep, then exit”The container takes the same flags as the binary, so a scheduler can run it without a long-lived service.
docker run --rm \ -v /etc/ghchronicle/config.yaml:/config.yaml:ro \ -v ghchronicle-state:/var/lib/ghchronicle \ -e GITHUB_TOKEN \ ghcr.io/jmrplens/ghchronicle -config /config.yaml -onceMount the state volume in this mode too. It is what makes the second run cheap.