IPCR Forge v2: Radicle-only with rad-actions
Snapshot of the Radicle-only forge (cib + act, DinD staging registry) before the Gitea-centric v3 POC replaces it. Also carries the gateway's IMPORT_PUBLISHER mode, multi-segment IPNS paths and IPCR 1.2.0.
This commit is contained in:
+182
-275
@@ -4,35 +4,41 @@ name: ipcr-forge
|
||||
#
|
||||
# git push rad main v1.2.3 (developer)
|
||||
# → radicle-node / radicle-api source of truth, peer-to-peer
|
||||
# → radicle-gitea-bridge copies repos that commit .gitea/workflows/
|
||||
# → gitea + gitea-runner GitHub-style Actions build the image
|
||||
# → gitea registry staging: gitea-<domain>/gitea_admin/<app>:1.2.3
|
||||
# → ipcr-gateway follows Gitea's registry imports every tag into IPFS, publishes IPNS
|
||||
# → rad-actions (cib + act) the node's own CI: GitHub-style workflows, run on the
|
||||
# commit straight from the node's storage
|
||||
# → rad-actions-registry staging: localhost:5000/<repo>:1.2.3 (internal only)
|
||||
# → ipcr-gateway follows that registry imports every tag into IPFS, publishes IPNS
|
||||
# → ipcr-kubo / ipcr-registry
|
||||
# docker pull ipcr.localhost:4767/ipns/<name>:1.2.3 (anyone)
|
||||
#
|
||||
# Radicle is the only place anything originates. There is no mirror, no second forge and no
|
||||
# bridge: the CI broker reads the node's events and its storage, and writes each result back
|
||||
# into the repository as a job COB signed by the node. The CI's pages (cib's reports and the run
|
||||
# logs) are static files the landing page serves under /ci/.
|
||||
#
|
||||
# The tile opens ipcr-forge-<domain>: a static landing page (nginx) that explains the pipeline,
|
||||
# links Radicle and Gitea, and lists every published image with the exact `image:` line to use
|
||||
# (IPCR's published.json, served as /images.json). <name> is the IPNS key IPCR minted for the
|
||||
# links Radicle and the CI runs, and lists every published image with the exact `image:` line to
|
||||
# use (IPCR's published.json, served as /images.json). <name> is the IPNS key IPCR minted for the
|
||||
# repository (k51…), not the repository's name, which is why the page lists it.
|
||||
#
|
||||
# Every service is copied verbatim from its standalone listing (Apps/Radicle, Apps/Gitea,
|
||||
# Apps/Radicle-Gitea-Bridge, Apps/IPCR), so each keeps its own comments and rationale.md.
|
||||
# Only the data folders move, one subfolder per part: $AppID/{radicle,gitea,bridge,ipcr}/.
|
||||
# Radicle and IPCR services are copied verbatim from their standalone listings (Apps/Radicle,
|
||||
# Apps/IPCR), so each keeps its own comments and rationale.md. Only the data folders move, one
|
||||
# subfolder per part: $AppID/{radicle,ci,ipcr}/.
|
||||
#
|
||||
# Replaces those four apps; it cannot sit beside them. It uses the same container names,
|
||||
# domains (radicle-, radicle-api-, gitea-) and host ports (8776, 4768, 127.0.0.1:4767).
|
||||
# Replaces the Radicle and IPCR apps; it cannot sit beside them. It uses the same container
|
||||
# names, domains (radicle-, radicle-api-) and host ports (8776, 4768, 127.0.0.1:4767).
|
||||
#
|
||||
# The handoff from Gitea's registry to IPFS is a pull, run by IPCR: the runner's jobs run in a
|
||||
# nested Docker that cannot reach ipcr.localhost (the *host's* loopback), so CI never pushes to
|
||||
# IPCR. Instead ipcr-gateway polls gitea:3000 over gitea-internal, with a read-only token the
|
||||
# bridge-credentials step mints. Gitea does not know IPCR exists.
|
||||
# The handoff from CI to IPFS is a pull, run by IPCR: jobs run in the CI's own Docker daemon,
|
||||
# which cannot reach ipcr.localhost (the *host's* loopback). So a workflow pushes to the staging
|
||||
# registry, which shares that daemon's network namespace and is therefore `localhost:5000` to
|
||||
# every job, and ipcr-gateway polls it over ci-internal.
|
||||
|
||||
services:
|
||||
# ═══ Landing page — the tile's web UI ══════════════════════════════════════════
|
||||
# Static: the page, its icon, and IPCR's list of published images (read-only). It shows only
|
||||
# links, documentation and image names that are public on IPFS anyway, so it is not behind
|
||||
# the SSO gate; Gitea keeps its own login, Radicle is public by design.
|
||||
# Static: the page, its icon, IPCR's list of published images and the CI's pages (read-only).
|
||||
# It shows only links, documentation, image names that are public on IPFS anyway, and build
|
||||
# logs of public repositories (the adapter does not run private ones), so it is not behind
|
||||
# the SSO gate; Radicle is public by design.
|
||||
ipcr-forge:
|
||||
image: nginxinc/nginx-unprivileged:1.29.3-alpine
|
||||
container_name: ipcr-forge
|
||||
@@ -56,6 +62,8 @@ services:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/web/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
# published.json → /images.json. Read-only; the location in default.conf serves that file only.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/state:/srv/ipcr-state:ro
|
||||
# The CI broker's report pages and the run logs → /ci/. Read-only.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/html:/srv/ci:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/"]
|
||||
interval: 30s
|
||||
@@ -202,143 +210,104 @@ services:
|
||||
networks:
|
||||
- radicle-internal
|
||||
|
||||
# ═══ Gitea — CI and the staging registry ═══════════════════════════════════════
|
||||
# Verbatim from Apps/Gitea; only its data folder moves to $AppID/gitea/.
|
||||
# ═══ CI — Radicle CI broker + act ═════════════════════════════════════════════════
|
||||
|
||||
# Gitea itself — git hosting, the Actions controller, and the OCI package
|
||||
# registry, all in one process. SQLite: a personal forge never has the write
|
||||
# concurrency that would justify a second container for Postgres.
|
||||
gitea:
|
||||
image: gitea/gitea:1.27.3-rootless
|
||||
container_name: gitea
|
||||
# cib (radicle-ci-broker) follows the node's events over its control socket, keeps the ones its
|
||||
# filters allow (changes signed by a delegate, in a repository with a workflow folder — see
|
||||
# ci/ci-broker.yaml in the IPCR repository), and runs the rad-actions adapter for each. The
|
||||
# adapter checks the commit out of the node's storage and runs the matching GitHub-style
|
||||
# workflows with act, against the Docker daemon below. cib writes HTML reports, and the result
|
||||
# of every run back into the repository as a job COB signed by this node.
|
||||
rad-actions:
|
||||
image: ghcr.io/yundera/rad-actions:0.1.0
|
||||
container_name: rad-actions
|
||||
user: $PUID:$PGID
|
||||
# Reaps act's and git's children: cib is not an init.
|
||||
init: true
|
||||
cap_drop: [ALL]
|
||||
security_opt: ["no-new-privileges:true"]
|
||||
restart: unless-stopped
|
||||
cpu_shares: 70
|
||||
cpu_shares: 20
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
memory: 512M
|
||||
environment:
|
||||
TZ: $TZ
|
||||
# Every URL Gitea hands out — clone URLs, the registry host, and the
|
||||
# server_url the CI runner passes to jobs — is derived from ROOT_URL, so
|
||||
# it has to be the address that works from outside as well as inside.
|
||||
GITEA__server__ROOT_URL: https://gitea-${APP_DOMAIN}/
|
||||
GITEA__server__DOMAIN: gitea-${APP_DOMAIN}
|
||||
GITEA__server__HTTP_PORT: "3000"
|
||||
GITEA__server__PROTOCOL: http
|
||||
# The address Gitea's own CLI uses to reach the running server's internal
|
||||
# API. It defaults to localhost, which is right for the server itself and
|
||||
# useless from the init container that runs
|
||||
# `gitea actions generate-runner-token`. It has to be set here rather than
|
||||
# there: the CLI reads it out of the shared app.ini, and only this
|
||||
# container's entrypoint renders environment into app.ini.
|
||||
GITEA__server__LOCAL_ROOT_URL: http://gitea:3000/
|
||||
# Git over SSH would need a published host port of its own. Everything
|
||||
# here — clone, push, CI, registry — goes over HTTPS instead.
|
||||
GITEA__server__DISABLE_SSH: "true"
|
||||
GITEA__server__START_SSH_SERVER: "false"
|
||||
# Skips the first-run install wizard: every value it would ask for is set
|
||||
# here, and the admin account is created by an init step below.
|
||||
GITEA__security__INSTALL_LOCK: "true"
|
||||
GITEA__database__DB_TYPE: sqlite3
|
||||
GITEA__database__PATH: /var/lib/gitea/data/gitea.db
|
||||
GITEA__service__DISABLE_REGISTRATION: "true"
|
||||
GITEA__service__REQUIRE_SIGNIN_VIEW: "true"
|
||||
GITEA__actions__ENABLED: "true"
|
||||
# Where a bare `uses: actions/checkout@v4` is resolved from.
|
||||
GITEA__actions__DEFAULT_ACTIONS_URL: github
|
||||
GITEA__packages__ENABLED: "true"
|
||||
# Lets a pull mirror name a repository on this server's own Docker
|
||||
# network — a Radicle node's git endpoint, for instance. See rationale.md.
|
||||
GITEA__migrations__ALLOW_LOCALNETWORKS: "true"
|
||||
# How often a pull mirror is re-fetched. The sync emits the same push
|
||||
# event a real push would, so this interval is also how quickly CI
|
||||
# reacts to a commit that arrives from the mirrored source.
|
||||
GITEA__mirror__DEFAULT_INTERVAL: 10m
|
||||
GITEA__mirror__MIN_INTERVAL: 10m
|
||||
# The daemon jobs run in. Its socket is the only thing shared with it.
|
||||
DOCKER_HOST: unix:///run/dind/docker.sock
|
||||
# The images act runs a `runs-on:` label in. act's own medium images (Ubuntu with Node and
|
||||
# the Docker CLI), the ones act users run on their laptops.
|
||||
RAD_ACTIONS_PLATFORMS: ubuntu-latest=catthehacker/ubuntu:act-latest,ubuntu-24.04=catthehacker/ubuntu:act-24.04,ubuntu-22.04=catthehacker/ubuntu:act-22.04
|
||||
# `vars` every workflow sees. IMAGE = IMAGE_PREFIX/<repository name>, the name IPCR imports.
|
||||
RAD_ACTIONS_VARS: REGISTRY=localhost:5000,IMAGE_PREFIX=localhost:5000
|
||||
# cib's pages link each run to its log there (served by ipcr-forge).
|
||||
RAD_ACTIONS_LOG_URL: /ci/logs/
|
||||
# The repository's web page, as the adapter's GitHub API stand-in reports it (html_url, which
|
||||
# docker/metadata-action turns into the image's org.opencontainers.image.source label).
|
||||
RAD_ACTIONS_REPO_URL: https://radicle-${APP_DOMAIN}/nodes/radicle-api-${APP_DOMAIN}/{rid}
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/data:/var/lib/gitea
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/config:/etc/gitea
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/healthz"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
expose:
|
||||
- 3000
|
||||
labels:
|
||||
caddy_0: gitea-${APP_DOMAIN}
|
||||
caddy_0.import: gateway_tls
|
||||
caddy_0.reverse_proxy: "{{upstreams 3000}}"
|
||||
caddy_1: gitea-${APP_PUBLIC_IP_DASH}.nip.io
|
||||
caddy_1.import: gateway_tls
|
||||
caddy_1.reverse_proxy: "{{upstreams 3000}}"
|
||||
caddy_2: gitea-${APP_PUBLIC_IP_DASH}.sslip.io
|
||||
caddy_2.reverse_proxy: "{{upstreams 3000}}"
|
||||
# The node's home: its control socket (events), its storage (checkouts, job COBs) and its
|
||||
# key, which signs the job COBs. The same mount radicle-api has.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/radicle/home/:/radicle-home/
|
||||
# cib's database, its pages and the run logs, act's action cache, and scratch checkouts.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/state:/ci/state
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/html:/ci/html
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/cache:/ci/cache
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/home:/ci/home
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/work:/ci/work
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/socket:/run/dind
|
||||
depends_on:
|
||||
- radicle-node
|
||||
- rad-actions-docker
|
||||
networks:
|
||||
- pcs
|
||||
- gitea-internal
|
||||
# Egress only: act fetches actions (actions/checkout, docker/*) from GitHub.
|
||||
- ci-internal
|
||||
|
||||
# The CI runner: act_runner with its own Docker daemon inside it (dind), so
|
||||
# workflow jobs and the images they build never touch the host's daemon.
|
||||
# See rationale.md for what `privileged` buys and what it costs.
|
||||
gitea-runner:
|
||||
image: gitea/act_runner:0.6.1-dind
|
||||
container_name: gitea-runner
|
||||
restart: unless-stopped
|
||||
# The daemon jobs run in: Docker-in-Docker, so jobs and the images they build never touch the
|
||||
# host's daemon. See rationale.md for what `privileged` buys and what it costs. No TCP listener:
|
||||
# its only API is a unix socket in a folder shared with rad-actions, owned by $PGID.
|
||||
rad-actions-docker:
|
||||
image: docker:29.1-dind
|
||||
container_name: rad-actions-docker
|
||||
privileged: true
|
||||
restart: unless-stopped
|
||||
cpu_shares: 20
|
||||
command: ["--host=unix:///run/dind/docker.sock", "--group=$PGID"]
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 2G
|
||||
environment:
|
||||
TZ: $TZ
|
||||
# This address is not just how the runner daemon reaches Gitea — it is
|
||||
# handed to every job as GITHUB_SERVER_URL, and a job runs inside the
|
||||
# runner's *nested* daemon, on a network where `gitea` does not resolve.
|
||||
# So it has to be an address that works from the outside: checkout and
|
||||
# `docker login` from a workflow both go out and back over HTTPS.
|
||||
GITEA_INSTANCE_URL: https://gitea-${APP_DOMAIN}
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN_FILE: /data/token
|
||||
GITEA_RUNNER_NAME: gitea-runner
|
||||
# Registration-time labels. runner-config/config.yaml repeats them, and
|
||||
# that copy wins on every start, so an installed runner follows store updates.
|
||||
GITEA_RUNNER_LABELS: ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04,ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04,docker:docker://docker:28.5-cli
|
||||
CONFIG_FILE: /config/config.yaml
|
||||
# DOCKER_TLS_CERTDIR is deliberately left at the image's default. Emptying
|
||||
# it does not turn the nested daemon's TCP listener off — it turns the
|
||||
# listener's TLS off, and 2375 then answers unauthenticated to anything
|
||||
# that can reach this container.
|
||||
# No certificates: with an explicit --host the entrypoint adds no TCP listener to protect.
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
volumes:
|
||||
# .runner (the registration this runner is identified by) and the
|
||||
# registration token the init step leaves for it.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner:/data
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner-config:/config:ro
|
||||
# Replaces the image's own start-up wait — see the comments in the
|
||||
# script. It is declared under x-compose-app.files rather than left to
|
||||
# the seed tree, because it has to arrive executable.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner-config/act_runner-run:/etc/s6/act_runner/run:ro
|
||||
# The inner daemon's image store. Without this every restart re-pulls
|
||||
# every base image a workflow uses.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner-docker:/var/lib/docker
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/socket:/run/dind
|
||||
# The daemon's image store. Without it every restart re-pulls every job image.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/docker:/var/lib/docker
|
||||
healthcheck:
|
||||
test: ["CMD", "docker", "-H", "unix:///run/dind/docker.sock", "info"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- gitea-internal
|
||||
- ci-internal
|
||||
|
||||
# ═══ Radicle → Gitea bridge ════════════════════════════════════════════════════
|
||||
# Verbatim from Apps/Radicle-Gitea-Bridge; only its data folder moves to $AppID/bridge/.
|
||||
|
||||
radicle-gitea-bridge:
|
||||
image: ghcr.io/worph/radicle-gitea-bridge:1.0.0
|
||||
container_name: radicle-gitea-bridge
|
||||
# The staging registry, in the daemon's network namespace: `localhost:5000` to the daemon and to
|
||||
# every job (act runs jobs on the host network, which is this namespace), so a plain
|
||||
# `docker push` or buildx with `network=host` works without TLS or credentials — Docker allows
|
||||
# plain HTTP to localhost only. ipcr-gateway reaches it as rad-actions-docker:5000. Nothing
|
||||
# outside ci-internal can.
|
||||
rad-actions-registry:
|
||||
image: registry:3.0.0
|
||||
container_name: rad-actions-registry
|
||||
user: $PUID:$PGID
|
||||
# Reads two HTTP APIs and runs git. Needs no capability at all.
|
||||
cap_drop: [ALL]
|
||||
security_opt: ["no-new-privileges:true"]
|
||||
restart: unless-stopped
|
||||
command: ["bridge"]
|
||||
network_mode: service:rad-actions-docker
|
||||
cpu_shares: 20
|
||||
deploy:
|
||||
resources:
|
||||
@@ -346,31 +315,14 @@ services:
|
||||
memory: 128M
|
||||
environment:
|
||||
TZ: $TZ
|
||||
# git wants a home; the image's is root's and this runs as $PUID.
|
||||
HOME: /tmp
|
||||
RAD_API: http://radicle-api:8080
|
||||
GITEA_URL: http://gitea:3000
|
||||
# The Gitea account that owns the copies — the one the Gitea app creates.
|
||||
GITEA_OWNER: gitea_admin
|
||||
GITEA_TOKEN_FILE: /secrets/gitea-token
|
||||
# A repository opts in by containing this path at its head.
|
||||
OPT_IN_PATH: .gitea/workflows
|
||||
# Seconds between passes. Releases are not latency-sensitive.
|
||||
INTERVAL: "30"
|
||||
REGISTRY_HTTP_ADDR: 0.0.0.0:5000
|
||||
# Staging only: IPCR has its own copy once imported, so old tags may be deleted.
|
||||
REGISTRY_STORAGE_DELETE_ENABLED: "true"
|
||||
OTEL_TRACES_EXPORTER: none
|
||||
volumes:
|
||||
# One bare repository per Radicle repository: a staging copy between fetch and
|
||||
# push, so a push only sends what changed. Disposable: rebuilt if deleted.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/bridge/cache:/cache
|
||||
# The Gitea token the init step mints. Read-only here.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/bridge/secrets:/secrets:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "test -s /secrets/gitea-token && curl -fsS -o /dev/null http://radicle-api:8080/api/v1 && curl -fsS -o /dev/null http://gitea:3000/api/healthz"]
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- pcs
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/registry:/var/lib/registry
|
||||
depends_on:
|
||||
- rad-actions-docker
|
||||
|
||||
# ═══ IPCR — the IPFS container registry ════════════════════════════════════════
|
||||
# Verbatim from Apps/IPCR; only its data folder moves to $AppID/ipcr/.
|
||||
@@ -379,18 +331,13 @@ services:
|
||||
# ipcrd: TLS front door on 127.0.0.1:4767. Passes /ipfs/<cid> through to the stock nerdctl
|
||||
# registry below, resolves /ipns/<name>:<tag> itself, and pins every image it serves.
|
||||
ipcr-gateway:
|
||||
image: ghcr.io/yundera/ipcr:1.1.0
|
||||
image: ghcr.io/yundera/ipcr:1.2.0
|
||||
container_name: ipcr-gateway
|
||||
# Root for one reason: it writes its CA into the host's Docker trust store (the narrow
|
||||
# bind below), which is root-owned. See rationale.md.
|
||||
user: "0:0"
|
||||
# Root needs no capabilities to write root-owned files it owns; drop them all.
|
||||
cap_drop: [ALL]
|
||||
# Forge only: one back, read-only. The import token below is written by bridge-credentials as
|
||||
# $PUID, mode 0600, in a 0700 folder; a root with no capabilities is bound by those modes like
|
||||
# anyone else (verified: "permission denied"). DAC_READ_SEARCH bypasses read and search checks
|
||||
# only, never write, and only on what this container mounts. See rationale.md.
|
||||
cap_add: [DAC_READ_SEARCH]
|
||||
security_opt: ["no-new-privileges:true"]
|
||||
restart: unless-stopped
|
||||
command: ["gateway"]
|
||||
@@ -405,21 +352,20 @@ services:
|
||||
# IPCR_IMPORT_REPOS) is copied into IPFS and published as /ipns/<owner-repo>:<tag>;
|
||||
# a tag is re-imported when it moves. Empty = off. IPCR_IMPORT_AUTH is user:token,
|
||||
# a read-only (read:package) token is enough.
|
||||
# Forge: watch this app's own Gitea, over gitea-internal (not through Caddy).
|
||||
IMPORT_WATCH: ${IPCR_IMPORT_WATCH:-http://gitea:3000}
|
||||
# Forge: watch the CI's staging registry, over ci-internal. Internal, so no credentials.
|
||||
IMPORT_WATCH: ${IPCR_IMPORT_WATCH:-http://rad-actions-docker:5000}
|
||||
IMPORT_AUTH: ${IPCR_IMPORT_AUTH:-}
|
||||
# Forge: a read:package token the bridge-credentials step mints. Re-read every poll
|
||||
# and preferred over IMPORT_AUTH, so it may appear after the gateway is up.
|
||||
IMPORT_AUTH_FILE: /data/secrets/registry-auth
|
||||
IMPORT_REPOS: ${IPCR_IMPORT_REPOS:-}
|
||||
# Forge: every repository this node builds is published under one IPNS key, "forge", as
|
||||
# /ipns/<forge key>/<repo>:<tag>. One name for the whole forge, which a DNSLink domain or an
|
||||
# ENS name can point at: ipcr.localhost:4767/ipns/example.eth/<repo>:<tag>.
|
||||
IMPORT_PUBLISHER: ${IPCR_IMPORT_PUBLISHER:-forge}
|
||||
IMPORT_INTERVAL: 60s
|
||||
STATE_DIR: /data/state
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/tls:/data/tls
|
||||
# What the watcher already imported (tag → manifest digest).
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/state:/data/state
|
||||
# Forge only: the credential above. Read-only here.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/secrets:/data/secrets:ro
|
||||
# The host's Docker trust store, so the CA above can be dropped into it: no daemon
|
||||
# reload, no daemon.json edit. The parent is mounted because Compose cannot create a
|
||||
# bind source whose name contains a colon (ipcr.localhost:4767). ipcrd writes exactly
|
||||
@@ -439,8 +385,8 @@ services:
|
||||
start_period: 10s
|
||||
networks:
|
||||
- ipcr-internal
|
||||
# Forge only: reaches gitea:3000 for the import poller above.
|
||||
- gitea-internal
|
||||
# Forge only: reaches the staging registry for the import poller above.
|
||||
- ci-internal
|
||||
cpu_shares: 50
|
||||
deploy:
|
||||
resources:
|
||||
@@ -450,7 +396,7 @@ services:
|
||||
# ── Stock nerdctl IPFS registry ─────────────────────────────────────────────
|
||||
# `nerdctl ipfs registry serve`, unmodified: translates the registry API into IPFS reads.
|
||||
ipcr-registry:
|
||||
image: ghcr.io/yundera/ipcr:1.1.0
|
||||
image: ghcr.io/yundera/ipcr:1.2.0
|
||||
container_name: ipcr-registry
|
||||
# Root because nerdctl run as any other uid assumes rootless mode and refuses to start
|
||||
# without a rootless containerd — which `ipfs registry serve` never uses. The service has
|
||||
@@ -531,8 +477,8 @@ networks:
|
||||
# From Apps/Radicle: egress only, for the node.
|
||||
radicle-internal:
|
||||
driver: bridge
|
||||
# From Apps/Gitea: gitea ↔ runner, nothing else.
|
||||
gitea-internal:
|
||||
# CI: rad-actions ↔ its daemon (and staging registry) ↔ ipcr-gateway; egress for builds.
|
||||
ci-internal:
|
||||
driver: bridge
|
||||
# From Apps/IPCR: gateway ↔ registry ↔ kubo, nothing else.
|
||||
ipcr-internal:
|
||||
@@ -554,77 +500,90 @@ x-casaos:
|
||||
title:
|
||||
en_us: IPCR Forge
|
||||
tagline:
|
||||
en_us: A decentralised forge in one install — Radicle, Gitea Actions and IPFS images.
|
||||
fr_fr: Une forge décentralisée en une installation — Radicle, Gitea Actions et images IPFS.
|
||||
en_us: A decentralised forge in one install — Radicle, GitHub-style CI and IPFS images.
|
||||
fr_fr: Une forge décentralisée en une installation — Radicle, CI au format GitHub et images IPFS.
|
||||
description:
|
||||
en_us: |
|
||||
IPCR Forge is the full path from code to running container, without a central service:
|
||||
|
||||
• **Radicle** holds your repositories on a peer-to-peer network. You push with `git push rad`.
|
||||
• **Gitea Actions** builds them with GitHub-style workflows, triggered by a release tag.
|
||||
• **Radicle CI** on this node builds them with GitHub-style workflows, triggered by a release tag.
|
||||
• **IPCR** serves the resulting images from IPFS, so anyone can `docker pull` them by name.
|
||||
|
||||
A repository joins the pipeline by committing a workflow under `.gitea/workflows/`: there
|
||||
is nothing to configure per project.
|
||||
Radicle is the single source of truth: builds read the commit straight from this node, and
|
||||
their results go back into the repository as Radicle jobs. There is no mirror to keep in sync.
|
||||
|
||||
Its own page explains how to publish a project and lists every published image with the
|
||||
line to put in a compose file.
|
||||
A repository joins the pipeline by committing a workflow under `.github/workflows/`, as on
|
||||
GitHub: there is nothing to configure per project.
|
||||
|
||||
This app bundles the Radicle, Gitea, Radicle Gitea Bridge and IPCR apps. Install it instead
|
||||
of them, not beside them.
|
||||
Its own page explains how to publish a project, links every build and its log, and lists
|
||||
every published image with the line to put in a compose file.
|
||||
|
||||
This app bundles the Radicle and IPCR apps. Install it instead of them, not beside them.
|
||||
fr_fr: |
|
||||
IPCR Forge couvre tout le chemin du code au conteneur en production, sans service central :
|
||||
|
||||
• **Radicle** garde vos dépôts sur un réseau pair-à-pair. Vous poussez avec `git push rad`.
|
||||
• **Gitea Actions** les construit avec des workflows au format GitHub, déclenchés par un tag.
|
||||
• **Radicle CI** sur ce nœud les construit avec des workflows au format GitHub, déclenchés par un tag.
|
||||
• **IPCR** sert les images produites depuis IPFS : n'importe qui peut les `docker pull`.
|
||||
|
||||
Un dépôt rejoint la chaîne en ajoutant un workflow sous `.gitea/workflows/` : rien à
|
||||
configurer par projet.
|
||||
Radicle est l'unique source de vérité : les builds lisent le commit directement sur ce nœud,
|
||||
et leurs résultats retournent dans le dépôt sous forme de jobs Radicle. Aucun miroir à
|
||||
synchroniser.
|
||||
|
||||
Sa page explique comment publier un projet et liste chaque image publiée avec la ligne à
|
||||
mettre dans un fichier compose.
|
||||
Un dépôt rejoint la chaîne en ajoutant un workflow sous `.github/workflows/`, comme sur
|
||||
GitHub : rien à configurer par projet.
|
||||
|
||||
Cette application regroupe les applications Radicle, Gitea, Radicle Gitea Bridge et IPCR.
|
||||
Installez-la à leur place, pas à côté.
|
||||
Sa page explique comment publier un projet, donne accès à chaque build et à son journal, et
|
||||
liste chaque image publiée avec la ligne à mettre dans un fichier compose.
|
||||
|
||||
Cette application regroupe les applications Radicle et IPCR. Installez-la à leur place, pas à
|
||||
côté.
|
||||
tips:
|
||||
before_install:
|
||||
en_us: |
|
||||
**Replaces four apps.** Uninstall Radicle, Gitea, Radicle Gitea Bridge and IPCR first:
|
||||
this app uses the same addresses and ports.
|
||||
**Replaces two apps.** Uninstall Radicle and IPCR first: this app uses the same addresses
|
||||
and ports.
|
||||
|
||||
| Address | What |
|
||||
| --- | --- |
|
||||
| `https://ipcr-forge-$APP_DOMAIN` | This app: how to publish, and every published image |
|
||||
| `https://ipcr-forge-$APP_DOMAIN` | This app: how to publish, every published image |
|
||||
| `https://ipcr-forge-$APP_DOMAIN/ci/` | CI runs and their logs (public) |
|
||||
| `https://radicle-$APP_DOMAIN` | Radicle explorer (public) |
|
||||
| `https://radicle-api-$APP_DOMAIN` | Radicle API and read-only git (public) |
|
||||
| `https://gitea-$APP_DOMAIN` | Gitea: builds, logs, staging registry (`gitea_admin`) |
|
||||
| `ipcr.localhost:4767` | IPCR registry, this server's Docker only |
|
||||
|
||||
**Ports.** TCP 8776 (Radicle peers) and 4768 TCP+UDP (IPFS peers) are published.
|
||||
|
||||
**Images reach IPFS on their own.** IPCR follows this app's Gitea registry: about a
|
||||
minute after a build pushes `gitea_admin/<repo>:<tag>`, it can be pulled as
|
||||
**Who can build.** Only changes signed by one of a repository's delegates trigger a build:
|
||||
release tags, the default branch and their patches. Builds run in their own Docker daemon.
|
||||
|
||||
**Images reach IPFS on their own.** A workflow pushes to `vars.IMAGE` (this node's
|
||||
staging registry); about a minute later the image can be pulled as
|
||||
`ipcr.localhost:4767/ipns/<name>:<tag>`, where `<name>` is the IPNS name (`k51…`) IPCR
|
||||
creates for the repository. The app's page lists each one with the line to copy.
|
||||
fr_fr: |
|
||||
**Remplace quatre applications.** Désinstallez d'abord Radicle, Gitea, Radicle Gitea
|
||||
Bridge et IPCR : cette application utilise les mêmes adresses et ports.
|
||||
**Remplace deux applications.** Désinstallez d'abord Radicle et IPCR : cette application
|
||||
utilise les mêmes adresses et ports.
|
||||
|
||||
| Adresse | Quoi |
|
||||
| --- | --- |
|
||||
| `https://ipcr-forge-$APP_DOMAIN` | Cette application : publier, et toutes les images publiées |
|
||||
| `https://ipcr-forge-$APP_DOMAIN` | Cette application : publier, toutes les images publiées |
|
||||
| `https://ipcr-forge-$APP_DOMAIN/ci/` | Builds et leurs journaux (public) |
|
||||
| `https://radicle-$APP_DOMAIN` | Explorateur Radicle (public) |
|
||||
| `https://radicle-api-$APP_DOMAIN` | API Radicle et git en lecture seule (public) |
|
||||
| `https://gitea-$APP_DOMAIN` | Gitea : builds, journaux, registre de transit (`gitea_admin`) |
|
||||
| `ipcr.localhost:4767` | Registre IPCR, pour le Docker de ce serveur uniquement |
|
||||
|
||||
**Ports.** TCP 8776 (pairs Radicle) et 4768 TCP+UDP (pairs IPFS) sont publiés.
|
||||
|
||||
**Les images arrivent sur IPFS d'elles-mêmes.** IPCR suit le registre Gitea de cette
|
||||
application : environ une minute après qu'un build a poussé `gitea_admin/<dépôt>:<tag>`,
|
||||
elle se tire avec `ipcr.localhost:4767/ipns/<nom>:<tag>`, où `<nom>` est le nom IPNS
|
||||
(`k51…`) qu'IPCR crée pour le dépôt. La page de l'application les liste, prêts à copier.
|
||||
**Qui peut lancer un build.** Seuls les changements signés par un délégué du dépôt
|
||||
déclenchent un build : tags de version, branche par défaut et leurs patches. Les builds
|
||||
tournent dans leur propre démon Docker.
|
||||
|
||||
**Les images arrivent sur IPFS d'elles-mêmes.** Un workflow pousse vers `vars.IMAGE`
|
||||
(le registre de transit de ce nœud) ; environ une minute plus tard, l'image se tire avec
|
||||
`ipcr.localhost:4767/ipns/<nom>:<tag>`, où `<nom>` est le nom IPNS (`k51…`) qu'IPCR crée
|
||||
pour le dépôt. La page de l'application les liste, prêts à copier.
|
||||
index: /
|
||||
webui_port: 8080
|
||||
store_app_id: ipcr-forge
|
||||
@@ -646,29 +605,35 @@ x-compose-app:
|
||||
- path: /DATA/AppData/$AppID/radicle/caddy
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
# ── Gitea ──
|
||||
- path: /DATA/AppData/$AppID/gitea/data
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/$AppID/gitea/config
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/$AppID/gitea/runner
|
||||
# ── CI ──
|
||||
- path: /DATA/AppData/$AppID/ci/state
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0700"
|
||||
- path: /DATA/AppData/$AppID/gitea/runner-config
|
||||
- path: /DATA/AppData/$AppID/ci/html/logs
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/$AppID/gitea/runner-docker
|
||||
- path: /DATA/AppData/$AppID/ci/cache
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/$AppID/ci/home
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0700"
|
||||
- path: /DATA/AppData/$AppID/ci/work
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0700"
|
||||
# The daemon's socket: created by root, group $PGID (dockerd --group), mode 0660.
|
||||
- path: /DATA/AppData/$AppID/ci/socket
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0750"
|
||||
- path: /DATA/AppData/$AppID/ci/docker
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0710"
|
||||
# ── Bridge ──
|
||||
- path: /DATA/AppData/$AppID/bridge/cache
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/$AppID/bridge/secrets
|
||||
- path: /DATA/AppData/$AppID/ci/registry
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0700"
|
||||
@@ -680,11 +645,6 @@ x-compose-app:
|
||||
- path: /DATA/AppData/$AppID/ipcr/init.d
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
# Written by bridge-credentials as $PUID, read by ipcr-gateway (root).
|
||||
- path: /DATA/AppData/$AppID/ipcr/secrets
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0700"
|
||||
files:
|
||||
# The landing page follows store updates: re-rendered on every up. None of the three holds a
|
||||
# dollar-brace sequence, so rendering cannot fail on an unresolved variable.
|
||||
@@ -709,17 +669,12 @@ x-compose-app:
|
||||
from: radicle/caddy/Caddyfile.tmpl
|
||||
ensure: always
|
||||
mode: "0644"
|
||||
- path: /DATA/AppData/$AppID/gitea/runner-config/act_runner-run
|
||||
from: gitea/runner-config/act_runner-run
|
||||
ensure: always
|
||||
mode: "0755"
|
||||
- path: /DATA/AppData/$AppID/ipcr/init.d/01-ipcr-node.sh
|
||||
from: ipcr/init.d/01-ipcr-node.sh
|
||||
ensure: always
|
||||
mode: "0755"
|
||||
init:
|
||||
# In order: Radicle's identity before its node starts, then Gitea's admin, then the
|
||||
# runner token and the bridge's credentials, which both need that admin to exist.
|
||||
# Radicle's identity before its node starts. The CI needs nothing: it uses the node's.
|
||||
- name: radicle-identity
|
||||
image: dirk1980/radicle-seed-node@sha256:78ccee526ad4b4f4d7c79c2323b032a7209858a48e1dc9c077272ff1660050e2
|
||||
user: $PUID:$PGID
|
||||
@@ -727,53 +682,5 @@ x-compose-app:
|
||||
- /DATA/AppData/$AppID/radicle/home:/radicle-home
|
||||
command: ["sh", "-c", "yes '' | rad auth --alias radicle-${APP_PUBLIC_IP_DASH} --stdin"]
|
||||
when: absent:/DATA/AppData/$AppID/radicle/home/keys/radicle
|
||||
- name: gitea-admin
|
||||
image: gitea/gitea:1.27.3-rootless
|
||||
user: $PUID:$PGID
|
||||
phase: post_up
|
||||
volumes:
|
||||
- /DATA/AppData/$AppID/gitea/data:/var/lib/gitea
|
||||
- /DATA/AppData/$AppID/gitea/config:/etc/gitea
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
timeout 240 sh -c 'until gitea admin user list >/dev/null 2>&1; do sleep 3; done'
|
||||
gitea admin user create --admin --username gitea_admin \
|
||||
--password "$APP_DEFAULT_PASSWORD" --email "$APP_EMAIL" \
|
||||
--must-change-password=false \
|
||||
|| echo "admin account already present, leaving it alone"
|
||||
when: always
|
||||
- name: gitea-runner-token
|
||||
image: gitea/gitea:1.27.3-rootless
|
||||
user: $PUID:$PGID
|
||||
phase: post_up
|
||||
network: $APP_NET
|
||||
volumes:
|
||||
- /DATA/AppData/$AppID/gitea/data:/var/lib/gitea
|
||||
- /DATA/AppData/$AppID/gitea/config:/etc/gitea
|
||||
- /DATA/AppData/$AppID/gitea/runner:/runner
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
timeout 240 sh -c 'until gitea actions generate-runner-token > /runner/token.new 2>/dev/null; do sleep 3; done'
|
||||
mv /runner/token.new /runner/token
|
||||
chmod 600 /runner/token
|
||||
when: absent:/DATA/AppData/$AppID/gitea/runner/.runner
|
||||
- name: bridge-credentials
|
||||
image: ghcr.io/worph/radicle-gitea-bridge:1.0.0
|
||||
user: $PUID:$PGID
|
||||
phase: post_up
|
||||
network: $APP_NET
|
||||
volumes:
|
||||
- /DATA/AppData/$AppID/bridge/secrets:/secrets
|
||||
- /DATA/AppData/$AppID/ipcr/secrets:/ipcr-secrets
|
||||
# Also mints IPCR's read-only token for following Gitea's registry.
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- GITEA_PASSWORD="$APP_DEFAULT_PASSWORD" IPCR_AUTH_FILE=/ipcr-secrets/registry-auth bridge-init
|
||||
when: absent:/DATA/AppData/$AppID/bridge/secrets/gitea-token
|
||||
webui-host: ipcr-forge-${domain}
|
||||
webui-path: /
|
||||
|
||||
Reference in New Issue
Block a user