johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
# knotless-serve and its pages, and knotless-run if asked, as
# /etc/knotless/docker-compose.yml.
# deploy.sh copies it there, beside the .env it reads; edit it here.
#
# TLS is left to a reverse proxy in front: it joins the network named by
# KNOTLESS_PROXY_NETWORK and proxies the domain to knotless-serve:2590.
#
# deploy.sh moves serve and pages with rollout.sh, which starts a new
# container beside the old under the same names and stops the old once the
# new one's healthcheck passes: neither sets a container_name, so two can
# run at once.
name: knotless
# Each container's output, JSON lines for serve and the runner, kept in
# the host's journal (deploy/README.md, Logs), which outlives the
# container: a rollout removes the old one, and its lines stay. Each line
# names its project and service (COM_DOCKER_COMPOSE_PROJECT and _SERVICE)
# and its container (CONTAINER_TAG).
x-logging: &logging
driver: journald
options:
labels: com.docker.compose.project,com.docker.compose.service
tag: "{{.Name}}"
services:
serve:
image: knotless:current
restart: always
logging: *logging
command:
- knotless-serve
- --listen
- 0.0.0.0:2590
- --public-url
- https://${KNOTLESS_DOMAIN:?set KNOTLESS_DOMAIN in .env}
- --index
- /data/index.db
# What feeds the index: Jetstream, a relay's firehose, or both.
# KNOTLESS_JETSTREAM set empty turns Jetstream off; KNOTLESS_FIREHOSE
# (a relay, wss://relay1.us-east.bsky.network) turns the firehose on.
# KNOTLESS_BACKFILL (https://relay1.us-east.bsky.network) refills the
# index from a relay at each start, for what a feed down missed.
- --jetstream
- ${KNOTLESS_JETSTREAM-wss://jetstream.us-east.bsky.network}
- --firehose
- ${KNOTLESS_FIREHOSE:-}
- --backfill
- ${KNOTLESS_BACKFILL:-}
# When an upgrade rebuilds the index, and every ten minutes after,
# it refills from the accounts this relay lists as holding knotless
# records.
- --relay
- ${KNOTLESS_RELAY:-https://relay1.us-east.bsky.network}
# Which accounts hold knotless records is asked of microcosm's
# lightrail before the relay, and each account's repository is read
# from its hubble mirror before the account's PDS. Either set empty
# is not asked.
- --directory
- ${KNOTLESS_DIRECTORY-https://lightrail.microcosm.blue}
- --mirror
- ${KNOTLESS_MIRROR-https://hubble.microcosm.blue}
- --web
- http://pages:2591
# The client's builds, as the repository's releases of its
# `knotless` package, which the release workflow publishes.
- --releases
- knotless://chadtmiller.com/knotless
# Repositories' sites, on a domain of their own (knotless.page), when
# KNOTLESS_PAGES_DOMAIN names one; setup.sh's Caddy holds its
# certificate. Empty answers none.
- --pages-domain
- ${KNOTLESS_PAGES_DOMAIN:-}
# Apple's push service, for the iOS app's notifications: deploy.sh
# sets the key's path once the key is on the host (deploy/README.md).
# Empty, none is sent.
- --apns-key
- ${KNOTLESS_APNS_KEY:-}
- --apns-key-id
- ${KNOTLESS_APNS_KEY_ID:-}
- --apns-team
- ${KNOTLESS_APNS_TEAM:-}
# /metrics and PUT /log, inside the container alone: read them
# with docker exec (deploy/README.md, Logs).
- --metrics
- 127.0.0.1:9464
# How it is used, for `knotless admin stats`: counts, never who.
- --usage
- /data/usage.db
# The lines it logs, kept to read back across deploys, for
# `knotless admin logs` and MCP's logs (deploy/README.md, Logs).
- --logs
- /data/logs.db
# Where the bots' OAuth sessions are kept once they log in at
# /oauth/bot: run and triage read them from there.
- --bots
- /data/bots
# Code search: the Zoekt shards of every public repository's default
# branch, written from its mirror as it is pushed.
- --code-index
- /data/code
# The runner a new public repository names unless its owner names
# others: the runner host's account (runner-host/README.md). Empty,
# none.
- --default-runner
- ${KNOTLESS_DEFAULT_RUNNER_DID:-}
# The oldest client version to ask for, and why: set when one harms
# repositories, and clients older refuse to run until upgraded.
# Empty, none.
- --client-minimum
- ${KNOTLESS_CLIENT_MINIMUM:-}
- --client-minimum-reason
- ${KNOTLESS_CLIENT_MINIMUM_REASON:-}
# The oldest build of the iOS app to let run, and why: older builds
# show only a way to update. Empty, any runs (ios/README.md).
- --ios-minimum-build
- ${KNOTLESS_IOS_MINIMUM_BUILD:-}
- --ios-minimum-reason
- ${KNOTLESS_IOS_MINIMUM_REASON:-}
environment:
# The gateway's OAuth client key and the sessions owners delegate
# are in the config directory; mirrors and identities in the cache.
KNOTLESS_CONFIG_DIR: /data/config
KNOTLESS_CACHE_DIR: /data/cache
# The accounts, by DID, that operate it: takedowns, reports, usage.
KNOTLESS_ADMIN_DIDS: ${KNOTLESS_ADMIN_DIDS:-}
# The accounts, by DID, that may log in at /oauth/bot: run's and
# triage's.
KNOTLESS_BOT_DIDS: ${KNOTLESS_BOT_DIDS:-}
# What is logged, as RUST_LOG takes it: info, unless .env says.
RUST_LOG: ${KNOTLESS_LOG:-info}
volumes:
# Owned by uid 1000, the node user the image runs as.
- ${KNOTLESS_DATA:-/var/lib/knotless}:/data
networks:
default: {}
# The name the reverse proxy reaches it by.
proxy:
aliases: [knotless-serve]
depends_on:
- pages
# Ahead of CI for the host's cores: a run's containers have docker's
# default share (1024), so while they keep every core busy the site
# still gets four times theirs.
cpu_shares: 4096
deploy:
resources:
limits:
cpus: "2"
memory: 2g
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:2590/healthz').then(r => process.exit(r.ok ? 0 : 1), () => process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
# Each second while it starts, so a rollout hands over as soon as it
# listens rather than at the first 30s check.
start_period: 120s
start_interval: 1s
# Stopped, it finishes the requests it has: a push or a clone can take
# a while.
stop_grace_period: 30s
pages:
image: knotless:current
restart: always
logging: *logging
command: ["node", "/app/build"]
environment:
HOST: 0.0.0.0
PORT: "2591"
KNOTLESS_SERVE: http://serve:2590
# knotless-serve hands on the host and scheme it was asked for.
HOST_HEADER: x-forwarded-host
PROTOCOL_HEADER: x-forwarded-proto
# A file the build writes, answered without asking serve anything.
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:2591/_app/version.json').then(r => process.exit(r.ok ? 0 : 1), () => process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
start_interval: 1s
# Stopped, it finishes the requests it has, for up to adapter-node's
# SHUTDOWN_TIMEOUT (30s).
stop_grace_period: 35s
cpu_shares: 4096
deploy:
resources:
limits:
# Two: at one, node's garbage collector and a render share a
# core, and the page waits out the rest of the period throttled.
cpus: "2"
memory: 512m
# knotless-run, only with COMPOSE_PROFILES=run, which deploy.sh sets
# when KNOTLESS_RUN_REPOS names repositories to watch.
run:
image: knotless:current
profiles: [run]
restart: always
logging: *logging
# Root: what holds the docker socket is root on the host already, and
# steps, run as root in their containers, leave files in the checkout
# that only root can remove.
user: root
# Stopped, it drops each run still waiting its turn, for the next
# start to run, and finishes only the runs whose steps have started,
# a batch's and those at a going deploy's commit: most finish in a few
# minutes, test in about 9, a release's build of the client in about
# 17. A deploy does not wait for that: it starts the next runner beside
# it (handover.sh), which lends the old one the jobs its runs hold, so
# waiting costs nothing, and only a stop by hand waits. --stop-wait is
# an hour, so that no run is cut off that its own limits let go on: a
# step that hangs ends at --step-timeout (30 minutes). Past the hour it
# cancels those still going itself, which a stop nothing comes to kill
# needs. 65 minutes is that with room to spare; past it, docker kills
# the runner, and the next ends what was cut off as an error and
# removes its containers.
stop_grace_period: 65m
command:
- sh
- -c
- >-
exec knotless-run
--jetstream ${KNOTLESS_JETSTREAM:-wss://jetstream.us-east.bsky.network}
--executor docker
--secrets /etc/knotless-run/run-secrets.json
--state ${KNOTLESS_RUN_DATA:-/var/lib/knotless-run}/state.json
--work ${KNOTLESS_RUN_DATA:-/var/lib/knotless-run}/work
--caches ${KNOTLESS_RUN_DATA:-/var/lib/knotless-run}/caches
--stop-wait 3600
${KNOTLESS_RUN_ARGS:-}
${KNOTLESS_RUN_REPOS:-}
environment:
# The appview beside it, where --pulls finds pulls.
KNOTLESS_APPVIEW: http://serve:2590
# The bots' sessions, which serve keeps (--bots).
KNOTLESS_CONFIG_DIR: /bots
KNOTLESS_CACHE_DIR: ${KNOTLESS_RUN_DATA:-/var/lib/knotless-run}/cache
RUST_LOG: ${KNOTLESS_LOG:-info}
env_file:
# KNOTLESS_IDENTIFIER, the account the checks are written in, and
# KNOTLESS_PASSWORD, an app password for it, used only until it logs
# in at /oauth/bot: an app password reads no private repositories.
- path: /etc/knotless/run.env
required: false
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# At the same path as on the host: a step's container mounts its
# checkout by the path the host's daemon knows it by.
- ${KNOTLESS_RUN_DATA:-/var/lib/knotless-run}:${KNOTLESS_RUN_DATA:-/var/lib/knotless-run}
# Which secrets each repository's workflows may have, by
# <handle>/<name>, in run-secrets.json, read again at each run. The
# directory, not the file: an edit that writes a new file, as most
# do, would leave a file's mount on the old one until a restart. The
# docker socket above gives the runner the host already, so the
# rest of /etc/knotless costs nothing more.
- /etc/knotless:/etc/knotless-run:ro
- ${KNOTLESS_DATA:-/var/lib/knotless}/bots:/bots
deploy:
resources:
limits:
cpus: "1"
memory: 512m
# Triages issues with a decision model, writing labels as its own
# account (knotless-triage). /etc/knotless/triage.env holds that
# account's KNOTLESS_IDENTIFIER and KNOTLESS_PASSWORD, an app password
# used until it logs in at /oauth/bot, and the model's endpoint and
# token, KNOTLESS_DECIDE_URL and KNOTLESS_DECISION_TOKEN. It keeps
# nothing but its session: what it has judged is in its records.
triage:
image: knotless:current
profiles: [triage]
restart: always
logging: *logging
command:
- sh
- -c
- >-
exec knotless-triage
--decide "$$KNOTLESS_DECIDE_URL"
--jetstream ${KNOTLESS_JETSTREAM:-wss://jetstream.us-east.bsky.network}
${KNOTLESS_TRIAGE_ARGS:-}
${KNOTLESS_TRIAGE_REPOS:-}
environment:
# The appview beside it, where it finds issues.
KNOTLESS_APPVIEW: http://serve:2590
KNOTLESS_CONFIG_DIR: /bots
KNOTLESS_CACHE_DIR: /tmp/knotless-triage/cache
RUST_LOG: ${KNOTLESS_LOG:-info}
env_file:
- path: /etc/knotless/triage.env
required: false
volumes:
- ${KNOTLESS_DATA:-/var/lib/knotless}/bots:/bots
deploy:
resources:
limits:
cpus: "0.25"
memory: 128m
# Reviews the pulls that ask its account for a review, with Claude Code
# (knotless-agent review, knotless-claude-review), writing each review
# as that account's. /etc/knotless/review.env holds the account's
# KNOTLESS_IDENTIFIER and KNOTLESS_PASSWORD, an app password, and
# Claude's login: CLAUDE_CODE_OAUTH_TOKEN, from `claude setup-token`,
# or ANTHROPIC_API_KEY. It keeps nothing but its session: what it has
# reviewed is in its records. Private repositories it reads with the
# OAuth session it gets at /oauth/bot, which KNOTLESS_BOT_DIDS lets it.
review:
image: knotless:current
profiles: [review]
restart: always
logging: *logging
command:
- sh
- -c
- >-
exec knotless-agent review
--command knotless-claude-review
--jetstream ${KNOTLESS_JETSTREAM:-wss://jetstream.us-east.bsky.network}
${KNOTLESS_REVIEW_ARGS:-}
${KNOTLESS_REVIEW_REPOS:-}
environment:
# The appview beside it, where it finds pulls; Claude reads them
# from there too.
KNOTLESS_APPVIEW: http://serve:2590
KNOTLESS_CONFIG_DIR: /bots
KNOTLESS_CACHE_DIR: /tmp/knotless-review/cache
RUST_LOG: ${KNOTLESS_LOG:-info}
env_file:
- path: /etc/knotless/review.env
required: false
volumes:
- ${KNOTLESS_DATA:-/var/lib/knotless}/bots:/bots
deploy:
resources:
limits:
cpus: "1"
memory: 1g
# Keeps the pulls the merge queue turned away moving (knotless-agent
# keep): one that conflicts only where both sides added lines it brings
# up to date and queues again. /etc/knotless/keep.env holds its
# account's KNOTLESS_IDENTIFIER and KNOTLESS_PASSWORD, an app password;
# the account is a collaborator of each repository it keeps. It runs
# no model and keeps nothing but its session.
keep:
image: knotless:current
profiles: [keep]
restart: always
logging: *logging
command:
- sh
- -c
- >-
exec knotless-agent keep
--jetstream ${KNOTLESS_JETSTREAM:-wss://jetstream.us-east.bsky.network}
${KNOTLESS_KEEP_ARGS:-}
${KNOTLESS_KEEP_REPOS:-}
environment:
KNOTLESS_APPVIEW: http://serve:2590
KNOTLESS_CONFIG_DIR: /bots
KNOTLESS_CACHE_DIR: /tmp/knotless-keep/cache
RUST_LOG: ${KNOTLESS_LOG:-info}
env_file:
- path: /etc/knotless/keep.env
required: false
volumes:
- ${KNOTLESS_DATA:-/var/lib/knotless}/bots:/bots
deploy:
resources:
limits:
cpus: "0.5"
memory: 512m
networks:
proxy:
name: ${KNOTLESS_PROXY_NETWORK:-knotless}
external: true